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
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")
}

View File

@ -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")
}

View File

@ -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) {

View File

@ -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()
}

View File

@ -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")
}