Merged in avdevice-interface (pull request #270)

revid: AVDevice and implementations

Approved-by: Alan Noble <anoble@gmail.com>
This commit is contained in:
Saxon Milton 2019-11-06 12:04:21 +00:00
commit 6c8b980b2f
19 changed files with 1259 additions and 304 deletions

View File

@ -1,6 +1,6 @@
/*
NAME
revid-cli - command line interface for Revid.
revid-cli - command line interface for revid.
DESCRIPTION
See Readme.md
@ -42,6 +42,7 @@ import (
"bitbucket.org/ausocean/av/container/mts"
"bitbucket.org/ausocean/av/container/mts/meta"
"bitbucket.org/ausocean/av/revid"
"bitbucket.org/ausocean/av/revid/config"
"bitbucket.org/ausocean/iot/pi/netsender"
"bitbucket.org/ausocean/iot/pi/sds"
"bitbucket.org/ausocean/iot/pi/smartlogger"
@ -102,8 +103,8 @@ func main() {
// handleFlags parses command line flags and returns a revid configuration
// based on them.
func handleFlags() revid.Config {
var cfg revid.Config
func handleFlags() config.Config {
var cfg config.Config
var (
cpuprofile = flag.String("cpuprofile", "", "write cpu profile to `file`")
@ -128,8 +129,8 @@ func handleFlags() revid.Config {
rotationPtr = flag.Uint("Rotation", 0, "Rotate video output. (0-359 degrees)")
brightnessPtr = flag.Uint("Brightness", 50, "Set brightness. (0-100) ")
saturationPtr = flag.Int("Saturation", 0, "Set Saturation. (100-100)")
exposurePtr = flag.String("Exposure", "auto", "Set exposure mode. ("+strings.Join(revid.ExposureModes[:], ",")+")")
autoWhiteBalancePtr = flag.String("Awb", "auto", "Set automatic white balance mode. ("+strings.Join(revid.AutoWhiteBalanceModes[:], ",")+")")
exposurePtr = flag.String("Exposure", "auto", "Set exposure mode. ("+strings.Join(config.ExposureModes[:], ",")+")")
autoWhiteBalancePtr = flag.String("Awb", "auto", "Set automatic white balance mode. ("+strings.Join(config.AutoWhiteBalanceModes[:], ",")+")")
// Audio specific flags.
sampleRatePtr = flag.Int("SampleRate", 48000, "Sample rate of recorded audio")
@ -179,15 +180,15 @@ func handleFlags() revid.Config {
switch *inputPtr {
case "Raspivid":
cfg.Input = revid.Raspivid
cfg.Input = config.InputRaspivid
case "v4l":
cfg.Input = revid.V4L
cfg.Input = config.InputV4L
case "File":
cfg.Input = revid.File
cfg.Input = config.InputFile
case "Audio":
cfg.Input = revid.Audio
cfg.Input = config.InputAudio
case "RTSP":
cfg.Input = revid.RTSP
cfg.Input = config.InputRTSP
case "":
default:
log.Log(logger.Error, pkg+"bad input argument")
@ -214,13 +215,13 @@ func handleFlags() revid.Config {
for _, o := range outputs {
switch o {
case "File":
cfg.Outputs = append(cfg.Outputs, revid.File)
cfg.Outputs = append(cfg.Outputs, config.OutputFile)
case "Http":
cfg.Outputs = append(cfg.Outputs, revid.HTTP)
cfg.Outputs = append(cfg.Outputs, config.OutputHTTP)
case "Rtmp":
cfg.Outputs = append(cfg.Outputs, revid.RTMP)
cfg.Outputs = append(cfg.Outputs, config.OutputRTMP)
case "Rtp":
cfg.Outputs = append(cfg.Outputs, revid.RTP)
cfg.Outputs = append(cfg.Outputs, config.OutputRTP)
case "":
default:
log.Log(logger.Error, pkg+"bad output argument", "arg", o)
@ -258,7 +259,7 @@ func handleFlags() revid.Config {
}
// initialize then run the main NetSender client
func run(cfg revid.Config) {
func run(cfg config.Config) {
log.Log(logger.Info, pkg+"running in NetSender mode")
var rv *revid.Revid

64
device/device.go Normal file
View File

@ -0,0 +1,64 @@
/*
DESCRIPTION
device.go provides AVDevice, an interface that describes a configurable
audio or video device that can be started and stopped from which data may
be obtained.
AUTHORS
Saxon A. Nelson-Milton <saxon@ausocean.org>
LICENSE
Copyright (C) 2019 the Australian Ocean Lab (AusOcean)
It is free software: you can redistribute it and/or modify them
under the terms of the GNU General Public License as published by the
Free Software Foundation, either version 3 of the License, or (at your
option) any later version.
It is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
for more details.
You should have received a copy of the GNU General Public License
in gpl.txt. If not, see http://www.gnu.org/licenses.
*/
package device
import (
"fmt"
"io"
"bitbucket.org/ausocean/av/revid/config"
)
// AVDevice describes a configurable audio or video device from which media data
// can be obtained. AVDevice is an io.Reader.
type AVDevice interface {
io.Reader
// Set allows for configuration of the AVDevice using a Config struct. All,
// some or none of the fields of the Config struct may be used for configuration
// by an implementation. An implementation should specify what fields are
// considered.
Set(c config.Config) error
// Start will start the AVDevice capturing media data; after which the Read
// method may be called to obtain the data. The format of the data may differ
// and should be specified by the implementation.
Start() error
// Stop will stop the AVDevice from capturing media data. From this point
// Reads will no longer be successful.
Stop() error
}
// multiError implements the built in error interface. multiError is used here
// to collect multi errors during validation of configruation parameters for o
// AVDevices.
type MultiError []error
func (me MultiError) Error() string {
return fmt.Sprintf("%v", me)
}

73
device/file/file.go Normal file
View File

@ -0,0 +1,73 @@
/*
DESCRIPTION
file.go provides an implementation of the AVDevice interface for media files.
AUTHORS
Saxon A. Nelson-Milton <saxon@ausocean.org>
LICENSE
Copyright (C) 2019 the Australian Ocean Lab (AusOcean)
It is free software: you can redistribute it and/or modify them
under the terms of the GNU General Public License as published by the
Free Software Foundation, either version 3 of the License, or (at your
option) any later version.
It is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
for more details.
You should have received a copy of the GNU General Public License
in gpl.txt. If not, see http://www.gnu.org/licenses.
*/
package file
import (
"errors"
"fmt"
"io"
"os"
"bitbucket.org/ausocean/av/revid/config"
)
// AVFile is an implementation of the AVDevice interface for a file containg
// audio or video data.
type AVFile struct {
f io.ReadCloser
cfg config.Config
}
// NewAVFile returns a new AVFile.
func NewAVFile() *AVFile { return &AVFile{} }
// Set simply sets the AVFile's config to the passed config.
func (m *AVFile) Set(c config.Config) error {
m.cfg = c
return nil
}
// Start will open the file at the location of the InputPath field of the
// config struct.
func (m *AVFile) Start() error {
var err error
m.f, err = os.Open(m.cfg.InputPath)
if err != nil {
return fmt.Errorf("could not open media file: %w", err)
}
return nil
}
// Stop will close the file such that any further reads will fail.
func (m *AVFile) Stop() error { return m.f.Close() }
// Read implements io.Reader. If start has not been called, or Start has been
// called and Stop has since been called, an error is returned.
func (m *AVFile) Read(p []byte) (int, error) {
if m.f != nil {
return m.f.Read(p)
}
return 0, errors.New("AV file is closed")
}

View File

@ -0,0 +1,329 @@
/*
DESCRIPTION
geovision.go provides an implementation of the AVDevice interface for the
GeoVision IP camera.
AUTHORS
Saxon A. Nelson-Milton <saxon@ausocean.org>
LICENSE
Copyright (C) 2019 the Australian Ocean Lab (AusOcean)
It is free software: you can redistribute it and/or modify them
under the terms of the GNU General Public License as published by the
Free Software Foundation, either version 3 of the License, or (at your
option) any later version.
It is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
for more details.
You should have received a copy of the GNU General Public License
in gpl.txt. If not, see http://www.gnu.org/licenses.
*/
package geovision
import (
"errors"
"fmt"
"net"
"strconv"
"strings"
"time"
"bitbucket.org/ausocean/av/codec/codecutil"
"bitbucket.org/ausocean/av/device"
"bitbucket.org/ausocean/av/device/geovision/gvctrl"
"bitbucket.org/ausocean/av/protocol/rtcp"
"bitbucket.org/ausocean/av/protocol/rtp"
"bitbucket.org/ausocean/av/protocol/rtsp"
"bitbucket.org/ausocean/av/revid/config"
"bitbucket.org/ausocean/utils/logger"
)
// Indicate package when logging.
const pkg = "geovision: "
// Constants for real time clients.
const (
rtpPort = 60000
rtcpPort = 60001
defaultServerRTCPPort = 17301
)
// TODO: remove this when gvctrl has configurable user and pass.
const (
ipCamUser = "admin"
ipCamPass = "admin"
)
// Configuration defaults.
const (
defaultCameraIP = "192.168.1.50"
defaultCodec = codecutil.H264
defaultHeight = 720
defaultFrameRate = 25
defaultBitrate = 400
defaultVBRBitrate = 400
defaultMinFrames = 100
defaultVBRQuality = config.QualityStandard
defaultCameraChan = 2
)
// Configuration field errors.
var (
errGVBadCameraIP = errors.New("camera IP bad or unset, defaulting")
errGVBadCodec = errors.New("codec bad or unset, defaulting")
errGVBadFrameRate = errors.New("frame rate bad or unset, defaulting")
errGVBadBitrate = errors.New("bitrate bad or unset, defaulting")
errGVBadVBRQuality = errors.New("VBR quality bad or unset, defaulting")
errGVBadHeight = errors.New("height bad or unset, defaulting")
errGVBadMinFrames = errors.New("min frames bad or unset, defaulting")
)
// GeoVision is an implementation of the AVDevice interface for a GeoVision
// IP camera. This has been designed to implement the GV-BX4700-8F in particular.
// Any other models are untested.
type GeoVision struct {
cfg config.Config
log config.Logger
rtpClt *rtp.Client
rtspClt *rtsp.Client
rtcpClt *rtcp.Client
}
// NewGeoVision returns a new GeoVision.
func NewGeoVision(l config.Logger) *GeoVision { return &GeoVision{log: l} }
// Set will take a Config struct, check the validity of the relevant fields
// and then performs any configuration necessary using gvctrl to control the
// GeoVision web interface. If fields are not valid, an error is added to the
// multiError and a default value is used for that particular field.
func (g *GeoVision) Set(c config.Config) error {
var errs device.MultiError
if c.CameraIP == "" {
errs = append(errs, errGVBadCameraIP)
c.CameraIP = defaultCameraIP
}
switch c.InputCodec {
case codecutil.H264, codecutil.H265, codecutil.MJPEG:
default:
errs = append(errs, errGVBadCodec)
c.InputCodec = defaultCodec
}
if c.Height <= 0 {
errs = append(errs, errGVBadHeight)
c.Height = defaultHeight
}
if c.FrameRate <= 0 {
errs = append(errs, errGVBadFrameRate)
c.FrameRate = defaultFrameRate
}
if c.Bitrate <= 0 {
errs = append(errs, errGVBadBitrate)
c.Bitrate = defaultBitrate
}
if c.MinFrames <= 0 {
errs = append(errs, errGVBadMinFrames)
c.MinFrames = defaultMinFrames
}
switch c.VBRQuality {
case config.QualityStandard, config.QualityFair, config.QualityGood, config.QualityGreat, config.QualityExcellent:
default:
errs = append(errs, errGVBadVBRQuality)
c.VBRQuality = defaultVBRQuality
}
if c.VBRBitrate <= 0 {
errs = append(errs, errGVBadVBRQuality)
c.VBRBitrate = defaultVBRBitrate
}
if c.CameraChan != 1 && c.CameraChan != 2 {
errs = append(errs, errGVBadVBRQuality)
c.CameraChan = defaultCameraChan
}
g.cfg = c
err := gvctrl.Set(
g.cfg.CameraIP,
gvctrl.Channel(g.cfg.CameraChan),
gvctrl.CodecOut(
map[uint8]gvctrl.Codec{
codecutil.H264: gvctrl.CodecH264,
codecutil.H265: gvctrl.CodecH265,
codecutil.MJPEG: gvctrl.CodecMJPEG,
}[g.cfg.InputCodec],
),
gvctrl.Height(int(g.cfg.Height)),
gvctrl.FrameRate(int(g.cfg.FrameRate)),
gvctrl.VariableBitrate(g.cfg.VBR),
gvctrl.VBRQuality(
map[config.Quality]gvctrl.Quality{
config.QualityStandard: gvctrl.QualityStandard,
config.QualityFair: gvctrl.QualityFair,
config.QualityGood: gvctrl.QualityGood,
config.QualityGreat: gvctrl.QualityGreat,
config.QualityExcellent: gvctrl.QualityExcellent,
}[g.cfg.VBRQuality],
),
gvctrl.VBRBitrate(g.cfg.VBRBitrate),
gvctrl.CBRBitrate(int(g.cfg.Bitrate)),
gvctrl.Refresh(float64(g.cfg.MinFrames)/float64(g.cfg.FrameRate)),
)
if err != nil {
return fmt.Errorf("could not set IPCamera settings: %w", err)
}
// Give the camera some time to change it's configuration.
const setupDelay = 5 * time.Second
time.Sleep(setupDelay)
return errs
}
// Start uses an RTSP client to communicate with the GeoVision RTSP server and
// request a stream that is then received by an RTP client, from which packets
// can be read from using the Read method.
func (g *GeoVision) Start() error {
var (
local, remote *net.TCPAddr
err error
)
g.rtspClt, local, remote, err = rtsp.NewClient("rtsp://" + ipCamUser + ":" + ipCamPass + "@" + g.cfg.CameraIP + ":8554/" + "CH002.sdp")
if err != nil {
return fmt.Errorf("could not create RTSP client: %w", err)
}
g.log.Log(logger.Info, pkg+"created RTSP client")
resp, err := g.rtspClt.Options()
if err != nil {
return fmt.Errorf("options request unsuccessful: %w", err)
}
g.log.Log(logger.Debug, pkg+"RTSP OPTIONS response", "response", resp.String())
resp, err = g.rtspClt.Describe()
if err != nil {
return fmt.Errorf("describe request unsuccessful: %w", err)
}
g.log.Log(logger.Debug, pkg+"RTSP DESCRIBE response", "response", resp.String())
resp, err = g.rtspClt.Setup("track1", fmt.Sprintf("RTP/AVP;unicast;client_port=%d-%d", rtpPort, rtcpPort))
if err != nil {
return fmt.Errorf("setup request unsuccessful: %w", err)
}
g.log.Log(logger.Debug, pkg+"RTSP SETUP response", "response", resp.String())
rtpCltAddr, rtcpCltAddr, rtcpSvrAddr, err := formAddrs(local, remote, *resp)
if err != nil {
return fmt.Errorf("could not format addresses: %w", err)
}
g.log.Log(logger.Info, pkg+"RTSP session setup complete")
g.rtpClt, err = rtp.NewClient(rtpCltAddr)
if err != nil {
return fmt.Errorf("could not create RTP client: %w", err)
}
g.rtcpClt, err = rtcp.NewClient(rtcpCltAddr, rtcpSvrAddr, g.rtpClt, g.log.Log)
if err != nil {
return fmt.Errorf("could not create RTCP client: %w", err)
}
g.log.Log(logger.Info, pkg+"RTCP and RTP clients created")
// Check errors from RTCP client until it has stopped running.
go func() {
for {
err, ok := <-g.rtcpClt.Err()
if ok {
g.log.Log(logger.Warning, pkg+"RTCP error", "error", err.Error())
} else {
return
}
}
}()
// Start the RTCP client.
g.rtcpClt.Start()
g.log.Log(logger.Info, pkg+"RTCP client started")
resp, err = g.rtspClt.Play()
if err != nil {
return fmt.Errorf("play request unsuccessful: %w", err)
}
g.log.Log(logger.Debug, pkg+"RTSP server PLAY response", "response", resp.String())
g.log.Log(logger.Info, pkg+"play requested, now receiving stream")
return nil
}
// Stop will close the RTSP, RTCP, and RTP connections and in turn end the
// stream from the GeoVision. Future reads using Read will result in error.
func (g *GeoVision) Stop() error {
err := g.rtpClt.Close()
if err != nil {
return fmt.Errorf("could not close RTP client: %w", err)
}
err = g.rtspClt.Close()
if err != nil {
return fmt.Errorf("could not close RTSP client: %w", err)
}
g.rtcpClt.Stop()
g.log.Log(logger.Info, pkg+"RTP, RTSP and RTCP clients stopped and closed")
return nil
}
// Read implements io.Reader. If the GeoVision has not been started an error is
// returned.
func (g *GeoVision) Read(p []byte) (int, error) {
if g.rtpClt != nil {
return g.rtpClt.Read(p)
}
return 0, errors.New("cannot read, GeoVision not streaming")
}
// formAddrs is a helper function to form the addresses for the RTP client,
// RTCP client, and the RTSP server's RTCP addr using the local, remote addresses
// of the RTSP conn, and the SETUP method response.
func formAddrs(local, remote *net.TCPAddr, setupResp rtsp.Response) (rtpCltAddr, rtcpCltAddr, rtcpSvrAddr string, err error) {
svrRTCPPort, err := parseSvrRTCPPort(setupResp)
if err != nil {
return "", "", "", err
}
rtpCltAddr = strings.Split(local.String(), ":")[0] + ":" + strconv.Itoa(rtpPort)
rtcpCltAddr = strings.Split(local.String(), ":")[0] + ":" + strconv.Itoa(rtcpPort)
rtcpSvrAddr = strings.Split(remote.String(), ":")[0] + ":" + strconv.Itoa(svrRTCPPort)
return
}
// parseServerRTCPPort is a helper function to get the RTSP server's RTCP port.
func parseSvrRTCPPort(resp rtsp.Response) (int, error) {
transport := resp.Header.Get("Transport")
for _, p := range strings.Split(transport, ";") {
if strings.Contains(p, "server_port") {
port, err := strconv.Atoi(strings.Split(p, "-")[1])
if err != nil {
return 0, err
}
return port, nil
}
}
return 0, errors.New("SETUP response did not provide RTCP port")
}

View File

@ -31,7 +31,7 @@ import (
"fmt"
"strconv"
"bitbucket.org/ausocean/av/input/gvctrl"
"bitbucket.org/ausocean/av/device/geovision/gvctrl"
)
func main() {

284
device/raspivid/raspivid.go Normal file
View File

@ -0,0 +1,284 @@
/*
DESCRIPTION
raspivid.go provides an implementation of the AVDevice interface for raspivid.
AUTHORS
Saxon A. Nelson-Milton <saxon@ausocean.org>
LICENSE
Copyright (C) 2019 the Australian Ocean Lab (AusOcean)
It is free software: you can redistribute it and/or modify them
under the terms of the GNU General Public License as published by the
Free Software Foundation, either version 3 of the License, or (at your
option) any later version.
It is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
for more details.
You should have received a copy of the GNU General Public License
in gpl.txt. If not, see http://www.gnu.org/licenses.
*/
package raspivid
import (
"errors"
"fmt"
"io"
"os/exec"
"strings"
"bitbucket.org/ausocean/av/codec/codecutil"
"bitbucket.org/ausocean/av/device"
"bitbucket.org/ausocean/av/revid/config"
"bitbucket.org/ausocean/utils/logger"
)
// To indicate package when logging.
const pkg = "raspivid: "
// Raspivid configuration defaults.
const (
defaultRaspividCodec = codecutil.H264
defaultRaspividRotation = 0
defaultRaspividWidth = 1280
defaultRaspividHeight = 720
defaultRaspividBrightness = 50
defaultRaspividSaturation = 0
defaultRaspividExposure = "auto"
defaultRaspividAutoWhiteBalance = "auto"
defaultRaspividMinFrames = 100
defaultRaspividQuantization = 30
defaultRaspividBitrate = 48000
defaultRaspividFramerate = 25
)
// Configuration errors.
var (
errBadCodec = errors.New("codec bad or unset, defaulting")
errBadRotation = errors.New("rotation bad or unset, defaulting")
errBadWidth = errors.New("width bad or unset, defaulting")
errBadHeight = errors.New("height bad or unset, defaulting")
errBadFrameRate = errors.New("framerate bad or unset, defaulting")
errBadBitrate = errors.New("bitrate bad or unset, defaulting")
errBadMinFrames = errors.New("min frames bad or unset, defaulting")
errBadSaturation = errors.New("saturation bad or unset, defaulting")
errBadBrightness = errors.New("brightness bad or unset, defaulting")
errBadExposure = errors.New("exposure bad or unset, defaulting")
errBadAutoWhiteBalance = errors.New("auto white balance bad or unset, defaulting")
errBadQuantization = errors.New("quantization bad or unset, defaulting")
)
// Possible modes for raspivid --exposure parameter.
var ExposureModes = [...]string{
"auto",
"night",
"nightpreview",
"backlight",
"spotlight",
"sports",
"snow",
"beach",
"verylong",
"fixedfps",
"antishake",
"fireworks",
}
// Possible modes for raspivid --awb parameter.
var AutoWhiteBalanceModes = [...]string{
"off",
"auto",
"sun",
"cloud",
"shade",
"tungsten",
"fluorescent",
"incandescent",
"flash",
"horizon",
}
// Raspivid is an implementation of AVDevice that provides control over the
// raspivid command to allow reading of data from a Raspberry Pi camera.
type Raspivid struct {
cfg config.Config
cmd *exec.Cmd
out io.ReadCloser
log config.Logger
}
// NewRaspivid returns a new Raspivid.
func NewRaspivid(l config.Logger) *Raspivid { return &Raspivid{log: l} }
// Set will take a Config struct, check the validity of the relevant fields
// and then performs any configuration necessary. If fields are not valid,
// an error is added to the multiError and a default value is used.
func (r *Raspivid) Set(c config.Config) error {
var errs device.MultiError
switch c.InputCodec {
case codecutil.H264, codecutil.MJPEG:
default:
c.InputCodec = defaultRaspividCodec
errs = append(errs, errBadCodec)
}
if c.Rotation > 359 {
c.Rotation = defaultRaspividRotation
errs = append(errs, errBadRotation)
}
if c.Width == 0 {
c.Width = defaultRaspividWidth
errs = append(errs, errBadWidth)
}
if c.Height == 0 {
c.Height = defaultRaspividHeight
errs = append(errs, errBadHeight)
}
if c.FrameRate == 0 {
c.FrameRate = defaultRaspividFramerate
errs = append(errs, errBadFrameRate)
}
if c.VBR {
c.Bitrate = 0
if c.Quantization < 10 || c.Quantization > 40 {
errs = append(errs, errBadQuantization)
c.Quantization = defaultRaspividQuantization
}
} else {
c.Quantization = 0
if c.Bitrate <= 0 {
errs = append(errs, errBadBitrate)
c.Bitrate = defaultRaspividBitrate
}
}
if c.MinFrames <= 0 {
errs = append(errs, errBadMinFrames)
c.MinFrames = defaultRaspividMinFrames
}
if c.Brightness <= 0 || c.Brightness > 100 {
errs = append(errs, errBadBrightness)
c.Brightness = defaultRaspividBrightness
}
if c.Saturation < -100 || c.Saturation > 100 {
errs = append(errs, errBadSaturation)
c.Saturation = defaultRaspividSaturation
}
if c.Exposure == "" || !stringInSlice(c.Exposure, config.ExposureModes[:]) {
errs = append(errs, errBadExposure)
c.Exposure = defaultRaspividExposure
}
if c.AutoWhiteBalance == "" || !stringInSlice(c.AutoWhiteBalance, AutoWhiteBalanceModes[:]) {
errs = append(errs, errBadAutoWhiteBalance)
c.AutoWhiteBalance = defaultRaspividAutoWhiteBalance
}
r.cfg = c
return errs
}
// Start will prepare the arguments for the raspivid command using the
// configuration set using the Set method then call the raspivid command,
// piping the video output from which the Read method will read from.
func (r *Raspivid) Start() error {
const disabled = "0"
args := []string{
"--output", "-",
"--nopreview",
"--timeout", disabled,
"--width", fmt.Sprint(r.cfg.Width),
"--height", fmt.Sprint(r.cfg.Height),
"--bitrate", fmt.Sprint(r.cfg.Bitrate * 1000), // Convert from kbps to bps.
"--framerate", fmt.Sprint(r.cfg.FrameRate),
"--rotation", fmt.Sprint(r.cfg.Rotation),
"--brightness", fmt.Sprint(r.cfg.Brightness),
"--saturation", fmt.Sprint(r.cfg.Saturation),
"--exposure", fmt.Sprint(r.cfg.Exposure),
"--awb", fmt.Sprint(r.cfg.AutoWhiteBalance),
}
if r.cfg.FlipHorizontal {
args = append(args, "--hflip")
}
if r.cfg.FlipVertical {
args = append(args, "--vflip")
}
if r.cfg.FlipHorizontal {
args = append(args, "--hflip")
}
switch r.cfg.InputCodec {
default:
return fmt.Errorf("revid: invalid input codec: %v", r.cfg.InputCodec)
case codecutil.H264:
args = append(args,
"--codec", "H264",
"--inline",
"--intra", fmt.Sprint(r.cfg.MinFrames),
)
if r.cfg.VBR {
args = append(args, "-qp", fmt.Sprint(r.cfg.Quantization))
}
case codecutil.MJPEG:
args = append(args, "--codec", "MJPEG")
}
r.cfg.Logger.Log(logger.Info, pkg+"raspivid args", "raspividArgs", strings.Join(args, " "))
r.cmd = exec.Command("raspivid", args...)
var err error
r.out, err = r.cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("could not pipe command output: %w", err)
}
err = r.cmd.Start()
if err != nil {
return fmt.Errorf("could not start raspivid command: %w", err)
}
return nil
}
// Read implements io.Reader. Calling read before Start has been called will
// result in 0 bytes read and an error.
func (r *Raspivid) Read(p []byte) (int, error) {
if r.out != nil {
return r.out.Read(p)
}
return 0, errors.New("cannot read, raspivid has not started")
}
// Stop will terminate the raspivid process and close the output pipe.
func (r *Raspivid) Stop() error {
if r.cmd == nil || r.cmd.Process == nil {
return errors.New("raspivid process was never started")
}
err := r.cmd.Process.Kill()
if err != nil {
return fmt.Errorf("could not kill raspivid process: %w", err)
}
return r.out.Close()
}
// stringInSlice returns true if want is in slice.
func stringInSlice(want string, slice []string) bool {
for _, s := range slice {
if s == want {
return true
}
}
return false
}

154
device/webcam/webcam.go Normal file
View File

@ -0,0 +1,154 @@
/*
DESCRIPTION
webcam.go provides an implementation of AVDevice for webcams.
AUTHORS
Saxon A. Nelson-Milton <saxon@ausocean.org>
LICENSE
Copyright (C) 2019 the Australian Ocean Lab (AusOcean)
It is free software: you can redistribute it and/or modify them
under the terms of the GNU General Public License as published by the
Free Software Foundation, either version 3 of the License, or (at your
option) any later version.
It is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
for more details.
You should have received a copy of the GNU General Public License
in gpl.txt. If not, see http://www.gnu.org/licenses.
*/
package webcam
import (
"errors"
"fmt"
"io"
"os/exec"
"strings"
"bitbucket.org/ausocean/av/device"
"bitbucket.org/ausocean/av/revid/config"
"bitbucket.org/ausocean/utils/logger"
)
// Used to indicate package in logging.
const pkg = "webcam: "
// Configuration defaults.
const (
defaultInputPath = "/dev/video0"
defaultFrameRate = 25
defaultBitrate = 400
defaultWidth = 1280
defaultHeight = 720
)
// Configuration field errors.
var (
errBadFrameRate = errors.New("frame rate bad or unset, defaulting")
errBadBitrate = errors.New("bitrate bad or unset, defaulting")
errBadWidth = errors.New("width bad or unset, defaulting")
errBadHeight = errors.New("height bad or unset, defaulting")
)
// Webcam is an implementation of the AVDevice interface for a Webcam. Webcam
// uses an ffmpeg process to pipe the video data from the webcam.
type Webcam struct {
out io.ReadCloser
log config.Logger
cfg config.Config
cmd *exec.Cmd
}
// NewWebcam returns a new Webcam.
func NewWebcam(l config.Logger) *Webcam {
return &Webcam{log: l}
}
// Set will validate the relevant fields of the given Config struct and assign
// the struct to the Webcam's Config. If fields are not valid, an error is
// added to the multiError and a default value is used.
func (w *Webcam) Set(c config.Config) error {
var errs device.MultiError
if c.Width == 0 {
errs = append(errs, errBadWidth)
c.Width = defaultWidth
}
if c.Height == 0 {
errs = append(errs, errBadHeight)
c.Height = defaultHeight
}
if c.FrameRate == 0 {
errs = append(errs, errBadFrameRate)
c.FrameRate = defaultFrameRate
}
if c.Bitrate <= 0 {
errs = append(errs, errBadBitrate)
c.Bitrate = defaultBitrate
}
w.cfg = c
return errs
}
// Start will build the required arguments for ffmpeg and then execute the
// command, piping video output where we can read using the Read method.
func (w *Webcam) Start() error {
args := []string{
"-i", w.cfg.InputPath,
"-f", "h264",
"-r", fmt.Sprint(w.cfg.FrameRate),
}
br := w.cfg.Bitrate * 1000
args = append(args,
"-b:v", fmt.Sprint(br),
"-maxrate", fmt.Sprint(br),
"-bufsize", fmt.Sprint(br/2),
"-s", fmt.Sprintf("%dx%d", w.cfg.Width, w.cfg.Height),
"-",
)
w.log.Log(logger.Info, pkg+"ffmpeg args", "args", strings.Join(args, " "))
w.cmd = exec.Command("ffmpeg", args...)
var err error
w.out, err = w.cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("failed to create pipe: %w", err)
}
err = w.cmd.Start()
if err != nil {
return fmt.Errorf("failed to start ffmpeg: %w", err)
}
return nil
}
// Stop will kill the ffmpeg process and close the output pipe.
func (w *Webcam) Stop() error {
if w.cmd == nil || w.cmd.Process == nil {
return errors.New("ffmpeg process was never started")
}
err := w.cmd.Process.Kill()
if err != nil {
return fmt.Errorf("could not kill ffmpeg process: %w", err)
}
return w.out.Close()
}
// Read implements io.Reader. If the pipe is nil a read error is returned.
func (w *Webcam) Read(p []byte) (int, error) {
if w.out != nil {
return w.out.Read(p)
}
return 0, errors.New("webcam not streaming")
}

