/* 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 revid import ( "errors" "fmt" "io" "os/exec" "strings" "bitbucket.org/ausocean/utils/logger" ) // Configuration defaults. const ( defaultWebcamInputPath = "/dev/video0" defaultWebcamFrameRate = 25 defaultWebcamBitrate = 400 defaultWebcamWidth = 1280 defaultWebcamHeight = 720 ) // Configuration field errors. var ( errWebcamBadFrameRate = errors.New("frame rate bad or unset, defaulting") errWebcamBadBitrate = errors.New("bitrate bad or unset, defaulting") errWebcamWidth = errors.New("width 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 { out io.ReadCloser log Logger cfg Config 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 = defaultWebcamWidth } if c.Height == 0 { errs = append(errs, errBadHeight) c.Height = defaultWebcamHeight } if c.FrameRate == 0 { errs = append(errs, errBadFrameRate) c.FrameRate = defaultWebcamFrameRate } if c.Bitrate <= 0 { errs = append(errs, errBadBitrate) 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, "-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("raspivid process was never started") } err := w.cmd.Process.Kill() if err != nil { return fmt.Errorf("could not kill raspivid 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") }