/* DESCRIPTION webcam.go provides an implementation of AVDevice for webcams. AUTHORS Saxon A. Nelson-Milton 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 provides an implementation of AVDevice for webcams. 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{} isRunning bool } // 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 if len(errs) != 0 { return errs } return nil } // 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.H264: args = append(args, "-f", "h264", "-maxrate", fmt.Sprint(br), "-bufsize", fmt.Sprint(br/2), ) case codecutil.MJPEG: 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) } w.isRunning = true 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 { if !w.isRunning { return nil } w.isRunning = false 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") } // IsRunning is used to determine if the webcam is running. func (w *Webcam) IsRunning() bool { return w.isRunning }