mirror of https://bitbucket.org/ausocean/av.git
revid: cleaned up AVDevice implementations and added documentation to them
This commit is contained in:
parent
50c7fe139b
commit
a6aef125fd
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue