mirror of https://bitbucket.org/ausocean/av.git
Merged in avdevice-interface (pull request #270)
revid: AVDevice and implementations Approved-by: Alan Noble <anoble@gmail.com>
This commit is contained in:
commit
6c8b980b2f
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
NAME
|
||||
revid-cli - command line interface for Revid.
|
||||
revid-cli - command line interface for revid.
|
||||
|
||||
DESCRIPTION
|
||||
See Readme.md
|
||||
|
@ -42,6 +42,7 @@ import (
|
|||
"bitbucket.org/ausocean/av/container/mts"
|
||||
"bitbucket.org/ausocean/av/container/mts/meta"
|
||||
"bitbucket.org/ausocean/av/revid"
|
||||
"bitbucket.org/ausocean/av/revid/config"
|
||||
"bitbucket.org/ausocean/iot/pi/netsender"
|
||||
"bitbucket.org/ausocean/iot/pi/sds"
|
||||
"bitbucket.org/ausocean/iot/pi/smartlogger"
|
||||
|
@ -102,8 +103,8 @@ func main() {
|
|||
|
||||
// handleFlags parses command line flags and returns a revid configuration
|
||||
// based on them.
|
||||
func handleFlags() revid.Config {
|
||||
var cfg revid.Config
|
||||
func handleFlags() config.Config {
|
||||
var cfg config.Config
|
||||
|
||||
var (
|
||||
cpuprofile = flag.String("cpuprofile", "", "write cpu profile to `file`")
|
||||
|
@ -128,8 +129,8 @@ func handleFlags() revid.Config {
|
|||
rotationPtr = flag.Uint("Rotation", 0, "Rotate video output. (0-359 degrees)")
|
||||
brightnessPtr = flag.Uint("Brightness", 50, "Set brightness. (0-100) ")
|
||||
saturationPtr = flag.Int("Saturation", 0, "Set Saturation. (100-100)")
|
||||
exposurePtr = flag.String("Exposure", "auto", "Set exposure mode. ("+strings.Join(revid.ExposureModes[:], ",")+")")
|
||||
autoWhiteBalancePtr = flag.String("Awb", "auto", "Set automatic white balance mode. ("+strings.Join(revid.AutoWhiteBalanceModes[:], ",")+")")
|
||||
exposurePtr = flag.String("Exposure", "auto", "Set exposure mode. ("+strings.Join(config.ExposureModes[:], ",")+")")
|
||||
autoWhiteBalancePtr = flag.String("Awb", "auto", "Set automatic white balance mode. ("+strings.Join(config.AutoWhiteBalanceModes[:], ",")+")")
|
||||
|
||||
// Audio specific flags.
|
||||
sampleRatePtr = flag.Int("SampleRate", 48000, "Sample rate of recorded audio")
|
||||
|
@ -179,15 +180,15 @@ func handleFlags() revid.Config {
|
|||
|
||||
switch *inputPtr {
|
||||
case "Raspivid":
|
||||
cfg.Input = revid.Raspivid
|
||||
cfg.Input = config.InputRaspivid
|
||||
case "v4l":
|
||||
cfg.Input = revid.V4L
|
||||
cfg.Input = config.InputV4L
|
||||
case "File":
|
||||
cfg.Input = revid.File
|
||||
cfg.Input = config.InputFile
|
||||
case "Audio":
|
||||
cfg.Input = revid.Audio
|
||||
cfg.Input = config.InputAudio
|
||||
case "RTSP":
|
||||
cfg.Input = revid.RTSP
|
||||
cfg.Input = config.InputRTSP
|
||||
case "":
|
||||
default:
|
||||
log.Log(logger.Error, pkg+"bad input argument")
|
||||
|
@ -214,13 +215,13 @@ func handleFlags() revid.Config {
|
|||
for _, o := range outputs {
|
||||
switch o {
|
||||
case "File":
|
||||
cfg.Outputs = append(cfg.Outputs, revid.File)
|
||||
cfg.Outputs = append(cfg.Outputs, config.OutputFile)
|
||||
case "Http":
|
||||
cfg.Outputs = append(cfg.Outputs, revid.HTTP)
|
||||
cfg.Outputs = append(cfg.Outputs, config.OutputHTTP)
|
||||
case "Rtmp":
|
||||
cfg.Outputs = append(cfg.Outputs, revid.RTMP)
|
||||
cfg.Outputs = append(cfg.Outputs, config.OutputRTMP)
|
||||
case "Rtp":
|
||||
cfg.Outputs = append(cfg.Outputs, revid.RTP)
|
||||
cfg.Outputs = append(cfg.Outputs, config.OutputRTP)
|
||||
case "":
|
||||
default:
|
||||
log.Log(logger.Error, pkg+"bad output argument", "arg", o)
|
||||
|
@ -258,7 +259,7 @@ func handleFlags() revid.Config {
|
|||
}
|
||||
|
||||
// initialize then run the main NetSender client
|
||||
func run(cfg revid.Config) {
|
||||
func run(cfg config.Config) {
|
||||
log.Log(logger.Info, pkg+"running in NetSender mode")
|
||||
|
||||
var rv *revid.Revid
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
DESCRIPTION
|
||||
device.go provides AVDevice, an interface that describes a configurable
|
||||
audio or video device that can be started and stopped from which data may
|
||||
be obtained.
|
||||
|
||||
AUTHORS
|
||||
Saxon A. Nelson-Milton <saxon@ausocean.org>
|
||||
|
||||
LICENSE
|
||||
Copyright (C) 2019 the Australian Ocean Lab (AusOcean)
|
||||
|
||||
It is free software: you can redistribute it and/or modify them
|
||||
under the terms of the GNU General Public License as published by the
|
||||
Free Software Foundation, either version 3 of the License, or (at your
|
||||
option) any later version.
|
||||
|
||||
It is distributed in the hope that it will be useful, but WITHOUT
|
||||
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
in gpl.txt. If not, see http://www.gnu.org/licenses.
|
||||
*/
|
||||
|
||||
package device
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"bitbucket.org/ausocean/av/revid/config"
|
||||
)
|
||||
|
||||
// AVDevice describes a configurable audio or video device from which media data
|
||||
// can be obtained. AVDevice is an io.Reader.
|
||||
type AVDevice interface {
|
||||
io.Reader
|
||||
|
||||
// Set allows for configuration of the AVDevice using a Config struct. All,
|
||||
// some or none of the fields of the Config struct may be used for configuration
|
||||
// by an implementation. An implementation should specify what fields are
|
||||
// considered.
|
||||
Set(c config.Config) error
|
||||
|
||||
// Start will start the AVDevice capturing media data; after which the Read
|
||||
// method may be called to obtain the data. The format of the data may differ
|
||||
// and should be specified by the implementation.
|
||||
Start() error
|
||||
|
||||
// Stop will stop the AVDevice from capturing media data. From this point
|
||||
// Reads will no longer be successful.
|
||||
Stop() error
|
||||
}
|
||||
|
||||
// multiError implements the built in error interface. multiError is used here
|
||||
// to collect multi errors during validation of configruation parameters for o
|
||||
// AVDevices.
|
||||
type MultiError []error
|
||||
|
||||
func (me MultiError) Error() string {
|
||||
return fmt.Sprintf("%v", me)
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
DESCRIPTION
|
||||
file.go provides an implementation of the AVDevice interface for media files.
|
||||
|
||||
AUTHORS
|
||||
Saxon A. Nelson-Milton <saxon@ausocean.org>
|
||||
|
||||
LICENSE
|
||||
Copyright (C) 2019 the Australian Ocean Lab (AusOcean)
|
||||
|
||||
It is free software: you can redistribute it and/or modify them
|
||||
under the terms of the GNU General Public License as published by the
|
||||
Free Software Foundation, either version 3 of the License, or (at your
|
||||
option) any later version.
|
||||
|
||||
It is distributed in the hope that it will be useful, but WITHOUT
|
||||
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
in gpl.txt. If not, see http://www.gnu.org/licenses.
|
||||
*/
|
||||
|
||||
package file
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"bitbucket.org/ausocean/av/revid/config"
|
||||
)
|
||||
|
||||
// AVFile is an implementation of the AVDevice interface for a file containg
|
||||
// audio or video data.
|
||||
type AVFile struct {
|
||||
f io.ReadCloser
|
||||
cfg config.Config
|
||||
}
|
||||
|
||||
// NewAVFile returns a new AVFile.
|
||||
func NewAVFile() *AVFile { return &AVFile{} }
|
||||
|
||||
// Set simply sets the AVFile's config to the passed config.
|
||||
func (m *AVFile) Set(c config.Config) error {
|
||||
m.cfg = c
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start will open the file at the location of the InputPath field of the
|
||||
// config struct.
|
||||
func (m *AVFile) Start() error {
|
||||
var err error
|
||||
m.f, err = os.Open(m.cfg.InputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not open media file: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop will close the file such that any further reads will fail.
|
||||
func (m *AVFile) Stop() error { return m.f.Close() }
|
||||
|
||||
// Read implements io.Reader. If start has not been called, or Start has been
|
||||
// called and Stop has since been called, an error is returned.
|
||||
func (m *AVFile) Read(p []byte) (int, error) {
|
||||
if m.f != nil {
|
||||
return m.f.Read(p)
|
||||
}
|
||||
return 0, errors.New("AV file is closed")
|
||||
}
|
|
@ -0,0 +1,329 @@
|
|||
/*
|
||||
DESCRIPTION
|
||||
geovision.go provides an implementation of the AVDevice interface for the
|
||||
GeoVision IP camera.
|
||||
|
||||
AUTHORS
|
||||
Saxon A. Nelson-Milton <saxon@ausocean.org>
|
||||
|
||||
LICENSE
|
||||
Copyright (C) 2019 the Australian Ocean Lab (AusOcean)
|
||||
|
||||
It is free software: you can redistribute it and/or modify them
|
||||
under the terms of the GNU General Public License as published by the
|
||||
Free Software Foundation, either version 3 of the License, or (at your
|
||||
option) any later version.
|
||||
|
||||
It is distributed in the hope that it will be useful, but WITHOUT
|
||||
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
in gpl.txt. If not, see http://www.gnu.org/licenses.
|
||||
*/
|
||||
|
||||
package geovision
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"bitbucket.org/ausocean/av/codec/codecutil"
|
||||
"bitbucket.org/ausocean/av/device"
|
||||
"bitbucket.org/ausocean/av/device/geovision/gvctrl"
|
||||
"bitbucket.org/ausocean/av/protocol/rtcp"
|
||||
"bitbucket.org/ausocean/av/protocol/rtp"
|
||||
"bitbucket.org/ausocean/av/protocol/rtsp"
|
||||
"bitbucket.org/ausocean/av/revid/config"
|
||||
"bitbucket.org/ausocean/utils/logger"
|
||||
)
|
||||
|
||||
// Indicate package when logging.
|
||||
const pkg = "geovision: "
|
||||
|
||||
// Constants for real time clients.
|
||||
const (
|
||||
rtpPort = 60000
|
||||
rtcpPort = 60001
|
||||
defaultServerRTCPPort = 17301
|
||||
)
|
||||
|
||||
// TODO: remove this when gvctrl has configurable user and pass.
|
||||
const (
|
||||
ipCamUser = "admin"
|
||||
ipCamPass = "admin"
|
||||
)
|
||||
|
||||
// Configuration defaults.
|
||||
const (
|
||||
defaultCameraIP = "192.168.1.50"
|
||||
defaultCodec = codecutil.H264
|
||||
defaultHeight = 720
|
||||
defaultFrameRate = 25
|
||||
defaultBitrate = 400
|
||||
defaultVBRBitrate = 400
|
||||
defaultMinFrames = 100
|
||||
defaultVBRQuality = config.QualityStandard
|
||||
defaultCameraChan = 2
|
||||
)
|
||||
|
||||
// Configuration field errors.
|
||||
var (
|
||||
errGVBadCameraIP = errors.New("camera IP bad or unset, defaulting")
|
||||
errGVBadCodec = errors.New("codec bad or unset, defaulting")
|
||||
errGVBadFrameRate = errors.New("frame rate bad or unset, defaulting")
|
||||
errGVBadBitrate = errors.New("bitrate bad or unset, defaulting")
|
||||
errGVBadVBRQuality = errors.New("VBR quality bad or unset, defaulting")
|
||||
errGVBadHeight = errors.New("height bad or unset, defaulting")
|
||||
errGVBadMinFrames = errors.New("min frames bad or unset, defaulting")
|
||||
)
|
||||
|
||||
// GeoVision is an implementation of the AVDevice interface for a GeoVision
|
||||
// IP camera. This has been designed to implement the GV-BX4700-8F in particular.
|
||||
// Any other models are untested.
|
||||
type GeoVision struct {
|
||||
cfg config.Config
|
||||
log config.Logger
|
||||
rtpClt *rtp.Client
|
||||
rtspClt *rtsp.Client
|
||||
rtcpClt *rtcp.Client
|
||||
}
|
||||
|
||||
// NewGeoVision returns a new GeoVision.
|
||||
func NewGeoVision(l config.Logger) *GeoVision { return &GeoVision{log: l} }
|
||||
|
||||
// Set will take a Config struct, check the validity of the relevant fields
|
||||
// and then performs any configuration necessary using gvctrl to control the
|
||||
// GeoVision web interface. If fields are not valid, an error is added to the
|
||||
// multiError and a default value is used for that particular field.
|
||||
func (g *GeoVision) Set(c config.Config) error {
|
||||
var errs device.MultiError
|
||||
if c.CameraIP == "" {
|
||||
errs = append(errs, errGVBadCameraIP)
|
||||
c.CameraIP = defaultCameraIP
|
||||
}
|
||||
|
||||
switch c.InputCodec {
|
||||
case codecutil.H264, codecutil.H265, codecutil.MJPEG:
|
||||
default:
|
||||
errs = append(errs, errGVBadCodec)
|
||||
c.InputCodec = defaultCodec
|
||||
}
|
||||
|
||||
if c.Height <= 0 {
|
||||
errs = append(errs, errGVBadHeight)
|
||||
c.Height = defaultHeight
|
||||
}
|
||||
|
||||
if c.FrameRate <= 0 {
|
||||
errs = append(errs, errGVBadFrameRate)
|
||||
c.FrameRate = defaultFrameRate
|
||||
}
|
||||
|
||||
if c.Bitrate <= 0 {
|
||||
errs = append(errs, errGVBadBitrate)
|
||||
c.Bitrate = defaultBitrate
|
||||
}
|
||||
|
||||
if c.MinFrames <= 0 {
|
||||
errs = append(errs, errGVBadMinFrames)
|
||||
c.MinFrames = defaultMinFrames
|
||||
}
|
||||
|
||||
switch c.VBRQuality {
|
||||
case config.QualityStandard, config.QualityFair, config.QualityGood, config.QualityGreat, config.QualityExcellent:
|
||||
default:
|
||||
errs = append(errs, errGVBadVBRQuality)
|
||||
c.VBRQuality = defaultVBRQuality
|
||||
}
|
||||
|
||||
if c.VBRBitrate <= 0 {
|
||||
errs = append(errs, errGVBadVBRQuality)
|
||||
c.VBRBitrate = defaultVBRBitrate
|
||||
}
|
||||
|
||||
if c.CameraChan != 1 && c.CameraChan != 2 {
|
||||
errs = append(errs, errGVBadVBRQuality)
|
||||
c.CameraChan = defaultCameraChan
|
||||
}
|
||||
|
||||
g.cfg = c
|
||||
|
||||
err := gvctrl.Set(
|
||||
g.cfg.CameraIP,
|
||||
gvctrl.Channel(g.cfg.CameraChan),
|
||||
gvctrl.CodecOut(
|
||||
map[uint8]gvctrl.Codec{
|
||||
codecutil.H264: gvctrl.CodecH264,
|
||||
codecutil.H265: gvctrl.CodecH265,
|
||||
codecutil.MJPEG: gvctrl.CodecMJPEG,
|
||||
}[g.cfg.InputCodec],
|
||||
),
|
||||
gvctrl.Height(int(g.cfg.Height)),
|
||||
gvctrl.FrameRate(int(g.cfg.FrameRate)),
|
||||
gvctrl.VariableBitrate(g.cfg.VBR),
|
||||
gvctrl.VBRQuality(
|
||||
map[config.Quality]gvctrl.Quality{
|
||||
config.QualityStandard: gvctrl.QualityStandard,
|
||||
config.QualityFair: gvctrl.QualityFair,
|
||||
config.QualityGood: gvctrl.QualityGood,
|
||||
config.QualityGreat: gvctrl.QualityGreat,
|
||||
config.QualityExcellent: gvctrl.QualityExcellent,
|
||||
}[g.cfg.VBRQuality],
|
||||
),
|
||||
gvctrl.VBRBitrate(g.cfg.VBRBitrate),
|
||||
gvctrl.CBRBitrate(int(g.cfg.Bitrate)),
|
||||
gvctrl.Refresh(float64(g.cfg.MinFrames)/float64(g.cfg.FrameRate)),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not set IPCamera settings: %w", err)
|
||||
}
|
||||
|
||||
// Give the camera some time to change it's configuration.
|
||||
const setupDelay = 5 * time.Second
|
||||
time.Sleep(setupDelay)
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
// Start uses an RTSP client to communicate with the GeoVision RTSP server and
|
||||
// request a stream that is then received by an RTP client, from which packets
|
||||
// can be read from using the Read method.
|
||||
func (g *GeoVision) Start() error {
|
||||
var (
|
||||
local, remote *net.TCPAddr
|
||||
err error
|
||||
)
|
||||
|
||||
g.rtspClt, local, remote, err = rtsp.NewClient("rtsp://" + ipCamUser + ":" + ipCamPass + "@" + g.cfg.CameraIP + ":8554/" + "CH002.sdp")
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create RTSP client: %w", err)
|
||||
}
|
||||
|
||||
g.log.Log(logger.Info, pkg+"created RTSP client")
|
||||
|
||||
resp, err := g.rtspClt.Options()
|
||||
if err != nil {
|
||||
return fmt.Errorf("options request unsuccessful: %w", err)
|
||||
}
|
||||
g.log.Log(logger.Debug, pkg+"RTSP OPTIONS response", "response", resp.String())
|
||||
|
||||
resp, err = g.rtspClt.Describe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("describe request unsuccessful: %w", err)
|
||||
}
|
||||
g.log.Log(logger.Debug, pkg+"RTSP DESCRIBE response", "response", resp.String())
|
||||
|
||||
resp, err = g.rtspClt.Setup("track1", fmt.Sprintf("RTP/AVP;unicast;client_port=%d-%d", rtpPort, rtcpPort))
|
||||
if err != nil {
|
||||
return fmt.Errorf("setup request unsuccessful: %w", err)
|
||||
}
|
||||
g.log.Log(logger.Debug, pkg+"RTSP SETUP response", "response", resp.String())
|
||||
|
||||
rtpCltAddr, rtcpCltAddr, rtcpSvrAddr, err := formAddrs(local, remote, *resp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not format addresses: %w", err)
|
||||
}
|
||||
|
||||
g.log.Log(logger.Info, pkg+"RTSP session setup complete")
|
||||
|
||||
g.rtpClt, err = rtp.NewClient(rtpCltAddr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create RTP client: %w", err)
|
||||
}
|
||||
|
||||
g.rtcpClt, err = rtcp.NewClient(rtcpCltAddr, rtcpSvrAddr, g.rtpClt, g.log.Log)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create RTCP client: %w", err)
|
||||
}
|
||||
|
||||
g.log.Log(logger.Info, pkg+"RTCP and RTP clients created")
|
||||
|
||||
// Check errors from RTCP client until it has stopped running.
|
||||
go func() {
|
||||
for {
|
||||
err, ok := <-g.rtcpClt.Err()
|
||||
if ok {
|
||||
g.log.Log(logger.Warning, pkg+"RTCP error", "error", err.Error())
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Start the RTCP client.
|
||||
g.rtcpClt.Start()
|
||||
g.log.Log(logger.Info, pkg+"RTCP client started")
|
||||
|
||||
resp, err = g.rtspClt.Play()
|
||||
if err != nil {
|
||||
return fmt.Errorf("play request unsuccessful: %w", err)
|
||||
}
|
||||
g.log.Log(logger.Debug, pkg+"RTSP server PLAY response", "response", resp.String())
|
||||
g.log.Log(logger.Info, pkg+"play requested, now receiving stream")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop will close the RTSP, RTCP, and RTP connections and in turn end the
|
||||
// stream from the GeoVision. Future reads using Read will result in error.
|
||||
func (g *GeoVision) Stop() error {
|
||||
err := g.rtpClt.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not close RTP client: %w", err)
|
||||
}
|
||||
|
||||
err = g.rtspClt.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not close RTSP client: %w", err)
|
||||
}
|
||||
|
||||
g.rtcpClt.Stop()
|
||||
|
||||
g.log.Log(logger.Info, pkg+"RTP, RTSP and RTCP clients stopped and closed")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Read implements io.Reader. If the GeoVision has not been started an error is
|
||||
// returned.
|
||||
func (g *GeoVision) Read(p []byte) (int, error) {
|
||||
if g.rtpClt != nil {
|
||||
return g.rtpClt.Read(p)
|
||||
}
|
||||
return 0, errors.New("cannot read, GeoVision not streaming")
|
||||
}
|
||||
|
||||
// formAddrs is a helper function to form the addresses for the RTP client,
|
||||
// RTCP client, and the RTSP server's RTCP addr using the local, remote addresses
|
||||
// of the RTSP conn, and the SETUP method response.
|
||||
func formAddrs(local, remote *net.TCPAddr, setupResp rtsp.Response) (rtpCltAddr, rtcpCltAddr, rtcpSvrAddr string, err error) {
|
||||
svrRTCPPort, err := parseSvrRTCPPort(setupResp)
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
rtpCltAddr = strings.Split(local.String(), ":")[0] + ":" + strconv.Itoa(rtpPort)
|
||||
rtcpCltAddr = strings.Split(local.String(), ":")[0] + ":" + strconv.Itoa(rtcpPort)
|
||||
rtcpSvrAddr = strings.Split(remote.String(), ":")[0] + ":" + strconv.Itoa(svrRTCPPort)
|
||||
return
|
||||
}
|
||||
|
||||
// parseServerRTCPPort is a helper function to get the RTSP server's RTCP port.
|
||||
func parseSvrRTCPPort(resp rtsp.Response) (int, error) {
|
||||
transport := resp.Header.Get("Transport")
|
||||
for _, p := range strings.Split(transport, ";") {
|
||||
if strings.Contains(p, "server_port") {
|
||||
port, err := strconv.Atoi(strings.Split(p, "-")[1])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return port, nil
|
||||
}
|
||||
}
|
||||
return 0, errors.New("SETUP response did not provide RTCP port")
|
||||
}
|
|
@ -31,7 +31,7 @@ import (
|
|||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"bitbucket.org/ausocean/av/input/gvctrl"
|
||||
"bitbucket.org/ausocean/av/device/geovision/gvctrl"
|
||||
)
|
||||
|
||||
func main() {
|
|
@ -0,0 +1,284 @@
|
|||
/*
|
||||
DESCRIPTION
|
||||
raspivid.go provides an implementation of the AVDevice interface for raspivid.
|
||||
|
||||
AUTHORS
|
||||
Saxon A. Nelson-Milton <saxon@ausocean.org>
|
||||
|
||||
LICENSE
|
||||
Copyright (C) 2019 the Australian Ocean Lab (AusOcean)
|
||||
|
||||
It is free software: you can redistribute it and/or modify them
|
||||
under the terms of the GNU General Public License as published by the
|
||||
Free Software Foundation, either version 3 of the License, or (at your
|
||||
option) any later version.
|
||||
|
||||
It is distributed in the hope that it will be useful, but WITHOUT
|
||||
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
in gpl.txt. If not, see http://www.gnu.org/licenses.
|
||||
*/
|
||||
|
||||
package raspivid
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"bitbucket.org/ausocean/av/codec/codecutil"
|
||||
"bitbucket.org/ausocean/av/device"
|
||||
"bitbucket.org/ausocean/av/revid/config"
|
||||
"bitbucket.org/ausocean/utils/logger"
|
||||
)
|
||||
|
||||
// To indicate package when logging.
|
||||
const pkg = "raspivid: "
|
||||
|
||||
// Raspivid configuration defaults.
|
||||
const (
|
||||
defaultRaspividCodec = codecutil.H264
|
||||
defaultRaspividRotation = 0
|
||||
defaultRaspividWidth = 1280
|
||||
defaultRaspividHeight = 720
|
||||
defaultRaspividBrightness = 50
|
||||
defaultRaspividSaturation = 0
|
||||
defaultRaspividExposure = "auto"
|
||||
defaultRaspividAutoWhiteBalance = "auto"
|
||||
defaultRaspividMinFrames = 100
|
||||
defaultRaspividQuantization = 30
|
||||
defaultRaspividBitrate = 48000
|
||||
defaultRaspividFramerate = 25
|
||||
)
|
||||
|
||||
// Configuration errors.
|
||||
var (
|
||||
errBadCodec = errors.New("codec bad or unset, defaulting")
|
||||
errBadRotation = errors.New("rotation bad or unset, defaulting")
|
||||
errBadWidth = errors.New("width bad or unset, defaulting")
|
||||
errBadHeight = errors.New("height bad or unset, defaulting")
|
||||
errBadFrameRate = errors.New("framerate bad or unset, defaulting")
|
||||
errBadBitrate = errors.New("bitrate bad or unset, defaulting")
|
||||
errBadMinFrames = errors.New("min frames bad or unset, defaulting")
|
||||
errBadSaturation = errors.New("saturation bad or unset, defaulting")
|
||||
errBadBrightness = errors.New("brightness bad or unset, defaulting")
|
||||
errBadExposure = errors.New("exposure bad or unset, defaulting")
|
||||
errBadAutoWhiteBalance = errors.New("auto white balance bad or unset, defaulting")
|
||||
errBadQuantization = errors.New("quantization bad or unset, defaulting")
|
||||
)
|
||||
|
||||
// Possible modes for raspivid --exposure parameter.
|
||||
var ExposureModes = [...]string{
|
||||
"auto",
|
||||
"night",
|
||||
"nightpreview",
|
||||
"backlight",
|
||||
"spotlight",
|
||||
"sports",
|
||||
"snow",
|
||||
"beach",
|
||||
"verylong",
|
||||
"fixedfps",
|
||||
"antishake",
|
||||
"fireworks",
|
||||
}
|
||||
|
||||
// Possible modes for raspivid --awb parameter.
|
||||
var AutoWhiteBalanceModes = [...]string{
|
||||
"off",
|
||||
"auto",
|
||||
"sun",
|
||||
"cloud",
|
||||
"shade",
|
||||
"tungsten",
|
||||
"fluorescent",
|
||||
"incandescent",
|
||||
"flash",
|
||||
"horizon",
|
||||
}
|
||||
|
||||
// Raspivid is an implementation of AVDevice that provides control over the
|
||||
// raspivid command to allow reading of data from a Raspberry Pi camera.
|
||||
type Raspivid struct {
|
||||
cfg config.Config
|
||||
cmd *exec.Cmd
|
||||
out io.ReadCloser
|
||||
log config.Logger
|
||||
}
|
||||
|
||||
// NewRaspivid returns a new Raspivid.
|
||||
func NewRaspivid(l config.Logger) *Raspivid { return &Raspivid{log: l} }
|
||||
|
||||
// Set will take a Config struct, check the validity of the relevant fields
|
||||
// and then performs any configuration necessary. If fields are not valid,
|
||||
// an error is added to the multiError and a default value is used.
|
||||
func (r *Raspivid) Set(c config.Config) error {
|
||||
var errs device.MultiError
|
||||
switch c.InputCodec {
|
||||
case codecutil.H264, codecutil.MJPEG:
|
||||
default:
|
||||
c.InputCodec = defaultRaspividCodec
|
||||
errs = append(errs, errBadCodec)
|
||||
}
|
||||
|
||||
if c.Rotation > 359 {
|
||||
c.Rotation = defaultRaspividRotation
|
||||
errs = append(errs, errBadRotation)
|
||||
}
|
||||
|
||||
if c.Width == 0 {
|
||||
c.Width = defaultRaspividWidth
|
||||
errs = append(errs, errBadWidth)
|
||||
}
|
||||
|
||||
if c.Height == 0 {
|
||||
c.Height = defaultRaspividHeight
|
||||
errs = append(errs, errBadHeight)
|
||||
}
|
||||
|
||||
if c.FrameRate == 0 {
|
||||
c.FrameRate = defaultRaspividFramerate
|
||||
errs = append(errs, errBadFrameRate)
|
||||
}
|
||||
|
||||
if c.VBR {
|
||||
c.Bitrate = 0
|
||||
if c.Quantization < 10 || c.Quantization > 40 {
|
||||
errs = append(errs, errBadQuantization)
|
||||
c.Quantization = defaultRaspividQuantization
|
||||
}
|
||||
} else {
|
||||
c.Quantization = 0
|
||||
if c.Bitrate <= 0 {
|
||||
errs = append(errs, errBadBitrate)
|
||||
c.Bitrate = defaultRaspividBitrate
|
||||
}
|
||||
}
|
||||
|
||||
if c.MinFrames <= 0 {
|
||||
errs = append(errs, errBadMinFrames)
|
||||
c.MinFrames = defaultRaspividMinFrames
|
||||
}
|
||||
|
||||
if c.Brightness <= 0 || c.Brightness > 100 {
|
||||
errs = append(errs, errBadBrightness)
|
||||
c.Brightness = defaultRaspividBrightness
|
||||
}
|
||||
|
||||
if c.Saturation < -100 || c.Saturation > 100 {
|
||||
errs = append(errs, errBadSaturation)
|
||||
c.Saturation = defaultRaspividSaturation
|
||||
}
|
||||
|
||||
if c.Exposure == "" || !stringInSlice(c.Exposure, config.ExposureModes[:]) {
|
||||
errs = append(errs, errBadExposure)
|
||||
c.Exposure = defaultRaspividExposure
|
||||
}
|
||||
|
||||
if c.AutoWhiteBalance == "" || !stringInSlice(c.AutoWhiteBalance, AutoWhiteBalanceModes[:]) {
|
||||
errs = append(errs, errBadAutoWhiteBalance)
|
||||
c.AutoWhiteBalance = defaultRaspividAutoWhiteBalance
|
||||
}
|
||||
|
||||
r.cfg = c
|
||||
return errs
|
||||
}
|
||||
|
||||
// Start will prepare the arguments for the raspivid command using the
|
||||
// configuration set using the Set method then call the raspivid command,
|
||||
// piping the video output from which the Read method will read from.
|
||||
func (r *Raspivid) Start() error {
|
||||
const disabled = "0"
|
||||
args := []string{
|
||||
"--output", "-",
|
||||
"--nopreview",
|
||||
"--timeout", disabled,
|
||||
"--width", fmt.Sprint(r.cfg.Width),
|
||||
"--height", fmt.Sprint(r.cfg.Height),
|
||||
"--bitrate", fmt.Sprint(r.cfg.Bitrate * 1000), // Convert from kbps to bps.
|
||||
"--framerate", fmt.Sprint(r.cfg.FrameRate),
|
||||
"--rotation", fmt.Sprint(r.cfg.Rotation),
|
||||
"--brightness", fmt.Sprint(r.cfg.Brightness),
|
||||
"--saturation", fmt.Sprint(r.cfg.Saturation),
|
||||
"--exposure", fmt.Sprint(r.cfg.Exposure),
|
||||
"--awb", fmt.Sprint(r.cfg.AutoWhiteBalance),
|
||||
}
|
||||
|
||||
if r.cfg.FlipHorizontal {
|
||||
args = append(args, "--hflip")
|
||||
}
|
||||
|
||||
if r.cfg.FlipVertical {
|
||||
args = append(args, "--vflip")
|
||||
}
|
||||
if r.cfg.FlipHorizontal {
|
||||
args = append(args, "--hflip")
|
||||
}
|
||||
|
||||
switch r.cfg.InputCodec {
|
||||
default:
|
||||
return fmt.Errorf("revid: invalid input codec: %v", r.cfg.InputCodec)
|
||||
case codecutil.H264:
|
||||
args = append(args,
|
||||
"--codec", "H264",
|
||||
"--inline",
|
||||
"--intra", fmt.Sprint(r.cfg.MinFrames),
|
||||
)
|
||||
if r.cfg.VBR {
|
||||
args = append(args, "-qp", fmt.Sprint(r.cfg.Quantization))
|
||||
}
|
||||
case codecutil.MJPEG:
|
||||
args = append(args, "--codec", "MJPEG")
|
||||
}
|
||||
r.cfg.Logger.Log(logger.Info, pkg+"raspivid args", "raspividArgs", strings.Join(args, " "))
|
||||
r.cmd = exec.Command("raspivid", args...)
|
||||
|
||||
var err error
|
||||
r.out, err = r.cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not pipe command output: %w", err)
|
||||
}
|
||||
|
||||
err = r.cmd.Start()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not start raspivid command: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Read implements io.Reader. Calling read before Start has been called will
|
||||
// result in 0 bytes read and an error.
|
||||
func (r *Raspivid) Read(p []byte) (int, error) {
|
||||
if r.out != nil {
|
||||
return r.out.Read(p)
|
||||
}
|
||||
return 0, errors.New("cannot read, raspivid has not started")
|
||||
}
|
||||
|
||||
// Stop will terminate the raspivid process and close the output pipe.
|
||||
func (r *Raspivid) Stop() error {
|
||||
if r.cmd == nil || r.cmd.Process == nil {
|
||||
return errors.New("raspivid process was never started")
|
||||
}
|
||||
err := r.cmd.Process.Kill()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not kill raspivid process: %w", err)
|
||||
}
|
||||
return r.out.Close()
|
||||
}
|
||||
|
||||
// stringInSlice returns true if want is in slice.
|
||||
func stringInSlice(want string, slice []string) bool {
|
||||
for _, s := range slice {
|
||||
if s == want {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
DESCRIPTION
|
||||
webcam.go provides an implementation of AVDevice for webcams.
|
||||
|
||||
AUTHORS
|
||||
Saxon A. Nelson-Milton <saxon@ausocean.org>
|
||||
|
||||
LICENSE
|
||||
Copyright (C) 2019 the Australian Ocean Lab (AusOcean)
|
||||
|
||||
It is free software: you can redistribute it and/or modify them
|
||||
under the terms of the GNU General Public License as published by the
|
||||
Free Software Foundation, either version 3 of the License, or (at your
|
||||
option) any later version.
|
||||
|
||||
It is distributed in the hope that it will be useful, but WITHOUT
|
||||
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
in gpl.txt. If not, see http://www.gnu.org/licenses.
|
||||
*/
|
||||
|
||||
package webcam
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"bitbucket.org/ausocean/av/device"
|
||||
"bitbucket.org/ausocean/av/revid/config"
|
||||
"bitbucket.org/ausocean/utils/logger"
|
||||
)
|
||||
|
||||
// Used to indicate package in logging.
|
||||
const pkg = "webcam: "
|
||||
|
||||
// Configuration defaults.
|
||||
const (
|
||||
defaultInputPath = "/dev/video0"
|
||||
defaultFrameRate = 25
|
||||
defaultBitrate = 400
|
||||
defaultWidth = 1280
|
||||
defaultHeight = 720
|
||||
)
|
||||
|
||||
// Configuration field errors.
|
||||
var (
|
||||
errBadFrameRate = errors.New("frame rate bad or unset, defaulting")
|
||||
errBadBitrate = errors.New("bitrate bad or unset, defaulting")
|
||||
errBadWidth = errors.New("width bad or unset, defaulting")
|
||||
errBadHeight = errors.New("height bad or unset, defaulting")
|
||||
)
|
||||
|
||||
// Webcam is an implementation of the AVDevice interface for a Webcam. Webcam
|
||||
// uses an ffmpeg process to pipe the video data from the webcam.
|
||||
type Webcam struct {
|
||||
out io.ReadCloser
|
||||
log config.Logger
|
||||
cfg config.Config
|
||||
cmd *exec.Cmd
|
||||
}
|
||||
|
||||
// NewWebcam returns a new Webcam.
|
||||
func NewWebcam(l config.Logger) *Webcam {
|
||||
return &Webcam{log: l}
|
||||
}
|
||||
|
||||
// Set will validate the relevant fields of the given Config struct and assign
|
||||
// the struct to the Webcam's Config. If fields are not valid, an error is
|
||||
// added to the multiError and a default value is used.
|
||||
func (w *Webcam) Set(c config.Config) error {
|
||||
var errs device.MultiError
|
||||
if c.Width == 0 {
|
||||
errs = append(errs, errBadWidth)
|
||||
c.Width = defaultWidth
|
||||
}
|
||||
|
||||
if c.Height == 0 {
|
||||
errs = append(errs, errBadHeight)
|
||||
c.Height = defaultHeight
|
||||
}
|
||||
|
||||
if c.FrameRate == 0 {
|
||||
errs = append(errs, errBadFrameRate)
|
||||
c.FrameRate = defaultFrameRate
|
||||
}
|
||||
|
||||
if c.Bitrate <= 0 {
|
||||
errs = append(errs, errBadBitrate)
|
||||
c.Bitrate = defaultBitrate
|
||||
}
|
||||
w.cfg = c
|
||||
return errs
|
||||
}
|
||||
|
||||
// Start will build the required arguments for ffmpeg and then execute the
|
||||
// command, piping video output where we can read using the Read method.
|
||||
func (w *Webcam) Start() error {
|
||||
args := []string{
|
||||
"-i", w.cfg.InputPath,
|
||||
"-f", "h264",
|
||||
"-r", fmt.Sprint(w.cfg.FrameRate),
|
||||
}
|
||||
|
||||
br := w.cfg.Bitrate * 1000
|
||||
args = append(args,
|
||||
"-b:v", fmt.Sprint(br),
|
||||
"-maxrate", fmt.Sprint(br),
|
||||
"-bufsize", fmt.Sprint(br/2),
|
||||
"-s", fmt.Sprintf("%dx%d", w.cfg.Width, w.cfg.Height),
|
||||
"-",
|
||||
)
|
||||
|
||||
w.log.Log(logger.Info, pkg+"ffmpeg args", "args", strings.Join(args, " "))
|
||||
w.cmd = exec.Command("ffmpeg", args...)
|
||||
|
||||
var err error
|
||||
w.out, err = w.cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create pipe: %w", err)
|
||||
}
|
||||
|
||||
err = w.cmd.Start()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start ffmpeg: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop will kill the ffmpeg process and close the output pipe.
|
||||
func (w *Webcam) Stop() error {
|
||||
if w.cmd == nil || w.cmd.Process == nil {
|
||||
return errors.New("ffmpeg process was never started")
|
||||
}
|
||||
err := w.cmd.Process.Kill()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not kill ffmpeg process: %w", err)
|
||||
}
|
||||
return w.out.Close()
|
||||
}
|
||||
|
||||
// Read implements io.Reader. If the pipe is nil a read error is returned.
|
||||
func (w *Webcam) Read(p []byte) (int, error) {
|
||||
if w.out != nil {
|
||||
return w.out.Read(p)
|
||||
}
|
||||
return 0, errors.New("webcam not streaming")
|
||||
}
|
5
go.mod
5
go.mod
|
@ -4,15 +4,16 @@ go 1.13
|
|||
|
||||
require (
|
||||
bitbucket.org/ausocean/iot v1.2.7
|
||||
bitbucket.org/ausocean/test v0.0.0-20190821085226-7a524f2344ba
|
||||
bitbucket.org/ausocean/test v0.0.0-20190821085226-7a524f2344ba // indirect
|
||||
bitbucket.org/ausocean/utils v1.2.10
|
||||
github.com/BurntSushi/toml v0.3.1 // indirect
|
||||
github.com/Comcast/gots v0.0.0-20190305015453-8d56e473f0f7
|
||||
github.com/go-audio/audio v0.0.0-20181013203223-7b2a6ca21480
|
||||
github.com/go-audio/wav v0.0.0-20181013172942-de841e69b884
|
||||
github.com/go-delve/delve v1.3.2
|
||||
github.com/mewkiz/flac v1.0.5
|
||||
github.com/pkg/errors v0.8.1
|
||||
github.com/yobert/alsa v0.0.0-20180630182551-d38d89fa843e
|
||||
gocv.io/x/gocv v0.21.0
|
||||
gocv.io/x/gocv v0.21.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.2.2 // indirect
|
||||
)
|
||||
|
|
49
go.sum
49
go.sum
|
@ -1,17 +1,6 @@
|
|||
bitbucket.org/ausocean/av v0.0.0-20190416003121-6ee286e98874/go.mod h1:DxZEprrNNQ2slHKAQVUHryDaWc5CbjxyHAvomhzg+AE=
|
||||
bitbucket.org/ausocean/av/input/gvctrl v0.0.0-20191017223116-ce6c12cce8cd h1:L99pvZZtdy3v54ym6GswYi8SOGgz+4Tr8hiIOI+nSiQ=
|
||||
bitbucket.org/ausocean/av/input/gvctrl v0.0.0-20191017223116-ce6c12cce8cd/go.mod h1:Hg522DOVaj23J7CIxknCxmNsLGdg1iZ+Td1FDcTOdLQ=
|
||||
bitbucket.org/ausocean/iot v1.2.4/go.mod h1:5HVLgPHccW2PxS7WDUQO6sKWMgk3Vfze/7d5bHs8EWU=
|
||||
bitbucket.org/ausocean/iot v1.2.6 h1:KAAY1KZDbyOpoKajT1dM8BawupHiW9hUOelseSV1Ptc=
|
||||
bitbucket.org/ausocean/iot v1.2.6/go.mod h1:71AYHh8yGZ8XyzDBskwIWMF+8E8ORagXpXE24wlhoE0=
|
||||
bitbucket.org/ausocean/iot v1.2.7 h1:dZgrmVtuXnzHgybDthn0bYgAJms9euTONXBsqsx9g5M=
|
||||
bitbucket.org/ausocean/iot v1.2.7/go.mod h1:aAWgPo2f8sD2OPmxae1E5/iD9+tKY/iW4pcQMQXUvHM=
|
||||
bitbucket.org/ausocean/test v0.0.0-20190821085226-7a524f2344ba/go.mod h1:MbKtu9Pu8l3hiVGX6ep8S1VwAVY5uCbifCFOYsm914w=
|
||||
bitbucket.org/ausocean/utils v0.0.0-20190408050157-66d3b4d4041e/go.mod h1:uXzX9z3PLemyURTMWRhVI8uLhPX4uuvaaO85v2hcob8=
|
||||
bitbucket.org/ausocean/utils v1.2.6/go.mod h1:uXzX9z3PLemyURTMWRhVI8uLhPX4uuvaaO85v2hcob8=
|
||||
bitbucket.org/ausocean/utils v1.2.8 h1:hyxAIqYBqjqCguG+6A/kKyrAihyeUt2LziZg6CH0gLU=
|
||||
bitbucket.org/ausocean/utils v1.2.8/go.mod h1:uXzX9z3PLemyURTMWRhVI8uLhPX4uuvaaO85v2hcob8=
|
||||
bitbucket.org/ausocean/utils v1.2.9 h1:g45C6KCNvCLOGFv+ZnmDbQOOdnwpIsvzuNOD141CTVI=
|
||||
bitbucket.org/ausocean/utils v1.2.9/go.mod h1:uXzX9z3PLemyURTMWRhVI8uLhPX4uuvaaO85v2hcob8=
|
||||
bitbucket.org/ausocean/utils v1.2.10 h1:JTS7n+K+0o/FQFWKjdGgA1ElZ4TQu9aHX3wTJXOayXw=
|
||||
bitbucket.org/ausocean/utils v1.2.10/go.mod h1:uXzX9z3PLemyURTMWRhVI8uLhPX4uuvaaO85v2hcob8=
|
||||
|
@ -24,28 +13,50 @@ github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMx
|
|||
github.com/adrianmo/go-nmea v1.1.1-0.20190109062325-c448653979f7/go.mod h1:HHPxPAm2kmev+61qmkZh7xgZF/7qHtSpsWppip2Ipv8=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||
github.com/cosiner/argv v0.0.0-20170225145430-13bacc38a0a5/go.mod h1:p/NrK5tF6ICIly4qwEDsf6VDirFiWWz0FenfYBwJaKQ=
|
||||
github.com/cpuguy83/go-md2man v1.0.8/go.mod h1:N6JayAiVKtlHSnuTCeuLSQVs75hb8q+dYQLjr7cDsKY=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/go-audio/aiff v0.0.0-20180403003018-6c3a8a6aff12/go.mod h1:AMSAp6W1zd0koOdX6QDgGIuBDTUvLa2SLQtm7d9eM3c=
|
||||
github.com/go-audio/audio v0.0.0-20180206231410-b697a35b5608/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs=
|
||||
github.com/go-audio/audio v0.0.0-20181013203223-7b2a6ca21480 h1:4sGU+UABMMsRJyD+Y2yzMYxq0GJFUsRRESI0P1gZ2ig=
|
||||
github.com/go-audio/audio v0.0.0-20181013203223-7b2a6ca21480/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs=
|
||||
github.com/go-audio/wav v0.0.0-20181013172942-de841e69b884 h1:2TaXIaVA4ff/MHHezOj83tCypALTFAcXOImcFWNa3jw=
|
||||
github.com/go-audio/wav v0.0.0-20181013172942-de841e69b884/go.mod h1:UiqzUyfX0zs3pJ/DPyvS5v8sN6s5bXPUDDIVA5v8dks=
|
||||
github.com/go-delve/delve v1.3.2 h1:K8VjV+Q2YnBYlPq0ctjrvc9h7h03wXszlszzfGW5Tog=
|
||||
github.com/go-delve/delve v1.3.2/go.mod h1:LLw6qJfIsRK9WcwV2IRRqsdlgrqzOeuGrQOCOIhDpt8=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jacobsa/go-serial v0.0.0-20180131005756-15cf729a72d4/go.mod h1:2RvX5ZjVtsznNZPEt4xwJXNJrM3VTZoQf7V6gk0ysvs=
|
||||
github.com/kidoman/embd v0.0.0-20170508013040-d3d8c0c5c68d/go.mod h1:ACKj9jnzOzj1lw2ETilpFGK7L9dtJhAzT7T1OhAGtRQ=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/mattetti/audio v0.0.0-20180912171649-01576cde1f21 h1:Hc1iKlyxNHp3CV59G2E/qabUkHvEwOIJxDK0CJ7CRjA=
|
||||
github.com/mattetti/audio v0.0.0-20180912171649-01576cde1f21/go.mod h1:LlQmBGkOuV/SKzEDXBPKauvN2UqCgzXO2XjecTGj40s=
|
||||
github.com/mattn/go-colorable v0.0.0-20170327083344-ded68f7a9561/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mewkiz/flac v1.0.5 h1:dHGW/2kf+/KZ2GGqSVayNEhL9pluKn/rr/h/QqD9Ogc=
|
||||
github.com/mewkiz/flac v1.0.5/go.mod h1:EHZNU32dMF6alpurYyKHDLYpW1lYpBZ5WrXi/VuNIGs=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/peterh/liner v0.0.0-20170317030525-88609521dc4b/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/profile v0.0.0-20170413231811-06b906832ed0/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/russross/blackfriday v0.0.0-20180428102519-11635eb403ff/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/sirupsen/logrus v0.0.0-20180523074243-ea8897e79973/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||
github.com/spf13/cobra v0.0.0-20170417170307-b6cb39589372/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/pflag v0.0.0-20170417173400-9e4c21054fa1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
|
@ -53,6 +64,7 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
|
|||
github.com/yobert/alsa v0.0.0-20180630182551-d38d89fa843e h1:3NIzz7weXhh3NToPgbtlQtKiVgerEaG4/nY2skGoGG0=
|
||||
github.com/yobert/alsa v0.0.0-20180630182551-d38d89fa843e/go.mod h1:CaowXBWOiSGWEpBBV8LoVnQTVPV4ycyviC9IBLj8dRw=
|
||||
github.com/yryz/ds18b20 v0.0.0-20180211073435-3cf383a40624/go.mod h1:MqFju5qeLDFh+S9PqxYT7TEla8xeW7bgGr/69q3oki0=
|
||||
go.starlark.net v0.0.0-20190702223751-32f345186213/go.mod h1:c1/X6cHgvdXj6pUlmWKMkuqRnW4K8x2vwt6JAaaircg=
|
||||
go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
|
||||
|
@ -60,12 +72,25 @@ go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/
|
|||
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
gocv.io/x/gocv v0.21.0 h1:dVjagrupZrfCRY0qPEaYWgoNMRpBel6GYDH4mvQOK8Y=
|
||||
gocv.io/x/gocv v0.21.0/go.mod h1:Rar2PS6DV+T4FL+PM535EImD/h13hGVaHhnCu1xarBs=
|
||||
golang.org/x/arch v0.0.0-20171004143515-077ac972c2e4/go.mod h1:cYlCBUl1MsqxdiKgmc4uh7TxZfWSFLOGSRR090WDxt8=
|
||||
golang.org/x/crypto v0.0.0-20180614174826-fd5f17ee7299/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190305064518-30e92a19ae4a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/tools v0.0.0-20181120060634-fc4f04983f62/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
|
|
@ -34,40 +34,40 @@ import (
|
|||
func (r *Revid) startAudioDevice() (func() error, error) {
|
||||
// Create audio device.
|
||||
ac := &audio.Config{
|
||||
SampleRate: r.config.SampleRate,
|
||||
Channels: r.config.Channels,
|
||||
RecPeriod: r.config.RecPeriod,
|
||||
BitDepth: r.config.BitDepth,
|
||||
Codec: r.config.InputCodec,
|
||||
SampleRate: r.cfg.SampleRate,
|
||||
Channels: r.cfg.Channels,
|
||||
RecPeriod: r.cfg.RecPeriod,
|
||||
BitDepth: r.cfg.BitDepth,
|
||||
Codec: r.cfg.InputCodec,
|
||||
}
|
||||
mts.Meta.Add("sampleRate", strconv.Itoa(r.config.SampleRate))
|
||||
mts.Meta.Add("channels", strconv.Itoa(r.config.Channels))
|
||||
mts.Meta.Add("period", fmt.Sprintf("%.6f", r.config.RecPeriod))
|
||||
mts.Meta.Add("bitDepth", strconv.Itoa(r.config.BitDepth))
|
||||
switch r.config.InputCodec {
|
||||
mts.Meta.Add("sampleRate", strconv.Itoa(r.cfg.SampleRate))
|
||||
mts.Meta.Add("channels", strconv.Itoa(r.cfg.Channels))
|
||||
mts.Meta.Add("period", fmt.Sprintf("%.6f", r.cfg.RecPeriod))
|
||||
mts.Meta.Add("bitDepth", strconv.Itoa(r.cfg.BitDepth))
|
||||
switch r.cfg.InputCodec {
|
||||
case codecutil.PCM:
|
||||
mts.Meta.Add("codec", "pcm")
|
||||
case codecutil.ADPCM:
|
||||
mts.Meta.Add("codec", "adpcm")
|
||||
default:
|
||||
r.config.Logger.Log(logger.Fatal, pkg+"no audio codec set in config")
|
||||
r.cfg.Logger.Log(logger.Fatal, pkg+"no audio codec set in config")
|
||||
}
|
||||
|
||||
ai, err := audio.NewDevice(ac, r.config.Logger)
|
||||
ai, err := audio.NewDevice(ac, r.cfg.Logger)
|
||||
if err != nil {
|
||||
r.config.Logger.Log(logger.Fatal, pkg+"failed to create audio device", "error", err.Error())
|
||||
r.cfg.Logger.Log(logger.Fatal, pkg+"failed to create audio device", "error", err.Error())
|
||||
}
|
||||
|
||||
// Start audio device
|
||||
err = ai.Start()
|
||||
if err != nil {
|
||||
r.config.Logger.Log(logger.Fatal, pkg+"failed to start audio device", "error", err.Error())
|
||||
r.cfg.Logger.Log(logger.Fatal, pkg+"failed to start audio device", "error", err.Error())
|
||||
}
|
||||
|
||||
// Process output from audio device.
|
||||
r.config.ChunkSize = ai.ChunkSize()
|
||||
r.cfg.ChunkSize = ai.ChunkSize()
|
||||
r.wg.Add(1)
|
||||
go r.processFrom(ai, time.Duration(float64(time.Second)/r.config.WriteRate))
|
||||
go r.processFrom(ai, time.Duration(float64(time.Second)/r.cfg.WriteRate))
|
||||
return func() error {
|
||||
ai.Stop()
|
||||
return nil
|
||||
|
|
|
@ -23,7 +23,7 @@ LICENSE
|
|||
along with revid in gpl.txt. If not, see http://www.gnu.org/licenses.
|
||||
*/
|
||||
|
||||
package revid
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
@ -63,17 +63,24 @@ var AutoWhiteBalanceModes = [...]string{
|
|||
"horizon",
|
||||
}
|
||||
|
||||
const pkg = "config: "
|
||||
|
||||
type Logger interface {
|
||||
SetLevel(int8)
|
||||
Log(level int8, message string, params ...interface{})
|
||||
}
|
||||
|
||||
// quality represents video quality.
|
||||
type quality int
|
||||
type Quality int
|
||||
|
||||
// The different video qualities that can be used for variable bitrate when
|
||||
// using the GeoVision camera.
|
||||
const (
|
||||
qualityStandard quality = iota
|
||||
qualityFair
|
||||
qualityGood
|
||||
qualityGreat
|
||||
qualityExcellent
|
||||
QualityStandard Quality = iota
|
||||
QualityFair
|
||||
QualityGood
|
||||
QualityGreat
|
||||
QualityExcellent
|
||||
)
|
||||
|
||||
// Enums to define inputs, outputs and codecs.
|
||||
|
@ -82,19 +89,19 @@ const (
|
|||
NothingDefined = iota
|
||||
|
||||
// Input/Output.
|
||||
File
|
||||
|
||||
// Inputs.
|
||||
Raspivid
|
||||
V4L
|
||||
RTSP
|
||||
Audio
|
||||
InputFile
|
||||
InputRaspivid
|
||||
InputV4L
|
||||
InputRTSP
|
||||
InputAudio
|
||||
|
||||
// Outputs.
|
||||
RTMP
|
||||
RTP
|
||||
HTTP
|
||||
MPEGTS
|
||||
OutputAudio
|
||||
OutputRTMP
|
||||
OutputRTP
|
||||
OutputHTTP
|
||||
OutputMPEGTS
|
||||
OutputFile
|
||||
|
||||
// Codecs.
|
||||
H264
|
||||
|
@ -105,8 +112,8 @@ const (
|
|||
// Default config settings
|
||||
const (
|
||||
// General revid defaults.
|
||||
defaultInput = Raspivid
|
||||
defaultOutput = HTTP
|
||||
defaultInput = InputRaspivid
|
||||
defaultOutput = OutputHTTP
|
||||
defaultFrameRate = 25
|
||||
defaultWriteRate = 25
|
||||
defaultTimeout = 0
|
||||
|
@ -115,7 +122,7 @@ const (
|
|||
defaultRtpAddr = "localhost:6970"
|
||||
defaultCameraIP = "192.168.1.50"
|
||||
defaultVBR = false
|
||||
defaultVBRQuality = qualityStandard
|
||||
defaultVBRQuality = QualityStandard
|
||||
defaultBurstPeriod = 10 // Seconds
|
||||
defaultVBRBitrate = 500 // kbps
|
||||
defaultCameraChan = 2
|
||||
|
@ -162,13 +169,13 @@ type Config struct {
|
|||
// Input defines the input data source.
|
||||
//
|
||||
// Valid values are defined by enums:
|
||||
// Raspivid:
|
||||
// InputRaspivid:
|
||||
// Read data from a Raspberry Pi Camera.
|
||||
// V4l:
|
||||
// InputV4l:
|
||||
// Read from webcam.
|
||||
// File:
|
||||
// InputFile:
|
||||
// Location must be specified in InputPath field.
|
||||
// RTSP:
|
||||
// InputRTSP:
|
||||
// CameraIP should also be defined.
|
||||
Input uint8
|
||||
|
||||
|
@ -180,16 +187,16 @@ type Config struct {
|
|||
// Outputs define the outputs we wish to output data too.
|
||||
//
|
||||
// Valid outputs are defined by enums:
|
||||
// File:
|
||||
// OutputFile:
|
||||
// Location must be defined by the OutputPath field. MPEG-TS packetization
|
||||
// is used.
|
||||
// HTTP:
|
||||
// OutputHTTP:
|
||||
// Destination is defined by the sh field located in /etc/netsender.conf.
|
||||
// MPEGT-TS packetization is used.
|
||||
// RTMP:
|
||||
// OutputRTMP:
|
||||
// Destination URL must be defined in the RtmpUrl field. FLV packetization
|
||||
// is used.
|
||||
// RTP:
|
||||
// OutputRTP:
|
||||
// Destination is defined by RtpAddr field, otherwise it will default to
|
||||
// localhost:6970. MPEGT-TS packetization is used.
|
||||
Outputs []uint8
|
||||
|
@ -236,7 +243,7 @@ type Config struct {
|
|||
// VBRQuality describes the general quality of video from the GeoVision camera
|
||||
// under variable bitrate. VBRQuality can be one 5 consts defined:
|
||||
// qualityStandard, qualityFair, qualityGood, qualityGreat and qualityExcellent.
|
||||
VBRQuality quality
|
||||
VBRQuality Quality
|
||||
|
||||
// VBRBitrate describes maximal bitrate for the GeoVision camera when under
|
||||
// variable bitrate.
|
||||
|
@ -327,7 +334,7 @@ func (c *Config) Validate() error {
|
|||
}
|
||||
|
||||
switch c.Input {
|
||||
case Raspivid, V4L, File, Audio, RTSP:
|
||||
case InputRaspivid, InputV4L, InputFile, InputAudio, InputRTSP:
|
||||
case NothingDefined:
|
||||
c.Logger.Log(logger.Info, pkg+"no input type defined, defaulting", "input", defaultInput)
|
||||
c.Input = defaultInput
|
||||
|
@ -339,7 +346,7 @@ func (c *Config) Validate() error {
|
|||
case codecutil.H264, codecutil.MJPEG, codecutil.PCM, codecutil.ADPCM:
|
||||
default:
|
||||
switch c.Input {
|
||||
case Audio:
|
||||
case OutputAudio:
|
||||
c.Logger.Log(logger.Info, pkg+"input is audio but no codec defined, defaulting", "inputCodec", defaultAudioInputCodec)
|
||||
c.InputCodec = defaultAudioInputCodec
|
||||
default:
|
||||
|
@ -358,8 +365,8 @@ func (c *Config) Validate() error {
|
|||
var haveRTMPOut bool
|
||||
for i, o := range c.Outputs {
|
||||
switch o {
|
||||
case File:
|
||||
case RTMP:
|
||||
case OutputFile:
|
||||
case OutputRTMP:
|
||||
haveRTMPOut = true
|
||||
if c.Bitrate == 0 {
|
||||
c.Bitrate = defaultBitrate
|
||||
|
@ -367,11 +374,11 @@ func (c *Config) Validate() error {
|
|||
c.Quantization = 0
|
||||
if c.RTMPURL == "" {
|
||||
c.Logger.Log(logger.Info, pkg+"no RTMP URL: falling back to HTTP")
|
||||
c.Outputs[i] = HTTP
|
||||
c.Outputs[i] = OutputHTTP
|
||||
haveRTMPOut = false
|
||||
}
|
||||
fallthrough
|
||||
case HTTP, RTP:
|
||||
case OutputHTTP, OutputRTP:
|
||||
if !haveRTMPOut {
|
||||
c.Bitrate = 0
|
||||
if c.Quantization == 0 {
|
||||
|
@ -518,7 +525,7 @@ func (c *Config) Validate() error {
|
|||
}
|
||||
|
||||
switch c.VBRQuality {
|
||||
case qualityStandard, qualityFair, qualityGood, qualityGreat, qualityExcellent:
|
||||
case QualityStandard, QualityFair, QualityGood, QualityGreat, QualityExcellent:
|
||||
default:
|
||||
c.Logger.Log(logger.Info, pkg+"VBRQuality bad or unset, defaulting", "VBRQuality", defaultVBRQuality)
|
||||
c.VBRQuality = defaultVBRQuality
|
|
@ -22,7 +22,7 @@ LICENSE
|
|||
in gpl.txt. If not, see http://www.gnu.org/licenses.
|
||||
*/
|
||||
|
||||
package revid
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
@ -41,7 +41,7 @@ func TestValidate(t *testing.T) {
|
|||
err error
|
||||
}{
|
||||
{
|
||||
in: Config{Outputs: []uint8{HTTP}, Logger: &dumbLogger{}},
|
||||
in: Config{Outputs: []uint8{OutputHTTP}, Logger: &dumbLogger{}},
|
||||
check: func(c Config) error {
|
||||
if c.Bitrate != 0 {
|
||||
return fmt.Errorf("did not get expected bitrate. Got: %v, Want: %v", c.Bitrate, 0)
|
||||
|
@ -53,7 +53,7 @@ func TestValidate(t *testing.T) {
|
|||
},
|
||||
},
|
||||
{
|
||||
in: Config{Outputs: []uint8{RTMP}, RTMPURL: "dummURL", Logger: &dumbLogger{}},
|
||||
in: Config{Outputs: []uint8{OutputRTMP}, RTMPURL: "dummURL", Logger: &dumbLogger{}},
|
||||
check: func(c Config) error {
|
||||
if c.Bitrate != defaultBitrate {
|
||||
return fmt.Errorf("did not get expected bitrate. Got: %v, Want: %v", c.Bitrate, defaultBitrate)
|
||||
|
@ -65,12 +65,12 @@ func TestValidate(t *testing.T) {
|
|||
},
|
||||
},
|
||||
{
|
||||
in: Config{Outputs: []uint8{HTTP}, Quantization: 50, Logger: &dumbLogger{}},
|
||||
in: Config{Outputs: []uint8{OutputHTTP}, Quantization: 50, Logger: &dumbLogger{}},
|
||||
check: func(c Config) error { return nil },
|
||||
err: errInvalidQuantization,
|
||||
},
|
||||
{
|
||||
in: Config{Outputs: []uint8{HTTP}, Quantization: 20, Logger: &dumbLogger{}},
|
||||
in: Config{Outputs: []uint8{OutputHTTP}, Quantization: 20, Logger: &dumbLogger{}},
|
||||
check: func(c Config) error {
|
||||
if c.Bitrate != 0 {
|
||||
return fmt.Errorf("did not get expected bitrate. Got: %v, Want: %v", c.Bitrate, 0)
|
140
revid/inputs.go
140
revid/inputs.go
|
@ -40,10 +40,11 @@ import (
|
|||
"time"
|
||||
|
||||
"bitbucket.org/ausocean/av/codec/codecutil"
|
||||
"bitbucket.org/ausocean/av/input/gvctrl"
|
||||
"bitbucket.org/ausocean/av/device/geovision/gvctrl"
|
||||
"bitbucket.org/ausocean/av/protocol/rtcp"
|
||||
"bitbucket.org/ausocean/av/protocol/rtp"
|
||||
"bitbucket.org/ausocean/av/protocol/rtsp"
|
||||
"bitbucket.org/ausocean/av/revid/config"
|
||||
"bitbucket.org/ausocean/utils/logger"
|
||||
)
|
||||
|
||||
|
@ -53,54 +54,61 @@ const (
|
|||
ipCamPass = "admin"
|
||||
)
|
||||
|
||||
// Constants for real time clients.
|
||||
const (
|
||||
rtpPort = 60000
|
||||
rtcpPort = 60001
|
||||
defaultServerRTCPPort = 17301
|
||||
)
|
||||
|
||||
// startRaspivid sets up things for input from raspivid i.e. starts
|
||||
// a raspivid process and pipes it's data output.
|
||||
func (r *Revid) startRaspivid() (func() error, error) {
|
||||
r.config.Logger.Log(logger.Info, pkg+"starting raspivid")
|
||||
r.cfg.Logger.Log(logger.Info, pkg+"starting raspivid")
|
||||
|
||||
const disabled = "0"
|
||||
args := []string{
|
||||
"--output", "-",
|
||||
"--nopreview",
|
||||
"--timeout", disabled,
|
||||
"--width", fmt.Sprint(r.config.Width),
|
||||
"--height", fmt.Sprint(r.config.Height),
|
||||
"--bitrate", fmt.Sprint(r.config.Bitrate * 1000), // Convert from kbps to bps.
|
||||
"--framerate", fmt.Sprint(r.config.FrameRate),
|
||||
"--rotation", fmt.Sprint(r.config.Rotation),
|
||||
"--brightness", fmt.Sprint(r.config.Brightness),
|
||||
"--saturation", fmt.Sprint(r.config.Saturation),
|
||||
"--exposure", fmt.Sprint(r.config.Exposure),
|
||||
"--awb", fmt.Sprint(r.config.AutoWhiteBalance),
|
||||
"--width", fmt.Sprint(r.cfg.Width),
|
||||
"--height", fmt.Sprint(r.cfg.Height),
|
||||
"--bitrate", fmt.Sprint(r.cfg.Bitrate * 1000), // Convert from kbps to bps.
|
||||
"--framerate", fmt.Sprint(r.cfg.FrameRate),
|
||||
"--rotation", fmt.Sprint(r.cfg.Rotation),
|
||||
"--brightness", fmt.Sprint(r.cfg.Brightness),
|
||||
"--saturation", fmt.Sprint(r.cfg.Saturation),
|
||||
"--exposure", fmt.Sprint(r.cfg.Exposure),
|
||||
"--awb", fmt.Sprint(r.cfg.AutoWhiteBalance),
|
||||
}
|
||||
|
||||
if r.config.FlipHorizontal {
|
||||
if r.cfg.FlipHorizontal {
|
||||
args = append(args, "--hflip")
|
||||
}
|
||||
|
||||
if r.config.FlipVertical {
|
||||
if r.cfg.FlipVertical {
|
||||
args = append(args, "--vflip")
|
||||
}
|
||||
if r.config.FlipHorizontal {
|
||||
if r.cfg.FlipHorizontal {
|
||||
args = append(args, "--hflip")
|
||||
}
|
||||
|
||||
switch r.config.InputCodec {
|
||||
switch r.cfg.InputCodec {
|
||||
default:
|
||||
return nil, fmt.Errorf("revid: invalid input codec: %v", r.config.InputCodec)
|
||||
return nil, fmt.Errorf("revid: invalid input codec: %v", r.cfg.InputCodec)
|
||||
case codecutil.H264:
|
||||
args = append(args,
|
||||
"--codec", "H264",
|
||||
"--inline",
|
||||
"--intra", fmt.Sprint(r.config.MinFrames),
|
||||
"--intra", fmt.Sprint(r.cfg.MinFrames),
|
||||
)
|
||||
if r.config.VBR {
|
||||
args = append(args, "-qp", fmt.Sprint(r.config.Quantization))
|
||||
if r.cfg.VBR {
|
||||
args = append(args, "-qp", fmt.Sprint(r.cfg.Quantization))
|
||||
}
|
||||
case codecutil.MJPEG:
|
||||
args = append(args, "--codec", "MJPEG")
|
||||
}
|
||||
r.config.Logger.Log(logger.Info, pkg+"raspivid args", "raspividArgs", strings.Join(args, " "))
|
||||
r.cfg.Logger.Log(logger.Info, pkg+"raspivid args", "raspividArgs", strings.Join(args, " "))
|
||||
r.cmd = exec.Command("raspivid", args...)
|
||||
|
||||
stdout, err := r.cmd.StdoutPipe()
|
||||
|
@ -121,28 +129,28 @@ func (r *Revid) startRaspivid() (func() error, error) {
|
|||
func (r *Revid) startV4L() (func() error, error) {
|
||||
const defaultVideo = "/dev/video0"
|
||||
|
||||
r.config.Logger.Log(logger.Info, pkg+"starting webcam")
|
||||
if r.config.InputPath == "" {
|
||||
r.config.Logger.Log(logger.Info, pkg+"using default video device", "device", defaultVideo)
|
||||
r.config.InputPath = defaultVideo
|
||||
r.cfg.Logger.Log(logger.Info, pkg+"starting webcam")
|
||||
if r.cfg.InputPath == "" {
|
||||
r.cfg.Logger.Log(logger.Info, pkg+"using default video device", "device", defaultVideo)
|
||||
r.cfg.InputPath = defaultVideo
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"-i", r.config.InputPath,
|
||||
"-i", r.cfg.InputPath,
|
||||
"-f", "h264",
|
||||
"-r", fmt.Sprint(r.config.FrameRate),
|
||||
"-r", fmt.Sprint(r.cfg.FrameRate),
|
||||
}
|
||||
|
||||
br := r.config.Bitrate * 1000
|
||||
br := r.cfg.Bitrate * 1000
|
||||
args = append(args,
|
||||
"-b:v", fmt.Sprint(br),
|
||||
"-maxrate", fmt.Sprint(br),
|
||||
"-bufsize", fmt.Sprint(br/2),
|
||||
"-s", fmt.Sprintf("%dx%d", r.config.Width, r.config.Height),
|
||||
"-s", fmt.Sprintf("%dx%d", r.cfg.Width, r.cfg.Height),
|
||||
"-",
|
||||
)
|
||||
|
||||
r.config.Logger.Log(logger.Info, pkg+"ffmpeg args", "args", strings.Join(args, " "))
|
||||
r.cfg.Logger.Log(logger.Info, pkg+"ffmpeg args", "args", strings.Join(args, " "))
|
||||
r.cmd = exec.Command("ffmpeg", args...)
|
||||
|
||||
stdout, err := r.cmd.StdoutPipe()
|
||||
|
@ -152,7 +160,7 @@ func (r *Revid) startV4L() (func() error, error) {
|
|||
|
||||
err = r.cmd.Start()
|
||||
if err != nil {
|
||||
r.config.Logger.Log(logger.Fatal, pkg+"cannot start webcam", "error", err.Error())
|
||||
r.cfg.Logger.Log(logger.Fatal, pkg+"cannot start webcam", "error", err.Error())
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
@ -164,9 +172,9 @@ func (r *Revid) startV4L() (func() error, error) {
|
|||
// setupInputForFile sets up input from file and starts the revid.processFrom
|
||||
// routine.
|
||||
func (r *Revid) setupInputForFile() (func() error, error) {
|
||||
f, err := os.Open(r.config.InputPath)
|
||||
f, err := os.Open(r.cfg.InputPath)
|
||||
if err != nil {
|
||||
r.config.Logger.Log(logger.Error, err.Error())
|
||||
r.cfg.Logger.Log(logger.Error, err.Error())
|
||||
r.Stop()
|
||||
return nil, err
|
||||
}
|
||||
|
@ -184,92 +192,92 @@ func (r *Revid) setupInputForFile() (func() error, error) {
|
|||
// TODO(saxon): this function should really be startGeoVision. It's much too
|
||||
// specific to be called startRTSPCamera.
|
||||
func (r *Revid) startRTSPCamera() (func() error, error) {
|
||||
r.config.Logger.Log(logger.Info, pkg+"starting geovision...")
|
||||
r.cfg.Logger.Log(logger.Info, pkg+"starting geovision...")
|
||||
|
||||
err := gvctrl.Set(
|
||||
r.config.CameraIP,
|
||||
gvctrl.Channel(r.config.CameraChan),
|
||||
r.cfg.CameraIP,
|
||||
gvctrl.Channel(r.cfg.CameraChan),
|
||||
gvctrl.CodecOut(
|
||||
map[uint8]gvctrl.Codec{
|
||||
codecutil.H264: gvctrl.CodecH264,
|
||||
codecutil.H265: gvctrl.CodecH265,
|
||||
codecutil.MJPEG: gvctrl.CodecMJPEG,
|
||||
}[r.config.InputCodec],
|
||||
}[r.cfg.InputCodec],
|
||||
),
|
||||
gvctrl.Height(int(r.config.Height)),
|
||||
gvctrl.FrameRate(int(r.config.FrameRate)),
|
||||
gvctrl.VariableBitrate(r.config.VBR),
|
||||
gvctrl.Height(int(r.cfg.Height)),
|
||||
gvctrl.FrameRate(int(r.cfg.FrameRate)),
|
||||
gvctrl.VariableBitrate(r.cfg.VBR),
|
||||
gvctrl.VBRQuality(
|
||||
map[quality]gvctrl.Quality{
|
||||
qualityStandard: gvctrl.QualityStandard,
|
||||
qualityFair: gvctrl.QualityFair,
|
||||
qualityGood: gvctrl.QualityGood,
|
||||
qualityGreat: gvctrl.QualityGreat,
|
||||
qualityExcellent: gvctrl.QualityExcellent,
|
||||
}[r.config.VBRQuality],
|
||||
map[config.Quality]gvctrl.Quality{
|
||||
config.QualityStandard: gvctrl.QualityStandard,
|
||||
config.QualityFair: gvctrl.QualityFair,
|
||||
config.QualityGood: gvctrl.QualityGood,
|
||||
config.QualityGreat: gvctrl.QualityGreat,
|
||||
config.QualityExcellent: gvctrl.QualityExcellent,
|
||||
}[r.cfg.VBRQuality],
|
||||
),
|
||||
gvctrl.VBRBitrate(r.config.VBRBitrate),
|
||||
gvctrl.CBRBitrate(int(r.config.Bitrate)),
|
||||
gvctrl.Refresh(float64(r.config.MinFrames)/float64(r.config.FrameRate)),
|
||||
gvctrl.VBRBitrate(r.cfg.VBRBitrate),
|
||||
gvctrl.CBRBitrate(int(r.cfg.Bitrate)),
|
||||
gvctrl.Refresh(float64(r.cfg.MinFrames)/float64(r.cfg.FrameRate)),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not set IPCamera settings: %w", err)
|
||||
}
|
||||
|
||||
r.config.Logger.Log(logger.Info, pkg+"completed geovision configuration")
|
||||
r.cfg.Logger.Log(logger.Info, pkg+"completed geovision configuration")
|
||||
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
rtspClt, local, remote, err := rtsp.NewClient("rtsp://" + ipCamUser + ":" + ipCamPass + "@" + r.config.CameraIP + ":8554/" + "CH002.sdp")
|
||||
rtspClt, local, remote, err := rtsp.NewClient("rtsp://" + ipCamUser + ":" + ipCamPass + "@" + r.cfg.CameraIP + ":8554/" + "CH002.sdp")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.config.Logger.Log(logger.Info, pkg+"created RTSP client")
|
||||
r.cfg.Logger.Log(logger.Info, pkg+"created RTSP client")
|
||||
|
||||
resp, err := rtspClt.Options()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.config.Logger.Log(logger.Debug, pkg+"RTSP OPTIONS response", "response", resp.String())
|
||||
r.cfg.Logger.Log(logger.Debug, pkg+"RTSP OPTIONS response", "response", resp.String())
|
||||
|
||||
resp, err = rtspClt.Describe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.config.Logger.Log(logger.Debug, pkg+"RTSP DESCRIBE response", "response", resp.String())
|
||||
r.cfg.Logger.Log(logger.Debug, pkg+"RTSP DESCRIBE response", "response", resp.String())
|
||||
|
||||
resp, err = rtspClt.Setup("track1", fmt.Sprintf("RTP/AVP;unicast;client_port=%d-%d", rtpPort, rtcpPort))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.config.Logger.Log(logger.Debug, pkg+"RTSP SETUP response", "response", resp.String())
|
||||
r.cfg.Logger.Log(logger.Debug, pkg+"RTSP SETUP response", "response", resp.String())
|
||||
|
||||
rtpCltAddr, rtcpCltAddr, rtcpSvrAddr, err := formAddrs(local, remote, *resp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.config.Logger.Log(logger.Info, pkg+"RTSP session setup complete")
|
||||
r.cfg.Logger.Log(logger.Info, pkg+"RTSP session setup complete")
|
||||
|
||||
rtpClt, err := rtp.NewClient(rtpCltAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rtcpClt, err := rtcp.NewClient(rtcpCltAddr, rtcpSvrAddr, rtpClt, r.config.Logger.Log)
|
||||
rtcpClt, err := rtcp.NewClient(rtcpCltAddr, rtcpSvrAddr, rtpClt, r.cfg.Logger.Log)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r.config.Logger.Log(logger.Info, pkg+"RTCP and RTP clients created")
|
||||
r.cfg.Logger.Log(logger.Info, pkg+"RTCP and RTP clients created")
|
||||
|
||||
// Check errors from RTCP client until it has stopped running.
|
||||
go func() {
|
||||
for {
|
||||
err, ok := <-rtcpClt.Err()
|
||||
if ok {
|
||||
r.config.Logger.Log(logger.Warning, pkg+"RTCP error", "error", err.Error())
|
||||
r.cfg.Logger.Log(logger.Warning, pkg+"RTCP error", "error", err.Error())
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
@ -279,20 +287,20 @@ func (r *Revid) startRTSPCamera() (func() error, error) {
|
|||
// Start the RTCP client.
|
||||
rtcpClt.Start()
|
||||
|
||||
r.config.Logger.Log(logger.Info, pkg+"RTCP client started")
|
||||
r.cfg.Logger.Log(logger.Info, pkg+"RTCP client started")
|
||||
|
||||
// Start reading data from the RTP client.
|
||||
r.wg.Add(1)
|
||||
go r.processFrom(rtpClt, time.Second/time.Duration(r.config.FrameRate))
|
||||
go r.processFrom(rtpClt, time.Second/time.Duration(r.cfg.FrameRate))
|
||||
|
||||
r.config.Logger.Log(logger.Info, pkg+"started input processor")
|
||||
r.cfg.Logger.Log(logger.Info, pkg+"started input processor")
|
||||
|
||||
resp, err = rtspClt.Play()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.config.Logger.Log(logger.Debug, pkg+"RTSP server PLAY response", "response", resp.String())
|
||||
r.config.Logger.Log(logger.Info, pkg+"play requested, now receiving stream")
|
||||
r.cfg.Logger.Log(logger.Debug, pkg+"RTSP server PLAY response", "response", resp.String())
|
||||
r.cfg.Logger.Log(logger.Info, pkg+"play requested, now receiving stream")
|
||||
|
||||
return func() error {
|
||||
err := rtpClt.Close()
|
||||
|
@ -307,7 +315,7 @@ func (r *Revid) startRTSPCamera() (func() error, error) {
|
|||
|
||||
rtcpClt.Stop()
|
||||
|
||||
r.config.Logger.Log(logger.Info, pkg+"RTP, RTSP and RTCP clients stopped and closed")
|
||||
r.cfg.Logger.Log(logger.Info, pkg+"RTP, RTSP and RTCP clients stopped and closed")
|
||||
return nil
|
||||
}, nil
|
||||
}
|
||||
|
@ -345,6 +353,6 @@ func parseSvrRTCPPort(resp rtsp.Response) (int, error) {
|
|||
// then send individual access units to revid's encoders.
|
||||
func (r *Revid) processFrom(read io.Reader, delay time.Duration) {
|
||||
r.err <- r.lexTo(r.encoders, read, delay)
|
||||
r.config.Logger.Log(logger.Info, pkg+"finished lexing")
|
||||
r.cfg.Logger.Log(logger.Info, pkg+"finished lexing")
|
||||
r.wg.Done()
|
||||
}
|
||||
|
|
288
revid/revid.go
288
revid/revid.go
|
@ -44,24 +44,32 @@ import (
|
|||
"bitbucket.org/ausocean/av/codec/mjpeg"
|
||||
"bitbucket.org/ausocean/av/container/flv"
|
||||
"bitbucket.org/ausocean/av/container/mts"
|
||||
"bitbucket.org/ausocean/av/revid/config"
|
||||
"bitbucket.org/ausocean/iot/pi/netsender"
|
||||
"bitbucket.org/ausocean/utils/ioext"
|
||||
"bitbucket.org/ausocean/utils/logger"
|
||||
"bitbucket.org/ausocean/utils/ring"
|
||||
)
|
||||
|
||||
// Ring buffer defaults.
|
||||
const (
|
||||
// MTS ring buffer defaults.
|
||||
defaultMTSRBSize = 1000
|
||||
defaultMTSRBElementSize = 100000
|
||||
defaultMTSRBWriteTimeout = 5
|
||||
|
||||
// RTMP ring buffer defaults.
|
||||
defaultRTMPRBSize = 1000
|
||||
defaultRTMPRBElementSize = 300000
|
||||
defaultRTMPRBWriteTimeout = 5
|
||||
)
|
||||
|
||||
// RTMP connection properties.
|
||||
const (
|
||||
rtmpConnectionMaxTries = 5
|
||||
rtmpConnectionTimeout = 10
|
||||
)
|
||||
|
||||
const (
|
||||
rtpPort = 60000
|
||||
rtcpPort = 60001
|
||||
defaultServerRTCPPort = 17301
|
||||
)
|
||||
|
||||
const pkg = "revid: "
|
||||
|
||||
type Logger interface {
|
||||
|
@ -76,7 +84,7 @@ type Revid struct {
|
|||
// For historical reasons it also handles logging.
|
||||
// FIXME(kortschak): The relationship of concerns
|
||||
// in config/ns is weird.
|
||||
config Config
|
||||
cfg config.Config
|
||||
|
||||
// ns holds the netsender.Sender responsible for HTTP.
|
||||
ns *netsender.Sender
|
||||
|
@ -116,13 +124,13 @@ type Revid struct {
|
|||
|
||||
// New returns a pointer to a new Revid with the desired configuration, and/or
|
||||
// an error if construction of the new instance was not successful.
|
||||
func New(c Config, ns *netsender.Sender) (*Revid, error) {
|
||||
func New(c config.Config, ns *netsender.Sender) (*Revid, error) {
|
||||
r := Revid{ns: ns, err: make(chan error)}
|
||||
err := r.setConfig(c)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not set config, failed with error: %v", err)
|
||||
}
|
||||
r.config.Logger.SetLevel(c.LogLevel)
|
||||
r.cfg.Logger.SetLevel(c.LogLevel)
|
||||
go r.handleErrors()
|
||||
return &r, nil
|
||||
}
|
||||
|
@ -130,8 +138,8 @@ func New(c Config, ns *netsender.Sender) (*Revid, error) {
|
|||
// Config returns a copy of revids current config.
|
||||
//
|
||||
// Config is not safe for concurrent use.
|
||||
func (r *Revid) Config() Config {
|
||||
return r.config
|
||||
func (r *Revid) Config() config.Config {
|
||||
return r.cfg
|
||||
}
|
||||
|
||||
// TODO(Saxon): put more thought into error severity and how to handle these.
|
||||
|
@ -139,7 +147,7 @@ func (r *Revid) handleErrors() {
|
|||
for {
|
||||
err := <-r.err
|
||||
if err != nil {
|
||||
r.config.Logger.Log(logger.Error, pkg+"async error", "error", err.Error())
|
||||
r.cfg.Logger.Log(logger.Error, pkg+"async error", "error", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -154,45 +162,45 @@ func (r *Revid) Bitrate() int {
|
|||
// reset swaps the current config of a Revid with the passed
|
||||
// configuration; checking validity and returning errors if not valid. It then
|
||||
// sets up the data pipeline accordingly to this configuration.
|
||||
func (r *Revid) reset(config Config) error {
|
||||
err := r.setConfig(config)
|
||||
func (r *Revid) reset(c config.Config) error {
|
||||
err := r.setConfig(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.config.Logger.SetLevel(config.LogLevel)
|
||||
r.cfg.Logger.SetLevel(c.LogLevel)
|
||||
|
||||
err = r.setupPipeline(
|
||||
func(dst io.WriteCloser, fps float64) (io.WriteCloser, error) {
|
||||
var st int
|
||||
var encOptions []func(*mts.Encoder) error
|
||||
|
||||
switch r.config.Input {
|
||||
case Raspivid:
|
||||
switch r.config.InputCodec {
|
||||
switch r.cfg.Input {
|
||||
case config.InputRaspivid:
|
||||
switch r.cfg.InputCodec {
|
||||
case codecutil.H264:
|
||||
st = mts.EncodeH264
|
||||
case codecutil.MJPEG:
|
||||
st = mts.EncodeMJPEG
|
||||
encOptions = append(encOptions, mts.PacketBasedPSI(int(r.config.MinFrames)))
|
||||
encOptions = append(encOptions, mts.PacketBasedPSI(int(r.cfg.MinFrames)))
|
||||
default:
|
||||
panic("unknown input codec for raspivid input")
|
||||
}
|
||||
case File, V4L:
|
||||
case config.InputFile, config.InputV4L:
|
||||
st = mts.EncodeH264
|
||||
case RTSP:
|
||||
switch r.config.InputCodec {
|
||||
case config.InputRTSP:
|
||||
switch r.cfg.InputCodec {
|
||||
case codecutil.H265:
|
||||
st = mts.EncodeH265
|
||||
case codecutil.H264:
|
||||
st = mts.EncodeH264
|
||||
case codecutil.MJPEG:
|
||||
st = mts.EncodeMJPEG
|
||||
encOptions = append(encOptions, mts.PacketBasedPSI(int(r.config.MinFrames)))
|
||||
encOptions = append(encOptions, mts.PacketBasedPSI(int(r.cfg.MinFrames)))
|
||||
default:
|
||||
panic("unknown input codec for RTSP input")
|
||||
}
|
||||
case Audio:
|
||||
case config.InputAudio:
|
||||
st = mts.EncodeAudio
|
||||
default:
|
||||
panic("unknown input type")
|
||||
|
@ -215,13 +223,13 @@ func (r *Revid) reset(config Config) error {
|
|||
|
||||
// setConfig takes a config, checks it's validity and then replaces the current
|
||||
// revid config.
|
||||
func (r *Revid) setConfig(config Config) error {
|
||||
r.config.Logger = config.Logger
|
||||
func (r *Revid) setConfig(config config.Config) error {
|
||||
r.cfg.Logger = config.Logger
|
||||
err := config.Validate()
|
||||
if err != nil {
|
||||
return errors.New("Config struct is bad: " + err.Error())
|
||||
}
|
||||
r.config = config
|
||||
r.cfg = config
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -244,38 +252,38 @@ func (r *Revid) setupPipeline(mtsEnc func(dst io.WriteCloser, rate float64) (io.
|
|||
// to mtsSenders if the output requires MPEGTS encoding, or flvSenders if the
|
||||
// output requires FLV encoding.
|
||||
var w io.WriteCloser
|
||||
for _, out := range r.config.Outputs {
|
||||
for _, out := range r.cfg.Outputs {
|
||||
switch out {
|
||||
case HTTP:
|
||||
case config.OutputHTTP:
|
||||
w = newMtsSender(
|
||||
newHttpSender(r.ns, r.config.Logger.Log),
|
||||
r.config.Logger.Log,
|
||||
ring.NewBuffer(r.config.MTSRBSize, r.config.MTSRBElementSize, time.Duration(r.config.MTSRBWriteTimeout)*time.Second),
|
||||
r.config.ClipDuration,
|
||||
newHttpSender(r.ns, r.cfg.Logger.Log),
|
||||
r.cfg.Logger.Log,
|
||||
ring.NewBuffer(r.cfg.MTSRBSize, r.cfg.MTSRBElementSize, time.Duration(r.cfg.MTSRBWriteTimeout)*time.Second),
|
||||
r.cfg.ClipDuration,
|
||||
)
|
||||
mtsSenders = append(mtsSenders, w)
|
||||
case RTP:
|
||||
w, err := newRtpSender(r.config.RTPAddress, r.config.Logger.Log, r.config.FrameRate)
|
||||
case config.OutputRTP:
|
||||
w, err := newRtpSender(r.cfg.RTPAddress, r.cfg.Logger.Log, r.cfg.FrameRate)
|
||||
if err != nil {
|
||||
r.config.Logger.Log(logger.Warning, pkg+"rtp connect error", "error", err.Error())
|
||||
r.cfg.Logger.Log(logger.Warning, pkg+"rtp connect error", "error", err.Error())
|
||||
}
|
||||
mtsSenders = append(mtsSenders, w)
|
||||
case File:
|
||||
w, err := newFileSender(r.config.OutputPath)
|
||||
case config.OutputFile:
|
||||
w, err := newFileSender(r.cfg.OutputPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mtsSenders = append(mtsSenders, w)
|
||||
case RTMP:
|
||||
case config.OutputRTMP:
|
||||
w, err := newRtmpSender(
|
||||
r.config.RTMPURL,
|
||||
r.cfg.RTMPURL,
|
||||
rtmpConnectionTimeout,
|
||||
rtmpConnectionMaxTries,
|
||||
ring.NewBuffer(r.config.RTMPRBSize, r.config.RTMPRBElementSize, time.Duration(r.config.RTMPRBWriteTimeout)*time.Second),
|
||||
r.config.Logger.Log,
|
||||
ring.NewBuffer(r.cfg.RTMPRBSize, r.cfg.RTMPRBElementSize, time.Duration(r.cfg.RTMPRBWriteTimeout)*time.Second),
|
||||
r.cfg.Logger.Log,
|
||||
)
|
||||
if err != nil {
|
||||
r.config.Logger.Log(logger.Warning, pkg+"rtmp connect error", "error", err.Error())
|
||||
r.cfg.Logger.Log(logger.Warning, pkg+"rtmp connect error", "error", err.Error())
|
||||
}
|
||||
flvSenders = append(flvSenders, w)
|
||||
}
|
||||
|
@ -286,7 +294,7 @@ func (r *Revid) setupPipeline(mtsEnc func(dst io.WriteCloser, rate float64) (io.
|
|||
// as a destination.
|
||||
if len(mtsSenders) != 0 {
|
||||
mw := multiWriter(mtsSenders...)
|
||||
e, _ := mtsEnc(mw, r.config.WriteRate)
|
||||
e, _ := mtsEnc(mw, r.cfg.WriteRate)
|
||||
encoders = append(encoders, e)
|
||||
}
|
||||
|
||||
|
@ -295,7 +303,7 @@ func (r *Revid) setupPipeline(mtsEnc func(dst io.WriteCloser, rate float64) (io.
|
|||
// as a destination.
|
||||
if len(flvSenders) != 0 {
|
||||
mw := multiWriter(flvSenders...)
|
||||
e, err := flvEnc(mw, int(r.config.FrameRate))
|
||||
e, err := flvEnc(mw, int(r.cfg.FrameRate))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -304,23 +312,23 @@ func (r *Revid) setupPipeline(mtsEnc func(dst io.WriteCloser, rate float64) (io.
|
|||
|
||||
r.encoders = multiWriter(encoders...)
|
||||
|
||||
switch r.config.Input {
|
||||
case Raspivid:
|
||||
switch r.cfg.Input {
|
||||
case config.InputRaspivid:
|
||||
r.setupInput = r.startRaspivid
|
||||
switch r.config.InputCodec {
|
||||
switch r.cfg.InputCodec {
|
||||
case codecutil.H264:
|
||||
r.lexTo = h264.Lex
|
||||
case codecutil.MJPEG:
|
||||
r.lexTo = mjpeg.Lex
|
||||
}
|
||||
case V4L:
|
||||
case config.InputV4L:
|
||||
r.setupInput = r.startV4L
|
||||
r.lexTo = h264.Lex
|
||||
case File:
|
||||
case config.InputFile:
|
||||
r.setupInput = r.setupInputForFile
|
||||
case RTSP:
|
||||
case config.InputRTSP:
|
||||
r.setupInput = r.startRTSPCamera
|
||||
switch r.config.InputCodec {
|
||||
switch r.cfg.InputCodec {
|
||||
case codecutil.H264:
|
||||
r.lexTo = h264.NewExtractor().Extract
|
||||
case codecutil.H265:
|
||||
|
@ -328,9 +336,9 @@ func (r *Revid) setupPipeline(mtsEnc func(dst io.WriteCloser, rate float64) (io.
|
|||
case codecutil.MJPEG:
|
||||
panic("not implemented")
|
||||
}
|
||||
case Audio:
|
||||
case config.InputAudio:
|
||||
r.setupInput = r.startAudioDevice
|
||||
r.lexTo = codecutil.NewByteLexer(&r.config.ChunkSize).Lex
|
||||
r.lexTo = codecutil.NewByteLexer(&r.cfg.ChunkSize).Lex
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -342,15 +350,15 @@ func (r *Revid) setupPipeline(mtsEnc func(dst io.WriteCloser, rate float64) (io.
|
|||
// Start is safe for concurrent use.
|
||||
func (r *Revid) Start() error {
|
||||
if r.IsRunning() {
|
||||
r.config.Logger.Log(logger.Warning, pkg+"start called, but revid already running")
|
||||
r.cfg.Logger.Log(logger.Warning, pkg+"start called, but revid already running")
|
||||
return nil
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
r.config.Logger.Log(logger.Info, pkg+"starting Revid")
|
||||
err := r.reset(r.config)
|
||||
r.cfg.Logger.Log(logger.Info, pkg+"starting Revid")
|
||||
err := r.reset(r.cfg)
|
||||
if err != nil {
|
||||
r.Stop()
|
||||
return err
|
||||
|
@ -369,7 +377,7 @@ func (r *Revid) Start() error {
|
|||
// Stop is safe for concurrent use.
|
||||
func (r *Revid) Stop() {
|
||||
if !r.IsRunning() {
|
||||
r.config.Logger.Log(logger.Warning, pkg+"stop called but revid isn't running")
|
||||
r.cfg.Logger.Log(logger.Warning, pkg+"stop called but revid isn't running")
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -379,26 +387,26 @@ func (r *Revid) Stop() {
|
|||
if r.closeInput != nil {
|
||||
err := r.closeInput()
|
||||
if err != nil {
|
||||
r.config.Logger.Log(logger.Error, pkg+"could not close input", "error", err.Error())
|
||||
r.cfg.Logger.Log(logger.Error, pkg+"could not close input", "error", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
r.config.Logger.Log(logger.Info, pkg+"closing pipeline")
|
||||
r.cfg.Logger.Log(logger.Info, pkg+"closing pipeline")
|
||||
err := r.encoders.Close()
|
||||
if err != nil {
|
||||
r.config.Logger.Log(logger.Error, pkg+"failed to close pipeline", "error", err.Error())
|
||||
r.cfg.Logger.Log(logger.Error, pkg+"failed to close pipeline", "error", err.Error())
|
||||
}
|
||||
r.config.Logger.Log(logger.Info, pkg+"closed pipeline")
|
||||
r.cfg.Logger.Log(logger.Info, pkg+"closed pipeline")
|
||||
|
||||
if r.cmd != nil && r.cmd.Process != nil {
|
||||
r.config.Logger.Log(logger.Info, pkg+"killing input proccess")
|
||||
r.cfg.Logger.Log(logger.Info, pkg+"killing input proccess")
|
||||
r.cmd.Process.Kill()
|
||||
}
|
||||
r.config.Logger.Log(logger.Info, pkg+"waiting for routines to close")
|
||||
r.cfg.Logger.Log(logger.Info, pkg+"waiting for routines to close")
|
||||
|
||||
r.wg.Wait()
|
||||
|
||||
r.config.Logger.Log(logger.Info, pkg+"revid stopped")
|
||||
r.cfg.Logger.Log(logger.Info, pkg+"revid stopped")
|
||||
r.running = false
|
||||
}
|
||||
|
||||
|
@ -423,239 +431,239 @@ func (r *Revid) Update(vars map[string]string) error {
|
|||
for key, value := range vars {
|
||||
switch key {
|
||||
case "Input":
|
||||
v, ok := map[string]uint8{"raspivid": Raspivid, "rtsp": RTSP}[strings.ToLower(value)]
|
||||
v, ok := map[string]uint8{"raspivid": config.InputRaspivid, "rtsp": config.InputRTSP}[strings.ToLower(value)]
|
||||
if !ok {
|
||||
r.config.Logger.Log(logger.Warning, pkg+"invalid input var", "value", value)
|
||||
r.cfg.Logger.Log(logger.Warning, pkg+"invalid input var", "value", value)
|
||||
break
|
||||
}
|
||||
r.config.Input = v
|
||||
r.cfg.Input = v
|
||||
case "Saturation":
|
||||
s, err := strconv.ParseInt(value, 10, 0)
|
||||
if err != nil {
|
||||
r.config.Logger.Log(logger.Warning, pkg+"invalid saturation param", "value", value)
|
||||
r.cfg.Logger.Log(logger.Warning, pkg+"invalid saturation param", "value", value)
|
||||
break
|
||||
}
|
||||
r.config.Saturation = int(s)
|
||||
r.cfg.Saturation = int(s)
|
||||
case "Brightness":
|
||||
b, err := strconv.ParseUint(value, 10, 0)
|
||||
if err != nil {
|
||||
r.config.Logger.Log(logger.Warning, pkg+"invalid brightness param", "value", value)
|
||||
r.cfg.Logger.Log(logger.Warning, pkg+"invalid brightness param", "value", value)
|
||||
break
|
||||
}
|
||||
r.config.Brightness = uint(b)
|
||||
r.cfg.Brightness = uint(b)
|
||||
case "Exposure":
|
||||
r.config.Exposure = value
|
||||
r.cfg.Exposure = value
|
||||
case "AutoWhiteBalance":
|
||||
r.config.AutoWhiteBalance = value
|
||||
r.cfg.AutoWhiteBalance = value
|
||||
case "InputCodec":
|
||||
switch value {
|
||||
case "H264":
|
||||
r.config.InputCodec = codecutil.H264
|
||||
r.cfg.InputCodec = codecutil.H264
|
||||
case "MJPEG":
|
||||
r.config.InputCodec = codecutil.MJPEG
|
||||
r.cfg.InputCodec = codecutil.MJPEG
|
||||
default:
|
||||
r.config.Logger.Log(logger.Warning, pkg+"invalid InputCodec variable value", "value", value)
|
||||
r.cfg.Logger.Log(logger.Warning, pkg+"invalid InputCodec variable value", "value", value)
|
||||
}
|
||||
case "Output":
|
||||
outputs := strings.Split(value, ",")
|
||||
r.config.Outputs = make([]uint8, len(outputs))
|
||||
r.cfg.Outputs = make([]uint8, len(outputs))
|
||||
|
||||
for i, output := range outputs {
|
||||
switch output {
|
||||
case "File":
|
||||
r.config.Outputs[i] = File
|
||||
r.cfg.Outputs[i] = config.OutputFile
|
||||
case "Http":
|
||||
r.config.Outputs[i] = HTTP
|
||||
r.cfg.Outputs[i] = config.OutputHTTP
|
||||
case "Rtmp":
|
||||
r.config.Outputs[i] = RTMP
|
||||
r.cfg.Outputs[i] = config.OutputRTMP
|
||||
case "Rtp":
|
||||
r.config.Outputs[i] = RTP
|
||||
r.cfg.Outputs[i] = config.OutputRTP
|
||||
default:
|
||||
r.config.Logger.Log(logger.Warning, pkg+"invalid output param", "value", value)
|
||||
r.cfg.Logger.Log(logger.Warning, pkg+"invalid output param", "value", value)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
case "RtmpUrl":
|
||||
r.config.RTMPURL = value
|
||||
r.cfg.RTMPURL = value
|
||||
case "RtpAddress":
|
||||
r.config.RTPAddress = value
|
||||
r.cfg.RTPAddress = value
|
||||
case "Bitrate":
|
||||
v, err := strconv.ParseUint(value, 10, 0)
|
||||
if err != nil {
|
||||
r.config.Logger.Log(logger.Warning, pkg+"invalid framerate param", "value", value)
|
||||
r.cfg.Logger.Log(logger.Warning, pkg+"invalid framerate param", "value", value)
|
||||
break
|
||||
}
|
||||
r.config.Bitrate = uint(v)
|
||||
r.cfg.Bitrate = uint(v)
|
||||
case "OutputPath":
|
||||
r.config.OutputPath = value
|
||||
r.cfg.OutputPath = value
|
||||
case "InputPath":
|
||||
r.config.InputPath = value
|
||||
r.cfg.InputPath = value
|
||||
case "Height":
|
||||
h, err := strconv.ParseUint(value, 10, 0)
|
||||
if err != nil {
|
||||
r.config.Logger.Log(logger.Warning, pkg+"invalid height param", "value", value)
|
||||
r.cfg.Logger.Log(logger.Warning, pkg+"invalid height param", "value", value)
|
||||
break
|
||||
}
|
||||
r.config.Height = uint(h)
|
||||
r.cfg.Height = uint(h)
|
||||
case "Width":
|
||||
w, err := strconv.ParseUint(value, 10, 0)
|
||||
if err != nil {
|
||||
r.config.Logger.Log(logger.Warning, pkg+"invalid width param", "value", value)
|
||||
r.cfg.Logger.Log(logger.Warning, pkg+"invalid width param", "value", value)
|
||||
break
|
||||
}
|
||||
r.config.Width = uint(w)
|
||||
r.cfg.Width = uint(w)
|
||||
case "FrameRate":
|
||||
v, err := strconv.ParseUint(value, 10, 0)
|
||||
if err != nil {
|
||||
r.config.Logger.Log(logger.Warning, pkg+"invalid framerate param", "value", value)
|
||||
r.cfg.Logger.Log(logger.Warning, pkg+"invalid framerate param", "value", value)
|
||||
break
|
||||
}
|
||||
r.config.FrameRate = uint(v)
|
||||
r.cfg.FrameRate = uint(v)
|
||||
case "Rotation":
|
||||
v, err := strconv.ParseUint(value, 10, 0)
|
||||
if err != nil || v > 359 {
|
||||
r.config.Logger.Log(logger.Warning, pkg+"invalid rotation param", "value", value)
|
||||
r.cfg.Logger.Log(logger.Warning, pkg+"invalid rotation param", "value", value)
|
||||
break
|
||||
}
|
||||
r.config.Rotation = uint(v)
|
||||
r.cfg.Rotation = uint(v)
|
||||
case "HttpAddress":
|
||||
r.config.HTTPAddress = value
|
||||
r.cfg.HTTPAddress = value
|
||||
case "Quantization":
|
||||
v, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
r.config.Logger.Log(logger.Warning, pkg+"invalid quantization param", "value", v)
|
||||
r.cfg.Logger.Log(logger.Warning, pkg+"invalid quantization param", "value", v)
|
||||
break
|
||||
}
|
||||
r.config.Quantization = uint(v)
|
||||
r.cfg.Quantization = uint(v)
|
||||
case "MinFrames":
|
||||
v, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
r.config.Logger.Log(logger.Warning, pkg+"invalid MinFrames param", "value", value)
|
||||
r.cfg.Logger.Log(logger.Warning, pkg+"invalid MinFrames param", "value", value)
|
||||
break
|
||||
}
|
||||
r.config.MinFrames = uint(v)
|
||||
r.cfg.MinFrames = uint(v)
|
||||
|
||||
case "ClipDuration":
|
||||
v, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
r.config.Logger.Log(logger.Warning, pkg+"invalid ClipDuration param", "value", value)
|
||||
r.cfg.Logger.Log(logger.Warning, pkg+"invalid ClipDuration param", "value", value)
|
||||
break
|
||||
}
|
||||
r.config.ClipDuration = time.Duration(v) * time.Second
|
||||
r.cfg.ClipDuration = time.Duration(v) * time.Second
|
||||
|
||||
case "HorizontalFlip":
|
||||
switch strings.ToLower(value) {
|
||||
case "true":
|
||||
r.config.FlipHorizontal = true
|
||||
r.cfg.FlipHorizontal = true
|
||||
case "false":
|
||||
r.config.FlipHorizontal = false
|
||||
r.cfg.FlipHorizontal = false
|
||||
default:
|
||||
r.config.Logger.Log(logger.Warning, pkg+"invalid HorizontalFlip param", "value", value)
|
||||
r.cfg.Logger.Log(logger.Warning, pkg+"invalid HorizontalFlip param", "value", value)
|
||||
}
|
||||
case "VerticalFlip":
|
||||
switch strings.ToLower(value) {
|
||||
case "true":
|
||||
r.config.FlipVertical = true
|
||||
r.cfg.FlipVertical = true
|
||||
case "false":
|
||||
r.config.FlipVertical = false
|
||||
r.cfg.FlipVertical = false
|
||||
default:
|
||||
r.config.Logger.Log(logger.Warning, pkg+"invalid VerticalFlip param", "value", value)
|
||||
r.cfg.Logger.Log(logger.Warning, pkg+"invalid VerticalFlip param", "value", value)
|
||||
}
|
||||
case "BurstPeriod":
|
||||
v, err := strconv.ParseUint(value, 10, 0)
|
||||
if err != nil {
|
||||
r.config.Logger.Log(logger.Warning, pkg+"invalid BurstPeriod param", "value", value)
|
||||
r.cfg.Logger.Log(logger.Warning, pkg+"invalid BurstPeriod param", "value", value)
|
||||
break
|
||||
}
|
||||
r.config.BurstPeriod = uint(v)
|
||||
r.cfg.BurstPeriod = uint(v)
|
||||
case "Logging":
|
||||
switch value {
|
||||
case "Debug":
|
||||
r.config.LogLevel = logger.Debug
|
||||
r.cfg.LogLevel = logger.Debug
|
||||
case "Info":
|
||||
r.config.LogLevel = logger.Info
|
||||
r.cfg.LogLevel = logger.Info
|
||||
case "Warning":
|
||||
r.config.LogLevel = logger.Warning
|
||||
r.cfg.LogLevel = logger.Warning
|
||||
case "Error":
|
||||
r.config.LogLevel = logger.Error
|
||||
r.cfg.LogLevel = logger.Error
|
||||
case "Fatal":
|
||||
r.config.LogLevel = logger.Fatal
|
||||
r.cfg.LogLevel = logger.Fatal
|
||||
default:
|
||||
r.config.Logger.Log(logger.Warning, pkg+"invalid Logging param", "value", value)
|
||||
r.cfg.Logger.Log(logger.Warning, pkg+"invalid Logging param", "value", value)
|
||||
}
|
||||
case "RTMPRBSize":
|
||||
v, err := strconv.Atoi(value)
|
||||
if err != nil || v < 0 {
|
||||
r.config.Logger.Log(logger.Warning, pkg+"invalid RTMPRBSize var", "value", value)
|
||||
r.cfg.Logger.Log(logger.Warning, pkg+"invalid RTMPRBSize var", "value", value)
|
||||
break
|
||||
}
|
||||
r.config.RTMPRBSize = v
|
||||
r.cfg.RTMPRBSize = v
|
||||
case "RTMPRBElementSize":
|
||||
v, err := strconv.Atoi(value)
|
||||
if err != nil || v < 0 {
|
||||
r.config.Logger.Log(logger.Warning, pkg+"invalid RTMPRBElementSize var", "value", value)
|
||||
r.cfg.Logger.Log(logger.Warning, pkg+"invalid RTMPRBElementSize var", "value", value)
|
||||
break
|
||||
}
|
||||
r.config.RTMPRBElementSize = v
|
||||
r.cfg.RTMPRBElementSize = v
|
||||
case "RTMPRBWriteTimeout":
|
||||
v, err := strconv.Atoi(value)
|
||||
if err != nil || v <= 0 {
|
||||
r.config.Logger.Log(logger.Warning, pkg+"invalid RTMPRBWriteTimeout var", "value", value)
|
||||
r.cfg.Logger.Log(logger.Warning, pkg+"invalid RTMPRBWriteTimeout var", "value", value)
|
||||
break
|
||||
}
|
||||
r.config.RTMPRBWriteTimeout = v
|
||||
r.cfg.RTMPRBWriteTimeout = v
|
||||
case "MTSRBSize":
|
||||
v, err := strconv.Atoi(value)
|
||||
if err != nil || v < 0 {
|
||||
r.config.Logger.Log(logger.Warning, pkg+"invalid MTSRBSize var", "value", value)
|
||||
r.cfg.Logger.Log(logger.Warning, pkg+"invalid MTSRBSize var", "value", value)
|
||||
break
|
||||
}
|
||||
r.config.MTSRBSize = v
|
||||
r.cfg.MTSRBSize = v
|
||||
case "MTSRBElementSize":
|
||||
v, err := strconv.Atoi(value)
|
||||
if err != nil || v < 0 {
|
||||
r.config.Logger.Log(logger.Warning, pkg+"invalid MTSRBElementSize var", "value", value)
|
||||
r.cfg.Logger.Log(logger.Warning, pkg+"invalid MTSRBElementSize var", "value", value)
|
||||
break
|
||||
}
|
||||
r.config.MTSRBElementSize = v
|
||||
r.cfg.MTSRBElementSize = v
|
||||
case "MTSRBWriteTimeout":
|
||||
v, err := strconv.Atoi(value)
|
||||
if err != nil || v <= 0 {
|
||||
r.config.Logger.Log(logger.Warning, pkg+"invalid MTSRBWriteTimeout var", "value", value)
|
||||
r.cfg.Logger.Log(logger.Warning, pkg+"invalid MTSRBWriteTimeout var", "value", value)
|
||||
break
|
||||
}
|
||||
r.config.MTSRBWriteTimeout = v
|
||||
r.cfg.MTSRBWriteTimeout = v
|
||||
case "VBR":
|
||||
v, ok := map[string]bool{"true": true, "false": false}[strings.ToLower(value)]
|
||||
if !ok {
|
||||
r.config.Logger.Log(logger.Warning, pkg+"invalid VBR var", "value", value)
|
||||
r.cfg.Logger.Log(logger.Warning, pkg+"invalid VBR var", "value", value)
|
||||
break
|
||||
}
|
||||
r.config.VBR = v
|
||||
r.cfg.VBR = v
|
||||
case "VBRQuality":
|
||||
v, ok := map[string]quality{"standard": qualityStandard, "fair": qualityFair, "good": qualityGood, "great": qualityGreat, "excellent": qualityExcellent}[strings.ToLower(value)]
|
||||
v, ok := map[string]config.Quality{"standard": config.QualityStandard, "fair": config.QualityFair, "good": config.QualityGood, "great": config.QualityGreat, "excellent": config.QualityExcellent}[strings.ToLower(value)]
|
||||
if !ok {
|
||||
r.config.Logger.Log(logger.Warning, pkg+"invalid VBRQuality var", "value", value)
|
||||
r.cfg.Logger.Log(logger.Warning, pkg+"invalid VBRQuality var", "value", value)
|
||||
break
|
||||
}
|
||||
r.config.VBRQuality = v
|
||||
r.cfg.VBRQuality = v
|
||||
case "VBRBitrate":
|
||||
v, err := strconv.Atoi(value)
|
||||
if err != nil || v <= 0 {
|
||||
r.config.Logger.Log(logger.Warning, pkg+"invalid VBRBitrate var", "value", value)
|
||||
r.cfg.Logger.Log(logger.Warning, pkg+"invalid VBRBitrate var", "value", value)
|
||||
break
|
||||
}
|
||||
r.config.VBRBitrate = v
|
||||
r.cfg.VBRBitrate = v
|
||||
case "CameraChan":
|
||||
v, err := strconv.Atoi(value)
|
||||
if err != nil || (v != 1 && v != 2) {
|
||||
r.config.Logger.Log(logger.Warning, pkg+"invalid CameraChan var", "value", value)
|
||||
r.cfg.Logger.Log(logger.Warning, pkg+"invalid CameraChan var", "value", value)
|
||||
break
|
||||
}
|
||||
r.config.CameraChan = v
|
||||
r.cfg.CameraChan = v
|
||||
}
|
||||
}
|
||||
r.config.Logger.Log(logger.Info, pkg+"revid config changed", "config", fmt.Sprintf("%+v", r.config))
|
||||
r.cfg.Logger.Log(logger.Info, pkg+"revid config changed", "config", fmt.Sprintf("%+v", r.cfg))
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ import (
|
|||
"runtime"
|
||||
"testing"
|
||||
|
||||
"bitbucket.org/ausocean/av/revid/config"
|
||||
"bitbucket.org/ausocean/iot/pi/netsender"
|
||||
)
|
||||
|
||||
|
@ -56,9 +57,9 @@ func TestRaspivid(t *testing.T) {
|
|||
t.Errorf("netsender.New failed with error %v", err)
|
||||
}
|
||||
|
||||
var c Config
|
||||
var c config.Config
|
||||
c.Logger = &logger
|
||||
c.Input = Raspivid
|
||||
c.Input = config.InputRaspivid
|
||||
|
||||
rv, err := New(c, ns)
|
||||
if err != nil {
|
||||
|
@ -148,7 +149,7 @@ func TestResetEncoderSenderSetup(t *testing.T) {
|
|||
encoders []encoder
|
||||
}{
|
||||
{
|
||||
outputs: []uint8{HTTP},
|
||||
outputs: []uint8{config.OutputHTTP},
|
||||
encoders: []encoder{
|
||||
{
|
||||
encoderType: mtsEncoderStr,
|
||||
|
@ -157,7 +158,7 @@ func TestResetEncoderSenderSetup(t *testing.T) {
|
|||
},
|
||||
},
|
||||
{
|
||||
outputs: []uint8{RTMP},
|
||||
outputs: []uint8{config.OutputRTMP},
|
||||
encoders: []encoder{
|
||||
{
|
||||
encoderType: flvEncoderStr,
|
||||
|
@ -166,7 +167,7 @@ func TestResetEncoderSenderSetup(t *testing.T) {
|
|||
},
|
||||
},
|
||||
{
|
||||
outputs: []uint8{RTP},
|
||||
outputs: []uint8{config.OutputRTP},
|
||||
encoders: []encoder{
|
||||
{
|
||||
encoderType: mtsEncoderStr,
|
||||
|
@ -175,7 +176,7 @@ func TestResetEncoderSenderSetup(t *testing.T) {
|
|||
},
|
||||
},
|
||||
{
|
||||
outputs: []uint8{HTTP, RTMP},
|
||||
outputs: []uint8{config.OutputHTTP, config.OutputRTMP},
|
||||
encoders: []encoder{
|
||||
{
|
||||
encoderType: mtsEncoderStr,
|
||||
|
@ -188,7 +189,7 @@ func TestResetEncoderSenderSetup(t *testing.T) {
|
|||
},
|
||||
},
|
||||
{
|
||||
outputs: []uint8{HTTP, RTP, RTMP},
|
||||
outputs: []uint8{config.OutputHTTP, config.OutputRTP, config.OutputRTMP},
|
||||
encoders: []encoder{
|
||||
{
|
||||
encoderType: mtsEncoderStr,
|
||||
|
@ -201,7 +202,7 @@ func TestResetEncoderSenderSetup(t *testing.T) {
|
|||
},
|
||||
},
|
||||
{
|
||||
outputs: []uint8{RTP, RTMP},
|
||||
outputs: []uint8{config.OutputRTP, config.OutputRTMP},
|
||||
encoders: []encoder{
|
||||
{
|
||||
encoderType: mtsEncoderStr,
|
||||
|
@ -215,7 +216,7 @@ func TestResetEncoderSenderSetup(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
rv, err := New(Config{Logger: &testLogger{}}, nil)
|
||||
rv, err := New(config.Config{Logger: &testLogger{}}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v", err)
|
||||
}
|
||||
|
@ -224,7 +225,7 @@ func TestResetEncoderSenderSetup(t *testing.T) {
|
|||
for testNum, test := range tests {
|
||||
// Create a new config and reset revid with it.
|
||||
const dummyURL = "rtmp://dummy"
|
||||
c := Config{Logger: &testLogger{}, Outputs: test.outputs, RTMPURL: dummyURL}
|
||||
c := config.Config{Logger: &testLogger{}, Outputs: test.outputs, RTMPURL: dummyURL}
|
||||
err := rv.setConfig(c)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v for test %v", err, testNum)
|
||||
|
|
Loading…
Reference in New Issue