mirror of https://bitbucket.org/ausocean/av.git
created av/device package and sub packages raspivid, geovision, webcam and file
av/device/device.go now contains the AVDevice interface and implementations of this interface, namely, raspivid, geovision, webcam and file are contained in the packages av/device/raspivid, av/device/geovision, av/device/webcam and av/device/file respctively. config.go and testing was also moved to a new package called config.go in order to remove would be circular dependency between AVDevice implementations and revid. Modifications were made elsewhere expecting config.Config to be part of the revid package.
This commit is contained in:
parent
9a93e92b50
commit
57d73a8d0a
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
NAME
|
NAME
|
||||||
revid-cli - command line interface for Revid.
|
revid-cli - command line interface for revid.
|
||||||
|
|
||||||
DESCRIPTION
|
DESCRIPTION
|
||||||
See Readme.md
|
See Readme.md
|
||||||
|
@ -42,6 +42,7 @@ import (
|
||||||
"bitbucket.org/ausocean/av/container/mts"
|
"bitbucket.org/ausocean/av/container/mts"
|
||||||
"bitbucket.org/ausocean/av/container/mts/meta"
|
"bitbucket.org/ausocean/av/container/mts/meta"
|
||||||
"bitbucket.org/ausocean/av/revid"
|
"bitbucket.org/ausocean/av/revid"
|
||||||
|
"bitbucket.org/ausocean/av/revid/config"
|
||||||
"bitbucket.org/ausocean/iot/pi/netsender"
|
"bitbucket.org/ausocean/iot/pi/netsender"
|
||||||
"bitbucket.org/ausocean/iot/pi/sds"
|
"bitbucket.org/ausocean/iot/pi/sds"
|
||||||
"bitbucket.org/ausocean/iot/pi/smartlogger"
|
"bitbucket.org/ausocean/iot/pi/smartlogger"
|
||||||
|
@ -102,8 +103,8 @@ func main() {
|
||||||
|
|
||||||
// handleFlags parses command line flags and returns a revid configuration
|
// handleFlags parses command line flags and returns a revid configuration
|
||||||
// based on them.
|
// based on them.
|
||||||
func handleFlags() revid.Config {
|
func handleFlags() config.Config {
|
||||||
var cfg revid.Config
|
var cfg config.Config
|
||||||
|
|
||||||
var (
|
var (
|
||||||
cpuprofile = flag.String("cpuprofile", "", "write cpu profile to `file`")
|
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)")
|
rotationPtr = flag.Uint("Rotation", 0, "Rotate video output. (0-359 degrees)")
|
||||||
brightnessPtr = flag.Uint("Brightness", 50, "Set brightness. (0-100) ")
|
brightnessPtr = flag.Uint("Brightness", 50, "Set brightness. (0-100) ")
|
||||||
saturationPtr = flag.Int("Saturation", 0, "Set Saturation. (100-100)")
|
saturationPtr = flag.Int("Saturation", 0, "Set Saturation. (100-100)")
|
||||||
exposurePtr = flag.String("Exposure", "auto", "Set exposure mode. ("+strings.Join(revid.ExposureModes[:], ",")+")")
|
exposurePtr = flag.String("Exposure", "auto", "Set exposure mode. ("+strings.Join(config.ExposureModes[:], ",")+")")
|
||||||
autoWhiteBalancePtr = flag.String("Awb", "auto", "Set automatic white balance mode. ("+strings.Join(revid.AutoWhiteBalanceModes[:], ",")+")")
|
autoWhiteBalancePtr = flag.String("Awb", "auto", "Set automatic white balance mode. ("+strings.Join(config.AutoWhiteBalanceModes[:], ",")+")")
|
||||||
|
|
||||||
// Audio specific flags.
|
// Audio specific flags.
|
||||||
sampleRatePtr = flag.Int("SampleRate", 48000, "Sample rate of recorded audio")
|
sampleRatePtr = flag.Int("SampleRate", 48000, "Sample rate of recorded audio")
|
||||||
|
@ -179,15 +180,15 @@ func handleFlags() revid.Config {
|
||||||
|
|
||||||
switch *inputPtr {
|
switch *inputPtr {
|
||||||
case "Raspivid":
|
case "Raspivid":
|
||||||
cfg.Input = revid.InputRaspivid
|
cfg.Input = config.InputRaspivid
|
||||||
case "v4l":
|
case "v4l":
|
||||||
cfg.Input = revid.InputV4L
|
cfg.Input = config.InputV4L
|
||||||
case "File":
|
case "File":
|
||||||
cfg.Input = revid.InputFile
|
cfg.Input = config.InputFile
|
||||||
case "Audio":
|
case "Audio":
|
||||||
cfg.Input = revid.InputAudio
|
cfg.Input = config.InputAudio
|
||||||
case "RTSP":
|
case "RTSP":
|
||||||
cfg.Input = revid.InputRTSP
|
cfg.Input = config.InputRTSP
|
||||||
case "":
|
case "":
|
||||||
default:
|
default:
|
||||||
log.Log(logger.Error, pkg+"bad input argument")
|
log.Log(logger.Error, pkg+"bad input argument")
|
||||||
|
@ -214,13 +215,13 @@ func handleFlags() revid.Config {
|
||||||
for _, o := range outputs {
|
for _, o := range outputs {
|
||||||
switch o {
|
switch o {
|
||||||
case "File":
|
case "File":
|
||||||
cfg.Outputs = append(cfg.Outputs, revid.OutputFile)
|
cfg.Outputs = append(cfg.Outputs, config.OutputFile)
|
||||||
case "Http":
|
case "Http":
|
||||||
cfg.Outputs = append(cfg.Outputs, revid.OutputHTTP)
|
cfg.Outputs = append(cfg.Outputs, config.OutputHTTP)
|
||||||
case "Rtmp":
|
case "Rtmp":
|
||||||
cfg.Outputs = append(cfg.Outputs, revid.OutputRTMP)
|
cfg.Outputs = append(cfg.Outputs, config.OutputRTMP)
|
||||||
case "Rtp":
|
case "Rtp":
|
||||||
cfg.Outputs = append(cfg.Outputs, revid.OutputRTP)
|
cfg.Outputs = append(cfg.Outputs, config.OutputRTP)
|
||||||
case "":
|
case "":
|
||||||
default:
|
default:
|
||||||
log.Log(logger.Error, pkg+"bad output argument", "arg", o)
|
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
|
// 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")
|
log.Log(logger.Info, pkg+"running in NetSender mode")
|
||||||
|
|
||||||
var rv *revid.Revid
|
var rv *revid.Revid
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
/*
|
||||||
|
DESCRIPTION
|
||||||
|
device.go provides AVDevice, and 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)
|
||||||
|
}
|
|
@ -22,27 +22,29 @@ LICENSE
|
||||||
in gpl.txt. If not, see http://www.gnu.org/licenses.
|
in gpl.txt. If not, see http://www.gnu.org/licenses.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package revid
|
package file
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"bitbucket.org/ausocean/av/revid/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AVFile is an implementation of the AVDevice interface for a file containg
|
// AVFile is an implementation of the AVDevice interface for a file containg
|
||||||
// audio or video data.
|
// audio or video data.
|
||||||
type AVFile struct {
|
type AVFile struct {
|
||||||
f io.ReadCloser
|
f io.ReadCloser
|
||||||
cfg Config
|
cfg config.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAVFile returns a new AVFile.
|
// NewAVFile returns a new AVFile.
|
||||||
func NewAVFile() *AVFile { return &AVFile{} }
|
func NewAVFile() *AVFile { return &AVFile{} }
|
||||||
|
|
||||||
// Set simply sets the AVFile's config to the passed config.
|
// Set simply sets the AVFile's config to the passed config.
|
||||||
func (m *AVFile) Set(c Config) error {
|
func (m *AVFile) Set(c config.Config) error {
|
||||||
m.cfg = c
|
m.cfg = c
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
|
@ -23,32 +23,53 @@ LICENSE
|
||||||
in gpl.txt. If not, see http://www.gnu.org/licenses.
|
in gpl.txt. If not, see http://www.gnu.org/licenses.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package revid
|
package geovision
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"bitbucket.org/ausocean/av/codec/codecutil"
|
"bitbucket.org/ausocean/av/codec/codecutil"
|
||||||
"bitbucket.org/ausocean/av/input/gvctrl"
|
"bitbucket.org/ausocean/av/device"
|
||||||
|
"bitbucket.org/ausocean/av/device/geovision/gvctrl"
|
||||||
"bitbucket.org/ausocean/av/protocol/rtcp"
|
"bitbucket.org/ausocean/av/protocol/rtcp"
|
||||||
"bitbucket.org/ausocean/av/protocol/rtp"
|
"bitbucket.org/ausocean/av/protocol/rtp"
|
||||||
"bitbucket.org/ausocean/av/protocol/rtsp"
|
"bitbucket.org/ausocean/av/protocol/rtsp"
|
||||||
|
"bitbucket.org/ausocean/av/revid/config"
|
||||||
"bitbucket.org/ausocean/utils/logger"
|
"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.
|
// Configuration defaults.
|
||||||
const (
|
const (
|
||||||
defaultGVCameraIP = "192.168.1.50"
|
defaultCameraIP = "192.168.1.50"
|
||||||
defaultGVCodec = codecutil.H264
|
defaultCodec = codecutil.H264
|
||||||
defaultGVHeight = 720
|
defaultHeight = 720
|
||||||
defaultGVFrameRate = 25
|
defaultFrameRate = 25
|
||||||
defaultGVBitrate = 400
|
defaultBitrate = 400
|
||||||
defaultGVVBRBitrate = 400
|
defaultVBRBitrate = 400
|
||||||
defaultGVMinFrames = 100
|
defaultMinFrames = 100
|
||||||
defaultGVVBRQuality = "standard"
|
defaultVBRQuality = config.QualityStandard
|
||||||
|
defaultCameraChan = 2
|
||||||
)
|
)
|
||||||
|
|
||||||
// Configuration field errors.
|
// Configuration field errors.
|
||||||
|
@ -66,56 +87,56 @@ var (
|
||||||
// IP camera. This has been designed to implement the GV-BX4700-8F in particular.
|
// IP camera. This has been designed to implement the GV-BX4700-8F in particular.
|
||||||
// Any other models are untested.
|
// Any other models are untested.
|
||||||
type GeoVision struct {
|
type GeoVision struct {
|
||||||
cfg Config
|
cfg config.Config
|
||||||
log Logger
|
log config.Logger
|
||||||
rtpClt *rtp.Client
|
rtpClt *rtp.Client
|
||||||
rtspClt *rtsp.Client
|
rtspClt *rtsp.Client
|
||||||
rtcpClt *rtcp.Client
|
rtcpClt *rtcp.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewGeoVision returns a new GeoVision.
|
// NewGeoVision returns a new GeoVision.
|
||||||
func NewGeoVision(l Logger) *GeoVision { return &GeoVision{log: l} }
|
func NewGeoVision(l config.Logger) *GeoVision { return &GeoVision{log: l} }
|
||||||
|
|
||||||
// Set will take a Config struct, check the validity of the relevant fields
|
// Set will take a Config struct, check the validity of the relevant fields
|
||||||
// and then performs any configuration necessary using gvctrl to control the
|
// 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
|
// 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.
|
// multiError and a default value is used for that particular field.
|
||||||
func (g *GeoVision) Set(c Config) error {
|
func (g *GeoVision) Set(c config.Config) error {
|
||||||
var errs multiError
|
var errs device.MultiError
|
||||||
if c.CameraIP == "" {
|
if c.CameraIP == "" {
|
||||||
errs = append(errs, errGVBadCameraIP)
|
errs = append(errs, errGVBadCameraIP)
|
||||||
c.CameraIP = defaultGVCameraIP
|
c.CameraIP = defaultCameraIP
|
||||||
}
|
}
|
||||||
|
|
||||||
switch c.InputCodec {
|
switch c.InputCodec {
|
||||||
case codecutil.H264, codecutil.H265, codecutil.MJPEG:
|
case codecutil.H264, codecutil.H265, codecutil.MJPEG:
|
||||||
default:
|
default:
|
||||||
errs = append(errs, errGVBadCodec)
|
errs = append(errs, errGVBadCodec)
|
||||||
c.InputCodec = defaultGVCodec
|
c.InputCodec = defaultCodec
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Height <= 0 {
|
if c.Height <= 0 {
|
||||||
errs = append(errs, errGVBadHeight)
|
errs = append(errs, errGVBadHeight)
|
||||||
c.Height = defaultGVHeight
|
c.Height = defaultHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.FrameRate <= 0 {
|
if c.FrameRate <= 0 {
|
||||||
errs = append(errs, errGVBadFrameRate)
|
errs = append(errs, errGVBadFrameRate)
|
||||||
c.FrameRate = defaultGVFrameRate
|
c.FrameRate = defaultFrameRate
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Bitrate <= 0 {
|
if c.Bitrate <= 0 {
|
||||||
errs = append(errs, errGVBadBitrate)
|
errs = append(errs, errGVBadBitrate)
|
||||||
c.Bitrate = defaultGVBitrate
|
c.Bitrate = defaultBitrate
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.MinFrames <= 0 {
|
if c.MinFrames <= 0 {
|
||||||
errs = append(errs, errGVBadMinFrames)
|
errs = append(errs, errGVBadMinFrames)
|
||||||
c.MinFrames = defaultGVMinFrames
|
c.MinFrames = defaultMinFrames
|
||||||
}
|
}
|
||||||
|
|
||||||
switch c.VBRQuality {
|
switch c.VBRQuality {
|
||||||
case qualityStandard, qualityFair, qualityGood, qualityGreat, qualityExcellent:
|
case config.QualityStandard, config.QualityFair, config.QualityGood, config.QualityGreat, config.QualityExcellent:
|
||||||
default:
|
default:
|
||||||
errs = append(errs, errGVBadVBRQuality)
|
errs = append(errs, errGVBadVBRQuality)
|
||||||
c.VBRQuality = defaultVBRQuality
|
c.VBRQuality = defaultVBRQuality
|
||||||
|
@ -147,12 +168,12 @@ func (g *GeoVision) Set(c Config) error {
|
||||||
gvctrl.FrameRate(int(g.cfg.FrameRate)),
|
gvctrl.FrameRate(int(g.cfg.FrameRate)),
|
||||||
gvctrl.VariableBitrate(g.cfg.VBR),
|
gvctrl.VariableBitrate(g.cfg.VBR),
|
||||||
gvctrl.VBRQuality(
|
gvctrl.VBRQuality(
|
||||||
map[quality]gvctrl.Quality{
|
map[config.Quality]gvctrl.Quality{
|
||||||
qualityStandard: gvctrl.QualityStandard,
|
config.QualityStandard: gvctrl.QualityStandard,
|
||||||
qualityFair: gvctrl.QualityFair,
|
config.QualityFair: gvctrl.QualityFair,
|
||||||
qualityGood: gvctrl.QualityGood,
|
config.QualityGood: gvctrl.QualityGood,
|
||||||
qualityGreat: gvctrl.QualityGreat,
|
config.QualityGreat: gvctrl.QualityGreat,
|
||||||
qualityExcellent: gvctrl.QualityExcellent,
|
config.QualityExcellent: gvctrl.QualityExcellent,
|
||||||
}[g.cfg.VBRQuality],
|
}[g.cfg.VBRQuality],
|
||||||
),
|
),
|
||||||
gvctrl.VBRBitrate(g.cfg.VBRBitrate),
|
gvctrl.VBRBitrate(g.cfg.VBRBitrate),
|
||||||
|
@ -167,7 +188,7 @@ func (g *GeoVision) Set(c Config) error {
|
||||||
const setupDelay = 5 * time.Second
|
const setupDelay = 5 * time.Second
|
||||||
time.Sleep(setupDelay)
|
time.Sleep(setupDelay)
|
||||||
|
|
||||||
return multiError(errs)
|
return errs
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start uses an RTSP client to communicate with the GeoVision RTSP server and
|
// Start uses an RTSP client to communicate with the GeoVision RTSP server and
|
||||||
|
@ -277,3 +298,32 @@ func (g *GeoVision) Read(p []byte) (int, error) {
|
||||||
}
|
}
|
||||||
return 0, errors.New("cannot read, GeoVision not streaming")
|
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"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"bitbucket.org/ausocean/av/input/gvctrl"
|
"bitbucket.org/ausocean/av/device/geovision/gvctrl"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
|
@ -22,7 +22,7 @@ LICENSE
|
||||||
in gpl.txt. If not, see http://www.gnu.org/licenses.
|
in gpl.txt. If not, see http://www.gnu.org/licenses.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package revid
|
package raspivid
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
@ -32,9 +32,14 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"bitbucket.org/ausocean/av/codec/codecutil"
|
"bitbucket.org/ausocean/av/codec/codecutil"
|
||||||
|
"bitbucket.org/ausocean/av/device"
|
||||||
|
"bitbucket.org/ausocean/av/revid/config"
|
||||||
"bitbucket.org/ausocean/utils/logger"
|
"bitbucket.org/ausocean/utils/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// To indicate package when logging.
|
||||||
|
const pkg = "raspivid: "
|
||||||
|
|
||||||
// Raspivid configuration defaults.
|
// Raspivid configuration defaults.
|
||||||
const (
|
const (
|
||||||
defaultRaspividCodec = codecutil.H264
|
defaultRaspividCodec = codecutil.H264
|
||||||
|
@ -67,23 +72,53 @@ var (
|
||||||
errBadQuantization = errors.New("quantization 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 is an implementation of AVDevice that provides control over the
|
||||||
// raspivid command to allow reading of data from a Raspberry Pi camera.
|
// raspivid command to allow reading of data from a Raspberry Pi camera.
|
||||||
type Raspivid struct {
|
type Raspivid struct {
|
||||||
cfg Config
|
cfg config.Config
|
||||||
cmd *exec.Cmd
|
cmd *exec.Cmd
|
||||||
out io.ReadCloser
|
out io.ReadCloser
|
||||||
log Logger
|
log config.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRaspivid returns a new Raspivid.
|
// NewRaspivid returns a new Raspivid.
|
||||||
func NewRaspivid(l Logger) *Raspivid { return &Raspivid{log: l} }
|
func NewRaspivid(l config.Logger) *Raspivid { return &Raspivid{log: l} }
|
||||||
|
|
||||||
// Set will take a Config struct, check the validity of the relevant fields
|
// Set will take a Config struct, check the validity of the relevant fields
|
||||||
// and then performs any configuration necessary. If fields are not valid,
|
// and then performs any configuration necessary. If fields are not valid,
|
||||||
// an error is added to the multiError and a default value is used.
|
// an error is added to the multiError and a default value is used.
|
||||||
func (r *Raspivid) Set(c Config) error {
|
func (r *Raspivid) Set(c config.Config) error {
|
||||||
var errs []error
|
var errs device.MultiError
|
||||||
switch c.InputCodec {
|
switch c.InputCodec {
|
||||||
case codecutil.H264, codecutil.MJPEG:
|
case codecutil.H264, codecutil.MJPEG:
|
||||||
default:
|
default:
|
||||||
|
@ -140,7 +175,7 @@ func (r *Raspivid) Set(c Config) error {
|
||||||
c.Saturation = defaultRaspividSaturation
|
c.Saturation = defaultRaspividSaturation
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Exposure == "" || !stringInSlice(c.Exposure, ExposureModes[:]) {
|
if c.Exposure == "" || !stringInSlice(c.Exposure, config.ExposureModes[:]) {
|
||||||
errs = append(errs, errBadExposure)
|
errs = append(errs, errBadExposure)
|
||||||
c.Exposure = defaultRaspividExposure
|
c.Exposure = defaultRaspividExposure
|
||||||
}
|
}
|
||||||
|
@ -151,7 +186,7 @@ func (r *Raspivid) Set(c Config) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
r.cfg = c
|
r.cfg = c
|
||||||
return multiError(errs)
|
return errs
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start will prepare the arguments for the raspivid command using the
|
// Start will prepare the arguments for the raspivid command using the
|
||||||
|
@ -237,3 +272,13 @@ func (r *Raspivid) Stop() error {
|
||||||
}
|
}
|
||||||
return r.out.Close()
|
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
|
||||||
|
}
|
|
@ -22,7 +22,7 @@ LICENSE
|
||||||
in gpl.txt. If not, see http://www.gnu.org/licenses.
|
in gpl.txt. If not, see http://www.gnu.org/licenses.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package revid
|
package webcam
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
@ -31,66 +31,71 @@ import (
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"bitbucket.org/ausocean/av/device"
|
||||||
|
"bitbucket.org/ausocean/av/revid/config"
|
||||||
"bitbucket.org/ausocean/utils/logger"
|
"bitbucket.org/ausocean/utils/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Used to indicate package in logging.
|
||||||
|
const pkg = "webcam: "
|
||||||
|
|
||||||
// Configuration defaults.
|
// Configuration defaults.
|
||||||
const (
|
const (
|
||||||
defaultWebcamInputPath = "/dev/video0"
|
defaultInputPath = "/dev/video0"
|
||||||
defaultWebcamFrameRate = 25
|
defaultFrameRate = 25
|
||||||
defaultWebcamBitrate = 400
|
defaultBitrate = 400
|
||||||
defaultWebcamWidth = 1280
|
defaultWidth = 1280
|
||||||
defaultWebcamHeight = 720
|
defaultHeight = 720
|
||||||
)
|
)
|
||||||
|
|
||||||
// Configuration field errors.
|
// Configuration field errors.
|
||||||
var (
|
var (
|
||||||
errWebcamBadFrameRate = errors.New("frame rate bad or unset, defaulting")
|
errBadFrameRate = errors.New("frame rate bad or unset, defaulting")
|
||||||
errWebcamBadBitrate = errors.New("bitrate bad or unset, defaulting")
|
errBadBitrate = errors.New("bitrate bad or unset, defaulting")
|
||||||
errWebcamWidth = errors.New("width bad or unset, defaulting")
|
errBadWidth = errors.New("width bad or unset, defaulting")
|
||||||
errWebcamHeight = errors.New("height bad or unset, defaulting")
|
errBadHeight = errors.New("height bad or unset, defaulting")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Webcam is an implementation of the AVDevice interface for a Webcam. Webcam
|
// Webcam is an implementation of the AVDevice interface for a Webcam. Webcam
|
||||||
// uses an ffmpeg process to pipe the video data from the webcam.
|
// uses an ffmpeg process to pipe the video data from the webcam.
|
||||||
type Webcam struct {
|
type Webcam struct {
|
||||||
out io.ReadCloser
|
out io.ReadCloser
|
||||||
log Logger
|
log config.Logger
|
||||||
cfg Config
|
cfg config.Config
|
||||||
cmd *exec.Cmd
|
cmd *exec.Cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewWebcam returns a new Webcam.
|
// NewWebcam returns a new Webcam.
|
||||||
func NewWebcam(l Logger) *Webcam {
|
func NewWebcam(l config.Logger) *Webcam {
|
||||||
return &Webcam{log: l}
|
return &Webcam{log: l}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set will validate the relevant fields of the given Config struct and assign
|
// 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
|
// 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.
|
// added to the multiError and a default value is used.
|
||||||
func (w *Webcam) Set(c Config) error {
|
func (w *Webcam) Set(c config.Config) error {
|
||||||
var errs []error
|
var errs device.MultiError
|
||||||
if c.Width == 0 {
|
if c.Width == 0 {
|
||||||
errs = append(errs, errBadWidth)
|
errs = append(errs, errBadWidth)
|
||||||
c.Width = defaultWebcamWidth
|
c.Width = defaultWidth
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Height == 0 {
|
if c.Height == 0 {
|
||||||
errs = append(errs, errBadHeight)
|
errs = append(errs, errBadHeight)
|
||||||
c.Height = defaultWebcamHeight
|
c.Height = defaultHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.FrameRate == 0 {
|
if c.FrameRate == 0 {
|
||||||
errs = append(errs, errBadFrameRate)
|
errs = append(errs, errBadFrameRate)
|
||||||
c.FrameRate = defaultWebcamFrameRate
|
c.FrameRate = defaultFrameRate
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Bitrate <= 0 {
|
if c.Bitrate <= 0 {
|
||||||
errs = append(errs, errBadBitrate)
|
errs = append(errs, errBadBitrate)
|
||||||
c.Bitrate = defaultWebcamBitrate
|
c.Bitrate = defaultBitrate
|
||||||
}
|
}
|
||||||
w.cfg = c
|
w.cfg = c
|
||||||
return multiError(errs)
|
return errs
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start will build the required arguments for ffmpeg and then execute the
|
// Start will build the required arguments for ffmpeg and then execute the
|
5
go.mod
5
go.mod
|
@ -4,15 +4,16 @@ go 1.13
|
||||||
|
|
||||||
require (
|
require (
|
||||||
bitbucket.org/ausocean/iot v1.2.7
|
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
|
bitbucket.org/ausocean/utils v1.2.10
|
||||||
github.com/BurntSushi/toml v0.3.1 // indirect
|
github.com/BurntSushi/toml v0.3.1 // indirect
|
||||||
github.com/Comcast/gots v0.0.0-20190305015453-8d56e473f0f7
|
github.com/Comcast/gots v0.0.0-20190305015453-8d56e473f0f7
|
||||||
github.com/go-audio/audio v0.0.0-20181013203223-7b2a6ca21480
|
github.com/go-audio/audio v0.0.0-20181013203223-7b2a6ca21480
|
||||||
github.com/go-audio/wav v0.0.0-20181013172942-de841e69b884
|
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/mewkiz/flac v1.0.5
|
||||||
github.com/pkg/errors v0.8.1
|
github.com/pkg/errors v0.8.1
|
||||||
github.com/yobert/alsa v0.0.0-20180630182551-d38d89fa843e
|
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
|
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 h1:dZgrmVtuXnzHgybDthn0bYgAJms9euTONXBsqsx9g5M=
|
||||||
bitbucket.org/ausocean/iot v1.2.7/go.mod h1:aAWgPo2f8sD2OPmxae1E5/iD9+tKY/iW4pcQMQXUvHM=
|
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/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.9/go.mod h1:uXzX9z3PLemyURTMWRhVI8uLhPX4uuvaaO85v2hcob8=
|
||||||
bitbucket.org/ausocean/utils v1.2.10 h1:JTS7n+K+0o/FQFWKjdGgA1ElZ4TQu9aHX3wTJXOayXw=
|
bitbucket.org/ausocean/utils v1.2.10 h1:JTS7n+K+0o/FQFWKjdGgA1ElZ4TQu9aHX3wTJXOayXw=
|
||||||
bitbucket.org/ausocean/utils v1.2.10/go.mod h1:uXzX9z3PLemyURTMWRhVI8uLhPX4uuvaaO85v2hcob8=
|
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/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 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
|
||||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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/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-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 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/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 h1:2TaXIaVA4ff/MHHezOj83tCypALTFAcXOImcFWNa3jw=
|
||||||
github.com/go-audio/wav v0.0.0-20181013172942-de841e69b884/go.mod h1:UiqzUyfX0zs3pJ/DPyvS5v8sN6s5bXPUDDIVA5v8dks=
|
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/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/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/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 h1:Hc1iKlyxNHp3CV59G2E/qabUkHvEwOIJxDK0CJ7CRjA=
|
||||||
github.com/mattetti/audio v0.0.0-20180912171649-01576cde1f21/go.mod h1:LlQmBGkOuV/SKzEDXBPKauvN2UqCgzXO2XjecTGj40s=
|
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 h1:dHGW/2kf+/KZ2GGqSVayNEhL9pluKn/rr/h/QqD9Ogc=
|
||||||
github.com/mewkiz/flac v1.0.5/go.mod h1:EHZNU32dMF6alpurYyKHDLYpW1lYpBZ5WrXi/VuNIGs=
|
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 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
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/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.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
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 h1:3NIzz7weXhh3NToPgbtlQtKiVgerEaG4/nY2skGoGG0=
|
||||||
github.com/yobert/alsa v0.0.0-20180630182551-d38d89fa843e/go.mod h1:CaowXBWOiSGWEpBBV8LoVnQTVPV4ycyviC9IBLj8dRw=
|
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=
|
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 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4=
|
||||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
|
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.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||||
go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM=
|
go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM=
|
||||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
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=
|
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-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 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
|
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 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
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) {
|
func (r *Revid) startAudioDevice() (func() error, error) {
|
||||||
// Create audio device.
|
// Create audio device.
|
||||||
ac := &audio.Config{
|
ac := &audio.Config{
|
||||||
SampleRate: r.config.SampleRate,
|
SampleRate: r.cfg.SampleRate,
|
||||||
Channels: r.config.Channels,
|
Channels: r.cfg.Channels,
|
||||||
RecPeriod: r.config.RecPeriod,
|
RecPeriod: r.cfg.RecPeriod,
|
||||||
BitDepth: r.config.BitDepth,
|
BitDepth: r.cfg.BitDepth,
|
||||||
Codec: r.config.InputCodec,
|
Codec: r.cfg.InputCodec,
|
||||||
}
|
}
|
||||||
mts.Meta.Add("sampleRate", strconv.Itoa(r.config.SampleRate))
|
mts.Meta.Add("sampleRate", strconv.Itoa(r.cfg.SampleRate))
|
||||||
mts.Meta.Add("channels", strconv.Itoa(r.config.Channels))
|
mts.Meta.Add("channels", strconv.Itoa(r.cfg.Channels))
|
||||||
mts.Meta.Add("period", fmt.Sprintf("%.6f", r.config.RecPeriod))
|
mts.Meta.Add("period", fmt.Sprintf("%.6f", r.cfg.RecPeriod))
|
||||||
mts.Meta.Add("bitDepth", strconv.Itoa(r.config.BitDepth))
|
mts.Meta.Add("bitDepth", strconv.Itoa(r.cfg.BitDepth))
|
||||||
switch r.config.InputCodec {
|
switch r.cfg.InputCodec {
|
||||||
case codecutil.PCM:
|
case codecutil.PCM:
|
||||||
mts.Meta.Add("codec", "pcm")
|
mts.Meta.Add("codec", "pcm")
|
||||||
case codecutil.ADPCM:
|
case codecutil.ADPCM:
|
||||||
mts.Meta.Add("codec", "adpcm")
|
mts.Meta.Add("codec", "adpcm")
|
||||||
default:
|
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 {
|
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
|
// Start audio device
|
||||||
err = ai.Start()
|
err = ai.Start()
|
||||||
if err != nil {
|
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.
|
// Process output from audio device.
|
||||||
r.config.ChunkSize = ai.ChunkSize()
|
r.cfg.ChunkSize = ai.ChunkSize()
|
||||||
r.wg.Add(1)
|
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 {
|
return func() error {
|
||||||
ai.Stop()
|
ai.Stop()
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -23,7 +23,7 @@ LICENSE
|
||||||
along with revid in gpl.txt. If not, see http://www.gnu.org/licenses.
|
along with revid in gpl.txt. If not, see http://www.gnu.org/licenses.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package revid
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
@ -63,17 +63,24 @@ var AutoWhiteBalanceModes = [...]string{
|
||||||
"horizon",
|
"horizon",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pkg = "config: "
|
||||||
|
|
||||||
|
type Logger interface {
|
||||||
|
SetLevel(int8)
|
||||||
|
Log(level int8, message string, params ...interface{})
|
||||||
|
}
|
||||||
|
|
||||||
// quality represents video quality.
|
// quality represents video quality.
|
||||||
type quality int
|
type Quality int
|
||||||
|
|
||||||
// The different video qualities that can be used for variable bitrate when
|
// The different video qualities that can be used for variable bitrate when
|
||||||
// using the GeoVision camera.
|
// using the GeoVision camera.
|
||||||
const (
|
const (
|
||||||
qualityStandard quality = iota
|
QualityStandard Quality = iota
|
||||||
qualityFair
|
QualityFair
|
||||||
qualityGood
|
QualityGood
|
||||||
qualityGreat
|
QualityGreat
|
||||||
qualityExcellent
|
QualityExcellent
|
||||||
)
|
)
|
||||||
|
|
||||||
// Enums to define inputs, outputs and codecs.
|
// Enums to define inputs, outputs and codecs.
|
||||||
|
@ -115,7 +122,7 @@ const (
|
||||||
defaultRtpAddr = "localhost:6970"
|
defaultRtpAddr = "localhost:6970"
|
||||||
defaultCameraIP = "192.168.1.50"
|
defaultCameraIP = "192.168.1.50"
|
||||||
defaultVBR = false
|
defaultVBR = false
|
||||||
defaultVBRQuality = qualityStandard
|
defaultVBRQuality = QualityStandard
|
||||||
defaultBurstPeriod = 10 // Seconds
|
defaultBurstPeriod = 10 // Seconds
|
||||||
defaultVBRBitrate = 500 // kbps
|
defaultVBRBitrate = 500 // kbps
|
||||||
defaultCameraChan = 2
|
defaultCameraChan = 2
|
||||||
|
@ -236,7 +243,7 @@ type Config struct {
|
||||||
// VBRQuality describes the general quality of video from the GeoVision camera
|
// VBRQuality describes the general quality of video from the GeoVision camera
|
||||||
// under variable bitrate. VBRQuality can be one 5 consts defined:
|
// under variable bitrate. VBRQuality can be one 5 consts defined:
|
||||||
// qualityStandard, qualityFair, qualityGood, qualityGreat and qualityExcellent.
|
// qualityStandard, qualityFair, qualityGood, qualityGreat and qualityExcellent.
|
||||||
VBRQuality quality
|
VBRQuality Quality
|
||||||
|
|
||||||
// VBRBitrate describes maximal bitrate for the GeoVision camera when under
|
// VBRBitrate describes maximal bitrate for the GeoVision camera when under
|
||||||
// variable bitrate.
|
// variable bitrate.
|
||||||
|
@ -518,7 +525,7 @@ func (c *Config) Validate() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
switch c.VBRQuality {
|
switch c.VBRQuality {
|
||||||
case qualityStandard, qualityFair, qualityGood, qualityGreat, qualityExcellent:
|
case QualityStandard, QualityFair, QualityGood, QualityGreat, QualityExcellent:
|
||||||
default:
|
default:
|
||||||
c.Logger.Log(logger.Info, pkg+"VBRQuality bad or unset, defaulting", "VBRQuality", defaultVBRQuality)
|
c.Logger.Log(logger.Info, pkg+"VBRQuality bad or unset, defaulting", "VBRQuality", defaultVBRQuality)
|
||||||
c.VBRQuality = defaultVBRQuality
|
c.VBRQuality = defaultVBRQuality
|
|
@ -22,7 +22,7 @@ LICENSE
|
||||||
in gpl.txt. If not, see http://www.gnu.org/licenses.
|
in gpl.txt. If not, see http://www.gnu.org/licenses.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package revid
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
168
revid/inputs.go
168
revid/inputs.go
|
@ -40,10 +40,11 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"bitbucket.org/ausocean/av/codec/codecutil"
|
"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/rtcp"
|
||||||
"bitbucket.org/ausocean/av/protocol/rtp"
|
"bitbucket.org/ausocean/av/protocol/rtp"
|
||||||
"bitbucket.org/ausocean/av/protocol/rtsp"
|
"bitbucket.org/ausocean/av/protocol/rtsp"
|
||||||
|
"bitbucket.org/ausocean/av/revid/config"
|
||||||
"bitbucket.org/ausocean/utils/logger"
|
"bitbucket.org/ausocean/utils/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -53,84 +54,61 @@ const (
|
||||||
ipCamPass = "admin"
|
ipCamPass = "admin"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AVDevice describes a configurable audio or video device from which media data
|
// Constants for real time clients.
|
||||||
// can be obtained. AVDevice is an io.Reader.
|
const (
|
||||||
type AVDevice interface {
|
rtpPort = 60000
|
||||||
io.Reader
|
rtcpPort = 60001
|
||||||
|
defaultServerRTCPPort = 17301
|
||||||
// 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) 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// startRaspivid sets up things for input from raspivid i.e. starts
|
// startRaspivid sets up things for input from raspivid i.e. starts
|
||||||
// a raspivid process and pipes it's data output.
|
// a raspivid process and pipes it's data output.
|
||||||
func (r *Revid) startRaspivid() (func() error, error) {
|
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"
|
const disabled = "0"
|
||||||
args := []string{
|
args := []string{
|
||||||
"--output", "-",
|
"--output", "-",
|
||||||
"--nopreview",
|
"--nopreview",
|
||||||
"--timeout", disabled,
|
"--timeout", disabled,
|
||||||
"--width", fmt.Sprint(r.config.Width),
|
"--width", fmt.Sprint(r.cfg.Width),
|
||||||
"--height", fmt.Sprint(r.config.Height),
|
"--height", fmt.Sprint(r.cfg.Height),
|
||||||
"--bitrate", fmt.Sprint(r.config.Bitrate * 1000), // Convert from kbps to bps.
|
"--bitrate", fmt.Sprint(r.cfg.Bitrate * 1000), // Convert from kbps to bps.
|
||||||
"--framerate", fmt.Sprint(r.config.FrameRate),
|
"--framerate", fmt.Sprint(r.cfg.FrameRate),
|
||||||
"--rotation", fmt.Sprint(r.config.Rotation),
|
"--rotation", fmt.Sprint(r.cfg.Rotation),
|
||||||
"--brightness", fmt.Sprint(r.config.Brightness),
|
"--brightness", fmt.Sprint(r.cfg.Brightness),
|
||||||
"--saturation", fmt.Sprint(r.config.Saturation),
|
"--saturation", fmt.Sprint(r.cfg.Saturation),
|
||||||
"--exposure", fmt.Sprint(r.config.Exposure),
|
"--exposure", fmt.Sprint(r.cfg.Exposure),
|
||||||
"--awb", fmt.Sprint(r.config.AutoWhiteBalance),
|
"--awb", fmt.Sprint(r.cfg.AutoWhiteBalance),
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.config.FlipHorizontal {
|
if r.cfg.FlipHorizontal {
|
||||||
args = append(args, "--hflip")
|
args = append(args, "--hflip")
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.config.FlipVertical {
|
if r.cfg.FlipVertical {
|
||||||
args = append(args, "--vflip")
|
args = append(args, "--vflip")
|
||||||
}
|
}
|
||||||
if r.config.FlipHorizontal {
|
if r.cfg.FlipHorizontal {
|
||||||
args = append(args, "--hflip")
|
args = append(args, "--hflip")
|
||||||
}
|
}
|
||||||
|
|
||||||
switch r.config.InputCodec {
|
switch r.cfg.InputCodec {
|
||||||
default:
|
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:
|
case codecutil.H264:
|
||||||
args = append(args,
|
args = append(args,
|
||||||
"--codec", "H264",
|
"--codec", "H264",
|
||||||
"--inline",
|
"--inline",
|
||||||
"--intra", fmt.Sprint(r.config.MinFrames),
|
"--intra", fmt.Sprint(r.cfg.MinFrames),
|
||||||
)
|
)
|
||||||
if r.config.VBR {
|
if r.cfg.VBR {
|
||||||
args = append(args, "-qp", fmt.Sprint(r.config.Quantization))
|
args = append(args, "-qp", fmt.Sprint(r.cfg.Quantization))
|
||||||
}
|
}
|
||||||
case codecutil.MJPEG:
|
case codecutil.MJPEG:
|
||||||
args = append(args, "--codec", "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...)
|
r.cmd = exec.Command("raspivid", args...)
|
||||||
|
|
||||||
stdout, err := r.cmd.StdoutPipe()
|
stdout, err := r.cmd.StdoutPipe()
|
||||||
|
@ -151,28 +129,28 @@ func (r *Revid) startRaspivid() (func() error, error) {
|
||||||
func (r *Revid) startV4L() (func() error, error) {
|
func (r *Revid) startV4L() (func() error, error) {
|
||||||
const defaultVideo = "/dev/video0"
|
const defaultVideo = "/dev/video0"
|
||||||
|
|
||||||
r.config.Logger.Log(logger.Info, pkg+"starting webcam")
|
r.cfg.Logger.Log(logger.Info, pkg+"starting webcam")
|
||||||
if r.config.InputPath == "" {
|
if r.cfg.InputPath == "" {
|
||||||
r.config.Logger.Log(logger.Info, pkg+"using default video device", "device", defaultVideo)
|
r.cfg.Logger.Log(logger.Info, pkg+"using default video device", "device", defaultVideo)
|
||||||
r.config.InputPath = defaultVideo
|
r.cfg.InputPath = defaultVideo
|
||||||
}
|
}
|
||||||
|
|
||||||
args := []string{
|
args := []string{
|
||||||
"-i", r.config.InputPath,
|
"-i", r.cfg.InputPath,
|
||||||
"-f", "h264",
|
"-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,
|
args = append(args,
|
||||||
"-b:v", fmt.Sprint(br),
|
"-b:v", fmt.Sprint(br),
|
||||||
"-maxrate", fmt.Sprint(br),
|
"-maxrate", fmt.Sprint(br),
|
||||||
"-bufsize", fmt.Sprint(br/2),
|
"-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...)
|
r.cmd = exec.Command("ffmpeg", args...)
|
||||||
|
|
||||||
stdout, err := r.cmd.StdoutPipe()
|
stdout, err := r.cmd.StdoutPipe()
|
||||||
|
@ -182,7 +160,7 @@ func (r *Revid) startV4L() (func() error, error) {
|
||||||
|
|
||||||
err = r.cmd.Start()
|
err = r.cmd.Start()
|
||||||
if err != nil {
|
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
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -194,9 +172,9 @@ func (r *Revid) startV4L() (func() error, error) {
|
||||||
// setupInputForFile sets up input from file and starts the revid.processFrom
|
// setupInputForFile sets up input from file and starts the revid.processFrom
|
||||||
// routine.
|
// routine.
|
||||||
func (r *Revid) setupInputForFile() (func() error, error) {
|
func (r *Revid) setupInputForFile() (func() error, error) {
|
||||||
f, err := os.Open(r.config.InputPath)
|
f, err := os.Open(r.cfg.InputPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.config.Logger.Log(logger.Error, err.Error())
|
r.cfg.Logger.Log(logger.Error, err.Error())
|
||||||
r.Stop()
|
r.Stop()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -214,92 +192,92 @@ func (r *Revid) setupInputForFile() (func() error, error) {
|
||||||
// TODO(saxon): this function should really be startGeoVision. It's much too
|
// TODO(saxon): this function should really be startGeoVision. It's much too
|
||||||
// specific to be called startRTSPCamera.
|
// specific to be called startRTSPCamera.
|
||||||
func (r *Revid) startRTSPCamera() (func() error, error) {
|
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(
|
err := gvctrl.Set(
|
||||||
r.config.CameraIP,
|
r.cfg.CameraIP,
|
||||||
gvctrl.Channel(r.config.CameraChan),
|
gvctrl.Channel(r.cfg.CameraChan),
|
||||||
gvctrl.CodecOut(
|
gvctrl.CodecOut(
|
||||||
map[uint8]gvctrl.Codec{
|
map[uint8]gvctrl.Codec{
|
||||||
codecutil.H264: gvctrl.CodecH264,
|
codecutil.H264: gvctrl.CodecH264,
|
||||||
codecutil.H265: gvctrl.CodecH265,
|
codecutil.H265: gvctrl.CodecH265,
|
||||||
codecutil.MJPEG: gvctrl.CodecMJPEG,
|
codecutil.MJPEG: gvctrl.CodecMJPEG,
|
||||||
}[r.config.InputCodec],
|
}[r.cfg.InputCodec],
|
||||||
),
|
),
|
||||||
gvctrl.Height(int(r.config.Height)),
|
gvctrl.Height(int(r.cfg.Height)),
|
||||||
gvctrl.FrameRate(int(r.config.FrameRate)),
|
gvctrl.FrameRate(int(r.cfg.FrameRate)),
|
||||||
gvctrl.VariableBitrate(r.config.VBR),
|
gvctrl.VariableBitrate(r.cfg.VBR),
|
||||||
gvctrl.VBRQuality(
|
gvctrl.VBRQuality(
|
||||||
map[quality]gvctrl.Quality{
|
map[config.Quality]gvctrl.Quality{
|
||||||
qualityStandard: gvctrl.QualityStandard,
|
config.QualityStandard: gvctrl.QualityStandard,
|
||||||
qualityFair: gvctrl.QualityFair,
|
config.QualityFair: gvctrl.QualityFair,
|
||||||
qualityGood: gvctrl.QualityGood,
|
config.QualityGood: gvctrl.QualityGood,
|
||||||
qualityGreat: gvctrl.QualityGreat,
|
config.QualityGreat: gvctrl.QualityGreat,
|
||||||
qualityExcellent: gvctrl.QualityExcellent,
|
config.QualityExcellent: gvctrl.QualityExcellent,
|
||||||
}[r.config.VBRQuality],
|
}[r.cfg.VBRQuality],
|
||||||
),
|
),
|
||||||
gvctrl.VBRBitrate(r.config.VBRBitrate),
|
gvctrl.VBRBitrate(r.cfg.VBRBitrate),
|
||||||
gvctrl.CBRBitrate(int(r.config.Bitrate)),
|
gvctrl.CBRBitrate(int(r.cfg.Bitrate)),
|
||||||
gvctrl.Refresh(float64(r.config.MinFrames)/float64(r.config.FrameRate)),
|
gvctrl.Refresh(float64(r.cfg.MinFrames)/float64(r.cfg.FrameRate)),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not set IPCamera settings: %w", err)
|
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)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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()
|
resp, err := rtspClt.Options()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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()
|
resp, err = rtspClt.Describe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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))
|
resp, err = rtspClt.Setup("track1", fmt.Sprintf("RTP/AVP;unicast;client_port=%d-%d", rtpPort, rtcpPort))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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)
|
rtpCltAddr, rtcpCltAddr, rtcpSvrAddr, err := formAddrs(local, remote, *resp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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)
|
rtpClt, err := rtp.NewClient(rtpCltAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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.
|
// Check errors from RTCP client until it has stopped running.
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
err, ok := <-rtcpClt.Err()
|
err, ok := <-rtcpClt.Err()
|
||||||
if ok {
|
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 {
|
} else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -309,20 +287,20 @@ func (r *Revid) startRTSPCamera() (func() error, error) {
|
||||||
// Start the RTCP client.
|
// Start the RTCP client.
|
||||||
rtcpClt.Start()
|
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.
|
// Start reading data from the RTP client.
|
||||||
r.wg.Add(1)
|
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()
|
resp, err = rtspClt.Play()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
r.config.Logger.Log(logger.Debug, pkg+"RTSP server PLAY response", "response", resp.String())
|
r.cfg.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.Info, pkg+"play requested, now receiving stream")
|
||||||
|
|
||||||
return func() error {
|
return func() error {
|
||||||
err := rtpClt.Close()
|
err := rtpClt.Close()
|
||||||
|
@ -337,7 +315,7 @@ func (r *Revid) startRTSPCamera() (func() error, error) {
|
||||||
|
|
||||||
rtcpClt.Stop()
|
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
|
return nil
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
@ -375,6 +353,6 @@ func parseSvrRTCPPort(resp rtsp.Response) (int, error) {
|
||||||
// then send individual access units to revid's encoders.
|
// then send individual access units to revid's encoders.
|
||||||
func (r *Revid) processFrom(read io.Reader, delay time.Duration) {
|
func (r *Revid) processFrom(read io.Reader, delay time.Duration) {
|
||||||
r.err <- r.lexTo(r.encoders, read, delay)
|
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()
|
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/codec/mjpeg"
|
||||||
"bitbucket.org/ausocean/av/container/flv"
|
"bitbucket.org/ausocean/av/container/flv"
|
||||||
"bitbucket.org/ausocean/av/container/mts"
|
"bitbucket.org/ausocean/av/container/mts"
|
||||||
|
"bitbucket.org/ausocean/av/revid/config"
|
||||||
"bitbucket.org/ausocean/iot/pi/netsender"
|
"bitbucket.org/ausocean/iot/pi/netsender"
|
||||||
"bitbucket.org/ausocean/utils/ioext"
|
"bitbucket.org/ausocean/utils/ioext"
|
||||||
"bitbucket.org/ausocean/utils/logger"
|
"bitbucket.org/ausocean/utils/logger"
|
||||||
"bitbucket.org/ausocean/utils/ring"
|
"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.
|
// RTMP connection properties.
|
||||||
const (
|
const (
|
||||||
rtmpConnectionMaxTries = 5
|
rtmpConnectionMaxTries = 5
|
||||||
rtmpConnectionTimeout = 10
|
rtmpConnectionTimeout = 10
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
rtpPort = 60000
|
|
||||||
rtcpPort = 60001
|
|
||||||
defaultServerRTCPPort = 17301
|
|
||||||
)
|
|
||||||
|
|
||||||
const pkg = "revid: "
|
const pkg = "revid: "
|
||||||
|
|
||||||
type Logger interface {
|
type Logger interface {
|
||||||
|
@ -76,7 +84,7 @@ type Revid struct {
|
||||||
// For historical reasons it also handles logging.
|
// For historical reasons it also handles logging.
|
||||||
// FIXME(kortschak): The relationship of concerns
|
// FIXME(kortschak): The relationship of concerns
|
||||||
// in config/ns is weird.
|
// in config/ns is weird.
|
||||||
config Config
|
cfg config.Config
|
||||||
|
|
||||||
// ns holds the netsender.Sender responsible for HTTP.
|
// ns holds the netsender.Sender responsible for HTTP.
|
||||||
ns *netsender.Sender
|
ns *netsender.Sender
|
||||||
|
@ -116,13 +124,13 @@ type Revid struct {
|
||||||
|
|
||||||
// New returns a pointer to a new Revid with the desired configuration, and/or
|
// 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.
|
// 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)}
|
r := Revid{ns: ns, err: make(chan error)}
|
||||||
err := r.setConfig(c)
|
err := r.setConfig(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("could not set config, failed with error: %v", err)
|
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()
|
go r.handleErrors()
|
||||||
return &r, nil
|
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 returns a copy of revids current config.
|
||||||
//
|
//
|
||||||
// Config is not safe for concurrent use.
|
// Config is not safe for concurrent use.
|
||||||
func (r *Revid) Config() Config {
|
func (r *Revid) Config() config.Config {
|
||||||
return r.config
|
return r.cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(Saxon): put more thought into error severity and how to handle these.
|
// TODO(Saxon): put more thought into error severity and how to handle these.
|
||||||
|
@ -139,7 +147,7 @@ func (r *Revid) handleErrors() {
|
||||||
for {
|
for {
|
||||||
err := <-r.err
|
err := <-r.err
|
||||||
if err != nil {
|
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
|
// reset swaps the current config of a Revid with the passed
|
||||||
// configuration; checking validity and returning errors if not valid. It then
|
// configuration; checking validity and returning errors if not valid. It then
|
||||||
// sets up the data pipeline accordingly to this configuration.
|
// sets up the data pipeline accordingly to this configuration.
|
||||||
func (r *Revid) reset(config Config) error {
|
func (r *Revid) reset(c config.Config) error {
|
||||||
err := r.setConfig(config)
|
err := r.setConfig(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
r.config.Logger.SetLevel(config.LogLevel)
|
r.cfg.Logger.SetLevel(c.LogLevel)
|
||||||
|
|
||||||
err = r.setupPipeline(
|
err = r.setupPipeline(
|
||||||
func(dst io.WriteCloser, fps float64) (io.WriteCloser, error) {
|
func(dst io.WriteCloser, fps float64) (io.WriteCloser, error) {
|
||||||
var st int
|
var st int
|
||||||
var encOptions []func(*mts.Encoder) error
|
var encOptions []func(*mts.Encoder) error
|
||||||
|
|
||||||
switch r.config.Input {
|
switch r.cfg.Input {
|
||||||
case InputRaspivid:
|
case config.InputRaspivid:
|
||||||
switch r.config.InputCodec {
|
switch r.cfg.InputCodec {
|
||||||
case codecutil.H264:
|
case codecutil.H264:
|
||||||
st = mts.EncodeH264
|
st = mts.EncodeH264
|
||||||
case codecutil.MJPEG:
|
case codecutil.MJPEG:
|
||||||
st = mts.EncodeMJPEG
|
st = mts.EncodeMJPEG
|
||||||
encOptions = append(encOptions, mts.PacketBasedPSI(int(r.config.MinFrames)))
|
encOptions = append(encOptions, mts.PacketBasedPSI(int(r.cfg.MinFrames)))
|
||||||
default:
|
default:
|
||||||
panic("unknown input codec for raspivid input")
|
panic("unknown input codec for raspivid input")
|
||||||
}
|
}
|
||||||
case InputFile, InputV4L:
|
case config.InputFile, config.InputV4L:
|
||||||
st = mts.EncodeH264
|
st = mts.EncodeH264
|
||||||
case InputRTSP:
|
case config.InputRTSP:
|
||||||
switch r.config.InputCodec {
|
switch r.cfg.InputCodec {
|
||||||
case codecutil.H265:
|
case codecutil.H265:
|
||||||
st = mts.EncodeH265
|
st = mts.EncodeH265
|
||||||
case codecutil.H264:
|
case codecutil.H264:
|
||||||
st = mts.EncodeH264
|
st = mts.EncodeH264
|
||||||
case codecutil.MJPEG:
|
case codecutil.MJPEG:
|
||||||
st = mts.EncodeMJPEG
|
st = mts.EncodeMJPEG
|
||||||
encOptions = append(encOptions, mts.PacketBasedPSI(int(r.config.MinFrames)))
|
encOptions = append(encOptions, mts.PacketBasedPSI(int(r.cfg.MinFrames)))
|
||||||
default:
|
default:
|
||||||
panic("unknown input codec for RTSP input")
|
panic("unknown input codec for RTSP input")
|
||||||
}
|
}
|
||||||
case InputAudio:
|
case config.InputAudio:
|
||||||
st = mts.EncodeAudio
|
st = mts.EncodeAudio
|
||||||
default:
|
default:
|
||||||
panic("unknown input type")
|
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
|
// setConfig takes a config, checks it's validity and then replaces the current
|
||||||
// revid config.
|
// revid config.
|
||||||
func (r *Revid) setConfig(config Config) error {
|
func (r *Revid) setConfig(config config.Config) error {
|
||||||
r.config.Logger = config.Logger
|
r.cfg.Logger = config.Logger
|
||||||
err := config.Validate()
|
err := config.Validate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("Config struct is bad: " + err.Error())
|
return errors.New("Config struct is bad: " + err.Error())
|
||||||
}
|
}
|
||||||
r.config = config
|
r.cfg = config
|
||||||
return nil
|
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
|
// to mtsSenders if the output requires MPEGTS encoding, or flvSenders if the
|
||||||
// output requires FLV encoding.
|
// output requires FLV encoding.
|
||||||
var w io.WriteCloser
|
var w io.WriteCloser
|
||||||
for _, out := range r.config.Outputs {
|
for _, out := range r.cfg.Outputs {
|
||||||
switch out {
|
switch out {
|
||||||
case OutputHTTP:
|
case config.OutputHTTP:
|
||||||
w = newMtsSender(
|
w = newMtsSender(
|
||||||
newHttpSender(r.ns, r.config.Logger.Log),
|
newHttpSender(r.ns, r.cfg.Logger.Log),
|
||||||
r.config.Logger.Log,
|
r.cfg.Logger.Log,
|
||||||
ring.NewBuffer(r.config.MTSRBSize, r.config.MTSRBElementSize, time.Duration(r.config.MTSRBWriteTimeout)*time.Second),
|
ring.NewBuffer(r.cfg.MTSRBSize, r.cfg.MTSRBElementSize, time.Duration(r.cfg.MTSRBWriteTimeout)*time.Second),
|
||||||
r.config.ClipDuration,
|
r.cfg.ClipDuration,
|
||||||
)
|
)
|
||||||
mtsSenders = append(mtsSenders, w)
|
mtsSenders = append(mtsSenders, w)
|
||||||
case OutputRTP:
|
case config.OutputRTP:
|
||||||
w, err := newRtpSender(r.config.RTPAddress, r.config.Logger.Log, r.config.FrameRate)
|
w, err := newRtpSender(r.cfg.RTPAddress, r.cfg.Logger.Log, r.cfg.FrameRate)
|
||||||
if err != nil {
|
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)
|
mtsSenders = append(mtsSenders, w)
|
||||||
case OutputFile:
|
case config.OutputFile:
|
||||||
w, err := newFileSender(r.config.OutputPath)
|
w, err := newFileSender(r.cfg.OutputPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
mtsSenders = append(mtsSenders, w)
|
mtsSenders = append(mtsSenders, w)
|
||||||
case OutputRTMP:
|
case config.OutputRTMP:
|
||||||
w, err := newRtmpSender(
|
w, err := newRtmpSender(
|
||||||
r.config.RTMPURL,
|
r.cfg.RTMPURL,
|
||||||
rtmpConnectionTimeout,
|
rtmpConnectionTimeout,
|
||||||
rtmpConnectionMaxTries,
|
rtmpConnectionMaxTries,
|
||||||
ring.NewBuffer(r.config.RTMPRBSize, r.config.RTMPRBElementSize, time.Duration(r.config.RTMPRBWriteTimeout)*time.Second),
|
ring.NewBuffer(r.cfg.RTMPRBSize, r.cfg.RTMPRBElementSize, time.Duration(r.cfg.RTMPRBWriteTimeout)*time.Second),
|
||||||
r.config.Logger.Log,
|
r.cfg.Logger.Log,
|
||||||
)
|
)
|
||||||
if err != nil {
|
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)
|
flvSenders = append(flvSenders, w)
|
||||||
}
|
}
|
||||||
|
@ -286,7 +294,7 @@ func (r *Revid) setupPipeline(mtsEnc func(dst io.WriteCloser, rate float64) (io.
|
||||||
// as a destination.
|
// as a destination.
|
||||||
if len(mtsSenders) != 0 {
|
if len(mtsSenders) != 0 {
|
||||||
mw := multiWriter(mtsSenders...)
|
mw := multiWriter(mtsSenders...)
|
||||||
e, _ := mtsEnc(mw, r.config.WriteRate)
|
e, _ := mtsEnc(mw, r.cfg.WriteRate)
|
||||||
encoders = append(encoders, e)
|
encoders = append(encoders, e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -295,7 +303,7 @@ func (r *Revid) setupPipeline(mtsEnc func(dst io.WriteCloser, rate float64) (io.
|
||||||
// as a destination.
|
// as a destination.
|
||||||
if len(flvSenders) != 0 {
|
if len(flvSenders) != 0 {
|
||||||
mw := multiWriter(flvSenders...)
|
mw := multiWriter(flvSenders...)
|
||||||
e, err := flvEnc(mw, int(r.config.FrameRate))
|
e, err := flvEnc(mw, int(r.cfg.FrameRate))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -304,23 +312,23 @@ func (r *Revid) setupPipeline(mtsEnc func(dst io.WriteCloser, rate float64) (io.
|
||||||
|
|
||||||
r.encoders = multiWriter(encoders...)
|
r.encoders = multiWriter(encoders...)
|
||||||
|
|
||||||
switch r.config.Input {
|
switch r.cfg.Input {
|
||||||
case InputRaspivid:
|
case config.InputRaspivid:
|
||||||
r.setupInput = r.startRaspivid
|
r.setupInput = r.startRaspivid
|
||||||
switch r.config.InputCodec {
|
switch r.cfg.InputCodec {
|
||||||
case codecutil.H264:
|
case codecutil.H264:
|
||||||
r.lexTo = h264.Lex
|
r.lexTo = h264.Lex
|
||||||
case codecutil.MJPEG:
|
case codecutil.MJPEG:
|
||||||
r.lexTo = mjpeg.Lex
|
r.lexTo = mjpeg.Lex
|
||||||
}
|
}
|
||||||
case InputV4L:
|
case config.InputV4L:
|
||||||
r.setupInput = r.startV4L
|
r.setupInput = r.startV4L
|
||||||
r.lexTo = h264.Lex
|
r.lexTo = h264.Lex
|
||||||
case InputFile:
|
case config.InputFile:
|
||||||
r.setupInput = r.setupInputForFile
|
r.setupInput = r.setupInputForFile
|
||||||
case InputRTSP:
|
case config.InputRTSP:
|
||||||
r.setupInput = r.startRTSPCamera
|
r.setupInput = r.startRTSPCamera
|
||||||
switch r.config.InputCodec {
|
switch r.cfg.InputCodec {
|
||||||
case codecutil.H264:
|
case codecutil.H264:
|
||||||
r.lexTo = h264.NewExtractor().Extract
|
r.lexTo = h264.NewExtractor().Extract
|
||||||
case codecutil.H265:
|
case codecutil.H265:
|
||||||
|
@ -328,9 +336,9 @@ func (r *Revid) setupPipeline(mtsEnc func(dst io.WriteCloser, rate float64) (io.
|
||||||
case codecutil.MJPEG:
|
case codecutil.MJPEG:
|
||||||
panic("not implemented")
|
panic("not implemented")
|
||||||
}
|
}
|
||||||
case InputAudio:
|
case config.InputAudio:
|
||||||
r.setupInput = r.startAudioDevice
|
r.setupInput = r.startAudioDevice
|
||||||
r.lexTo = codecutil.NewByteLexer(&r.config.ChunkSize).Lex
|
r.lexTo = codecutil.NewByteLexer(&r.cfg.ChunkSize).Lex
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -342,15 +350,15 @@ func (r *Revid) setupPipeline(mtsEnc func(dst io.WriteCloser, rate float64) (io.
|
||||||
// Start is safe for concurrent use.
|
// Start is safe for concurrent use.
|
||||||
func (r *Revid) Start() error {
|
func (r *Revid) Start() error {
|
||||||
if r.IsRunning() {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
r.mu.Lock()
|
r.mu.Lock()
|
||||||
defer r.mu.Unlock()
|
defer r.mu.Unlock()
|
||||||
|
|
||||||
r.config.Logger.Log(logger.Info, pkg+"starting Revid")
|
r.cfg.Logger.Log(logger.Info, pkg+"starting Revid")
|
||||||
err := r.reset(r.config)
|
err := r.reset(r.cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.Stop()
|
r.Stop()
|
||||||
return err
|
return err
|
||||||
|
@ -369,7 +377,7 @@ func (r *Revid) Start() error {
|
||||||
// Stop is safe for concurrent use.
|
// Stop is safe for concurrent use.
|
||||||
func (r *Revid) Stop() {
|
func (r *Revid) Stop() {
|
||||||
if !r.IsRunning() {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -379,26 +387,26 @@ func (r *Revid) Stop() {
|
||||||
if r.closeInput != nil {
|
if r.closeInput != nil {
|
||||||
err := r.closeInput()
|
err := r.closeInput()
|
||||||
if err != nil {
|
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()
|
err := r.encoders.Close()
|
||||||
if err != nil {
|
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 {
|
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.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.wg.Wait()
|
||||||
|
|
||||||
r.config.Logger.Log(logger.Info, pkg+"revid stopped")
|
r.cfg.Logger.Log(logger.Info, pkg+"revid stopped")
|
||||||
r.running = false
|
r.running = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -423,239 +431,239 @@ func (r *Revid) Update(vars map[string]string) error {
|
||||||
for key, value := range vars {
|
for key, value := range vars {
|
||||||
switch key {
|
switch key {
|
||||||
case "Input":
|
case "Input":
|
||||||
v, ok := map[string]uint8{"raspivid": InputRaspivid, "rtsp": InputRTSP}[strings.ToLower(value)]
|
v, ok := map[string]uint8{"raspivid": config.InputRaspivid, "rtsp": config.InputRTSP}[strings.ToLower(value)]
|
||||||
if !ok {
|
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
|
break
|
||||||
}
|
}
|
||||||
r.config.Input = v
|
r.cfg.Input = v
|
||||||
case "Saturation":
|
case "Saturation":
|
||||||
s, err := strconv.ParseInt(value, 10, 0)
|
s, err := strconv.ParseInt(value, 10, 0)
|
||||||
if err != nil {
|
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
|
break
|
||||||
}
|
}
|
||||||
r.config.Saturation = int(s)
|
r.cfg.Saturation = int(s)
|
||||||
case "Brightness":
|
case "Brightness":
|
||||||
b, err := strconv.ParseUint(value, 10, 0)
|
b, err := strconv.ParseUint(value, 10, 0)
|
||||||
if err != nil {
|
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
|
break
|
||||||
}
|
}
|
||||||
r.config.Brightness = uint(b)
|
r.cfg.Brightness = uint(b)
|
||||||
case "Exposure":
|
case "Exposure":
|
||||||
r.config.Exposure = value
|
r.cfg.Exposure = value
|
||||||
case "AutoWhiteBalance":
|
case "AutoWhiteBalance":
|
||||||
r.config.AutoWhiteBalance = value
|
r.cfg.AutoWhiteBalance = value
|
||||||
case "InputCodec":
|
case "InputCodec":
|
||||||
switch value {
|
switch value {
|
||||||
case "H264":
|
case "H264":
|
||||||
r.config.InputCodec = codecutil.H264
|
r.cfg.InputCodec = codecutil.H264
|
||||||
case "MJPEG":
|
case "MJPEG":
|
||||||
r.config.InputCodec = codecutil.MJPEG
|
r.cfg.InputCodec = codecutil.MJPEG
|
||||||
default:
|
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":
|
case "Output":
|
||||||
outputs := strings.Split(value, ",")
|
outputs := strings.Split(value, ",")
|
||||||
r.config.Outputs = make([]uint8, len(outputs))
|
r.cfg.Outputs = make([]uint8, len(outputs))
|
||||||
|
|
||||||
for i, output := range outputs {
|
for i, output := range outputs {
|
||||||
switch output {
|
switch output {
|
||||||
case "File":
|
case "File":
|
||||||
r.config.Outputs[i] = OutputFile
|
r.cfg.Outputs[i] = config.OutputFile
|
||||||
case "Http":
|
case "Http":
|
||||||
r.config.Outputs[i] = OutputHTTP
|
r.cfg.Outputs[i] = config.OutputHTTP
|
||||||
case "Rtmp":
|
case "Rtmp":
|
||||||
r.config.Outputs[i] = OutputRTMP
|
r.cfg.Outputs[i] = config.OutputRTMP
|
||||||
case "Rtp":
|
case "Rtp":
|
||||||
r.config.Outputs[i] = OutputRTP
|
r.cfg.Outputs[i] = config.OutputRTP
|
||||||
default:
|
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
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case "RtmpUrl":
|
case "RtmpUrl":
|
||||||
r.config.RTMPURL = value
|
r.cfg.RTMPURL = value
|
||||||
case "RtpAddress":
|
case "RtpAddress":
|
||||||
r.config.RTPAddress = value
|
r.cfg.RTPAddress = value
|
||||||
case "Bitrate":
|
case "Bitrate":
|
||||||
v, err := strconv.ParseUint(value, 10, 0)
|
v, err := strconv.ParseUint(value, 10, 0)
|
||||||
if err != nil {
|
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
|
break
|
||||||
}
|
}
|
||||||
r.config.Bitrate = uint(v)
|
r.cfg.Bitrate = uint(v)
|
||||||
case "OutputPath":
|
case "OutputPath":
|
||||||
r.config.OutputPath = value
|
r.cfg.OutputPath = value
|
||||||
case "InputPath":
|
case "InputPath":
|
||||||
r.config.InputPath = value
|
r.cfg.InputPath = value
|
||||||
case "Height":
|
case "Height":
|
||||||
h, err := strconv.ParseUint(value, 10, 0)
|
h, err := strconv.ParseUint(value, 10, 0)
|
||||||
if err != nil {
|
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
|
break
|
||||||
}
|
}
|
||||||
r.config.Height = uint(h)
|
r.cfg.Height = uint(h)
|
||||||
case "Width":
|
case "Width":
|
||||||
w, err := strconv.ParseUint(value, 10, 0)
|
w, err := strconv.ParseUint(value, 10, 0)
|
||||||
if err != nil {
|
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
|
break
|
||||||
}
|
}
|
||||||
r.config.Width = uint(w)
|
r.cfg.Width = uint(w)
|
||||||
case "FrameRate":
|
case "FrameRate":
|
||||||
v, err := strconv.ParseUint(value, 10, 0)
|
v, err := strconv.ParseUint(value, 10, 0)
|
||||||
if err != nil {
|
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
|
break
|
||||||
}
|
}
|
||||||
r.config.FrameRate = uint(v)
|
r.cfg.FrameRate = uint(v)
|
||||||
case "Rotation":
|
case "Rotation":
|
||||||
v, err := strconv.ParseUint(value, 10, 0)
|
v, err := strconv.ParseUint(value, 10, 0)
|
||||||
if err != nil || v > 359 {
|
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
|
break
|
||||||
}
|
}
|
||||||
r.config.Rotation = uint(v)
|
r.cfg.Rotation = uint(v)
|
||||||
case "HttpAddress":
|
case "HttpAddress":
|
||||||
r.config.HTTPAddress = value
|
r.cfg.HTTPAddress = value
|
||||||
case "Quantization":
|
case "Quantization":
|
||||||
v, err := strconv.Atoi(value)
|
v, err := strconv.Atoi(value)
|
||||||
if err != nil {
|
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
|
break
|
||||||
}
|
}
|
||||||
r.config.Quantization = uint(v)
|
r.cfg.Quantization = uint(v)
|
||||||
case "MinFrames":
|
case "MinFrames":
|
||||||
v, err := strconv.Atoi(value)
|
v, err := strconv.Atoi(value)
|
||||||
if err != nil {
|
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
|
break
|
||||||
}
|
}
|
||||||
r.config.MinFrames = uint(v)
|
r.cfg.MinFrames = uint(v)
|
||||||
|
|
||||||
case "ClipDuration":
|
case "ClipDuration":
|
||||||
v, err := strconv.Atoi(value)
|
v, err := strconv.Atoi(value)
|
||||||
if err != nil {
|
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
|
break
|
||||||
}
|
}
|
||||||
r.config.ClipDuration = time.Duration(v) * time.Second
|
r.cfg.ClipDuration = time.Duration(v) * time.Second
|
||||||
|
|
||||||
case "HorizontalFlip":
|
case "HorizontalFlip":
|
||||||
switch strings.ToLower(value) {
|
switch strings.ToLower(value) {
|
||||||
case "true":
|
case "true":
|
||||||
r.config.FlipHorizontal = true
|
r.cfg.FlipHorizontal = true
|
||||||
case "false":
|
case "false":
|
||||||
r.config.FlipHorizontal = false
|
r.cfg.FlipHorizontal = false
|
||||||
default:
|
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":
|
case "VerticalFlip":
|
||||||
switch strings.ToLower(value) {
|
switch strings.ToLower(value) {
|
||||||
case "true":
|
case "true":
|
||||||
r.config.FlipVertical = true
|
r.cfg.FlipVertical = true
|
||||||
case "false":
|
case "false":
|
||||||
r.config.FlipVertical = false
|
r.cfg.FlipVertical = false
|
||||||
default:
|
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":
|
case "BurstPeriod":
|
||||||
v, err := strconv.ParseUint(value, 10, 0)
|
v, err := strconv.ParseUint(value, 10, 0)
|
||||||
if err != nil {
|
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
|
break
|
||||||
}
|
}
|
||||||
r.config.BurstPeriod = uint(v)
|
r.cfg.BurstPeriod = uint(v)
|
||||||
case "Logging":
|
case "Logging":
|
||||||
switch value {
|
switch value {
|
||||||
case "Debug":
|
case "Debug":
|
||||||
r.config.LogLevel = logger.Debug
|
r.cfg.LogLevel = logger.Debug
|
||||||
case "Info":
|
case "Info":
|
||||||
r.config.LogLevel = logger.Info
|
r.cfg.LogLevel = logger.Info
|
||||||
case "Warning":
|
case "Warning":
|
||||||
r.config.LogLevel = logger.Warning
|
r.cfg.LogLevel = logger.Warning
|
||||||
case "Error":
|
case "Error":
|
||||||
r.config.LogLevel = logger.Error
|
r.cfg.LogLevel = logger.Error
|
||||||
case "Fatal":
|
case "Fatal":
|
||||||
r.config.LogLevel = logger.Fatal
|
r.cfg.LogLevel = logger.Fatal
|
||||||
default:
|
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":
|
case "RTMPRBSize":
|
||||||
v, err := strconv.Atoi(value)
|
v, err := strconv.Atoi(value)
|
||||||
if err != nil || v < 0 {
|
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
|
break
|
||||||
}
|
}
|
||||||
r.config.RTMPRBSize = v
|
r.cfg.RTMPRBSize = v
|
||||||
case "RTMPRBElementSize":
|
case "RTMPRBElementSize":
|
||||||
v, err := strconv.Atoi(value)
|
v, err := strconv.Atoi(value)
|
||||||
if err != nil || v < 0 {
|
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
|
break
|
||||||
}
|
}
|
||||||
r.config.RTMPRBElementSize = v
|
r.cfg.RTMPRBElementSize = v
|
||||||
case "RTMPRBWriteTimeout":
|
case "RTMPRBWriteTimeout":
|
||||||
v, err := strconv.Atoi(value)
|
v, err := strconv.Atoi(value)
|
||||||
if err != nil || v <= 0 {
|
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
|
break
|
||||||
}
|
}
|
||||||
r.config.RTMPRBWriteTimeout = v
|
r.cfg.RTMPRBWriteTimeout = v
|
||||||
case "MTSRBSize":
|
case "MTSRBSize":
|
||||||
v, err := strconv.Atoi(value)
|
v, err := strconv.Atoi(value)
|
||||||
if err != nil || v < 0 {
|
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
|
break
|
||||||
}
|
}
|
||||||
r.config.MTSRBSize = v
|
r.cfg.MTSRBSize = v
|
||||||
case "MTSRBElementSize":
|
case "MTSRBElementSize":
|
||||||
v, err := strconv.Atoi(value)
|
v, err := strconv.Atoi(value)
|
||||||
if err != nil || v < 0 {
|
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
|
break
|
||||||
}
|
}
|
||||||
r.config.MTSRBElementSize = v
|
r.cfg.MTSRBElementSize = v
|
||||||
case "MTSRBWriteTimeout":
|
case "MTSRBWriteTimeout":
|
||||||
v, err := strconv.Atoi(value)
|
v, err := strconv.Atoi(value)
|
||||||
if err != nil || v <= 0 {
|
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
|
break
|
||||||
}
|
}
|
||||||
r.config.MTSRBWriteTimeout = v
|
r.cfg.MTSRBWriteTimeout = v
|
||||||
case "VBR":
|
case "VBR":
|
||||||
v, ok := map[string]bool{"true": true, "false": false}[strings.ToLower(value)]
|
v, ok := map[string]bool{"true": true, "false": false}[strings.ToLower(value)]
|
||||||
if !ok {
|
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
|
break
|
||||||
}
|
}
|
||||||
r.config.VBR = v
|
r.cfg.VBR = v
|
||||||
case "VBRQuality":
|
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 {
|
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
|
break
|
||||||
}
|
}
|
||||||
r.config.VBRQuality = v
|
r.cfg.VBRQuality = v
|
||||||
case "VBRBitrate":
|
case "VBRBitrate":
|
||||||
v, err := strconv.Atoi(value)
|
v, err := strconv.Atoi(value)
|
||||||
if err != nil || v <= 0 {
|
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
|
break
|
||||||
}
|
}
|
||||||
r.config.VBRBitrate = v
|
r.cfg.VBRBitrate = v
|
||||||
case "CameraChan":
|
case "CameraChan":
|
||||||
v, err := strconv.Atoi(value)
|
v, err := strconv.Atoi(value)
|
||||||
if err != nil || (v != 1 && v != 2) {
|
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
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,7 @@ import (
|
||||||
"runtime"
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"bitbucket.org/ausocean/av/revid/config"
|
||||||
"bitbucket.org/ausocean/iot/pi/netsender"
|
"bitbucket.org/ausocean/iot/pi/netsender"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -56,9 +57,9 @@ func TestRaspivid(t *testing.T) {
|
||||||
t.Errorf("netsender.New failed with error %v", err)
|
t.Errorf("netsender.New failed with error %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var c Config
|
var c config.Config
|
||||||
c.Logger = &logger
|
c.Logger = &logger
|
||||||
c.Input = InputRaspivid
|
c.Input = config.InputRaspivid
|
||||||
|
|
||||||
rv, err := New(c, ns)
|
rv, err := New(c, ns)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -148,7 +149,7 @@ func TestResetEncoderSenderSetup(t *testing.T) {
|
||||||
encoders []encoder
|
encoders []encoder
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
outputs: []uint8{OutputHTTP},
|
outputs: []uint8{config.OutputHTTP},
|
||||||
encoders: []encoder{
|
encoders: []encoder{
|
||||||
{
|
{
|
||||||
encoderType: mtsEncoderStr,
|
encoderType: mtsEncoderStr,
|
||||||
|
@ -157,7 +158,7 @@ func TestResetEncoderSenderSetup(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
outputs: []uint8{OutputRTMP},
|
outputs: []uint8{config.OutputRTMP},
|
||||||
encoders: []encoder{
|
encoders: []encoder{
|
||||||
{
|
{
|
||||||
encoderType: flvEncoderStr,
|
encoderType: flvEncoderStr,
|
||||||
|
@ -166,7 +167,7 @@ func TestResetEncoderSenderSetup(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
outputs: []uint8{OutputRTP},
|
outputs: []uint8{config.OutputRTP},
|
||||||
encoders: []encoder{
|
encoders: []encoder{
|
||||||
{
|
{
|
||||||
encoderType: mtsEncoderStr,
|
encoderType: mtsEncoderStr,
|
||||||
|
@ -175,7 +176,7 @@ func TestResetEncoderSenderSetup(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
outputs: []uint8{OutputHTTP, OutputRTMP},
|
outputs: []uint8{config.OutputHTTP, config.OutputRTMP},
|
||||||
encoders: []encoder{
|
encoders: []encoder{
|
||||||
{
|
{
|
||||||
encoderType: mtsEncoderStr,
|
encoderType: mtsEncoderStr,
|
||||||
|
@ -188,7 +189,7 @@ func TestResetEncoderSenderSetup(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
outputs: []uint8{OutputHTTP, OutputRTP, OutputRTMP},
|
outputs: []uint8{config.OutputHTTP, config.OutputRTP, config.OutputRTMP},
|
||||||
encoders: []encoder{
|
encoders: []encoder{
|
||||||
{
|
{
|
||||||
encoderType: mtsEncoderStr,
|
encoderType: mtsEncoderStr,
|
||||||
|
@ -201,7 +202,7 @@ func TestResetEncoderSenderSetup(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
outputs: []uint8{OutputRTP, OutputRTMP},
|
outputs: []uint8{config.OutputRTP, config.OutputRTMP},
|
||||||
encoders: []encoder{
|
encoders: []encoder{
|
||||||
{
|
{
|
||||||
encoderType: mtsEncoderStr,
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("unexpected err: %v", err)
|
t.Fatalf("unexpected err: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -224,7 +225,7 @@ func TestResetEncoderSenderSetup(t *testing.T) {
|
||||||
for testNum, test := range tests {
|
for testNum, test := range tests {
|
||||||
// Create a new config and reset revid with it.
|
// Create a new config and reset revid with it.
|
||||||
const dummyURL = "rtmp://dummy"
|
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)
|
err := rv.setConfig(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error: %v for test %v", err, testNum)
|
t.Fatalf("unexpected error: %v for test %v", err, testNum)
|
||||||
|
|
Loading…
Reference in New Issue