/* DESCRIPTION raspivid.go provides an implementation of the AVDevice interface for raspivid. 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 raspivid provides an implementation of AVDevice for the raspberry // pi camera. package raspivid import ( "errors" "fmt" "io" "io/ioutil" "os/exec" "strconv" "strings" "bitbucket.org/ausocean/av/codec/codecutil" "bitbucket.org/ausocean/av/device" "bitbucket.org/ausocean/av/revid/config" "bitbucket.org/ausocean/utils/logger" "bitbucket.org/ausocean/utils/sliceutils" ) // To indicate package when logging. const pkg = "raspivid: " // Raspivid configuration defaults. const ( defaultRaspividCodec = codecutil.H264 defaultRaspividRotation = 0 defaultRaspividWidth = 1280 defaultRaspividHeight = 720 defaultRaspividBrightness = 50 defaultRaspividSaturation = 0 defaultRaspividExposure = "auto" defaultRaspividAutoWhiteBalance = "auto" defaultRaspividMinFrames = 100 defaultRaspividQuantization = 30 defaultRaspividBitrate = 4800 defaultRaspividFramerate = 25 defaultRaspividSharpness = 0 defaultRaspividContrast = 0 defaultRaspividISO = 100 defaultRaspividEV = 0 defaultRaspividAWBGains = "1.0,1.0" ) // Configuration errors. var ( errBadCodec = errors.New("codec bad or unset, defaulting") errBadRotation = errors.New("rotation bad or unset, defaulting") errBadWidth = errors.New("width bad or unset, defaulting") errBadHeight = errors.New("height bad or unset, defaulting") errBadFrameRate = errors.New("framerate bad or unset, defaulting") errBadBitrate = errors.New("bitrate bad or unset, defaulting") errBadMinFrames = errors.New("min frames bad or unset, defaulting") errBadSaturation = errors.New("saturation bad or unset, defaulting") errBadBrightness = errors.New("brightness bad or unset, defaulting") errBadExposure = errors.New("exposure bad or unset, defaulting") errBadAutoWhiteBalance = errors.New("auto white balance bad or unset, defaulting") errBadQuantization = errors.New("quantization bad or unset, defaulting") errBadAWBGains = errors.New("auto white balance gains bad or unset, defaulting") errBadEV = errors.New("exposure value bad or unset, defaulting") errBadContrast = errors.New("contrast bad or unset, defaulting") errBadSharpness = errors.New("sharpness bad or unset, defaulting") errBadISO = errors.New("iso bad or unset, defaulting") ) // Possible modes for raspivid --exposure parameter. var ExposureModes = [...]string{ "auto", "night", "nightpreview", "backlight", "spotlight", "sports", "snow", "beach", "verylong", "fixedfps", "antishake", "fireworks", } // Possible modes for raspivid --awb parameter. var AutoWhiteBalanceModes = [...]string{ "off", "auto", "sun", "cloud", "shade", "tungsten", "fluorescent", "incandescent", "flash", "horizon", } // 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 { cfg config.Config cmd *exec.Cmd out io.ReadCloser log config.Logger done chan struct{} isRunning bool } // New returns a new Raspivid. func New(l config.Logger) *Raspivid { return &Raspivid{ log: l, done: make(chan struct{}), } } // Name returns the name of the device. func (r *Raspivid) Name() string { return "Raspivid" } // 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.Config) error { var errs device.MultiError switch c.InputCodec { case codecutil.H264, codecutil.MJPEG: default: c.InputCodec = defaultRaspividCodec errs = append(errs, errBadCodec) } if c.Rotation > 359 { c.Rotation = defaultRaspividRotation errs = append(errs, errBadRotation) } if c.Width == 0 { c.Width = defaultRaspividWidth errs = append(errs, errBadWidth) } if c.Height == 0 { c.Height = defaultRaspividHeight errs = append(errs, errBadHeight) } if c.FrameRate == 0 { c.FrameRate = defaultRaspividFramerate errs = append(errs, errBadFrameRate) } if c.CBR || sliceutils.ContainsUint8(c.Outputs, config.OutputRTMP) { c.Quantization = 0 if c.Bitrate <= 0 { errs = append(errs, errBadBitrate) c.Bitrate = defaultRaspividBitrate } } else { c.Bitrate = 0 if c.Quantization < 10 || c.Quantization > 40 { errs = append(errs, errBadQuantization) c.Quantization = defaultRaspividQuantization } } if c.MinFrames <= 0 { errs = append(errs, errBadMinFrames) c.MinFrames = defaultRaspividMinFrames } if c.Brightness <= 0 || c.Brightness > 100 { errs = append(errs, errBadBrightness) c.Brightness = defaultRaspividBrightness } if c.Saturation < -100 || c.Saturation > 100 { errs = append(errs, errBadSaturation) c.Saturation = defaultRaspividSaturation } if c.Exposure == "" || !sliceutils.ContainsString(ExposureModes[:], c.Exposure) { errs = append(errs, errBadExposure) c.Exposure = defaultRaspividExposure } if c.EV < -10 || c.EV > 10 { errs = append(errs, errBadEV) c.EV = defaultRaspividEV } if c.Contrast < -100 || c.Contrast > 100 { errs = append(errs, errBadContrast) c.Contrast = defaultRaspividContrast } if c.Sharpness < -100 || c.Sharpness > 100 { errs = append(errs, errBadSharpness) c.Sharpness = defaultRaspividSharpness } if c.AutoWhiteBalance == "" || !sliceutils.ContainsString(AutoWhiteBalanceModes[:], c.AutoWhiteBalance) { errs = append(errs, errBadAutoWhiteBalance) c.AutoWhiteBalance = defaultRaspividAutoWhiteBalance } if !goodAWBGains(c.AWBGains) { errs = append(errs, errBadAWBGains) c.AWBGains = defaultRaspividAWBGains } if c.ISO == 0 || c.ISO < 100 || c.ISO > 800 { errs = append(errs, errBadISO) c.ISO = defaultRaspividISO } r.cfg = c return errs } func goodAWBGains(g string) bool { parts := strings.Split(g, ",") if len(parts) != 2 { return false } bg, err := strconv.ParseFloat(parts[0], 64) if err != nil { return false } rg, err := strconv.ParseFloat(parts[1], 64) if err != nil { return false } if bg < 0 || rg < 0 { return false } return true } // 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 { args, err := r.createArgs() if err != nil { return fmt.Errorf("could not create raspivid args: %w", err) } r.cfg.Logger.Log(logger.Info, pkg+"raspivid args", "raspividArgs", strings.Join(args, " ")) r.cmd = exec.Command("raspivid", args...) r.out, err = r.cmd.StdoutPipe() if err != nil { return fmt.Errorf("could not pipe command output: %w", err) } stderr, err := r.cmd.StderrPipe() if err != nil { return fmt.Errorf("could not pipe command error: %w", err) } go func() { for { select { case <-r.done: r.cfg.Logger.Log(logger.Info, "raspivid.Stop() called, finished checking stderr") return default: buf, err := ioutil.ReadAll(stderr) if err != nil { r.cfg.Logger.Log(logger.Error, "could not read stderr", "error", err) return } if len(buf) != 0 { r.cfg.Logger.Log(logger.Error, "error from raspivid stderr", "error", string(buf)) return } } } }() err = r.cmd.Start() if err != nil { return fmt.Errorf("could not start raspivid command: %w", err) } r.isRunning = true 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) { 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 { if r.isRunning == false { return nil } close(r.done) if r.cmd == nil || r.cmd.Process == nil { return errors.New("raspivid process was never started") } err := r.cmd.Process.Kill() if err != nil { return fmt.Errorf("could not kill raspivid process: %w", err) } r.isRunning = false return r.out.Close() } // IsRunning is used to determine if the pi's camera is running. func (r *Raspivid) IsRunning() bool { return r.isRunning } func (r *Raspivid) createArgs() ([]string, error) { const disabled = "0" args := []string{ "--output", "-", "--nopreview", "--timeout", disabled, "--width", fmt.Sprint(r.cfg.Width), "--height", fmt.Sprint(r.cfg.Height), "--bitrate", fmt.Sprint(r.cfg.Bitrate * 1000), // Convert from kbps to bps. "--framerate", fmt.Sprint(r.cfg.FrameRate), "--rotation", fmt.Sprint(r.cfg.Rotation), "--brightness", fmt.Sprint(r.cfg.Brightness), "--saturation", fmt.Sprint(r.cfg.Saturation), "--sharpness", fmt.Sprint(r.cfg.Sharpness), "--contrast", fmt.Sprint(r.cfg.Contrast), "--awb", fmt.Sprint(r.cfg.AutoWhiteBalance), "--exposure", fmt.Sprint(r.cfg.Exposure), } if r.cfg.ISO != defaultRaspividISO { args = append(args, []string{"--ISO", fmt.Sprint(r.cfg.ISO)}...) } if r.cfg.Exposure == "off" { args = append(args, []string{"--ev", fmt.Sprint(r.cfg.EV)}...) } if r.cfg.AutoWhiteBalance == "off" { args = append(args, []string{"--awbgains", fmt.Sprint(r.cfg.AWBGains)}...) } if r.cfg.HorizontalFlip { args = append(args, "--hflip") } if r.cfg.VerticalFlip { args = append(args, "--vflip") } if r.cfg.HorizontalFlip { args = append(args, "--hflip") } switch r.cfg.InputCodec { default: return []string{}, fmt.Errorf("revid: invalid input codec: %v", r.cfg.InputCodec) case codecutil.H264: args = append(args, "--codec", "H264", "--inline", "--intra", fmt.Sprint(r.cfg.MinFrames), ) if !r.cfg.CBR { args = append(args, "-qp", fmt.Sprint(r.cfg.Quantization)) } case codecutil.MJPEG: args = append(args, "--codec", "MJPEG") } return args, nil }