5
go.mod
View File

@ -4,15 +4,16 @@ go 1.13
require (
bitbucket.org/ausocean/iot v1.2.7
bitbucket.org/ausocean/test v0.0.0-20190821085226-7a524f2344ba
bitbucket.org/ausocean/test v0.0.0-20190821085226-7a524f2344ba // indirect
bitbucket.org/ausocean/utils v1.2.10
github.com/BurntSushi/toml v0.3.1 // indirect
github.com/Comcast/gots v0.0.0-20190305015453-8d56e473f0f7
github.com/go-audio/audio v0.0.0-20181013203223-7b2a6ca21480
github.com/go-audio/wav v0.0.0-20181013172942-de841e69b884
github.com/go-delve/delve v1.3.2
github.com/mewkiz/flac v1.0.5
github.com/pkg/errors v0.8.1
github.com/yobert/alsa v0.0.0-20180630182551-d38d89fa843e
gocv.io/x/gocv v0.21.0
gocv.io/x/gocv v0.21.0 // indirect
gopkg.in/yaml.v2 v2.2.2 // indirect
)

49
go.sum
View File

@ -1,17 +1,6 @@
bitbucket.org/ausocean/av v0.0.0-20190416003121-6ee286e98874/go.mod h1:DxZEprrNNQ2slHKAQVUHryDaWc5CbjxyHAvomhzg+AE=
bitbucket.org/ausocean/av/input/gvctrl v0.0.0-20191017223116-ce6c12cce8cd h1:L99pvZZtdy3v54ym6GswYi8SOGgz+4Tr8hiIOI+nSiQ=
bitbucket.org/ausocean/av/input/gvctrl v0.0.0-20191017223116-ce6c12cce8cd/go.mod h1:Hg522DOVaj23J7CIxknCxmNsLGdg1iZ+Td1FDcTOdLQ=
bitbucket.org/ausocean/iot v1.2.4/go.mod h1:5HVLgPHccW2PxS7WDUQO6sKWMgk3Vfze/7d5bHs8EWU=
bitbucket.org/ausocean/iot v1.2.6 h1:KAAY1KZDbyOpoKajT1dM8BawupHiW9hUOelseSV1Ptc=
bitbucket.org/ausocean/iot v1.2.6/go.mod h1:71AYHh8yGZ8XyzDBskwIWMF+8E8ORagXpXE24wlhoE0=
bitbucket.org/ausocean/iot v1.2.7 h1:dZgrmVtuXnzHgybDthn0bYgAJms9euTONXBsqsx9g5M=
bitbucket.org/ausocean/iot v1.2.7/go.mod h1:aAWgPo2f8sD2OPmxae1E5/iD9+tKY/iW4pcQMQXUvHM=
bitbucket.org/ausocean/test v0.0.0-20190821085226-7a524f2344ba/go.mod h1:MbKtu9Pu8l3hiVGX6ep8S1VwAVY5uCbifCFOYsm914w=
bitbucket.org/ausocean/utils v0.0.0-20190408050157-66d3b4d4041e/go.mod h1:uXzX9z3PLemyURTMWRhVI8uLhPX4uuvaaO85v2hcob8=
bitbucket.org/ausocean/utils v1.2.6/go.mod h1:uXzX9z3PLemyURTMWRhVI8uLhPX4uuvaaO85v2hcob8=
bitbucket.org/ausocean/utils v1.2.8 h1:hyxAIqYBqjqCguG+6A/kKyrAihyeUt2LziZg6CH0gLU=
bitbucket.org/ausocean/utils v1.2.8/go.mod h1:uXzX9z3PLemyURTMWRhVI8uLhPX4uuvaaO85v2hcob8=
bitbucket.org/ausocean/utils v1.2.9 h1:g45C6KCNvCLOGFv+ZnmDbQOOdnwpIsvzuNOD141CTVI=
bitbucket.org/ausocean/utils v1.2.9/go.mod h1:uXzX9z3PLemyURTMWRhVI8uLhPX4uuvaaO85v2hcob8=
bitbucket.org/ausocean/utils v1.2.10 h1:JTS7n+K+0o/FQFWKjdGgA1ElZ4TQu9aHX3wTJXOayXw=
bitbucket.org/ausocean/utils v1.2.10/go.mod h1:uXzX9z3PLemyURTMWRhVI8uLhPX4uuvaaO85v2hcob8=
@ -24,28 +13,50 @@ github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMx
github.com/adrianmo/go-nmea v1.1.1-0.20190109062325-c448653979f7/go.mod h1:HHPxPAm2kmev+61qmkZh7xgZF/7qHtSpsWppip2Ipv8=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/cosiner/argv v0.0.0-20170225145430-13bacc38a0a5/go.mod h1:p/NrK5tF6ICIly4qwEDsf6VDirFiWWz0FenfYBwJaKQ=
github.com/cpuguy83/go-md2man v1.0.8/go.mod h1:N6JayAiVKtlHSnuTCeuLSQVs75hb8q+dYQLjr7cDsKY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/go-audio/aiff v0.0.0-20180403003018-6c3a8a6aff12/go.mod h1:AMSAp6W1zd0koOdX6QDgGIuBDTUvLa2SLQtm7d9eM3c=
github.com/go-audio/audio v0.0.0-20180206231410-b697a35b5608/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs=
github.com/go-audio/audio v0.0.0-20181013203223-7b2a6ca21480 h1:4sGU+UABMMsRJyD+Y2yzMYxq0GJFUsRRESI0P1gZ2ig=
github.com/go-audio/audio v0.0.0-20181013203223-7b2a6ca21480/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs=
github.com/go-audio/wav v0.0.0-20181013172942-de841e69b884 h1:2TaXIaVA4ff/MHHezOj83tCypALTFAcXOImcFWNa3jw=
github.com/go-audio/wav v0.0.0-20181013172942-de841e69b884/go.mod h1:UiqzUyfX0zs3pJ/DPyvS5v8sN6s5bXPUDDIVA5v8dks=
github.com/go-delve/delve v1.3.2 h1:K8VjV+Q2YnBYlPq0ctjrvc9h7h03wXszlszzfGW5Tog=
github.com/go-delve/delve v1.3.2/go.mod h1:LLw6qJfIsRK9WcwV2IRRqsdlgrqzOeuGrQOCOIhDpt8=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jacobsa/go-serial v0.0.0-20180131005756-15cf729a72d4/go.mod h1:2RvX5ZjVtsznNZPEt4xwJXNJrM3VTZoQf7V6gk0ysvs=
github.com/kidoman/embd v0.0.0-20170508013040-d3d8c0c5c68d/go.mod h1:ACKj9jnzOzj1lw2ETilpFGK7L9dtJhAzT7T1OhAGtRQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mattetti/audio v0.0.0-20180912171649-01576cde1f21 h1:Hc1iKlyxNHp3CV59G2E/qabUkHvEwOIJxDK0CJ7CRjA=
github.com/mattetti/audio v0.0.0-20180912171649-01576cde1f21/go.mod h1:LlQmBGkOuV/SKzEDXBPKauvN2UqCgzXO2XjecTGj40s=
github.com/mattn/go-colorable v0.0.0-20170327083344-ded68f7a9561/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mewkiz/flac v1.0.5 h1:dHGW/2kf+/KZ2GGqSVayNEhL9pluKn/rr/h/QqD9Ogc=
github.com/mewkiz/flac v1.0.5/go.mod h1:EHZNU32dMF6alpurYyKHDLYpW1lYpBZ5WrXi/VuNIGs=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/peterh/liner v0.0.0-20170317030525-88609521dc4b/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v0.0.0-20170413231811-06b906832ed0/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday v0.0.0-20180428102519-11635eb403ff/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sirupsen/logrus v0.0.0-20180523074243-ea8897e79973/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
github.com/spf13/cobra v0.0.0-20170417170307-b6cb39589372/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/pflag v0.0.0-20170417173400-9e4c21054fa1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
@ -53,6 +64,7 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/yobert/alsa v0.0.0-20180630182551-d38d89fa843e h1:3NIzz7weXhh3NToPgbtlQtKiVgerEaG4/nY2skGoGG0=
github.com/yobert/alsa v0.0.0-20180630182551-d38d89fa843e/go.mod h1:CaowXBWOiSGWEpBBV8LoVnQTVPV4ycyviC9IBLj8dRw=
github.com/yryz/ds18b20 v0.0.0-20180211073435-3cf383a40624/go.mod h1:MqFju5qeLDFh+S9PqxYT7TEla8xeW7bgGr/69q3oki0=
go.starlark.net v0.0.0-20190702223751-32f345186213/go.mod h1:c1/X6cHgvdXj6pUlmWKMkuqRnW4K8x2vwt6JAaaircg=
go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
@ -60,12 +72,25 @@ go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
gocv.io/x/gocv v0.21.0 h1:dVjagrupZrfCRY0qPEaYWgoNMRpBel6GYDH4mvQOK8Y=
gocv.io/x/gocv v0.21.0/go.mod h1:Rar2PS6DV+T4FL+PM535EImD/h13hGVaHhnCu1xarBs=
golang.org/x/arch v0.0.0-20171004143515-077ac972c2e4/go.mod h1:cYlCBUl1MsqxdiKgmc4uh7TxZfWSFLOGSRR090WDxt8=
golang.org/x/crypto v0.0.0-20180614174826-fd5f17ee7299/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190305064518-30e92a19ae4a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20181120060634-fc4f04983f62/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@ -34,40 +34,40 @@ import (
func (r *Revid) startAudioDevice() (func() error, error) {
// Create audio device.
ac := &audio.Config{
SampleRate: r.config.SampleRate,
Channels: r.config.Channels,
RecPeriod: r.config.RecPeriod,
BitDepth: r.config.BitDepth,
Codec: r.config.InputCodec,
SampleRate: r.cfg.SampleRate,
Channels: r.cfg.Channels,
RecPeriod: r.cfg.RecPeriod,
BitDepth: r.cfg.BitDepth,
Codec: r.cfg.InputCodec,
}
mts.Meta.Add("sampleRate", strconv.Itoa(r.config.SampleRate))
mts.Meta.Add("channels", strconv.Itoa(r.config.Channels))
mts.Meta.Add("period", fmt.Sprintf("%.6f", r.config.RecPeriod))
mts.Meta.Add("bitDepth", strconv.Itoa(r.config.BitDepth))
switch r.config.InputCodec {
mts.Meta.Add("sampleRate", strconv.Itoa(r.cfg.SampleRate))
mts.Meta.Add("channels", strconv.Itoa(r.cfg.Channels))
mts.Meta.Add("period", fmt.Sprintf("%.6f", r.cfg.RecPeriod))
mts.Meta.Add("bitDepth", strconv.Itoa(r.cfg.BitDepth))
switch r.cfg.InputCodec {
case codecutil.PCM:
mts.Meta.Add("codec", "pcm")
case codecutil.ADPCM:
mts.Meta.Add("codec", "adpcm")
default:
r.config.Logger.Log(logger.Fatal, pkg+"no audio codec set in config")
r.cfg.Logger.Log(logger.Fatal, pkg+"no audio codec set in config")
}
ai, err := audio.NewDevice(ac, r.config.Logger)
ai, err := audio.NewDevice(ac, r.cfg.Logger)
if err != nil {
r.config.Logger.Log(logger.Fatal, pkg+"failed to create audio device", "error", err.Error())
r.cfg.Logger.Log(logger.Fatal, pkg+"failed to create audio device", "error", err.Error())
}
// Start audio device
err = ai.Start()
if err != nil {
r.config.Logger.Log(logger.Fatal, pkg+"failed to start audio device", "error", err.Error())
r.cfg.Logger.Log(logger.Fatal, pkg+"failed to start audio device", "error", err.Error())
}
// Process output from audio device.
r.config.ChunkSize = ai.ChunkSize()
r.cfg.ChunkSize = ai.ChunkSize()
r.wg.Add(1)
go r.processFrom(ai, time.Duration(float64(time.Second)/r.config.WriteRate))
go r.processFrom(ai, time.Duration(float64(time.Second)/r.cfg.WriteRate))
return func() error {
ai.Stop()
return nil

View File

@ -23,7 +23,7 @@ LICENSE
along with revid in gpl.txt. If not, see http://www.gnu.org/licenses.
*/
package revid
package config
import (
"errors"
@ -63,17 +63,24 @@ var AutoWhiteBalanceModes = [...]string{
"horizon",
}
const pkg = "config: "
type Logger interface {
SetLevel(int8)
Log(level int8, message string, params ...interface{})
}
// quality represents video quality.
type quality int
type Quality int
// The different video qualities that can be used for variable bitrate when
// using the GeoVision camera.
const (
qualityStandard quality = iota
qualityFair
qualityGood
qualityGreat
qualityExcellent
QualityStandard Quality = iota
QualityFair
QualityGood
QualityGreat
QualityExcellent
)
// Enums to define inputs, outputs and codecs.
@ -82,19 +89,19 @@ const (
NothingDefined = iota
// Input/Output.
File
// Inputs.
Raspivid
V4L
RTSP
Audio
InputFile
InputRaspivid
InputV4L
InputRTSP
InputAudio
// Outputs.
RTMP
RTP
HTTP
MPEGTS
OutputAudio
OutputRTMP
OutputRTP
OutputHTTP
OutputMPEGTS
OutputFile
// Codecs.
H264
@ -105,8 +112,8 @@ const (
// Default config settings
const (
// General revid defaults.
defaultInput = Raspivid
defaultOutput = HTTP
defaultInput = InputRaspivid
defaultOutput = OutputHTTP
defaultFrameRate = 25
defaultWriteRate = 25
defaultTimeout = 0
@ -115,7 +122,7 @@ const (
defaultRtpAddr = "localhost:6970"
defaultCameraIP = "192.168.1.50"
defaultVBR = false
defaultVBRQuality = qualityStandard
defaultVBRQuality = QualityStandard
defaultBurstPeriod = 10 // Seconds
defaultVBRBitrate = 500 // kbps
defaultCameraChan = 2
@ -162,13 +169,13 @@ type Config struct {
// Input defines the input data source.
//
// Valid values are defined by enums:
// Raspivid:
// InputRaspivid:
// Read data from a Raspberry Pi Camera.
// V4l:
// InputV4l:
// Read from webcam.
// File:
// InputFile:
// Location must be specified in InputPath field.
// RTSP:
// InputRTSP:
// CameraIP should also be defined.
Input uint8
@ -180,16 +187,16 @@ type Config struct {
// Outputs define the outputs we wish to output data too.
//
// Valid outputs are defined by enums:
// File:
// OutputFile:
// Location must be defined by the OutputPath field. MPEG-TS packetization
// is used.
// HTTP:
// OutputHTTP:
// Destination is defined by the sh field located in /etc/netsender.conf.
// MPEGT-TS packetization is used.
// RTMP:
// OutputRTMP:
// Destination URL must be defined in the RtmpUrl field. FLV packetization
// is used.
// RTP:
// OutputRTP:
// Destination is defined by RtpAddr field, otherwise it will default to
// localhost:6970. MPEGT-TS packetization is used.
Outputs []uint8
@ -236,7 +243,7 @@ type Config struct {
// VBRQuality describes the general quality of video from the GeoVision camera
// under variable bitrate. VBRQuality can be one 5 consts defined:
// qualityStandard, qualityFair, qualityGood, qualityGreat and qualityExcellent.
VBRQuality quality
VBRQuality Quality
// VBRBitrate describes maximal bitrate for the GeoVision camera when under
// variable bitrate.
@ -327,7 +334,7 @@ func (c *Config) Validate() error {
}
switch c.Input {
case Raspivid, V4L, File, Audio, RTSP:
case InputRaspivid, InputV4L, InputFile, InputAudio, InputRTSP:
case NothingDefined:
c.Logger.Log(logger.Info, pkg+"no input type defined, defaulting", "input", defaultInput)
c.Input = defaultInput
@ -339,7 +346,7 @@ func (c *Config) Validate() error {
case codecutil.H264, codecutil.MJPEG, codecutil.PCM, codecutil.ADPCM:
default:
switch c.Input {
case Audio:
case OutputAudio:
c.Logger.Log(logger.Info, pkg+"input is audio but no codec defined, defaulting", "inputCodec", defaultAudioInputCodec)
c.InputCodec = defaultAudioInputCodec
default:
@ -358,8 +365,8 @@ func (c *Config) Validate() error {
var haveRTMPOut bool
for i, o := range c.Outputs {
switch o {
case File:
case RTMP:
case OutputFile:
case OutputRTMP:
haveRTMPOut = true
if c.Bitrate == 0 {
c.Bitrate = defaultBitrate
@ -367,11 +374,11 @@ func (c *Config) Validate() error {
c.Quantization = 0
if c.RTMPURL == "" {
c.Logger.Log(logger.Info, pkg+"no RTMP URL: falling back to HTTP")
c.Outputs[i] = HTTP
c.Outputs[i] = OutputHTTP
haveRTMPOut = false
}
fallthrough
case HTTP, RTP:
case OutputHTTP, OutputRTP:
if !haveRTMPOut {
c.Bitrate = 0
if c.Quantization == 0 {
@ -518,7 +525,7 @@ func (c *Config) Validate() error {
}
switch c.VBRQuality {
case qualityStandard, qualityFair, qualityGood, qualityGreat, qualityExcellent:
case QualityStandard, QualityFair, QualityGood, QualityGreat, QualityExcellent:
default:
c.Logger.Log(logger.Info, pkg+"VBRQuality bad or unset, defaulting", "VBRQuality", defaultVBRQuality)
c.VBRQuality = defaultVBRQuality

View File

@ -22,7 +22,7 @@ LICENSE
in gpl.txt. If not, see http://www.gnu.org/licenses.
*/
package revid
package config
import (
"fmt"
@ -41,7 +41,7 @@ func TestValidate(t *testing.T) {
err error
}{
{
in: Config{Outputs: []uint8{HTTP}, Logger: &dumbLogger{}},
in: Config{Outputs: []uint8{OutputHTTP}, Logger: &dumbLogger{}},
check: func(c Config) error {
if c.Bitrate != 0 {
return fmt.Errorf("did not get expected bitrate. Got: %v, Want: %v", c.Bitrate, 0)
@ -53,7 +53,7 @@ func TestValidate(t *testing.T) {
},
},
{
in: Config{Outputs: []uint8{RTMP}, RTMPURL: "dummURL", Logger: &dumbLogger{}},
in: Config{Outputs: []uint8{OutputRTMP}, RTMPURL: "dummURL", Logger: &dumbLogger{}},
check: func(c Config) error {
if c.Bitrate != defaultBitrate {
return fmt.Errorf("did not get expected bitrate. Got: %v, Want: %v", c.Bitrate, defaultBitrate)
@ -65,12 +65,12 @@ func TestValidate(t *testing.T) {
},
},
{
in: Config{Outputs: []uint8{HTTP}, Quantization: 50, Logger: &dumbLogger{}},
in: Config{Outputs: []uint8{OutputHTTP}, Quantization: 50, Logger: &dumbLogger{}},
check: func(c Config) error { return nil },
err: errInvalidQuantization,
},
{
in: Config{Outputs: []uint8{HTTP}, Quantization: 20, Logger: &dumbLogger{}},
in: Config{Outputs: []uint8{OutputHTTP}, Quantization: 20, Logger: &dumbLogger{}},
check: func(c Config) error {
if c.Bitrate != 0 {
return fmt.Errorf("did not get expected bitrate. Got: %v, Want: %v", c.Bitrate, 0)

View File

@ -40,10 +40,11 @@ import (
"time"
"bitbucket.org/ausocean/av/codec/codecutil"
"bitbucket.org/ausocean/av/input/gvctrl"
"bitbucket.org/ausocean/av/device/geovision/gvctrl"
"bitbucket.org/ausocean/av/protocol/rtcp"
"bitbucket.org/ausocean/av/protocol/rtp"
"bitbucket.org/ausocean/av/protocol/rtsp"
"bitbucket.org/ausocean/av/revid/config"
"bitbucket.org/ausocean/utils/logger"
)
@ -53,54 +54,61 @@ const (
ipCamPass = "admin"
)
// Constants for real time clients.
const (
rtpPort = 60000
rtcpPort = 60001
defaultServerRTCPPort = 17301
)
// startRaspivid sets up things for input from raspivid i.e. starts
// a raspivid process and pipes it's data output.
func (r *Revid) startRaspivid() (func() error, error) {
r.config.Logger.Log(logger.Info, pkg+"starting raspivid")
r.cfg.Logger.Log(logger.Info, pkg+"starting raspivid")
const disabled = "0"
args := []string{
"--output", "-",
"--nopreview",
"--timeout", disabled,
"--width", fmt.Sprint(r.config.Width),
"--height", fmt.Sprint(r.config.Height),
"--bitrate", fmt.Sprint(r.config.Bitrate * 1000), // Convert from kbps to bps.
"--framerate", fmt.Sprint(r.config.FrameRate),
"--rotation", fmt.Sprint(r.config.Rotation),
"--brightness", fmt.Sprint(r.config.Brightness),
"--saturation", fmt.Sprint(r.config.Saturation),
"--exposure", fmt.Sprint(r.config.Exposure),
"--awb", fmt.Sprint(r.config.AutoWhiteBalance),
"--width", fmt.Sprint(r.cfg.Width),
"--height", fmt.Sprint(r.cfg.Height),
"--bitrate", fmt.Sprint(r.cfg.Bitrate * 1000), // Convert from kbps to bps.
"--framerate", fmt.Sprint(r.cfg.FrameRate),
"--rotation", fmt.Sprint(r.cfg.Rotation),
"--brightness", fmt.Sprint(r.cfg.Brightness),
"--saturation", fmt.Sprint(r.cfg.Saturation),
"--exposure", fmt.Sprint(r.cfg.Exposure),
"--awb", fmt.Sprint(r.cfg.AutoWhiteBalance),
}
if r.config.FlipHorizontal {
if r.cfg.FlipHorizontal {
args = append(args, "--hflip")
}
if r.config.FlipVertical {
if r.cfg.FlipVertical {
args = append(args, "--vflip")
}
if r.config.FlipHorizontal {
if r.cfg.FlipHorizontal {
args = append(args, "--hflip")
}
switch r.config.InputCodec {
switch r.cfg.InputCodec {
default:
return nil, fmt.Errorf("revid: invalid input codec: %v", r.config.InputCodec)
return nil, fmt.Errorf("revid: invalid input codec: %v", r.cfg.InputCodec)
case codecutil.H264:
args = append(args,
"--codec", "H264",
"--inline",
"--intra", fmt.Sprint(r.config.MinFrames),
"--intra", fmt.Sprint(r.cfg.MinFrames),
)
if r.config.VBR {
args = append(args, "-qp", fmt.Sprint(r.config.Quantization))
if r.cfg.VBR {
args = append(args, "-qp", fmt.Sprint(r.cfg.Quantization))
}
case codecutil.MJPEG:
args = append(args, "--codec", "MJPEG")
}
r.config.Logger.Log(logger.Info, pkg+"raspivid args", "raspividArgs", strings.Join(args, " "))
r.cfg.Logger.Log(logger.Info, pkg+"raspivid args", "raspividArgs", strings.Join(args, " "))
r.cmd = exec.Command("raspivid", args...)
stdout, err := r.cmd.StdoutPipe()
@ -121,28 +129,28 @@ func (r *Revid) startRaspivid() (func() error, error) {
func (r *Revid) startV4L() (func() error, error) {
const defaultVideo = "/dev/video0"
r.config.Logger.Log(logger.Info, pkg+"starting webcam")
if r.config.InputPath == "" {
r.config.Logger.Log(logger.Info, pkg+"using default video device", "device", defaultVideo)
r.config.InputPath = defaultVideo
r.cfg.Logger.Log(logger.Info, pkg+"starting webcam")
if r.cfg.InputPath == "" {
r.cfg.Logger.Log(logger.Info, pkg+"using default video device", "device", defaultVideo)
r.cfg.InputPath = defaultVideo
}
args := []string{
"-i", r.config.InputPath,
"-i", r.cfg.InputPath,
"-f", "h264",
"-r", fmt.Sprint(r.config.FrameRate),
"-r", fmt.Sprint(r.cfg.FrameRate),
}
br := r.config.Bitrate * 1000
br := r.cfg.Bitrate * 1000
args = append(args,
"-b:v", fmt.Sprint(br),
"-maxrate", fmt.Sprint(br),
"-bufsize", fmt.Sprint(br/2),
"-s", fmt.Sprintf("%dx%d", r.config.Width, r.config.Height),
"-s", fmt.Sprintf("%dx%d", r.cfg.Width, r.cfg.Height),
"-",
)
r.config.Logger.Log(logger.Info, pkg+"ffmpeg args", "args", strings.Join(args, " "))
r.cfg.Logger.Log(logger.Info, pkg+"ffmpeg args", "args", strings.Join(args, " "))
r.cmd = exec.Command("ffmpeg", args...)
stdout, err := r.cmd.StdoutPipe()
@ -152,7 +160,7 @@ func (r *Revid) startV4L() (func() error, error) {
err = r.cmd.Start()
if err != nil {
r.config.Logger.Log(logger.Fatal, pkg+"cannot start webcam", "error", err.Error())
r.cfg.Logger.Log(logger.Fatal, pkg+"cannot start webcam", "error", err.Error())
return nil, nil
}
@ -164,9 +172,9 @@ func (r *Revid) startV4L() (func() error, error) {
// setupInputForFile sets up input from file and starts the revid.processFrom
// routine.
func (r *Revid) setupInputForFile() (func() error, error) {
f, err := os.Open(r.config.InputPath)
f, err := os.Open(r.cfg.InputPath)
if err != nil {
r.config.Logger.Log(logger.Error, err.Error())
r.cfg.Logger.Log(logger.Error, err.Error())
r.Stop()
return nil, err
}
@ -184,92 +192,92 @@ func (r *Revid) setupInputForFile() (func() error, error) {
// TODO(saxon): this function should really be startGeoVision. It's much too
// specific to be called startRTSPCamera.
func (r *Revid) startRTSPCamera() (func() error, error) {
r.config.Logger.Log(logger.Info, pkg+"starting geovision...")
r.cfg.Logger.Log(logger.Info, pkg+"starting geovision...")
err := gvctrl.Set(
r.config.CameraIP,
gvctrl.Channel(r.config.CameraChan),
r.cfg.CameraIP,
gvctrl.Channel(r.cfg.CameraChan),
gvctrl.CodecOut(
map[uint8]gvctrl.Codec{
codecutil.H264: gvctrl.CodecH264,
codecutil.H265: gvctrl.CodecH265,
codecutil.MJPEG: gvctrl.CodecMJPEG,
}[r.config.InputCodec],
}[r.cfg.InputCodec],
),
gvctrl.Height(int(r.config.Height)),
gvctrl.FrameRate(int(r.config.FrameRate)),
gvctrl.VariableBitrate(r.config.VBR),
gvctrl.Height(int(r.cfg.Height)),
gvctrl.FrameRate(int(r.cfg.FrameRate)),
gvctrl.VariableBitrate(r.cfg.VBR),
gvctrl.VBRQuality(
map[quality]gvctrl.Quality{
qualityStandard: gvctrl.QualityStandard,
qualityFair: gvctrl.QualityFair,
qualityGood: gvctrl.QualityGood,
qualityGreat: gvctrl.QualityGreat,
qualityExcellent: gvctrl.QualityExcellent,
}[r.config.VBRQuality],
map[config.Quality]gvctrl.Quality{
config.QualityStandard: gvctrl.QualityStandard,
config.QualityFair: gvctrl.QualityFair,
config.QualityGood: gvctrl.QualityGood,
config.QualityGreat: gvctrl.QualityGreat,
config.QualityExcellent: gvctrl.QualityExcellent,
}[r.cfg.VBRQuality],
),
gvctrl.VBRBitrate(r.config.VBRBitrate),
gvctrl.CBRBitrate(int(r.config.Bitrate)),
gvctrl.Refresh(float64(r.config.MinFrames)/float64(r.config.FrameRate)),
gvctrl.VBRBitrate(r.cfg.VBRBitrate),
gvctrl.CBRBitrate(int(r.cfg.Bitrate)),
gvctrl.Refresh(float64(r.cfg.MinFrames)/float64(r.cfg.FrameRate)),
)
if err != nil {
return nil, fmt.Errorf("could not set IPCamera settings: %w", err)
}
r.config.Logger.Log(logger.Info, pkg+"completed geovision configuration")
r.cfg.Logger.Log(logger.Info, pkg+"completed geovision configuration")
time.Sleep(5 * time.Second)
rtspClt, local, remote, err := rtsp.NewClient("rtsp://" + ipCamUser + ":" + ipCamPass + "@" + r.config.CameraIP + ":8554/" + "CH002.sdp")
rtspClt, local, remote, err := rtsp.NewClient("rtsp://" + ipCamUser + ":" + ipCamPass + "@" + r.cfg.CameraIP + ":8554/" + "CH002.sdp")
if err != nil {
return nil, err
}
r.config.Logger.Log(logger.Info, pkg+"created RTSP client")
r.cfg.Logger.Log(logger.Info, pkg+"created RTSP client")
resp, err := rtspClt.Options()
if err != nil {
return nil, err
}
r.config.Logger.Log(logger.Debug, pkg+"RTSP OPTIONS response", "response", resp.String())
r.cfg.Logger.Log(logger.Debug, pkg+"RTSP OPTIONS response", "response", resp.String())
resp, err = rtspClt.Describe()
if err != nil {
return nil, err
}
r.config.Logger.Log(logger.Debug, pkg+"RTSP DESCRIBE response", "response", resp.String())
r.cfg.Logger.Log(logger.Debug, pkg+"RTSP DESCRIBE response", "response", resp.String())
resp, err = rtspClt.Setup("track1", fmt.Sprintf("RTP/AVP;unicast;client_port=%d-%d", rtpPort, rtcpPort))
if err != nil {
return nil, err
}
r.config.Logger.Log(logger.Debug, pkg+"RTSP SETUP response", "response", resp.String())
r.cfg.Logger.Log(logger.Debug, pkg+"RTSP SETUP response", "response", resp.String())
rtpCltAddr, rtcpCltAddr, rtcpSvrAddr, err := formAddrs(local, remote, *resp)
if err != nil {
return nil, err
}
r.config.Logger.Log(logger.Info, pkg+"RTSP session setup complete")
r.cfg.Logger.Log(logger.Info, pkg+"RTSP session setup complete")
rtpClt, err := rtp.NewClient(rtpCltAddr)
if err != nil {
return nil, err
}
rtcpClt, err := rtcp.NewClient(rtcpCltAddr, rtcpSvrAddr, rtpClt, r.config.Logger.Log)
rtcpClt, err := rtcp.NewClient(rtcpCltAddr, rtcpSvrAddr, rtpClt, r.cfg.Logger.Log)
if err != nil {
return nil, err
}
r.config.Logger.Log(logger.Info, pkg+"RTCP and RTP clients created")
r.cfg.Logger.Log(logger.Info, pkg+"RTCP and RTP clients created")
// Check errors from RTCP client until it has stopped running.
go func() {
for {
err, ok := <-rtcpClt.Err()
if ok {
r.config.Logger.Log(logger.Warning, pkg+"RTCP error", "error", err.Error())
r.cfg.Logger.Log(logger.Warning, pkg+"RTCP error", "error", err.Error())
} else {
return
}
@ -279,20 +287,20 @@ func (r *Revid) startRTSPCamera() (func() error, error) {
// Start the RTCP client.
rtcpClt.Start()
r.config.Logger.Log(logger.Info, pkg+"RTCP client started")
r.cfg.Logger.Log(logger.Info, pkg+"RTCP client started")
// Start reading data from the RTP client.
r.wg.Add(1)
go r.processFrom(rtpClt, time.Second/time.Duration(r.config.FrameRate))
go r.processFrom(rtpClt, time.Second/time.Duration(r.cfg.FrameRate))
r.config.Logger.Log(logger.Info, pkg+"started input processor")
r.cfg.Logger.Log(logger.Info, pkg+"started input processor")
resp, err = rtspClt.Play()
if err != nil {
return nil, err
}
r.config.Logger.Log(logger.Debug, pkg+"RTSP server PLAY response", "response", resp.String())
r.config.Logger.Log(logger.Info, pkg+"play requested, now receiving stream")
r.cfg.Logger.Log(logger.Debug, pkg+"RTSP server PLAY response", "response", resp.String())
r.cfg.Logger.Log(logger.Info, pkg+"play requested, now receiving stream")
return func() error {
err := rtpClt.Close()
@ -307,7 +315,7 @@ func (r *Revid) startRTSPCamera() (func() error, error) {
rtcpClt.Stop()
r.config.Logger.Log(logger.Info, pkg+"RTP, RTSP and RTCP clients stopped and closed")
r.cfg.Logger.Log(logger.Info, pkg+"RTP, RTSP and RTCP clients stopped and closed")
return nil
}, nil
}
@ -345,6 +353,6 @@ func parseSvrRTCPPort(resp rtsp.Response) (int, error) {
// then send individual access units to revid's encoders.
func (r *Revid) processFrom(read io.Reader, delay time.Duration) {
r.err <- r.lexTo(r.encoders, read, delay)
r.config.Logger.Log(logger.Info, pkg+"finished lexing")
r.cfg.Logger.Log(logger.Info, pkg+"finished lexing")
r.wg.Done()
}

View File

@ -44,24 +44,32 @@ import (
"bitbucket.org/ausocean/av/codec/mjpeg"
"bitbucket.org/ausocean/av/container/flv"
"bitbucket.org/ausocean/av/container/mts"
"bitbucket.org/ausocean/av/revid/config"
"bitbucket.org/ausocean/iot/pi/netsender"
"bitbucket.org/ausocean/utils/ioext"
"bitbucket.org/ausocean/utils/logger"
"bitbucket.org/ausocean/utils/ring"
)
// Ring buffer defaults.
const (
// MTS ring buffer defaults.
defaultMTSRBSize = 1000
defaultMTSRBElementSize = 100000
defaultMTSRBWriteTimeout = 5
// RTMP ring buffer defaults.
defaultRTMPRBSize = 1000
defaultRTMPRBElementSize = 300000
defaultRTMPRBWriteTimeout = 5
)
// RTMP connection properties.
const (
rtmpConnectionMaxTries = 5
rtmpConnectionTimeout = 10
)
const (
rtpPort = 60000
rtcpPort = 60001
defaultServerRTCPPort = 17301
)
const pkg = "revid: "
type Logger interface {
@ -76,7 +84,7 @@ type Revid struct {
// For historical reasons it also handles logging.
// FIXME(kortschak): The relationship of concerns
// in config/ns is weird.
config Config
cfg config.Config
// ns holds the netsender.Sender responsible for HTTP.
ns *netsender.Sender
@ -116,13 +124,13 @@ type Revid struct {
// New returns a pointer to a new Revid with the desired configuration, and/or
// an error if construction of the new instance was not successful.
func New(c Config, ns *netsender.Sender) (*Revid, error) {
func New(c config.Config, ns *netsender.Sender) (*Revid, error) {
r := Revid{ns: ns, err: make(chan error)}
err := r.setConfig(c)
if err != nil {
return nil, fmt.Errorf("could not set config, failed with error: %v", err)
}
r.config.Logger.SetLevel(c.LogLevel)
r.cfg.Logger.SetLevel(c.LogLevel)
go r.handleErrors()
return &r, nil
}
@ -130,8 +138,8 @@ func New(c Config, ns *netsender.Sender) (*Revid, error) {
// Config returns a copy of revids current config.
//
// Config is not safe for concurrent use.
func (r *Revid) Config() Config {
return r.config
func (r *Revid) Config() config.Config {
return r.cfg
}
// TODO(Saxon): put more thought into error severity and how to handle these.
@ -139,7 +147,7 @@ func (r *Revid) handleErrors() {
for {
err := <-r.err
if err != nil {
r.config.Logger.Log(logger.Error, pkg+"async error", "error", err.Error())
r.cfg.Logger.Log(logger.Error, pkg+"async error", "error", err.Error())
}
}
}
@ -154,45 +162,45 @@ func (r *Revid) Bitrate() int {
// reset swaps the current config of a Revid with the passed
// configuration; checking validity and returning errors if not valid. It then
// sets up the data pipeline accordingly to this configuration.
func (r *Revid) reset(config Config) error {
err := r.setConfig(config)
func (r *Revid) reset(c config.Config) error {
err := r.setConfig(c)
if err != nil {
return err
}
r.config.Logger.SetLevel(config.LogLevel)
r.cfg.Logger.SetLevel(c.LogLevel)
err = r.setupPipeline(
func(dst io.WriteCloser, fps float64) (io.WriteCloser, error) {
var st int
var encOptions []func(*mts.Encoder) error
switch r.config.Input {
case Raspivid:
switch r.config.InputCodec {
switch r.cfg.Input {
case config.InputRaspivid:
switch r.cfg.InputCodec {
case codecutil.H264:
st = mts.EncodeH264
case codecutil.MJPEG:
st = mts.EncodeMJPEG
encOptions = append(encOptions, mts.PacketBasedPSI(int(r.config.MinFrames)))
encOptions = append(encOptions, mts.PacketBasedPSI(int(r.cfg.MinFrames)))
default:
panic("unknown input codec for raspivid input")
}
case File, V4L:
case config.InputFile, config.InputV4L:
st = mts.EncodeH264
case RTSP:
switch r.config.InputCodec {
case config.InputRTSP:
switch r.cfg.InputCodec {
case codecutil.H265:
st = mts.EncodeH265
case codecutil.H264:
st = mts.EncodeH264
case codecutil.MJPEG:
st = mts.EncodeMJPEG
encOptions = append(encOptions, mts.PacketBasedPSI(int(r.config.MinFrames)))
encOptions = append(encOptions, mts.PacketBasedPSI(int(r.cfg.MinFrames)))
default:
panic("unknown input codec for RTSP input")
}
case Audio:
case config.InputAudio:
st = mts.EncodeAudio
default:
panic("unknown input type")
@ -215,13 +223,13 @@ func (r *Revid) reset(config Config) error {
// setConfig takes a config, checks it's validity and then replaces the current
// revid config.
func (r *Revid) setConfig(config Config) error {
r.config.Logger = config.Logger
func (r *Revid) setConfig(config config.Config) error {
r.cfg.Logger = config.Logger
err := config.Validate()
if err != nil {
return errors.New("Config struct is bad: " + err.Error())
}
r.config = config
r.cfg = config
return nil
}
@ -244,38 +252,38 @@ func (r *Revid) setupPipeline(mtsEnc func(dst io.WriteCloser, rate float64) (io.
// to mtsSenders if the output requires MPEGTS encoding, or flvSenders if the
// output requires FLV encoding.
var w io.WriteCloser
for _, out := range r.config.Outputs {
for _, out := range r.cfg.Outputs {
switch out {
case HTTP:
case config.OutputHTTP:
w = newMtsSender(
newHttpSender(r.ns, r.config.Logger.Log),
r.config.Logger.Log,
ring.NewBuffer(r.config.MTSRBSize, r.config.MTSRBElementSize, time.Duration(r.config.MTSRBWriteTimeout)*time.Second),
r.config.ClipDuration,
newHttpSender(r.ns, r.cfg.Logger.Log),
r.cfg.Logger.Log,
ring.NewBuffer(r.cfg.MTSRBSize, r.cfg.MTSRBElementSize, time.Duration(r.cfg.MTSRBWriteTimeout)*time.Second),
r.cfg.ClipDuration,
)
mtsSenders = append(mtsSenders, w)
case RTP:
w, err := newRtpSender(r.config.RTPAddress, r.config.Logger.Log, r.config.FrameRate)
case config.OutputRTP:
w, err := newRtpSender(r.cfg.RTPAddress, r.cfg.Logger.Log, r.cfg.FrameRate)
if err != nil {
r.config.Logger.Log(logger.Warning, pkg+"rtp connect error", "error", err.Error())
r.cfg.Logger.Log(logger.Warning, pkg+"rtp connect error", "error", err.Error())
}
mtsSenders = append(mtsSenders, w)
case File:
w, err := newFileSender(r.config.OutputPath)
case config.OutputFile:
w, err := newFileSender(r.cfg.OutputPath)
if err != nil {
return err
}
mtsSenders = append(mtsSenders, w)
case RTMP:
case config.OutputRTMP:
w, err := newRtmpSender(
r.config.RTMPURL,
r.cfg.RTMPURL,
rtmpConnectionTimeout,
rtmpConnectionMaxTries,
ring.NewBuffer(r.config.RTMPRBSize, r.config.RTMPRBElementSize, time.Duration(r.config.RTMPRBWriteTimeout)*time.Second),
r.config.Logger.Log,
ring.NewBuffer(r.cfg.RTMPRBSize, r.cfg.RTMPRBElementSize, time.Duration(r.cfg.RTMPRBWriteTimeout)*time.Second),
r.cfg.Logger.Log,
)
if err != nil {
r.config.Logger.Log(logger.Warning, pkg+"rtmp connect error", "error", err.Error())
r.cfg.Logger.Log(logger.Warning, pkg+"rtmp connect error", "error", err.Error())
}
flvSenders = append(flvSenders, w)
}
@ -286,7 +294,7 @@ func (r *Revid) setupPipeline(mtsEnc func(dst io.WriteCloser, rate float64) (io.
// as a destination.
if len(mtsSenders) != 0 {
mw := multiWriter(mtsSenders...)
e, _ := mtsEnc(mw, r.config.WriteRate)
e, _ := mtsEnc(mw, r.cfg.WriteRate)
encoders = append(encoders, e)
}
@ -295,7 +303,7 @@ func (r *Revid) setupPipeline(mtsEnc func(dst io.WriteCloser, rate float64) (io.
// as a destination.
if len(flvSenders) != 0 {
mw := multiWriter(flvSenders...)
e, err := flvEnc(mw, int(r.config.FrameRate))
e, err := flvEnc(mw, int(r.cfg.FrameRate))
if err != nil {
return err
}
@ -304,23 +312,23 @@ func (r *Revid) setupPipeline(mtsEnc func(dst io.WriteCloser, rate float64) (io.
r.encoders = multiWriter(encoders...)
switch r.config.Input {
case Raspivid:
switch r.cfg.Input {
case config.InputRaspivid:
r.setupInput = r.startRaspivid
switch r.config.InputCodec {
switch r.cfg.InputCodec {
case codecutil.H264:
r.lexTo = h264.Lex
case codecutil.MJPEG:
r.lexTo = mjpeg.Lex
}
case V4L:
case config.InputV4L:
r.setupInput = r.startV4L
r.lexTo = h264.Lex
case File:
case config.InputFile:
r.setupInput = r.setupInputForFile
case RTSP:
case config.InputRTSP:
r.setupInput = r.startRTSPCamera
switch r.config.InputCodec {
switch r.cfg.InputCodec {
case codecutil.H264:
r.lexTo = h264.NewExtractor().Extract
case codecutil.H265:
@ -328,9 +336,9 @@ func (r *Revid) setupPipeline(mtsEnc func(dst io.WriteCloser, rate float64) (io.
case codecutil.MJPEG:
panic("not implemented")
}
case Audio:
case config.InputAudio:
r.setupInput = r.startAudioDevice
r.lexTo = codecutil.NewByteLexer(&r.config.ChunkSize).Lex
r.lexTo = codecutil.NewByteLexer(&r.cfg.ChunkSize).Lex
}
return nil
@ -342,15 +350,15 @@ func (r *Revid) setupPipeline(mtsEnc func(dst io.WriteCloser, rate float64) (io.
// Start is safe for concurrent use.
func (r *Revid) Start() error {
if r.IsRunning() {
r.config.Logger.Log(logger.Warning, pkg+"start called, but revid already running")
r.cfg.Logger.Log(logger.Warning, pkg+"start called, but revid already running")
return nil
}
r.mu.Lock()
defer r.mu.Unlock()
r.config.Logger.Log(logger.Info, pkg+"starting Revid")
err := r.reset(r.config)
r.cfg.Logger.Log(logger.Info, pkg+"starting Revid")
err := r.reset(r.cfg)
if err != nil {
r.Stop()
return err
@ -369,7 +377,7 @@ func (r *Revid) Start() error {
// Stop is safe for concurrent use.
func (r *Revid) Stop() {
if !r.IsRunning() {
r.config.Logger.Log(logger.Warning, pkg+"stop called but revid isn't running")
r.cfg.Logger.Log(logger.Warning, pkg+"stop called but revid isn't running")
return
}
@ -379,26 +387,26 @@ func (r *Revid) Stop() {
if r.closeInput != nil {
err := r.closeInput()
if err != nil {
r.config.Logger.Log(logger.Error, pkg+"could not close input", "error", err.Error())
r.cfg.Logger.Log(logger.Error, pkg+"could not close input", "error", err.Error())
}
}
r.config.Logger.Log(logger.Info, pkg+"closing pipeline")
r.cfg.Logger.Log(logger.Info, pkg+"closing pipeline")
err := r.encoders.Close()
if err != nil {
r.config.Logger.Log(logger.Error, pkg+"failed to close pipeline", "error", err.Error())
r.cfg.Logger.Log(logger.Error, pkg+"failed to close pipeline", "error", err.Error())
}
r.config.Logger.Log(logger.Info, pkg+"closed pipeline")
r.cfg.Logger.Log(logger.Info, pkg+"closed pipeline")
if r.cmd != nil && r.cmd.Process != nil {
r.config.Logger.Log(logger.Info, pkg+"killing input proccess")
r.cfg.Logger.Log(logger.Info, pkg+"killing input proccess")
r.cmd.Process.Kill()
}
r.config.Logger.Log(logger.Info, pkg+"waiting for routines to close")
r.cfg.Logger.Log(logger.Info, pkg+"waiting for routines to close")
r.wg.Wait()
r.config.Logger.Log(logger.Info, pkg+"revid stopped")
r.cfg.Logger.Log(logger.Info, pkg+"revid stopped")
r.running = false
}
@ -423,239 +431,239 @@ func (r *Revid) Update(vars map[string]string) error {
for key, value := range vars {
switch key {
case "Input":
v, ok := map[string]uint8{"raspivid": Raspivid, "rtsp": RTSP}[strings.ToLower(value)]
v, ok := map[string]uint8{"raspivid": config.InputRaspivid, "rtsp": config.InputRTSP}[strings.ToLower(value)]
if !ok {
r.config.Logger.Log(logger.Warning, pkg+"invalid input var", "value", value)
r.cfg.Logger.Log(logger.Warning, pkg+"invalid input var", "value", value)
break
}
r.config.Input = v
r.cfg.Input = v
case "Saturation":
s, err := strconv.ParseInt(value, 10, 0)
if err != nil {
r.config.Logger.Log(logger.Warning, pkg+"invalid saturation param", "value", value)
r.cfg.Logger.Log(logger.Warning, pkg+"invalid saturation param", "value", value)
break
}
r.config.Saturation = int(s)
r.cfg.Saturation = int(s)
case "Brightness":
b, err := strconv.ParseUint(value, 10, 0)
if err != nil {
r.config.Logger.Log(logger.Warning, pkg+"invalid brightness param", "value", value)
r.cfg.Logger.Log(logger.Warning, pkg+"invalid brightness param", "value", value)
break
}
r.config.Brightness = uint(b)
r.cfg.Brightness = uint(b)
case "Exposure":
r.config.Exposure = value
r.cfg.Exposure = value
case "AutoWhiteBalance":
r.config.AutoWhiteBalance = value
r.cfg.AutoWhiteBalance = value
case "InputCodec":
switch value {
case "H264":
r.config.InputCodec = codecutil.H264
r.cfg.InputCodec = codecutil.H264
case "MJPEG":
r.config.InputCodec = codecutil.MJPEG
r.cfg.InputCodec = codecutil.MJPEG
default:
r.config.Logger.Log(logger.Warning, pkg+"invalid InputCodec variable value", "value", value)
r.cfg.Logger.Log(logger.Warning, pkg+"invalid InputCodec variable value", "value", value)
}
case "Output":
outputs := strings.Split(value, ",")
r.config.Outputs = make([]uint8, len(outputs))
r.cfg.Outputs = make([]uint8, len(outputs))
for i, output := range outputs {
switch output {
case "File":
r.config.Outputs[i] = File
r.cfg.Outputs[i] = config.OutputFile
case "Http":
r.config.Outputs[i] = HTTP
r.cfg.Outputs[i] = config.OutputHTTP
case "Rtmp":
r.config.Outputs[i] = RTMP
r.cfg.Outputs[i] = config.OutputRTMP
case "Rtp":
r.config.Outputs[i] = RTP
r.cfg.Outputs[i] = config.OutputRTP
default:
r.config.Logger.Log(logger.Warning, pkg+"invalid output param", "value", value)
r.cfg.Logger.Log(logger.Warning, pkg+"invalid output param", "value", value)
continue
}
}
case "RtmpUrl":
r.config.RTMPURL = value
r.cfg.RTMPURL = value
case "RtpAddress":
r.config.RTPAddress = value
r.cfg.RTPAddress = value
case "Bitrate":
v, err := strconv.ParseUint(value, 10, 0)
if err != nil {
r.config.Logger.Log(logger.Warning, pkg+"invalid framerate param", "value", value)
r.cfg.Logger.Log(logger.Warning, pkg+"invalid framerate param", "value", value)
break
}
r.config.Bitrate = uint(v)
r.cfg.Bitrate = uint(v)
case "OutputPath":
r.config.OutputPath = value
r.cfg.OutputPath = value
case "InputPath":
r.config.InputPath = value
r.cfg.InputPath = value
case "Height":
h, err := strconv.ParseUint(value, 10, 0)
if err != nil {
r.config.Logger.Log(logger.Warning, pkg+"invalid height param", "value", value)
r.cfg.Logger.Log(logger.Warning, pkg+"invalid height param", "value", value)
break
}
r.config.Height = uint(h)
r.cfg.Height = uint(h)
case "Width":
w, err := strconv.ParseUint(value, 10, 0)
if err != nil {
r.config.Logger.Log(logger.Warning, pkg+"invalid width param", "value", value)
r.cfg.Logger.Log(logger.Warning, pkg+"invalid width param", "value", value)
break
}
r.config.Width = uint(w)
r.cfg.Width = uint(w)
case "FrameRate":
v, err := strconv.ParseUint(value, 10, 0)
if err != nil {
r.config.Logger.Log(logger.Warning, pkg+"invalid framerate param", "value", value)
r.cfg.Logger.Log(logger.Warning, pkg+"invalid framerate param", "value", value)
break
}
r.config.FrameRate = uint(v)
r.cfg.FrameRate = uint(v)
case "Rotation":
v, err := strconv.ParseUint(value, 10, 0)
if err != nil || v > 359 {
r.config.Logger.Log(logger.Warning, pkg+"invalid rotation param", "value", value)
r.cfg.Logger.Log(logger.Warning, pkg+"invalid rotation param", "value", value)
break
}
r.config.Rotation = uint(v)
r.cfg.Rotation = uint(v)
case "HttpAddress":
r.config.HTTPAddress = value
r.cfg.HTTPAddress = value
case "Quantization":
v, err := strconv.Atoi(value)
if err != nil {
r.config.Logger.Log(logger.Warning, pkg+"invalid quantization param", "value", v)
r.cfg.Logger.Log(logger.Warning, pkg+"invalid quantization param", "value", v)
break
}
r.config.Quantization = uint(v)
r.cfg.Quantization = uint(v)
case "MinFrames":
v, err := strconv.Atoi(value)
if err != nil {
r.config.Logger.Log(logger.Warning, pkg+"invalid MinFrames param", "value", value)
r.cfg.Logger.Log(logger.Warning, pkg+"invalid MinFrames param", "value", value)
break
}
r.config.MinFrames = uint(v)
r.cfg.MinFrames = uint(v)
case "ClipDuration":
v, err := strconv.Atoi(value)
if err != nil {
r.config.Logger.Log(logger.Warning, pkg+"invalid ClipDuration param", "value", value)
r.cfg.Logger.Log(logger.Warning, pkg+"invalid ClipDuration param", "value", value)
break
}
r.config.ClipDuration = time.Duration(v) * time.Second
r.cfg.ClipDuration = time.Duration(v) * time.Second
case "HorizontalFlip":
switch strings.ToLower(value) {
case "true":
r.config.FlipHorizontal = true
r.cfg.FlipHorizontal = true
case "false":
r.config.FlipHorizontal = false
r.cfg.FlipHorizontal = false
default:
r.config.Logger.Log(logger.Warning, pkg+"invalid HorizontalFlip param", "value", value)
r.cfg.Logger.Log(logger.Warning, pkg+"invalid HorizontalFlip param", "value", value)
}
case "VerticalFlip":
switch strings.ToLower(value) {
case "true":
r.config.FlipVertical = true
r.cfg.FlipVertical = true
case "false":
r.config.FlipVertical = false
r.cfg.FlipVertical = false
default:
r.config.Logger.Log(logger.Warning, pkg+"invalid VerticalFlip param", "value", value)
r.cfg.Logger.Log(logger.Warning, pkg+"invalid VerticalFlip param", "value", value)
}
case "BurstPeriod":
v, err := strconv.ParseUint(value, 10, 0)
if err != nil {
r.config.Logger.Log(logger.Warning, pkg+"invalid BurstPeriod param", "value", value)
r.cfg.Logger.Log(logger.Warning, pkg+"invalid BurstPeriod param", "value", value)
break
}
r.config.BurstPeriod = uint(v)
r.cfg.BurstPeriod = uint(v)
case "Logging":
switch value {
case "Debug":
r.config.LogLevel = logger.Debug
r.cfg.LogLevel = logger.Debug
case "Info":
r.config.LogLevel = logger.Info
r.cfg.LogLevel = logger.Info
case "Warning":
r.config.LogLevel = logger.Warning
r.cfg.LogLevel = logger.Warning
case "Error":
r.config.LogLevel = logger.Error
r.cfg.LogLevel = logger.Error
case "Fatal":
r.config.LogLevel = logger.Fatal
r.cfg.LogLevel = logger.Fatal
default:
r.config.Logger.Log(logger.Warning, pkg+"invalid Logging param", "value", value)
r.cfg.Logger.Log(logger.Warning, pkg+"invalid Logging param", "value", value)
}
case "RTMPRBSize":
v, err := strconv.Atoi(value)
if err != nil || v < 0 {
r.config.Logger.Log(logger.Warning, pkg+"invalid RTMPRBSize var", "value", value)
r.cfg.Logger.Log(logger.Warning, pkg+"invalid RTMPRBSize var", "value", value)
break
}
r.config.RTMPRBSize = v
r.cfg.RTMPRBSize = v
case "RTMPRBElementSize":
v, err := strconv.Atoi(value)
if err != nil || v < 0 {
r.config.Logger.Log(logger.Warning, pkg+"invalid RTMPRBElementSize var", "value", value)
r.cfg.Logger.Log(logger.Warning, pkg+"invalid RTMPRBElementSize var", "value", value)
break
}
r.config.RTMPRBElementSize = v
r.cfg.RTMPRBElementSize = v
case "RTMPRBWriteTimeout":
v, err := strconv.Atoi(value)
if err != nil || v <= 0 {
r.config.Logger.Log(logger.Warning, pkg+"invalid RTMPRBWriteTimeout var", "value", value)
r.cfg.Logger.Log(logger.Warning, pkg+"invalid RTMPRBWriteTimeout var", "value", value)
break
}
r.config.RTMPRBWriteTimeout = v
r.cfg.RTMPRBWriteTimeout = v
case "MTSRBSize":
v, err := strconv.Atoi(value)
if err != nil || v < 0 {
r.config.Logger.Log(logger.Warning, pkg+"invalid MTSRBSize var", "value", value)
r.cfg.Logger.Log(logger.Warning, pkg+"invalid MTSRBSize var", "value", value)
break
}
r.config.MTSRBSize = v
r.cfg.MTSRBSize = v
case "MTSRBElementSize":
v, err := strconv.Atoi(value)
if err != nil || v < 0 {
r.config.Logger.Log(logger.Warning, pkg+"invalid MTSRBElementSize var", "value", value)
r.cfg.Logger.Log(logger.Warning, pkg+"invalid MTSRBElementSize var", "value", value)
break
}
r.config.MTSRBElementSize = v
r.cfg.MTSRBElementSize = v
case "MTSRBWriteTimeout":
v, err := strconv.Atoi(value)
if err != nil || v <= 0 {
r.config.Logger.Log(logger.Warning, pkg+"invalid MTSRBWriteTimeout var", "value", value)
r.cfg.Logger.Log(logger.Warning, pkg+"invalid MTSRBWriteTimeout var", "value", value)
break
}
r.config.MTSRBWriteTimeout = v
r.cfg.MTSRBWriteTimeout = v
case "VBR":
v, ok := map[string]bool{"true": true, "false": false}[strings.ToLower(value)]
if !ok {
r.config.Logger.Log(logger.Warning, pkg+"invalid VBR var", "value", value)
r.cfg.Logger.Log(logger.Warning, pkg+"invalid VBR var", "value", value)
break
}
r.config.VBR = v
r.cfg.VBR = v
case "VBRQuality":
v, ok := map[string]quality{"standard": qualityStandard, "fair": qualityFair, "good": qualityGood, "great": qualityGreat, "excellent": qualityExcellent}[strings.ToLower(value)]
v, ok := map[string]config.Quality{"standard": config.QualityStandard, "fair": config.QualityFair, "good": config.QualityGood, "great": config.QualityGreat, "excellent": config.QualityExcellent}[strings.ToLower(value)]
if !ok {
r.config.Logger.Log(logger.Warning, pkg+"invalid VBRQuality var", "value", value)
r.cfg.Logger.Log(logger.Warning, pkg+"invalid VBRQuality var", "value", value)
break
}
r.config.VBRQuality = v
r.cfg.VBRQuality = v
case "VBRBitrate":
v, err := strconv.Atoi(value)
if err != nil || v <= 0 {
r.config.Logger.Log(logger.Warning, pkg+"invalid VBRBitrate var", "value", value)
r.cfg.Logger.Log(logger.Warning, pkg+"invalid VBRBitrate var", "value", value)
break
}
r.config.VBRBitrate = v
r.cfg.VBRBitrate = v
case "CameraChan":
v, err := strconv.Atoi(value)
if err != nil || (v != 1 && v != 2) {
r.config.Logger.Log(logger.Warning, pkg+"invalid CameraChan var", "value", value)
r.cfg.Logger.Log(logger.Warning, pkg+"invalid CameraChan var", "value", value)
break
}
r.config.CameraChan = v
r.cfg.CameraChan = v
}
}
r.config.Logger.Log(logger.Info, pkg+"revid config changed", "config", fmt.Sprintf("%+v", r.config))
r.cfg.Logger.Log(logger.Info, pkg+"revid config changed", "config", fmt.Sprintf("%+v", r.cfg))
return nil
}

View File

@ -35,6 +35,7 @@ import (
"runtime"
"testing"
"bitbucket.org/ausocean/av/revid/config"
"bitbucket.org/ausocean/iot/pi/netsender"
)
@ -56,9 +57,9 @@ func TestRaspivid(t *testing.T) {
t.Errorf("netsender.New failed with error %v", err)
}
var c Config
var c config.Config
c.Logger = &logger
c.Input = Raspivid
c.Input = config.InputRaspivid
rv, err := New(c, ns)
if err != nil {
@ -148,7 +149,7 @@ func TestResetEncoderSenderSetup(t *testing.T) {
encoders []encoder
}{
{
outputs: []uint8{HTTP},
outputs: []uint8{config.OutputHTTP},
encoders: []encoder{
{
encoderType: mtsEncoderStr,
@ -157,7 +158,7 @@ func TestResetEncoderSenderSetup(t *testing.T) {
},
},
{
outputs: []uint8{RTMP},
outputs: []uint8{config.OutputRTMP},
encoders: []encoder{
{
encoderType: flvEncoderStr,
@ -166,7 +167,7 @@ func TestResetEncoderSenderSetup(t *testing.T) {
},
},
{
outputs: []uint8{RTP},
outputs: []uint8{config.OutputRTP},
encoders: []encoder{
{
encoderType: mtsEncoderStr,
@ -175,7 +176,7 @@ func TestResetEncoderSenderSetup(t *testing.T) {
},
},
{
outputs: []uint8{HTTP, RTMP},
outputs: []uint8{config.OutputHTTP, config.OutputRTMP},
encoders: []encoder{
{
encoderType: mtsEncoderStr,
@ -188,7 +189,7 @@ func TestResetEncoderSenderSetup(t *testing.T) {
},
},
{
outputs: []uint8{HTTP, RTP, RTMP},
outputs: []uint8{config.OutputHTTP, config.OutputRTP, config.OutputRTMP},
encoders: []encoder{
{
encoderType: mtsEncoderStr,
@ -201,7 +202,7 @@ func TestResetEncoderSenderSetup(t *testing.T) {
},
},
{
outputs: []uint8{RTP, RTMP},
outputs: []uint8{config.OutputRTP, config.OutputRTMP},
encoders: []encoder{
{
encoderType: mtsEncoderStr,
@ -215,7 +216,7 @@ func TestResetEncoderSenderSetup(t *testing.T) {
},
}
rv, err := New(Config{Logger: &testLogger{}}, nil)
rv, err := New(config.Config{Logger: &testLogger{}}, nil)
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
@ -224,7 +225,7 @@ func TestResetEncoderSenderSetup(t *testing.T) {
for testNum, test := range tests {
// Create a new config and reset revid with it.
const dummyURL = "rtmp://dummy"
c := Config{Logger: &testLogger{}, Outputs: test.outputs, RTMPURL: dummyURL}
c := config.Config{Logger: &testLogger{}, Outputs: test.outputs, RTMPURL: dummyURL}
err := rv.setConfig(c)
if err != nil {
t.Fatalf("unexpected error: %v for test %v", err, testNum)