/* 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 import ( "errors" "fmt" "io" "os/exec" "strings" "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 } // New returns a new Webcam. func New(l config.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.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 { args := []string{ "-i", w.cfg.InputPath, "-f", "h264", "-r", fmt.Sprint(w.cfg.FrameRate), } br := w.cfg.Bitrate * 1000 args = append(args, "-b:v", fmt.Sprint(br), "-maxrate", fmt.Sprint(br), "-bufsize", fmt.Sprint(br/2), "-s", fmt.Sprintf("%dx%d", w.cfg.Width, w.cfg.Height), "-", ) 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) } err = w.cmd.Start() if err != nil { return fmt.Errorf("failed to start ffmpeg: %w", err) } 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("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") }