mirror of https://bitbucket.org/ausocean/av.git
Merged in raspistill (pull request #449)
device: add raspistill package housing release and testing implementations Approved-by: Trek Hopton
This commit is contained in:
commit
9db5e7acb3
|
@ -0,0 +1,138 @@
|
||||||
|
// +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 <saxon@ausocean.org>
|
||||||
|
|
||||||
|
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"
|
||||||
|
"bufio"
|
||||||
|
"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 {
|
||||||
|
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.JPEGQuality),
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
|
errScnr := bufio.NewScanner(stderr)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-r.done:
|
||||||
|
r.log.Info("raspistill.Stop() called, finished checking stderr")
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
|
@ -0,0 +1,175 @@
|
||||||
|
// +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
|
||||||
|
specific test JPEG images when Raspistill.Read() is called.
|
||||||
|
|
||||||
|
AUTHORS
|
||||||
|
Saxon Nelson-Milton <saxon@ausocean.org>
|
||||||
|
|
||||||
|
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 (
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"bitbucket.org/ausocean/av/revid/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// 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
|
||||||
|
buf []byte // Holds frame data to be read.
|
||||||
|
term chan struct{} // Signals termination when close() is called.
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func new(l config.Logger) raspistill {
|
||||||
|
l.Debug("creating new test raspistill input")
|
||||||
|
|
||||||
|
r := raspistill{log: l}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
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. Future
|
||||||
|
// calls on Raspistill.read will return an error.
|
||||||
|
func (r *Raspistill) stop() error {
|
||||||
|
r.log.Debug("stopping test raspistill")
|
||||||
|
r.setRunning(false)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
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", "nImg", r.imgCnt)
|
||||||
|
imgBytes := r.images[r.imgCnt%nImages]
|
||||||
|
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 image to buf", "nBytes", 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.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
|
||||||
|
}
|
|
@ -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 <saxon@ausocean.org>
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
// Raspistill configuration defaults.
|
||||||
|
const (
|
||||||
|
defaultRotation = 0 // degrees
|
||||||
|
defaultWidth = 1280 // pixels
|
||||||
|
defaultHeight = 720 // pixels
|
||||||
|
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)
|
||||||
|
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 errors.
|
||||||
|
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 Raspistill.
|
||||||
|
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.JPEGQuality < 0 || c.JPEGQuality > 100 {
|
||||||
|
c.JPEGQuality = defaultJPEGQuality
|
||||||
|
errs = append(errs, errBadJPEGQuality)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
|
@ -35,6 +35,11 @@ import (
|
||||||
type Logger interface {
|
type Logger interface {
|
||||||
SetLevel(int8)
|
SetLevel(int8)
|
||||||
Log(level int8, message string, params ...interface{})
|
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.
|
// Enums to define inputs, outputs and codecs.
|
||||||
|
@ -45,6 +50,7 @@ const (
|
||||||
// Input/Output.
|
// Input/Output.
|
||||||
InputFile
|
InputFile
|
||||||
InputRaspivid
|
InputRaspivid
|
||||||
|
InputRaspistill
|
||||||
InputV4L
|
InputV4L
|
||||||
InputRTSP
|
InputRTSP
|
||||||
InputAudio
|
InputAudio
|
||||||
|
@ -55,10 +61,12 @@ const (
|
||||||
OutputHTTP
|
OutputHTTP
|
||||||
OutputMPEGTS
|
OutputMPEGTS
|
||||||
OutputFile
|
OutputFile
|
||||||
|
OutputFiles
|
||||||
|
|
||||||
// Codecs.
|
// Codecs.
|
||||||
H264
|
H264
|
||||||
H265
|
H265
|
||||||
|
MJPEG
|
||||||
JPEG
|
JPEG
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -142,7 +150,9 @@ type Config struct {
|
||||||
//
|
//
|
||||||
// Valid values are defined by enums:
|
// Valid values are defined by enums:
|
||||||
// InputRaspivid:
|
// 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:
|
// InputV4l:
|
||||||
// Read from webcam.
|
// Read from webcam.
|
||||||
// InputFile:
|
// InputFile:
|
||||||
|
@ -198,7 +208,7 @@ type Config struct {
|
||||||
// Outputs define the outputs we wish to output data too.
|
// Outputs define the outputs we wish to output data too.
|
||||||
//
|
//
|
||||||
// Valid outputs are defined by enums:
|
// Valid outputs are defined by enums:
|
||||||
// OutputFile:
|
// OutputFile & OutputFiles:
|
||||||
// Location must be defined by the OutputPath field. MPEG-TS packetization
|
// Location must be defined by the OutputPath field. MPEG-TS packetization
|
||||||
// is used.
|
// is used.
|
||||||
// OutputHTTP:
|
// OutputHTTP:
|
||||||
|
@ -215,6 +225,7 @@ type Config struct {
|
||||||
PSITime uint // Sets the time between a packet being sent.
|
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.
|
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.
|
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.
|
RBWriteTimeout uint // The ringbuffer write timeout in seconds.
|
||||||
RecPeriod float64 // How many seconds to record at a time.
|
RecPeriod float64 // How many seconds to record at a time.
|
||||||
Rotation uint // Rotation defines the video rotation angle in degrees Raspivid input.
|
Rotation uint // Rotation defines the video rotation angle in degrees Raspivid input.
|
||||||
|
@ -222,7 +233,22 @@ type Config struct {
|
||||||
RTPAddress string // RTPAddress defines the RTP output destination.
|
RTPAddress string // RTPAddress defines the RTP output destination.
|
||||||
SampleRate uint // Samples a second (Hz).
|
SampleRate uint // Samples a second (Hz).
|
||||||
Saturation int
|
Saturation int
|
||||||
|
|
||||||
|
// JPEGQuality is a value 0-100 inclusive, controlling JPEG compression of the
|
||||||
|
// timelapse snaps. 100 represents minimal compression and 0 represents the most
|
||||||
|
// compression.
|
||||||
|
JPEGQuality int
|
||||||
|
|
||||||
Suppress bool // Holds logger suppression state.
|
Suppress bool // Holds logger suppression state.
|
||||||
|
|
||||||
|
// TimelapseInterval defines the interval between timelapse images 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.
|
VBRBitrate uint // VBRBitrate describes maximal variable bitrate.
|
||||||
|
|
||||||
// VBRQuality describes the general quality of video from the GeoVision camera
|
// VBRQuality describes the general quality of video from the GeoVision camera
|
||||||
|
|
|
@ -37,6 +37,11 @@ type dumbLogger struct{}
|
||||||
|
|
||||||
func (dl *dumbLogger) Log(l int8, m string, a ...interface{}) {}
|
func (dl *dumbLogger) Log(l int8, m string, a ...interface{}) {}
|
||||||
func (dl *dumbLogger) SetLevel(l int8) {}
|
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) {
|
func TestValidate(t *testing.T) {
|
||||||
dl := &dumbLogger{}
|
dl := &dumbLogger{}
|
||||||
|
@ -55,6 +60,7 @@ func TestValidate(t *testing.T) {
|
||||||
PSITime: defaultPSITime,
|
PSITime: defaultPSITime,
|
||||||
FileFPS: defaultFileFPS,
|
FileFPS: defaultFileFPS,
|
||||||
RBCapacity: defaultRBCapacity,
|
RBCapacity: defaultRBCapacity,
|
||||||
|
RBStartElementSize: defaultRBStartElementSize,
|
||||||
RBWriteTimeout: defaultRBWriteTimeout,
|
RBWriteTimeout: defaultRBWriteTimeout,
|
||||||
MinFPS: defaultMinFPS,
|
MinFPS: defaultMinFPS,
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@ import (
|
||||||
"bitbucket.org/ausocean/utils/logger"
|
"bitbucket.org/ausocean/utils/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config map keys.
|
// Config map Keys.
|
||||||
const (
|
const (
|
||||||
KeyAutoWhiteBalance = "AutoWhiteBalance"
|
KeyAutoWhiteBalance = "AutoWhiteBalance"
|
||||||
KeyBitDepth = "BitDepth"
|
KeyBitDepth = "BitDepth"
|
||||||
|
@ -78,6 +78,7 @@ const (
|
||||||
KeyPSITime = "PSITime"
|
KeyPSITime = "PSITime"
|
||||||
KeyQuantization = "Quantization"
|
KeyQuantization = "Quantization"
|
||||||
KeyRBCapacity = "RBCapacity"
|
KeyRBCapacity = "RBCapacity"
|
||||||
|
KeyRBStartElementSize = "RBStartElementSize"
|
||||||
KeyRBWriteTimeout = "RBWriteTimeout"
|
KeyRBWriteTimeout = "RBWriteTimeout"
|
||||||
KeyRecPeriod = "RecPeriod"
|
KeyRecPeriod = "RecPeriod"
|
||||||
KeyRotation = "Rotation"
|
KeyRotation = "Rotation"
|
||||||
|
@ -85,7 +86,10 @@ const (
|
||||||
KeyRTPAddress = "RTPAddress"
|
KeyRTPAddress = "RTPAddress"
|
||||||
KeySampleRate = "SampleRate"
|
KeySampleRate = "SampleRate"
|
||||||
KeySaturation = "Saturation"
|
KeySaturation = "Saturation"
|
||||||
|
KeyJPEGQuality = "JPEGQuality"
|
||||||
KeySuppress = "Suppress"
|
KeySuppress = "Suppress"
|
||||||
|
KeyTimelapseDuration = "TimelapseDuration"
|
||||||
|
KeyTimelapseInterval = "TimelapseInterval"
|
||||||
KeyVBRBitrate = "VBRBitrate"
|
KeyVBRBitrate = "VBRBitrate"
|
||||||
KeyVBRQuality = "VBRQuality"
|
KeyVBRQuality = "VBRQuality"
|
||||||
KeyVerticalFlip = "VerticalFlip"
|
KeyVerticalFlip = "VerticalFlip"
|
||||||
|
@ -95,6 +99,7 @@ const (
|
||||||
// Config map parameter types.
|
// Config map parameter types.
|
||||||
const (
|
const (
|
||||||
typeString = "string"
|
typeString = "string"
|
||||||
|
typeInt = "int"
|
||||||
typeUint = "uint"
|
typeUint = "uint"
|
||||||
typeBool = "bool"
|
typeBool = "bool"
|
||||||
typeFloat = "float"
|
typeFloat = "float"
|
||||||
|
@ -118,6 +123,7 @@ const (
|
||||||
|
|
||||||
// Ring buffer defaults.
|
// Ring buffer defaults.
|
||||||
defaultRBCapacity = 50000000 // => 50MB
|
defaultRBCapacity = 50000000 // => 50MB
|
||||||
|
defaultRBStartElementSize = 1000 // bytes
|
||||||
defaultRBWriteTimeout = 5 // Seconds.
|
defaultRBWriteTimeout = 5 // Seconds.
|
||||||
|
|
||||||
// Motion filter parameter defaults.
|
// Motion filter parameter defaults.
|
||||||
|
@ -264,13 +270,14 @@ var Variables = []struct {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: KeyInput,
|
Name: KeyInput,
|
||||||
Type_: "enum:raspivid,rtsp,v4l,file,audio",
|
Type_: "enum:raspivid,raspistill,rtsp,v4l,file,audio",
|
||||||
Update: func(c *Config, v string) {
|
Update: func(c *Config, v string) {
|
||||||
c.Input = parseEnum(
|
c.Input = parseEnum(
|
||||||
KeyInput,
|
KeyInput,
|
||||||
v,
|
v,
|
||||||
map[string]uint8{
|
map[string]uint8{
|
||||||
"raspivid": InputRaspivid,
|
"raspivid": InputRaspivid,
|
||||||
|
"raspistill": InputRaspistill,
|
||||||
"rtsp": InputRTSP,
|
"rtsp": InputRTSP,
|
||||||
"v4l": InputV4L,
|
"v4l": InputV4L,
|
||||||
"file": InputFile,
|
"file": InputFile,
|
||||||
|
@ -281,7 +288,7 @@ var Variables = []struct {
|
||||||
},
|
},
|
||||||
Validate: func(c *Config) {
|
Validate: func(c *Config) {
|
||||||
switch c.Input {
|
switch c.Input {
|
||||||
case InputRaspivid, InputV4L, InputFile, InputAudio, InputRTSP:
|
case InputRaspivid, InputRaspistill, InputV4L, InputFile, InputAudio, InputRTSP:
|
||||||
default:
|
default:
|
||||||
c.LogInvalidField(KeyInput, defaultInput)
|
c.LogInvalidField(KeyInput, defaultInput)
|
||||||
c.Input = defaultInput
|
c.Input = defaultInput
|
||||||
|
@ -290,7 +297,7 @@ var Variables = []struct {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: KeyInputCodec,
|
Name: KeyInputCodec,
|
||||||
Type_: "enum:H264,H265,MJPEG,PCM,ADPCM",
|
Type_: "enum:H264,H265,MJPEG,JPEG,PCM,ADPCM",
|
||||||
Update: func(c *Config, v string) {
|
Update: func(c *Config, v string) {
|
||||||
c.InputCodec = parseEnum(
|
c.InputCodec = parseEnum(
|
||||||
KeyInputCodec,
|
KeyInputCodec,
|
||||||
|
@ -299,6 +306,7 @@ var Variables = []struct {
|
||||||
"h264": codecutil.H264,
|
"h264": codecutil.H264,
|
||||||
"h265": codecutil.H265,
|
"h265": codecutil.H265,
|
||||||
"mjpeg": codecutil.MJPEG,
|
"mjpeg": codecutil.MJPEG,
|
||||||
|
"jpeg": codecutil.JPEG,
|
||||||
"pcm": codecutil.PCM,
|
"pcm": codecutil.PCM,
|
||||||
"adpcm": codecutil.ADPCM,
|
"adpcm": codecutil.ADPCM,
|
||||||
},
|
},
|
||||||
|
@ -307,7 +315,7 @@ var Variables = []struct {
|
||||||
},
|
},
|
||||||
Validate: func(c *Config) {
|
Validate: func(c *Config) {
|
||||||
switch c.InputCodec {
|
switch c.InputCodec {
|
||||||
case codecutil.H264, codecutil.MJPEG, codecutil.PCM, codecutil.ADPCM:
|
case codecutil.H264, codecutil.MJPEG, codecutil.JPEG, codecutil.PCM, codecutil.ADPCM:
|
||||||
default:
|
default:
|
||||||
c.LogInvalidField(KeyInputCodec, defaultInputCodec)
|
c.LogInvalidField(KeyInputCodec, defaultInputCodec)
|
||||||
c.InputCodec = defaultInputCodec
|
c.InputCodec = defaultInputCodec
|
||||||
|
@ -440,6 +448,8 @@ var Variables = []struct {
|
||||||
switch strings.ToLower(v) {
|
switch strings.ToLower(v) {
|
||||||
case "file":
|
case "file":
|
||||||
c.Outputs[0] = OutputFile
|
c.Outputs[0] = OutputFile
|
||||||
|
case "files":
|
||||||
|
c.Outputs[0] = OutputFiles
|
||||||
case "http":
|
case "http":
|
||||||
c.Outputs[0] = OutputHTTP
|
c.Outputs[0] = OutputHTTP
|
||||||
case "rtmp":
|
case "rtmp":
|
||||||
|
@ -466,6 +476,8 @@ var Variables = []struct {
|
||||||
switch strings.ToLower(output) {
|
switch strings.ToLower(output) {
|
||||||
case "file":
|
case "file":
|
||||||
c.Outputs[i] = OutputFile
|
c.Outputs[i] = OutputFile
|
||||||
|
case "files":
|
||||||
|
c.Outputs[i] = OutputFiles
|
||||||
case "http":
|
case "http":
|
||||||
c.Outputs[i] = OutputHTTP
|
c.Outputs[i] = OutputHTTP
|
||||||
case "rtmp":
|
case "rtmp":
|
||||||
|
@ -501,6 +513,14 @@ var Variables = []struct {
|
||||||
Update: func(c *Config, v string) { c.RBCapacity = parseUint(KeyRBCapacity, v, c) },
|
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) },
|
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,
|
Name: KeyRBWriteTimeout,
|
||||||
Type_: typeUint,
|
Type_: typeUint,
|
||||||
|
@ -548,7 +568,7 @@ var Variables = []struct {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: KeySaturation,
|
Name: KeySaturation,
|
||||||
Type_: "int",
|
Type_: typeInt,
|
||||||
Update: func(c *Config, v string) {
|
Update: func(c *Config, v string) {
|
||||||
_v, err := strconv.Atoi(v)
|
_v, err := strconv.Atoi(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -557,6 +577,17 @@ var Variables = []struct {
|
||||||
c.Saturation = _v
|
c.Saturation = _v
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: KeyJPEGQuality,
|
||||||
|
Type_: typeUint,
|
||||||
|
Update: func(c *Config, v string) {
|
||||||
|
_v, err := strconv.Atoi(v)
|
||||||
|
if err != nil {
|
||||||
|
c.Logger.Log(logger.Warning, "invalid JPEGQuality param", "value", v)
|
||||||
|
}
|
||||||
|
c.JPEGQuality = _v
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: KeySuppress,
|
Name: KeySuppress,
|
||||||
Type_: typeBool,
|
Type_: typeBool,
|
||||||
|
@ -565,6 +596,28 @@ var Variables = []struct {
|
||||||
c.Logger.(*logger.Logger).SetSuppress(c.Suppress)
|
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,
|
Name: KeyVBRBitrate,
|
||||||
Type_: typeUint,
|
Type_: typeUint,
|
||||||
|
|
Loading…
Reference in New Issue