From 45c019a0629af56310efbac6488a66fea26abc03 Mon Sep 17 00:00:00 2001 From: Saxon Nelson-Milton Date: Wed, 20 Jan 2021 11:49:01 +1030 Subject: [PATCH 1/3] device: add raspistill package housing release and testing implementations --- codec/jpeg/lex.go | 2 +- codec/jpeg/lex_test.go | 8 +- container/mts/pes/helpers.go | 2 +- device/raspistill/imp_release.go | 137 +++++++++++++++++++++++ device/raspistill/imp_testing.go | 181 +++++++++++++++++++++++++++++++ device/raspistill/raspistill.go | 139 ++++++++++++++++++++++++ revid/config/config.go | 54 ++++++--- revid/config/config_test.go | 40 ++++--- revid/config/variables.go | 181 ++++++++++++++++++++----------- 9 files changed, 643 insertions(+), 101 deletions(-) create mode 100644 device/raspistill/imp_release.go create mode 100644 device/raspistill/imp_testing.go create mode 100644 device/raspistill/raspistill.go diff --git a/codec/jpeg/lex.go b/codec/jpeg/lex.go index e4c38073..dca41c98 100644 --- a/codec/jpeg/lex.go +++ b/codec/jpeg/lex.go @@ -98,7 +98,7 @@ func Lex(dst io.Writer, src io.Reader, delay time.Duration) error { if nImg == 0 { <-tick - Log.Debug("writing buf","len(buf)",len(buf)) + Log.Debug("writing buf", "len(buf)", len(buf)) _, err = dst.Write(buf) if err != nil { return err diff --git a/codec/jpeg/lex_test.go b/codec/jpeg/lex_test.go index 4a1870c7..1b031378 100644 --- a/codec/jpeg/lex_test.go +++ b/codec/jpeg/lex_test.go @@ -56,14 +56,14 @@ var jpegTests = []struct { input: []byte{0xff, 0xd8, 0xff, 0xd9}, delay: 0, want: [][]byte{{0xff, 0xd8, 0xff, 0xd9}}, - err: io.ErrUnexpectedEOF, + err: io.ErrUnexpectedEOF, }, { name: "null delayed", input: []byte{0xff, 0xd8, 0xff, 0xd9}, delay: time.Millisecond, want: [][]byte{{0xff, 0xd8, 0xff, 0xd9}}, - err: io.ErrUnexpectedEOF, + err: io.ErrUnexpectedEOF, }, { name: "full", @@ -82,7 +82,7 @@ var jpegTests = []struct { {0xff, 0xd8, 'l', 'e', 'n', 'g', 't', 'h', 0xff, 0xd9}, {0xff, 0xd8, 's', 'p', 'r', 'e', 'a', 'd', 0xff, 0xd9}, }, - err: io.ErrUnexpectedEOF, + err: io.ErrUnexpectedEOF, }, { name: "full delayed", @@ -101,7 +101,7 @@ var jpegTests = []struct { {0xff, 0xd8, 'l', 'e', 'n', 'g', 't', 'h', 0xff, 0xd9}, {0xff, 0xd8, 's', 'p', 'r', 'e', 'a', 'd', 0xff, 0xd9}, }, - err: io.ErrUnexpectedEOF, + err: io.ErrUnexpectedEOF, }, } diff --git a/container/mts/pes/helpers.go b/container/mts/pes/helpers.go index 23b8ee94..f0abd203 100644 --- a/container/mts/pes/helpers.go +++ b/container/mts/pes/helpers.go @@ -31,7 +31,7 @@ const ( H264SID = 27 H265SID = 36 MJPEGSID = 136 - JPEGSID = 137 + JPEGSID = 137 PCMSID = 192 ADPCMSID = 193 ) diff --git a/device/raspistill/imp_release.go b/device/raspistill/imp_release.go new file mode 100644 index 00000000..bc6e3dc5 --- /dev/null +++ b/device/raspistill/imp_release.go @@ -0,0 +1,137 @@ +// +build !test + +/* +DESCRIPTION + release.go provides implementations for the Raspistill struct for release + conditions, i.e. we're running on a raspberry pi with access to the actual + raspistill utility with a pi camera connected. The code here runs a raspistill + background process and reads captured images from the camera. + +AUTHORS + Saxon 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 raspistill + +import ( + "errors" + "fmt" + "io" + "io/ioutil" + "os/exec" + "strings" + + "bitbucket.org/ausocean/av/revid/config" +) + +type raspistill struct { + cfg config.Config + cmd *exec.Cmd + out io.ReadCloser + log config.Logger + done chan struct{} + isRunning bool +} + +func new(l config.Logger) raspistill { + return raspistill{ + log: l, + done: make(chan struct{}), + } +} + +func (r *Raspistill) stop() error { + if r.isRunning == false { + return nil + } + close(r.done) + if r.cmd == nil || r.cmd.Process == nil { + return errors.New("raspistill process was never started") + } + err := r.cmd.Process.Kill() + if err != nil { + return fmt.Errorf("could not kill raspistill process: %w", err) + } + r.isRunning = false + return r.out.Close() +} + +func (r *Raspistill) start() error { + const disabled = "0" + args := []string{ + "--output", "-", + "--nopreview", + "--width", fmt.Sprint(r.cfg.Width), + "--height", fmt.Sprint(r.cfg.Height), + "--rotation", fmt.Sprint(r.cfg.Rotation), + "--timeout", fmt.Sprint(r.cfg.TimelapseDuration), + "--timelapse", fmt.Sprint(r.cfg.TimelapseInterval), + "--quality", fmt.Sprint(r.cfg.SnapQuality), + } + + r.log.Info(pkg+"raspistill args", "args", strings.Join(args, " ")) + r.cmd = exec.Command("raspistill", args...) + + var err error + 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.log.Info("raspistill.Stop() called, finished checking stderr") + return + default: + buf, err := ioutil.ReadAll(stderr) + if err != nil { + r.log.Error("could not read stderr", "error", err) + return + } + + if len(buf) != 0 { + r.log.Error("error from raspistill stderr", "error", string(buf)) + return + } + } + } + }() + + err = r.cmd.Start() + if err != nil { + return fmt.Errorf("could not start raspistill process: %w", err) + } + r.isRunning = true + + return nil +} + +func (r *Raspistill) read(p []byte) (int, error) { + if r.out != nil { + return r.out.Read(p) + } + return 0, errNotStarted +} diff --git a/device/raspistill/imp_testing.go b/device/raspistill/imp_testing.go new file mode 100644 index 00000000..6741bfc8 --- /dev/null +++ b/device/raspistill/imp_testing.go @@ -0,0 +1,181 @@ +// +build test + +/* +DESCRIPTION + test.go provides test implementations of the raspistill methods when the + "test" build tag is specified. In this mode, raspistill simply provides + arbitrary loaded JPEG images when Raspistill.Read() is called. + +AUTHORS + Saxon 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 raspistill + +import ( + "fmt" + "io" + "io/ioutil" + "os" + "strconv" + "sync" + "time" + + "bitbucket.org/ausocean/av/revid/config" +) + +const ( + noOfImages = 6 + imgDir = "/go/src/bitbucket.org/ausocean/test/test-data/av/input/jpeg/" + jpgExt = ".jpg" +) + +type raspistill struct { + log config.Logger + cfg config.Config + isRunning bool + images [noOfImages][]byte + imgCnt int + durTicker *time.Ticker + intvlTicker *time.Ticker + buf []byte + init bool + term chan struct{} + mu sync.Mutex +} + +func new(l config.Logger) raspistill { + l.Debug("creating new test raspistill input") + + r := raspistill{log: l} + + // Get to the right file location of the JPEG images. + home, err := os.UserHomeDir() + if err != nil { + panic(fmt.Sprintf("could not get home directory: %v", err)) + } + + // Load the test images into the images slice. + // We expect the 6 images test images to be named 0.jpg through to 5.jpg. + r.log.Debug("loading test JPEG images") + for i, _ := range r.images { + path := imgDir + strconv.Itoa(i) + jpgExt + r.images[i], err = ioutil.ReadFile(home + imgDir + strconv.Itoa(i) + jpgExt) + + if err != nil { + r.log.Fatal("error loading test image", "imageNum", i, "error", err) + } + r.log.Debug("image loaded", "path/name", path, "size", len(r.images[i])) + } + return r +} + +// stop sets isRunning flag to false, indicating no further captures. Calls on +// Raspistill.read return error. +func (r *Raspistill) stop() error { + r.log.Debug("stopping test raspistill") + r.isRunning = false + return nil +} + +// start creates the timelapse interval and duration tickers i.e. also starting +// them and sets isRunning flag to true indicating that raspistill is capturing. +func (r *Raspistill) start() error { + r.log.Debug("starting test raspistill") + r.durTicker = time.NewTicker(r.cfg.TimelapseDuration) + r.intvlTicker = time.NewTicker(r.cfg.TimelapseInterval) + + r.term = make(chan struct{}) + + r.loadImg() + + go r.capture() + + r.setRunning(true) + + return nil +} + +func (r *Raspistill) loadImg() { + r.log.Debug("appending new image on to buffer and copying next image p", "imgCnt", r.imgCnt) + imgBytes := r.images[r.imgCnt%noOfImages] + if len(imgBytes) == 0 { + panic("length of image bytes should not be 0") + } + r.imgCnt++ + + r.mu.Lock() + r.buf = append(r.buf, imgBytes...) + r.log.Debug("added img to buf", "len(imgBytes)", len(imgBytes)) + r.mu.Unlock() +} + +func (r *Raspistill) capture() { + for { + select { + case t := <-r.intvlTicker.C: + r.log.Debug("got interval tick", "tick", t) + r.loadImg() + r.intvlTicker.Reset(r.cfg.TimelapseInterval) + + case <-r.term: + r.setRunning(false) + return + + case t := <-r.durTicker.C: + r.log.Debug("got duration tick, timelapse over", "tick", t) + r.buf = nil + return + } + } +} + +// read blocks until either another timelapse interval has completed, in which +// case we provide the next jpeg to p, or, the timelapse duration has completed +// in which case we don't read and provide io.EOF error. +func (r *Raspistill) read(p []byte) (int, error) { + r.log.Debug("reading from test raspistill") + if !r.running() { + return 0, errNotStarted + } + + r.mu.Lock() + defer r.mu.Unlock() + if r.buf == nil { + return 0, io.EOF + } + n := copy(p, r.buf) + r.log.Debug("copied", "p[:2]", p[:2], "p[n-2:n]", p[n-2:n]) + r.buf = r.buf[n:] + + return n, nil +} + +func (r *Raspistill) setRunning(s bool) { + r.mu.Lock() + r.isRunning = s + r.mu.Unlock() +} + +func (r *Raspistill) running() bool { + r.mu.Lock() + ir := r.isRunning + r.mu.Unlock() + return ir +} diff --git a/device/raspistill/raspistill.go b/device/raspistill/raspistill.go new file mode 100644 index 00000000..ce2db70c --- /dev/null +++ b/device/raspistill/raspistill.go @@ -0,0 +1,139 @@ +/* +DESCRIPTION + raspistill.go provides an implementation of the AVDevice interface for the + raspistill raspberry pi camera interfacing utility. This allows for the + capture of single frames over time, i.e. a timelapse form of capture. + +AUTHORS + Saxon 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 raspistill rovides an implementation of the AVDevice interface for the +// raspistill raspberry pi camera interfacing utility. This allows for the +// capture of single frames over time, i.e. a timelapse form of capture. +package raspistill + +import ( + "errors" + "fmt" + "time" + + "bitbucket.org/ausocean/av/device" + "bitbucket.org/ausocean/av/revid/config" +) + +// To indicate package when logging. +const pkg = "raspistill: " + +// Config field validation bounds. +const ( + minTimelapseDuration = 30 * time.Second // s + maxTimelapseDuration = 86400 * time.Second // s = 24 hours + minTimelapseInterval = 1 * time.Second // s + maxTimelapseInterval = 86400 * time.Second // s = 24 hours +) + +// Raspivid configuration defaults. +const ( + defaultRotation = 0 // degrees + defaultWidth = 1280 // pixels + defaultHeight = 720 // pixels + defaultSnapQuality = 75 // % + defaultTimelapseDuration = maxTimelapseDuration // ms + defaultTimelapseInterval = 3600 * time.Second // ms +) + +// Configuration errors. +var ( + errBadRotation = fmt.Errorf("rotation bad or unset, defaulting to: %v", defaultRotation) + errBadWidth = fmt.Errorf("width bad or unset, defaulting to: %v", defaultWidth) + errBadHeight = fmt.Errorf("height bad or unset, defaulting to: %v", defaultHeight) + errBadSnapQuality = fmt.Errorf("SnapQuality bad or unset, defaulting to: %v", defaultSnapQuality) + errBadTimelapseDuration = fmt.Errorf("TimelapseDuration bad or unset, defaulting to: %v", defaultTimelapseDuration) + errBadTimelapseInterval = fmt.Errorf("TimelapseInterval bad or unset, defaulting to: %v", defaultTimelapseInterval) +) + +// Misc erros. +var errNotStarted = errors.New("cannot read, raspistill not started") + +// Raspistill is an implementation of AVDevice that provides control over the +// raspistill utility for using the raspberry pi camera for the capture of +// singular images. +type Raspistill struct{ raspistill } + +// New returns a new Raspivid. +func New(l config.Logger) *Raspistill { return &Raspistill{raspistill: new(l)} } + +// Start will prepare the arguments for the raspistill command using the +// configuration set using the Set method then call the raspistill command, +// piping the image output from which the Read method will read from. +func (r *Raspistill) Start() error { return r.start() } + +// Read implements io.Reader. Calling read before Start has been called will +// result in 0 bytes read and an error. +func (r *Raspistill) Read(p []byte) (int, error) { return r.read(p) } + +// Stop will terminate the raspistill process and close the output pipe. +func (r *Raspistill) Stop() error { return r.stop() } + +// IsRunning is used to determine if the pi's camera is running. +func (r *Raspistill) IsRunning() bool { return r.isRunning } + +// Name returns the name of the device. +func (r *Raspistill) Name() string { return "Raspistill" } + +// 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 *Raspistill) Set(c config.Config) error { + var errs device.MultiError + + if c.Rotation > 359 || c.Rotation < 0 { + c.Rotation = defaultRotation + errs = append(errs, errBadRotation) + } + + if c.Width == 0 { + c.Width = defaultWidth + errs = append(errs, errBadWidth) + } + + if c.Height == 0 { + c.Height = defaultHeight + errs = append(errs, errBadHeight) + } + + if c.SnapQuality < 0 || c.SnapQuality > 100 { + c.SnapQuality = defaultSnapQuality + errs = append(errs, errBadSnapQuality) + } + + if c.TimelapseDuration > maxTimelapseDuration || c.TimelapseDuration < minTimelapseDuration { + c.TimelapseDuration = defaultTimelapseDuration + errs = append(errs, errBadTimelapseDuration) + } + + if c.TimelapseInterval > maxTimelapseInterval || c.TimelapseInterval < minTimelapseInterval { + c.TimelapseInterval = defaultTimelapseInterval + errs = append(errs, errBadTimelapseInterval) + } + + r.cfg = c + return errs +} diff --git a/revid/config/config.go b/revid/config/config.go index 673677ea..f727ffdc 100644 --- a/revid/config/config.go +++ b/revid/config/config.go @@ -35,6 +35,11 @@ import ( type Logger interface { SetLevel(int8) Log(level int8, message string, params ...interface{}) + Debug(msg string, params ...interface{}) + Info(msg string, params ...interface{}) + Warning(msg string, params ...interface{}) + Error(msg string, params ...interface{}) + Fatal(msg string, params ...interface{}) } // Enums to define inputs, outputs and codecs. @@ -45,6 +50,7 @@ const ( // Input/Output. InputFile InputRaspivid + InputRaspistill InputV4L InputRTSP InputAudio @@ -55,10 +61,12 @@ const ( OutputHTTP OutputMPEGTS OutputFile + OutputFiles // Codecs. H264 H265 + MJPEG JPEG ) @@ -142,7 +150,9 @@ type Config struct { // // Valid values are defined by enums: // InputRaspivid: - // Read data from a Raspberry Pi Camera. + // Use raspivid utility to capture video from Raspberry Pi Camera. + // InputRaspistill: + // Use raspistill utility to capture images from the Raspberry Pi Camera. // InputV4l: // Read from webcam. // InputFile: @@ -198,7 +208,7 @@ type Config struct { // Outputs define the outputs we wish to output data too. // // Valid outputs are defined by enums: - // OutputFile: + // OutputFile & OutputFiles: // Location must be defined by the OutputPath field. MPEG-TS packetization // is used. // OutputHTTP: @@ -212,18 +222,34 @@ type Config struct { // localhost:6970. MPEGT-TS packetization is used. Outputs []uint8 - PSITime uint // Sets the time between a packet being sent. - Quantization uint // Quantization defines the quantization level, which will determine variable bitrate quality in the case of input from the Pi Camera. - RBCapacity uint // The number of bytes the ring buffer will occupy. - RBWriteTimeout uint // The ringbuffer write timeout in seconds. - RecPeriod float64 // How many seconds to record at a time. - Rotation uint // Rotation defines the video rotation angle in degrees Raspivid input. - RTMPURL string // RTMPURL specifies the Rtmp output destination URL. This must be defined if RTMP is to be used as an output. - RTPAddress string // RTPAddress defines the RTP output destination. - SampleRate uint // Samples a second (Hz). - Saturation int - Suppress bool // Holds logger suppression state. - VBRBitrate uint // VBRBitrate describes maximal variable bitrate. + PSITime uint // Sets the time between a packet being sent. + Quantization uint // Quantization defines the quantization level, which will determine variable bitrate quality in the case of input from the Pi Camera. + RBCapacity uint // The number of bytes the ring buffer will occupy. + RBStartElementSize uint // The starting element size of the ring buffer from which element size will increase to accomodate frames. + RBWriteTimeout uint // The ringbuffer write timeout in seconds. + RecPeriod float64 // How many seconds to record at a time. + Rotation uint // Rotation defines the video rotation angle in degrees Raspivid input. + RTMPURL string // RTMPURL specifies the Rtmp output destination URL. This must be defined if RTMP is to be used as an output. + RTPAddress string // RTPAddress defines the RTP output destination. + SampleRate uint // Samples a second (Hz). + Saturation int + + // SnapQuality is a value 0-100 inclusive, controlling JPEG compression of the + // timelapse snaps. 100 represents minimal compression and 0 represents the most + // compression. + SnapQuality int + + Suppress bool // Holds logger suppression state. + + // TimelapseInterval defines the interval between timelapse snaps when using + // raspistill input. + TimelapseInterval time.Duration + + // TimelapseDuration defines the duration of timelapse i.e. duration over + // which all snaps are taken, when using raspistill input. + TimelapseDuration time.Duration + + VBRBitrate uint // VBRBitrate describes maximal variable bitrate. // VBRQuality describes the general quality of video from the GeoVision camera // under variable bitrate. VBRQuality can be one 5 consts defined: diff --git a/revid/config/config_test.go b/revid/config/config_test.go index b59410e3..43484226 100644 --- a/revid/config/config_test.go +++ b/revid/config/config_test.go @@ -35,28 +35,34 @@ import ( type dumbLogger struct{} -func (dl *dumbLogger) Log(l int8, m string, a ...interface{}) {} -func (dl *dumbLogger) SetLevel(l int8) {} +func (dl *dumbLogger) Log(l int8, m string, a ...interface{}) {} +func (dl *dumbLogger) SetLevel(l int8) {} +func (dl *dumbLogger) Debug(msg string, args ...interface{}) {} +func (dl *dumbLogger) Info(msg string, args ...interface{}) {} +func (dl *dumbLogger) Warning(msg string, args ...interface{}) {} +func (dl *dumbLogger) Error(msg string, args ...interface{}) {} +func (dl *dumbLogger) Fatal(msg string, args ...interface{}) {} func TestValidate(t *testing.T) { dl := &dumbLogger{} want := Config{ - Logger: dl, - Input: defaultInput, - Outputs: []uint8{defaultOutput}, - InputCodec: defaultInputCodec, - RTPAddress: defaultRTPAddr, - CameraIP: defaultCameraIP, - BurstPeriod: defaultBurstPeriod, - MinFrames: defaultMinFrames, - FrameRate: defaultFrameRate, - ClipDuration: defaultClipDuration, - PSITime: defaultPSITime, - FileFPS: defaultFileFPS, - RBCapacity: defaultRBCapacity, - RBWriteTimeout: defaultRBWriteTimeout, - MinFPS: defaultMinFPS, + Logger: dl, + Input: defaultInput, + Outputs: []uint8{defaultOutput}, + InputCodec: defaultInputCodec, + RTPAddress: defaultRTPAddr, + CameraIP: defaultCameraIP, + BurstPeriod: defaultBurstPeriod, + MinFrames: defaultMinFrames, + FrameRate: defaultFrameRate, + ClipDuration: defaultClipDuration, + PSITime: defaultPSITime, + FileFPS: defaultFileFPS, + RBCapacity: defaultRBCapacity, + RBStartElementSize: defaultRBStartElementSize, + RBWriteTimeout: defaultRBWriteTimeout, + MinFPS: defaultMinFPS, } got := Config{Logger: dl} diff --git a/revid/config/variables.go b/revid/config/variables.go index f7216c10..e25533c6 100644 --- a/revid/config/variables.go +++ b/revid/config/variables.go @@ -37,64 +37,69 @@ import ( "bitbucket.org/ausocean/utils/logger" ) -// Config map keys. +// Config map Keys. const ( - KeyAutoWhiteBalance = "AutoWhiteBalance" - KeyBitDepth = "BitDepth" - KeyBitrate = "Bitrate" - KeyBrightness = "Brightness" - KeyBurstPeriod = "BurstPeriod" - KeyCameraChan = "CameraChan" - KeyCameraIP = "CameraIP" - KeyCBR = "CBR" - KeyClipDuration = "ClipDuration" - KeyChannels = "Channels" - KeyExposure = "Exposure" - KeyFileFPS = "FileFPS" - KeyFilters = "Filters" - KeyFrameRate = "FrameRate" - KeyHeight = "Height" - KeyHorizontalFlip = "HorizontalFlip" - KeyHTTPAddress = "HTTPAddress" - KeyInput = "Input" - KeyInputCodec = "InputCodec" - KeyInputPath = "InputPath" - KeyLogging = "logging" - KeyLoop = "Loop" - KeyMinFPS = "MinFPS" - KeyMinFrames = "MinFrames" - KeyMode = "mode" - KeyMotionDownscaling = "MotionDownscaling" - KeyMotionHistory = "MotionHistory" - KeyMotionInterval = "MotionInterval" - KeyMotionKernel = "MotionKernel" - KeyMotionMinArea = "MotionMinArea" - KeyMotionPadding = "MotionPadding" - KeyMotionPixels = "MotionPixels" - KeyMotionThreshold = "MotionThreshold" - KeyOutput = "Output" - KeyOutputPath = "OutputPath" - KeyOutputs = "Outputs" - KeyPSITime = "PSITime" - KeyQuantization = "Quantization" - KeyRBCapacity = "RBCapacity" - KeyRBWriteTimeout = "RBWriteTimeout" - KeyRecPeriod = "RecPeriod" - KeyRotation = "Rotation" - KeyRTMPURL = "RTMPURL" - KeyRTPAddress = "RTPAddress" - KeySampleRate = "SampleRate" - KeySaturation = "Saturation" - KeySuppress = "Suppress" - KeyVBRBitrate = "VBRBitrate" - KeyVBRQuality = "VBRQuality" - KeyVerticalFlip = "VerticalFlip" - KeyWidth = "Width" + KeyAutoWhiteBalance = "AutoWhiteBalance" + KeyBitDepth = "BitDepth" + KeyBitrate = "Bitrate" + KeyBrightness = "Brightness" + KeyBurstPeriod = "BurstPeriod" + KeyCameraChan = "CameraChan" + KeyCameraIP = "CameraIP" + KeyCBR = "CBR" + KeyClipDuration = "ClipDuration" + KeyChannels = "Channels" + KeyExposure = "Exposure" + KeyFileFPS = "FileFPS" + KeyFilters = "Filters" + KeyFrameRate = "FrameRate" + KeyHeight = "Height" + KeyHorizontalFlip = "HorizontalFlip" + KeyHTTPAddress = "HTTPAddress" + KeyInput = "Input" + KeyInputCodec = "InputCodec" + KeyInputPath = "InputPath" + KeyLogging = "logging" + KeyLoop = "Loop" + KeyMinFPS = "MinFPS" + KeyMinFrames = "MinFrames" + KeyMode = "mode" + KeyMotionDownscaling = "MotionDownscaling" + KeyMotionHistory = "MotionHistory" + KeyMotionInterval = "MotionInterval" + KeyMotionKernel = "MotionKernel" + KeyMotionMinArea = "MotionMinArea" + KeyMotionPadding = "MotionPadding" + KeyMotionPixels = "MotionPixels" + KeyMotionThreshold = "MotionThreshold" + KeyOutput = "Output" + KeyOutputPath = "OutputPath" + KeyOutputs = "Outputs" + KeyPSITime = "PSITime" + KeyQuantization = "Quantization" + KeyRBCapacity = "RBCapacity" + KeyRBStartElementSize = "RBStartElementSize" + KeyRBWriteTimeout = "RBWriteTimeout" + KeyRecPeriod = "RecPeriod" + KeyRotation = "Rotation" + KeyRTMPURL = "RTMPURL" + KeyRTPAddress = "RTPAddress" + KeySampleRate = "SampleRate" + KeySaturation = "Saturation" + KeySnapQuality = "SnapQuality" + KeySuppress = "Suppress" + KeyTimelapseDuration = "TimelapseDuration" + KeyTimelapseInterval = "TimelapseInterval" + KeyVBRBitrate = "VBRBitrate" + KeyVBRQuality = "VBRQuality" + KeyVerticalFlip = "VerticalFlip" + KeyWidth = "Width" ) // Config map parameter types. const ( typeString = "string" + typeInt = "int" typeUint = "uint" typeBool = "bool" typeFloat = "float" @@ -117,8 +122,9 @@ const ( defaultFileFPS = 0 // Ring buffer defaults. - defaultRBCapacity = 50000000 // => 50MB - defaultRBWriteTimeout = 5 // Seconds. + defaultRBCapacity = 50000000 // => 50MB + defaultRBStartElementSize = 1000 // bytes + defaultRBWriteTimeout = 5 // Seconds. // Motion filter parameter defaults. defaultMinFPS = 1.0 @@ -264,24 +270,25 @@ var Variables = []struct { }, { Name: KeyInput, - Type_: "enum:raspivid,rtsp,v4l,file,audio", + Type_: "enum:raspivid,raspistill,rtsp,v4l,file,audio", Update: func(c *Config, v string) { c.Input = parseEnum( KeyInput, v, map[string]uint8{ - "raspivid": InputRaspivid, - "rtsp": InputRTSP, - "v4l": InputV4L, - "file": InputFile, - "audio": InputAudio, + "raspivid": InputRaspivid, + "raspistill": InputRaspistill, + "rtsp": InputRTSP, + "v4l": InputV4L, + "file": InputFile, + "audio": InputAudio, }, c, ) }, Validate: func(c *Config) { switch c.Input { - case InputRaspivid, InputV4L, InputFile, InputAudio, InputRTSP: + case InputRaspivid, InputRaspistill, InputV4L, InputFile, InputAudio, InputRTSP: default: c.LogInvalidField(KeyInput, defaultInput) c.Input = defaultInput @@ -290,7 +297,7 @@ var Variables = []struct { }, { Name: KeyInputCodec, - Type_: "enum:H264,H265,MJPEG,PCM,ADPCM", + Type_: "enum:H264,H265,MJPEG,JPEG,PCM,ADPCM", Update: func(c *Config, v string) { c.InputCodec = parseEnum( KeyInputCodec, @@ -299,6 +306,7 @@ var Variables = []struct { "h264": codecutil.H264, "h265": codecutil.H265, "mjpeg": codecutil.MJPEG, + "jpeg": codecutil.JPEG, "pcm": codecutil.PCM, "adpcm": codecutil.ADPCM, }, @@ -307,7 +315,7 @@ var Variables = []struct { }, Validate: func(c *Config) { switch c.InputCodec { - case codecutil.H264, codecutil.MJPEG, codecutil.PCM, codecutil.ADPCM: + case codecutil.H264, codecutil.MJPEG, codecutil.JPEG, codecutil.PCM, codecutil.ADPCM: default: c.LogInvalidField(KeyInputCodec, defaultInputCodec) c.InputCodec = defaultInputCodec @@ -440,6 +448,8 @@ var Variables = []struct { switch strings.ToLower(v) { case "file": c.Outputs[0] = OutputFile + case "files": + c.Outputs[0] = OutputFiles case "http": c.Outputs[0] = OutputHTTP case "rtmp": @@ -466,6 +476,8 @@ var Variables = []struct { switch strings.ToLower(output) { case "file": c.Outputs[i] = OutputFile + case "files": + c.Outputs[i] = OutputFiles case "http": c.Outputs[i] = OutputHTTP case "rtmp": @@ -501,6 +513,14 @@ var Variables = []struct { Update: func(c *Config, v string) { c.RBCapacity = parseUint(KeyRBCapacity, v, c) }, Validate: func(c *Config) { c.RBCapacity = lessThanOrEqual(KeyRBCapacity, c.RBCapacity, 0, c, defaultRBCapacity) }, }, + { + Name: KeyRBStartElementSize, + Type_: typeUint, + Update: func(c *Config, v string) { c.RBStartElementSize = parseUint("RBStartElementSize", v, c) }, + Validate: func(c *Config) { + c.RBStartElementSize = lessThanOrEqual("RBStartElementSize", c.RBStartElementSize, 0, c, defaultRBStartElementSize) + }, + }, { Name: KeyRBWriteTimeout, Type_: typeUint, @@ -548,7 +568,7 @@ var Variables = []struct { }, { Name: KeySaturation, - Type_: "int", + Type_: typeInt, Update: func(c *Config, v string) { _v, err := strconv.Atoi(v) if err != nil { @@ -557,6 +577,17 @@ var Variables = []struct { c.Saturation = _v }, }, + { + Name: KeySnapQuality, + Type_: typeUint, + Update: func(c *Config, v string) { + _v, err := strconv.Atoi(v) + if err != nil { + c.Logger.Log(logger.Warning, "invalid SnapQuality param", "value", v) + } + c.SnapQuality = _v + }, + }, { Name: KeySuppress, Type_: typeBool, @@ -565,6 +596,28 @@ var Variables = []struct { c.Logger.(*logger.Logger).SetSuppress(c.Suppress) }, }, + { + Name: KeyTimelapseInterval, + Type_: typeUint, + Update: func(c *Config, v string) { + _v, err := strconv.Atoi(v) + if err != nil { + c.Logger.Log(logger.Warning, "invalid TimelapseInterval param", "value", v) + } + c.TimelapseInterval = time.Duration(_v) * time.Second + }, + }, + { + Name: KeyTimelapseDuration, + Type_: typeUint, + Update: func(c *Config, v string) { + _v, err := strconv.Atoi(v) + if err != nil { + c.Logger.Log(logger.Warning, "invalid TimelapseDuration param", "value", v) + } + c.TimelapseDuration = time.Duration(_v) * time.Second + }, + }, { Name: KeyVBRBitrate, Type_: typeUint, From 8a792099a545eef2737fe68416cb995ccc046062 Mon Sep 17 00:00:00 2001 From: Saxon Nelson-Milton Date: Thu, 21 Jan 2021 13:14:45 +1030 Subject: [PATCH 2/3] device/raspistill/imp_release.go: improve error checking routine and removed unused const --- device/raspistill/imp_release.go | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/device/raspistill/imp_release.go b/device/raspistill/imp_release.go index bc6e3dc5..72af63b9 100644 --- a/device/raspistill/imp_release.go +++ b/device/raspistill/imp_release.go @@ -33,7 +33,7 @@ import ( "errors" "fmt" "io" - "io/ioutil" + "bufio" "os/exec" "strings" @@ -73,7 +73,6 @@ func (r *Raspistill) stop() error { } func (r *Raspistill) start() error { - const disabled = "0" args := []string{ "--output", "-", "--nopreview", @@ -99,23 +98,25 @@ func (r *Raspistill) start() error { return fmt.Errorf("could not pipe command error: %w", err) } + go func() { + errScnr := bufio.NewScanner(stderr) for { select { case <-r.done: r.log.Info("raspistill.Stop() called, finished checking stderr") return default: - buf, err := ioutil.ReadAll(stderr) - if err != nil { - r.log.Error("could not read stderr", "error", err) - return - } + } - if len(buf) != 0 { - r.log.Error("error from raspistill stderr", "error", string(buf)) - return - } + if errScnr.Scan() { + r.log.Error("error line from raspistill stderr","error",errScnr.Text()) + continue + } + + err := errScnr.Err() + if err != nil { + r.log.Error("error from stderr scan","error",err) } } }() From c18e263d9502e6e4295329784d4e917530f768f8 Mon Sep 17 00:00:00 2001 From: Saxon Nelson-Milton Date: Wed, 27 Jan 2021 13:41:29 +1030 Subject: [PATCH 3/3] device/raspistill & revid/config: PR problem fixes run 1 --- device/raspistill/imp_release.go | 2 +- device/raspistill/imp_testing.go | 54 ++++++++++++++------------------ device/raspistill/raspistill.go | 22 ++++++------- revid/config/config.go | 6 ++-- revid/config/variables.go | 8 ++--- 5 files changed, 43 insertions(+), 49 deletions(-) diff --git a/device/raspistill/imp_release.go b/device/raspistill/imp_release.go index 72af63b9..58658e7d 100644 --- a/device/raspistill/imp_release.go +++ b/device/raspistill/imp_release.go @@ -81,7 +81,7 @@ func (r *Raspistill) start() error { "--rotation", fmt.Sprint(r.cfg.Rotation), "--timeout", fmt.Sprint(r.cfg.TimelapseDuration), "--timelapse", fmt.Sprint(r.cfg.TimelapseInterval), - "--quality", fmt.Sprint(r.cfg.SnapQuality), + "--quality", fmt.Sprint(r.cfg.JPEGQuality), } r.log.Info(pkg+"raspistill args", "args", strings.Join(args, " ")) diff --git a/device/raspistill/imp_testing.go b/device/raspistill/imp_testing.go index 6741bfc8..94027e7b 100644 --- a/device/raspistill/imp_testing.go +++ b/device/raspistill/imp_testing.go @@ -4,7 +4,7 @@ DESCRIPTION test.go provides test implementations of the raspistill methods when the "test" build tag is specified. In this mode, raspistill simply provides - arbitrary loaded JPEG images when Raspistill.Read() is called. + specific test JPEG images when Raspistill.Read() is called. AUTHORS Saxon Nelson-Milton @@ -29,10 +29,9 @@ LICENSE package raspistill import ( - "fmt" "io" "io/ioutil" - "os" + "path/filepath" "strconv" "sync" "time" @@ -41,22 +40,23 @@ import ( ) const ( - noOfImages = 6 - imgDir = "/go/src/bitbucket.org/ausocean/test/test-data/av/input/jpeg/" - jpgExt = ".jpg" + // TODO(Saxon): find nImages programmatically ? + nImages = 6 + + imgPath = "../../../test/test-data/av/input/jpeg/" + jpgExt = ".jpg" ) type raspistill struct { + images [nImages][]byte + imgCnt int // Number of images that have been loaded thus far. + durTicker *time.Ticker // Tracks timelapse duration. + intvlTicker *time.Ticker // Tracks current interval in the timelapse. log config.Logger cfg config.Config isRunning bool - images [noOfImages][]byte - imgCnt int - durTicker *time.Ticker - intvlTicker *time.Ticker - buf []byte - init bool - term chan struct{} + buf []byte // Holds frame data to be read. + term chan struct{} // Signals termination when close() is called. mu sync.Mutex } @@ -65,18 +65,13 @@ func new(l config.Logger) raspistill { r := raspistill{log: l} - // Get to the right file location of the JPEG images. - home, err := os.UserHomeDir() - if err != nil { - panic(fmt.Sprintf("could not get home directory: %v", err)) - } - // Load the test images into the images slice. // We expect the 6 images test images to be named 0.jpg through to 5.jpg. r.log.Debug("loading test JPEG images") for i, _ := range r.images { - path := imgDir + strconv.Itoa(i) + jpgExt - r.images[i], err = ioutil.ReadFile(home + imgDir + strconv.Itoa(i) + jpgExt) + absPath, err := filepath.Abs(imgPath) + path := absPath + "/" + strconv.Itoa(i) + jpgExt + r.images[i], err = ioutil.ReadFile(path) if err != nil { r.log.Fatal("error loading test image", "imageNum", i, "error", err) @@ -86,16 +81,16 @@ func new(l config.Logger) raspistill { return r } -// stop sets isRunning flag to false, indicating no further captures. Calls on -// Raspistill.read return error. +// stop sets isRunning flag to false, indicating no further captures. Future +// calls on Raspistill.read will return an error. func (r *Raspistill) stop() error { r.log.Debug("stopping test raspistill") - r.isRunning = false + r.setRunning(false) return nil } -// start creates the timelapse interval and duration tickers i.e. also starting -// them and sets isRunning flag to true indicating that raspistill is capturing. +// start creates and starts the timelapse and duration tickers and sets +// isRunning flag to true indicating that raspistill is capturing. func (r *Raspistill) start() error { r.log.Debug("starting test raspistill") r.durTicker = time.NewTicker(r.cfg.TimelapseDuration) @@ -113,8 +108,8 @@ func (r *Raspistill) start() error { } func (r *Raspistill) loadImg() { - r.log.Debug("appending new image on to buffer and copying next image p", "imgCnt", r.imgCnt) - imgBytes := r.images[r.imgCnt%noOfImages] + r.log.Debug("appending new image on to buffer and copying next image p", "nImg", r.imgCnt) + imgBytes := r.images[r.imgCnt%nImages] if len(imgBytes) == 0 { panic("length of image bytes should not be 0") } @@ -122,7 +117,7 @@ func (r *Raspistill) loadImg() { r.mu.Lock() r.buf = append(r.buf, imgBytes...) - r.log.Debug("added img to buf", "len(imgBytes)", len(imgBytes)) + r.log.Debug("added image to buf", "nBytes", len(imgBytes)) r.mu.Unlock() } @@ -161,7 +156,6 @@ func (r *Raspistill) read(p []byte) (int, error) { return 0, io.EOF } n := copy(p, r.buf) - r.log.Debug("copied", "p[:2]", p[:2], "p[n-2:n]", p[n-2:n]) r.buf = r.buf[n:] return n, nil diff --git a/device/raspistill/raspistill.go b/device/raspistill/raspistill.go index ce2db70c..67213565 100644 --- a/device/raspistill/raspistill.go +++ b/device/raspistill/raspistill.go @@ -49,27 +49,27 @@ const ( maxTimelapseInterval = 86400 * time.Second // s = 24 hours ) -// Raspivid configuration defaults. +// Raspistill configuration defaults. const ( defaultRotation = 0 // degrees defaultWidth = 1280 // pixels defaultHeight = 720 // pixels - defaultSnapQuality = 75 // % + defaultJPEGQuality = 75 // % defaultTimelapseDuration = maxTimelapseDuration // ms defaultTimelapseInterval = 3600 * time.Second // ms ) // Configuration errors. var ( - errBadRotation = fmt.Errorf("rotation bad or unset, defaulting to: %v", defaultRotation) - errBadWidth = fmt.Errorf("width bad or unset, defaulting to: %v", defaultWidth) - errBadHeight = fmt.Errorf("height bad or unset, defaulting to: %v", defaultHeight) - errBadSnapQuality = fmt.Errorf("SnapQuality bad or unset, defaulting to: %v", defaultSnapQuality) + errBadRotation = fmt.Errorf("Rotation bad or unset, defaulting to: %v", defaultRotation) + errBadWidth = fmt.Errorf("Width bad or unset, defaulting to: %v", defaultWidth) + errBadHeight = fmt.Errorf("Height bad or unset, defaulting to: %v", defaultHeight) + errBadJPEGQuality = fmt.Errorf("JPEGQuality bad or unset, defaulting to: %v", defaultJPEGQuality) errBadTimelapseDuration = fmt.Errorf("TimelapseDuration bad or unset, defaulting to: %v", defaultTimelapseDuration) errBadTimelapseInterval = fmt.Errorf("TimelapseInterval bad or unset, defaulting to: %v", defaultTimelapseInterval) ) -// Misc erros. +// Misc errors. var errNotStarted = errors.New("cannot read, raspistill not started") // Raspistill is an implementation of AVDevice that provides control over the @@ -77,7 +77,7 @@ var errNotStarted = errors.New("cannot read, raspistill not started") // singular images. type Raspistill struct{ raspistill } -// New returns a new Raspivid. +// New returns a new Raspistill. func New(l config.Logger) *Raspistill { return &Raspistill{raspistill: new(l)} } // Start will prepare the arguments for the raspistill command using the @@ -119,9 +119,9 @@ func (r *Raspistill) Set(c config.Config) error { errs = append(errs, errBadHeight) } - if c.SnapQuality < 0 || c.SnapQuality > 100 { - c.SnapQuality = defaultSnapQuality - errs = append(errs, errBadSnapQuality) + if c.JPEGQuality < 0 || c.JPEGQuality > 100 { + c.JPEGQuality = defaultJPEGQuality + errs = append(errs, errBadJPEGQuality) } if c.TimelapseDuration > maxTimelapseDuration || c.TimelapseDuration < minTimelapseDuration { diff --git a/revid/config/config.go b/revid/config/config.go index f727ffdc..012c8b46 100644 --- a/revid/config/config.go +++ b/revid/config/config.go @@ -234,14 +234,14 @@ type Config struct { SampleRate uint // Samples a second (Hz). Saturation int - // SnapQuality is a value 0-100 inclusive, controlling JPEG compression of the + // JPEGQuality is a value 0-100 inclusive, controlling JPEG compression of the // timelapse snaps. 100 represents minimal compression and 0 represents the most // compression. - SnapQuality int + JPEGQuality int Suppress bool // Holds logger suppression state. - // TimelapseInterval defines the interval between timelapse snaps when using + // TimelapseInterval defines the interval between timelapse images when using // raspistill input. TimelapseInterval time.Duration diff --git a/revid/config/variables.go b/revid/config/variables.go index e25533c6..0edc815a 100644 --- a/revid/config/variables.go +++ b/revid/config/variables.go @@ -86,7 +86,7 @@ const ( KeyRTPAddress = "RTPAddress" KeySampleRate = "SampleRate" KeySaturation = "Saturation" - KeySnapQuality = "SnapQuality" + KeyJPEGQuality = "JPEGQuality" KeySuppress = "Suppress" KeyTimelapseDuration = "TimelapseDuration" KeyTimelapseInterval = "TimelapseInterval" @@ -578,14 +578,14 @@ var Variables = []struct { }, }, { - Name: KeySnapQuality, + Name: KeyJPEGQuality, Type_: typeUint, Update: func(c *Config, v string) { _v, err := strconv.Atoi(v) if err != nil { - c.Logger.Log(logger.Warning, "invalid SnapQuality param", "value", v) + c.Logger.Log(logger.Warning, "invalid JPEGQuality param", "value", v) } - c.SnapQuality = _v + c.JPEGQuality = _v }, }, {