mirror of https://bitbucket.org/ausocean/av.git
215 lines
5.1 KiB
Go
215 lines
5.1 KiB
Go
/*
|
|
DESCRIPTION
|
|
webcam.go provides an implementation of AVDevice for webcams.
|
|
|
|
AUTHORS
|
|
Saxon A. Nelson-Milton <saxon@ausocean.org>
|
|
|
|
LICENSE
|
|
Copyright (C) 2019 the Australian Ocean Lab (AusOcean)
|
|
|
|
It is free software: you can redistribute it and/or modify them
|
|
under the terms of the GNU General Public License as published by the
|
|
Free Software Foundation, either version 3 of the License, or (at your
|
|
option) any later version.
|
|
|
|
It is distributed in the hope that it will be useful, but WITHOUT
|
|
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
|
for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
in gpl.txt. If not, see http://www.gnu.org/licenses.
|
|
*/
|
|
|
|
package webcam
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"os/exec"
|
|
"strings"
|
|
|
|
"bitbucket.org/ausocean/av/codec/codecutil"
|
|
"bitbucket.org/ausocean/av/device"
|
|
"bitbucket.org/ausocean/av/revid/config"
|
|
"bitbucket.org/ausocean/utils/logger"
|
|
)
|
|
|
|
// Used to indicate package in logging.
|
|
const pkg = "webcam: "
|
|
|
|
// Configuration defaults.
|
|
const (
|
|
defaultInputPath = "/dev/video0"
|
|
defaultFrameRate = 25
|
|
defaultBitrate = 400
|
|
defaultWidth = 1280
|
|
defaultHeight = 720
|
|
)
|
|
|
|
// Configuration field errors.
|
|
var (
|
|
errBadFrameRate = errors.New("frame rate bad or unset, defaulting")
|
|
errBadBitrate = errors.New("bitrate bad or unset, defaulting")
|
|
errBadWidth = errors.New("width bad or unset, defaulting")
|
|
errBadHeight = errors.New("height bad or unset, defaulting")
|
|
errBadInputPath = errors.New("input path 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 config.Logger
|
|
cfg config.Config
|
|
cmd *exec.Cmd
|
|
done chan struct{}
|
|
}
|
|
|
|
// New returns a new Webcam.
|
|
func New(l config.Logger) *Webcam {
|
|
return &Webcam{
|
|
log: l,
|
|
done: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
// Name returns the name of the device.
|
|
func (w *Webcam) Name() string {
|
|
return "Webcam"
|
|
}
|
|
|
|
// 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.Config) error {
|
|
var errs device.MultiError
|
|
if c.InputPath == "" {
|
|
const defaultInputPath = "/dev/video0"
|
|
errs = append(errs, errBadInputPath)
|
|
c.InputPath = defaultInputPath
|
|
}
|
|
|
|
if c.Width == 0 {
|
|
errs = append(errs, errBadWidth)
|
|
c.Width = defaultWidth
|
|
}
|
|
|
|
if c.Height == 0 {
|
|
errs = append(errs, errBadHeight)
|
|
c.Height = defaultHeight
|
|
}
|
|
|
|
if c.FrameRate == 0 {
|
|
errs = append(errs, errBadFrameRate)
|
|
c.FrameRate = defaultFrameRate
|
|
}
|
|
|
|
if c.Bitrate <= 0 {
|
|
errs = append(errs, errBadBitrate)
|
|
c.Bitrate = defaultBitrate
|
|
}
|
|
w.cfg = c
|
|
return 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 {
|
|
br := w.cfg.Bitrate * 1000
|
|
|
|
args := []string{
|
|
"-i", w.cfg.InputPath,
|
|
"-r", fmt.Sprint(w.cfg.FrameRate),
|
|
"-b:v", fmt.Sprint(br),
|
|
"-s", fmt.Sprintf("%dx%d", w.cfg.Width, w.cfg.Height),
|
|
}
|
|
|
|
switch w.cfg.InputCodec {
|
|
default:
|
|
return fmt.Errorf("revid: invalid input codec: %v", w.cfg.InputCodec)
|
|
case codecutil.CodecH264:
|
|
args = append(args,
|
|
"-f", "h264",
|
|
"-maxrate", fmt.Sprint(br),
|
|
"-bufsize", fmt.Sprint(br/2),
|
|
)
|
|
case codecutil.CodecMJPEG:
|
|
args = append(args,
|
|
"-f", "mjpeg",
|
|
)
|
|
}
|
|
|
|
args = append(args,
|
|
"-",
|
|
)
|
|
|
|
w.log.Log(logger.Info, pkg+"ffmpeg args", "args", strings.Join(args, " "))
|
|
w.cmd = exec.Command("ffmpeg", args...)
|
|
|
|
var err error
|
|
w.out, err = w.cmd.StdoutPipe()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create pipe: %w", err)
|
|
}
|
|
|
|
stderr, err := w.cmd.StderrPipe()
|
|
if err != nil {
|
|
return fmt.Errorf("could not pipe command error: %w", err)
|
|
}
|
|
|
|
go func() {
|
|
for {
|
|
select {
|
|
case <-w.done:
|
|
w.cfg.Logger.Log(logger.Info, "webcam.Stop() called, finished checking stderr")
|
|
return
|
|
default:
|
|
buf, err := ioutil.ReadAll(stderr)
|
|
if err != nil {
|
|
w.cfg.Logger.Log(logger.Error, "could not read stderr", "error", err)
|
|
return
|
|
}
|
|
|
|
if len(buf) != 0 {
|
|
w.cfg.Logger.Log(logger.Error, "error from webcam stderr", "error", string(buf))
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
|
|
w.cfg.Logger.Log(logger.Info, "starting webcam")
|
|
err = w.cmd.Start()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to start ffmpeg: %w", err)
|
|
}
|
|
w.cfg.Logger.Log(logger.Info, "webcam started")
|
|
|
|
return nil
|
|
}
|
|
|
|
// Stop will kill the ffmpeg process and close the output pipe.
|
|
func (w *Webcam) Stop() error {
|
|
close(w.done)
|
|
if w.cmd == nil || w.cmd.Process == nil {
|
|
return errors.New("ffmpeg process was never started")
|
|
}
|
|
err := w.cmd.Process.Kill()
|
|
if err != nil {
|
|
return fmt.Errorf("could not kill ffmpeg process: %w", err)
|
|
}
|
|
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) {
|
|
if w.out != nil {
|
|
return w.out.Read(p)
|
|
}
|
|
return 0, errors.New("webcam not streaming")
|
|
}
|