revid: cleaned up AVDevice implementations and added documentation to them

This commit is contained in:
Saxon 2019-11-05 20:14:04 +10:30
parent 50c7fe139b
commit a6aef125fd
5 changed files with 143 additions and 55 deletions

View File

@ -25,25 +25,30 @@ LICENSE
package revid package revid
import ( import (
"errors"
"fmt" "fmt"
"io" "io"
"os" "os"
) )
// AVFile is an implementation of the AVDevice interface for a file containg
// audio or video data.
type AVFile struct { type AVFile struct {
f io.ReadCloser f io.ReadCloser
cfg Config cfg Config
} }
func NewAVFile() *AVFile { return &AVFile{} } // NewAVFile returns a new AVFile.
func (m *AVFile) Stop() error { return m.f.Close() } func NewAVFile() *AVFile { return &AVFile{} }
func (m *AVFile) Read(p []byte) (int, error) { return m.f.Read(p) }
// Set simply sets the AVFile's config to the passed config.
func (m *AVFile) Set(c Config) error { func (m *AVFile) Set(c Config) error {
m.cfg = c m.cfg = c
return nil return nil
} }
// Start will open the file at the location of the InputPath field of the
// config struct.
func (m *AVFile) Start() error { func (m *AVFile) Start() error {
var err error var err error
m.f, err = os.Open(m.cfg.InputPath) m.f, err = os.Open(m.cfg.InputPath)
@ -52,3 +57,15 @@ func (m *AVFile) Start() error {
} }
return nil 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

@ -39,17 +39,19 @@ import (
"bitbucket.org/ausocean/utils/logger" "bitbucket.org/ausocean/utils/logger"
) )
// Configuration defaults.
const ( const (
gvDefaultCameraIP = "192.168.1.50" defaultGVCameraIP = "192.168.1.50"
gvDefaultCodec = codecutil.H264 defaultGVCodec = codecutil.H264
gvDefaultHeight = 720 defaultGVHeight = 720
gvDefaultFrameRate = 25 defaultGVFrameRate = 25
gvDefaultBitrate = 400 defaultGVBitrate = 400
gvDefaultVBRBitrate = 400 defaultGVVBRBitrate = 400
gvDefaultMinFrames = 100 defaultGVMinFrames = 100
gvDefaultVBRQuality = "standard" defaultGVVBRQuality = "standard"
) )
// Configuration field errors.
var ( var (
errGVBadCameraIP = errors.New("camera IP bad or unset, defaulting") errGVBadCameraIP = errors.New("camera IP bad or unset, defaulting")
errGVBadCodec = errors.New("codec bad or unset, defaulting") errGVBadCodec = errors.New("codec bad or unset, defaulting")
@ -60,6 +62,9 @@ var (
errGVBadMinFrames = errors.New("min frames 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 { type GeoVision struct {
cfg Config cfg Config
log Logger log Logger
@ -68,40 +73,45 @@ type GeoVision struct {
rtcpClt *rtcp.Client rtcpClt *rtcp.Client
} }
// NewGeoVision returns a new GeoVision.
func NewGeovision(l Logger) *GeoVision { return &GeoVision{log: l} } func NewGeovision(l 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) error { func (g *GeoVision) Set(c Config) error {
var errs multiError var errs multiError
if c.CameraIP == "" { if c.CameraIP == "" {
errs = append(errs, errGVBadCameraIP) errs = append(errs, errGVBadCameraIP)
c.CameraIP = gvDefaultCameraIP c.CameraIP = defaultGVCameraIP
} }
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 = gvDefaultCodec c.InputCodec = defaultGVCodec
} }
if c.Height <= 0 { if c.Height <= 0 {
errs = append(errs, errGVBadHeight) errs = append(errs, errGVBadHeight)
c.Height = gvDefaultHeight c.Height = defaultGVHeight
} }
if c.FrameRate <= 0 { if c.FrameRate <= 0 {
errs = append(errs, errGVBadFrameRate) errs = append(errs, errGVBadFrameRate)
c.FrameRate = gvDefaultFrameRate c.FrameRate = defaultGVFrameRate
} }
if c.Bitrate <= 0 { if c.Bitrate <= 0 {
errs = append(errs, errGVBadBitrate) errs = append(errs, errGVBadBitrate)
c.Bitrate = gvDefaultBitrate c.Bitrate = defaultGVBitrate
} }
if c.MinFrames <= 0 { if c.MinFrames <= 0 {
errs = append(errs, errGVBadMinFrames) errs = append(errs, errGVBadMinFrames)
c.MinFrames = gvDefaultMinFrames c.MinFrames = defaultGVMinFrames
} }
switch c.VBRQuality { switch c.VBRQuality {
@ -160,6 +170,9 @@ func (g *GeoVision) Set(c Config) error {
return multiError(errs) return multiError(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 { func (g *GeoVision) Start() error {
var ( var (
local, remote *net.TCPAddr local, remote *net.TCPAddr
@ -236,6 +249,8 @@ func (g *GeoVision) Start() error {
return nil 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 { func (g *GeoVision) Stop() error {
err := g.rtpClt.Close() err := g.rtpClt.Close()
if err != nil { if err != nil {
@ -254,6 +269,11 @@ func (g *GeoVision) Stop() error {
return nil 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) { func (g *GeoVision) Read(p []byte) (int, error) {
return g.rtpClt.Read(p) if g.rtpClt != nil {
return g.rtpClt.Read(p)
}
return 0, errors.New("cannot read, GeoVision not streaming")
} }

View File

@ -53,13 +53,36 @@ const (
ipCamPass = "admin" ipCamPass = "admin"
) )
// AVDevice describes a configurable audio or video device from which media data
// can be obtained. AVDevice implements io.Reader.
type AVDevice interface { type AVDevice interface {
io.Reader 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) error 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 Start() error
// Stop will stop the AVDevice from capturing media data. From this point
// Reads will no longer be successful.
Stop() error 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) {

View File

@ -35,22 +35,23 @@ import (
"bitbucket.org/ausocean/utils/logger" "bitbucket.org/ausocean/utils/logger"
) )
// Raspivid AVDevice configuration defaults. // Raspivid configuration defaults.
const ( const (
raspividDefaultCodec = codecutil.H264 defaultRaspividCodec = codecutil.H264
raspividDefaultRotation = 0 defaultRaspividRotation = 0
raspividDefaultWidth = 1280 defaultRaspividWidth = 1280
raspividDefaultHeight = 720 defaultRaspividHeight = 720
raspividDefaultBrightness = 50 defaultRaspividBrightness = 50
raspividDefaultSaturation = 0 defaultRaspividSaturation = 0
raspividDefaultExposure = "auto" defaultRaspividExposure = "auto"
raspividDefaultAutoWhiteBalance = "auto" defaultRaspividAutoWhiteBalance = "auto"
raspividDefaultMinFrames = 100 defaultRaspividMinFrames = 100
raspividDefaultQuantization = 30 defaultRaspividQuantization = 30
raspividDefaultBitrate = 48000 defaultRaspividBitrate = 48000
raspividDefaultFramerate = 25 defaultRaspividFramerate = 25
) )
// Configuration errors.
var ( var (
errBadCodec = errors.New("codec bad or unset, defaulting") errBadCodec = errors.New("codec bad or unset, defaulting")
errBadRotation = errors.New("rotation bad or unset, defaulting") errBadRotation = errors.New("rotation bad or unset, defaulting")
@ -66,44 +67,47 @@ var (
errBadQuantization = errors.New("quantization bad or unset, defaulting") errBadQuantization = errors.New("quantization bad or unset, defaulting")
) )
// 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 { type Raspivid struct {
cfg Config cfg Config
cmd *exec.Cmd cmd *exec.Cmd
out io.ReadCloser out io.ReadCloser
log Logger
} }
type multiError []error // NewRaspivid returns a new Raspivid.
func NewRaspivid(l Logger) *Raspivid { return &Raspivid{log: l} }
func (me multiError) Error() string {
return fmt.Sprintf("%v", me)
}
// 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) error { func (r *Raspivid) Set(c Config) error {
var errs []error var errs []error
switch c.InputCodec { switch c.InputCodec {
case codecutil.H264, codecutil.MJPEG: case codecutil.H264, codecutil.MJPEG:
default: default:
c.InputCodec = raspividDefaultCodec c.InputCodec = defaultRaspividCodec
errs = append(errs, errBadCodec) errs = append(errs, errBadCodec)
} }
if c.Rotation > 359 { if c.Rotation > 359 {
c.Rotation = raspividDefaultRotation c.Rotation = defaultRaspividRotation
errs = append(errs, errBadRotation) errs = append(errs, errBadRotation)
} }
if c.Width == 0 { if c.Width == 0 {
c.Width = raspividDefaultWidth c.Width = defaultRaspividWidth
errs = append(errs, errBadWidth) errs = append(errs, errBadWidth)
} }
if c.Height == 0 { if c.Height == 0 {
c.Height = raspividDefaultHeight c.Height = defaultRaspividHeight
errs = append(errs, errBadHeight) errs = append(errs, errBadHeight)
} }
if c.FrameRate == 0 { if c.FrameRate == 0 {
c.FrameRate = raspividDefaultFramerate c.FrameRate = defaultRaspividFramerate
errs = append(errs, errBadFrameRate) errs = append(errs, errBadFrameRate)
} }
@ -111,45 +115,48 @@ func (r *Raspivid) Set(c Config) error {
c.Bitrate = 0 c.Bitrate = 0
if c.Quantization < 10 || c.Quantization > 40 { if c.Quantization < 10 || c.Quantization > 40 {
errs = append(errs, errBadQuantization) errs = append(errs, errBadQuantization)
c.Quantization = raspividDefaultQuantization c.Quantization = defaultRaspividQuantization
} }
} else { } else {
c.Quantization = 0 c.Quantization = 0
if c.Bitrate <= 0 { if c.Bitrate <= 0 {
errs = append(errs, errBadBitrate) errs = append(errs, errBadBitrate)
c.Bitrate = raspividDefaultBitrate c.Bitrate = defaultRaspividBitrate
} }
} }
if c.MinFrames <= 0 { if c.MinFrames <= 0 {
errs = append(errs, errBadMinFrames) errs = append(errs, errBadMinFrames)
c.MinFrames = raspividDefaultMinFrames c.MinFrames = defaultRaspividMinFrames
} }
if c.Brightness <= 0 || c.Brightness > 100 { if c.Brightness <= 0 || c.Brightness > 100 {
errs = append(errs, errBadBrightness) errs = append(errs, errBadBrightness)
c.Brightness = raspividDefaultBrightness c.Brightness = defaultRaspividBrightness
} }
if c.Saturation < -100 || c.Saturation > 100 { if c.Saturation < -100 || c.Saturation > 100 {
errs = append(errs, errBadSaturation) errs = append(errs, errBadSaturation)
c.Saturation = raspividDefaultSaturation c.Saturation = defaultRaspividSaturation
} }
if c.Exposure == "" || !stringInSlice(c.Exposure, ExposureModes[:]) { if c.Exposure == "" || !stringInSlice(c.Exposure, ExposureModes[:]) {
errs = append(errs, errBadExposure) errs = append(errs, errBadExposure)
c.Exposure = raspividDefaultExposure c.Exposure = defaultRaspividExposure
} }
if c.AutoWhiteBalance == "" || !stringInSlice(c.AutoWhiteBalance, AutoWhiteBalanceModes[:]) { if c.AutoWhiteBalance == "" || !stringInSlice(c.AutoWhiteBalance, AutoWhiteBalanceModes[:]) {
errs = append(errs, errBadAutoWhiteBalance) errs = append(errs, errBadAutoWhiteBalance)
c.AutoWhiteBalance = raspividDefaultAutoWhiteBalance c.AutoWhiteBalance = defaultRaspividAutoWhiteBalance
} }
r.cfg = c r.cfg = c
return multiError(errs) return multiError(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 { func (r *Raspivid) Start() error {
const disabled = "0" const disabled = "0"
args := []string{ args := []string{
@ -210,10 +217,16 @@ func (r *Raspivid) Start() error {
return nil 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) { func (r *Raspivid) Read(p []byte) (int, error) {
return r.out.Read(p) 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 { func (r *Raspivid) Stop() error {
if r.cmd == nil || r.cmd.Process == nil { if r.cmd == nil || r.cmd.Process == nil {
return errors.New("raspivid process was never started") return errors.New("raspivid process was never started")
@ -222,5 +235,5 @@ func (r *Raspivid) Stop() error {
if err != nil { if err != nil {
return fmt.Errorf("could not kill raspivid process: %w", err) return fmt.Errorf("could not kill raspivid process: %w", err)
} }
return nil return r.out.Close()
} }

View File

@ -34,6 +34,7 @@ import (
"bitbucket.org/ausocean/utils/logger" "bitbucket.org/ausocean/utils/logger"
) )
// Configuration defaults.
const ( const (
defaultWebcamInputPath = "/dev/video0" defaultWebcamInputPath = "/dev/video0"
defaultWebcamFrameRate = 25 defaultWebcamFrameRate = 25
@ -42,6 +43,7 @@ const (
defaultWebcamHeight = 720 defaultWebcamHeight = 720
) )
// Configuration field errors.
var ( var (
errWebcamBadFrameRate = errors.New("frame rate bad or unset, defaulting") errWebcamBadFrameRate = errors.New("frame rate bad or unset, defaulting")
errWebcamBadBitrate = errors.New("bitrate bad or unset, defaulting") errWebcamBadBitrate = errors.New("bitrate bad or unset, defaulting")
@ -49,6 +51,8 @@ var (
errWebcamHeight = errors.New("height bad or unset, defaulting") errWebcamHeight = 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 { type Webcam struct {
out io.ReadCloser out io.ReadCloser
log Logger log Logger
@ -56,35 +60,41 @@ type Webcam struct {
cmd *exec.Cmd cmd *exec.Cmd
} }
// NewWebcam returns a new Webcam.
func NewWebcam(l Logger) *Webcam { func NewWebcam(l Logger) *Webcam {
return &Webcam{log: l} 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) error { func (w *Webcam) Set(c Config) error {
var errs []error var errs []error
if c.Width == 0 { if c.Width == 0 {
errs = append(errs, errBadWidth) errs = append(errs, errBadWidth)
c.Width = raspividDefaultWidth c.Width = defaultWebcamWidth
} }
if c.Height == 0 { if c.Height == 0 {
errs = append(errs, errBadHeight) errs = append(errs, errBadHeight)
c.Height = raspividDefaultHeight c.Height = defaultWebcamHeight
} }
if c.FrameRate == 0 { if c.FrameRate == 0 {
errs = append(errs, errBadFrameRate) errs = append(errs, errBadFrameRate)
c.FrameRate = raspividDefaultFramerate c.FrameRate = defaultWebcamFrameRate
} }
if c.Bitrate <= 0 { if c.Bitrate <= 0 {
errs = append(errs, errBadBitrate) errs = append(errs, errBadBitrate)
c.Bitrate = raspividDefaultBitrate c.Bitrate = defaultWebcamBitrate
} }
w.cfg = c w.cfg = c
return multiError(errs) return multiError(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 { func (w *Webcam) Start() error {
args := []string{ args := []string{
"-i", w.cfg.InputPath, "-i", w.cfg.InputPath,
@ -118,6 +128,7 @@ func (w *Webcam) Start() error {
return nil return nil
} }
// Stop will kill the ffmpeg process and close the output pipe.
func (w *Webcam) Stop() error { func (w *Webcam) Stop() error {
if w.cmd == nil || w.cmd.Process == nil { if w.cmd == nil || w.cmd.Process == nil {
return errors.New("raspivid process was never started") return errors.New("raspivid process was never started")
@ -129,6 +140,10 @@ func (w *Webcam) Stop() error {
return w.out.Close() 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) { func (w *Webcam) Read(p []byte) (int, error) {
return w.out.Read(p) if w.out != nil {
return w.out.Read(p)
}
return 0, errors.New("webcam not streaming")
} }