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
|
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")
|
||||||
|
}
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue