From a6aef125fd0d0ea135ab7d32b3f05d3bc81f7592 Mon Sep 17 00:00:00 2001 From: Saxon Date: Tue, 5 Nov 2019 20:14:04 +1030 Subject: [PATCH] revid: cleaned up AVDevice implementations and added documentation to them --- revid/file.go | 23 ++++++++++++-- revid/geovision.go | 50 +++++++++++++++++++++--------- revid/inputs.go | 23 ++++++++++++++ revid/raspivid.go | 77 +++++++++++++++++++++++++++------------------- revid/webcam.go | 25 ++++++++++++--- 5 files changed, 143 insertions(+), 55 deletions(-) diff --git a/revid/file.go b/revid/file.go index 61eae8d1..d19ef69f 100644 --- a/revid/file.go +++ b/revid/file.go @@ -25,25 +25,30 @@ LICENSE package revid import ( + "errors" "fmt" "io" "os" ) +// AVFile is an implementation of the AVDevice interface for a file containg +// audio or video data. type AVFile struct { f io.ReadCloser cfg Config } -func NewAVFile() *AVFile { return &AVFile{} } -func (m *AVFile) Stop() error { return m.f.Close() } -func (m *AVFile) Read(p []byte) (int, error) { return m.f.Read(p) } +// 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) 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) @@ -52,3 +57,15 @@ func (m *AVFile) Start() error { } 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") +} diff --git a/revid/geovision.go b/revid/geovision.go index f0582316..713ebd97 100644 --- a/revid/geovision.go +++ b/revid/geovision.go @@ -39,17 +39,19 @@ import ( "bitbucket.org/ausocean/utils/logger" ) +// Configuration defaults. const ( - gvDefaultCameraIP = "192.168.1.50" - gvDefaultCodec = codecutil.H264 - gvDefaultHeight = 720 - gvDefaultFrameRate = 25 - gvDefaultBitrate = 400 - gvDefaultVBRBitrate = 400 - gvDefaultMinFrames = 100 - gvDefaultVBRQuality = "standard" + defaultGVCameraIP = "192.168.1.50" + defaultGVCodec = codecutil.H264 + defaultGVHeight = 720 + defaultGVFrameRate = 25 + defaultGVBitrate = 400 + defaultGVVBRBitrate = 400 + defaultGVMinFrames = 100 + defaultGVVBRQuality = "standard" ) +// Configuration field errors. var ( errGVBadCameraIP = errors.New("camera IP 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") ) +// 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 log Logger @@ -68,40 +73,45 @@ type GeoVision struct { rtcpClt *rtcp.Client } +// NewGeoVision returns a new GeoVision. 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 { var errs multiError if c.CameraIP == "" { errs = append(errs, errGVBadCameraIP) - c.CameraIP = gvDefaultCameraIP + c.CameraIP = defaultGVCameraIP } switch c.InputCodec { case codecutil.H264, codecutil.H265, codecutil.MJPEG: default: errs = append(errs, errGVBadCodec) - c.InputCodec = gvDefaultCodec + c.InputCodec = defaultGVCodec } if c.Height <= 0 { errs = append(errs, errGVBadHeight) - c.Height = gvDefaultHeight + c.Height = defaultGVHeight } if c.FrameRate <= 0 { errs = append(errs, errGVBadFrameRate) - c.FrameRate = gvDefaultFrameRate + c.FrameRate = defaultGVFrameRate } if c.Bitrate <= 0 { errs = append(errs, errGVBadBitrate) - c.Bitrate = gvDefaultBitrate + c.Bitrate = defaultGVBitrate } if c.MinFrames <= 0 { errs = append(errs, errGVBadMinFrames) - c.MinFrames = gvDefaultMinFrames + c.MinFrames = defaultGVMinFrames } switch c.VBRQuality { @@ -160,6 +170,9 @@ func (g *GeoVision) Set(c Config) error { 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 { var ( local, remote *net.TCPAddr @@ -236,6 +249,8 @@ func (g *GeoVision) Start() error { 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 { @@ -254,6 +269,11 @@ func (g *GeoVision) Stop() error { 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) { - return g.rtpClt.Read(p) + if g.rtpClt != nil { + return g.rtpClt.Read(p) + } + return 0, errors.New("cannot read, GeoVision not streaming") } diff --git a/revid/inputs.go b/revid/inputs.go index 775cf8ce..abe50bba 100644 --- a/revid/inputs.go +++ b/revid/inputs.go @@ -53,13 +53,36 @@ const ( ipCamPass = "admin" ) +// AVDevice describes a configurable audio or video device from which media data +// can be obtained. AVDevice implements 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) error + + // Start will start the AVDevice capturing media data; after which the Read + // method may be called to obtain the data. The format of the data may differ + // and should be specified by the implementation. Start() error + + // Stop will stop the AVDevice from capturing media data. From this point + // Reads will no longer be successful. Stop() error } +// multiError implements the built in error interface. multiError is used here +// to collect multi errors during validation of configruation parameters for o +// AVDevices. +type multiError []error + +func (me multiError) Error() string { + return fmt.Sprintf("%v", me) +} + // startRaspivid sets up things for input from raspivid i.e. starts // a raspivid process and pipes it's data output. func (r *Revid) startRaspivid() (func() error, error) { diff --git a/revid/raspivid.go b/revid/raspivid.go index 8e57202e..43f94cf3 100644 --- a/revid/raspivid.go +++ b/revid/raspivid.go @@ -35,22 +35,23 @@ import ( "bitbucket.org/ausocean/utils/logger" ) -// Raspivid AVDevice configuration defaults. +// Raspivid configuration defaults. const ( - raspividDefaultCodec = codecutil.H264 - raspividDefaultRotation = 0 - raspividDefaultWidth = 1280 - raspividDefaultHeight = 720 - raspividDefaultBrightness = 50 - raspividDefaultSaturation = 0 - raspividDefaultExposure = "auto" - raspividDefaultAutoWhiteBalance = "auto" - raspividDefaultMinFrames = 100 - raspividDefaultQuantization = 30 - raspividDefaultBitrate = 48000 - raspividDefaultFramerate = 25 + 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") @@ -66,44 +67,47 @@ var ( 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 { cfg Config cmd *exec.Cmd out io.ReadCloser + log Logger } -type multiError []error - -func (me multiError) Error() string { - return fmt.Sprintf("%v", me) -} +// NewRaspivid returns a new Raspivid. +func NewRaspivid(l 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) error { var errs []error switch c.InputCodec { case codecutil.H264, codecutil.MJPEG: default: - c.InputCodec = raspividDefaultCodec + c.InputCodec = defaultRaspividCodec errs = append(errs, errBadCodec) } if c.Rotation > 359 { - c.Rotation = raspividDefaultRotation + c.Rotation = defaultRaspividRotation errs = append(errs, errBadRotation) } if c.Width == 0 { - c.Width = raspividDefaultWidth + c.Width = defaultRaspividWidth errs = append(errs, errBadWidth) } if c.Height == 0 { - c.Height = raspividDefaultHeight + c.Height = defaultRaspividHeight errs = append(errs, errBadHeight) } if c.FrameRate == 0 { - c.FrameRate = raspividDefaultFramerate + c.FrameRate = defaultRaspividFramerate errs = append(errs, errBadFrameRate) } @@ -111,45 +115,48 @@ func (r *Raspivid) Set(c Config) error { c.Bitrate = 0 if c.Quantization < 10 || c.Quantization > 40 { errs = append(errs, errBadQuantization) - c.Quantization = raspividDefaultQuantization + c.Quantization = defaultRaspividQuantization } } else { c.Quantization = 0 if c.Bitrate <= 0 { errs = append(errs, errBadBitrate) - c.Bitrate = raspividDefaultBitrate + c.Bitrate = defaultRaspividBitrate } } if c.MinFrames <= 0 { errs = append(errs, errBadMinFrames) - c.MinFrames = raspividDefaultMinFrames + c.MinFrames = defaultRaspividMinFrames } if c.Brightness <= 0 || c.Brightness > 100 { errs = append(errs, errBadBrightness) - c.Brightness = raspividDefaultBrightness + c.Brightness = defaultRaspividBrightness } if c.Saturation < -100 || c.Saturation > 100 { errs = append(errs, errBadSaturation) - c.Saturation = raspividDefaultSaturation + c.Saturation = defaultRaspividSaturation } if c.Exposure == "" || !stringInSlice(c.Exposure, ExposureModes[:]) { errs = append(errs, errBadExposure) - c.Exposure = raspividDefaultExposure + c.Exposure = defaultRaspividExposure } if c.AutoWhiteBalance == "" || !stringInSlice(c.AutoWhiteBalance, AutoWhiteBalanceModes[:]) { errs = append(errs, errBadAutoWhiteBalance) - c.AutoWhiteBalance = raspividDefaultAutoWhiteBalance + c.AutoWhiteBalance = defaultRaspividAutoWhiteBalance } r.cfg = c 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 { const disabled = "0" args := []string{ @@ -210,10 +217,16 @@ func (r *Raspivid) Start() error { 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) { - 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 { if r.cmd == nil || r.cmd.Process == nil { return errors.New("raspivid process was never started") @@ -222,5 +235,5 @@ func (r *Raspivid) Stop() error { if err != nil { return fmt.Errorf("could not kill raspivid process: %w", err) } - return nil + return r.out.Close() } diff --git a/revid/webcam.go b/revid/webcam.go index b3ccb1c5..218bb031 100644 --- a/revid/webcam.go +++ b/revid/webcam.go @@ -34,6 +34,7 @@ import ( "bitbucket.org/ausocean/utils/logger" ) +// Configuration defaults. const ( defaultWebcamInputPath = "/dev/video0" defaultWebcamFrameRate = 25 @@ -42,6 +43,7 @@ const ( defaultWebcamHeight = 720 ) +// Configuration field errors. var ( errWebcamBadFrameRate = errors.New("frame rate 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") ) +// 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 Logger @@ -56,35 +60,41 @@ type Webcam struct { cmd *exec.Cmd } +// NewWebcam returns a new Webcam. func NewWebcam(l 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) error { var errs []error if c.Width == 0 { errs = append(errs, errBadWidth) - c.Width = raspividDefaultWidth + c.Width = defaultWebcamWidth } if c.Height == 0 { errs = append(errs, errBadHeight) - c.Height = raspividDefaultHeight + c.Height = defaultWebcamHeight } if c.FrameRate == 0 { errs = append(errs, errBadFrameRate) - c.FrameRate = raspividDefaultFramerate + c.FrameRate = defaultWebcamFrameRate } if c.Bitrate <= 0 { errs = append(errs, errBadBitrate) - c.Bitrate = raspividDefaultBitrate + c.Bitrate = defaultWebcamBitrate } w.cfg = c 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 { args := []string{ "-i", w.cfg.InputPath, @@ -118,6 +128,7 @@ func (w *Webcam) Start() error { 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("raspivid process was never started") @@ -129,6 +140,10 @@ func (w *Webcam) Stop() error { 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) { - return w.out.Read(p) + if w.out != nil { + return w.out.Read(p) + } + return 0, errors.New("webcam not streaming") }