mirror of https://bitbucket.org/ausocean/av.git
device: add raspistill package housing release and testing implementations
This commit is contained in:
parent
60187c797d
commit
45c019a062
|
@ -98,7 +98,7 @@ func Lex(dst io.Writer, src io.Reader, delay time.Duration) error {
|
||||||
|
|
||||||
if nImg == 0 {
|
if nImg == 0 {
|
||||||
<-tick
|
<-tick
|
||||||
Log.Debug("writing buf","len(buf)",len(buf))
|
Log.Debug("writing buf", "len(buf)", len(buf))
|
||||||
_, err = dst.Write(buf)
|
_, err = dst.Write(buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -56,14 +56,14 @@ var jpegTests = []struct {
|
||||||
input: []byte{0xff, 0xd8, 0xff, 0xd9},
|
input: []byte{0xff, 0xd8, 0xff, 0xd9},
|
||||||
delay: 0,
|
delay: 0,
|
||||||
want: [][]byte{{0xff, 0xd8, 0xff, 0xd9}},
|
want: [][]byte{{0xff, 0xd8, 0xff, 0xd9}},
|
||||||
err: io.ErrUnexpectedEOF,
|
err: io.ErrUnexpectedEOF,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "null delayed",
|
name: "null delayed",
|
||||||
input: []byte{0xff, 0xd8, 0xff, 0xd9},
|
input: []byte{0xff, 0xd8, 0xff, 0xd9},
|
||||||
delay: time.Millisecond,
|
delay: time.Millisecond,
|
||||||
want: [][]byte{{0xff, 0xd8, 0xff, 0xd9}},
|
want: [][]byte{{0xff, 0xd8, 0xff, 0xd9}},
|
||||||
err: io.ErrUnexpectedEOF,
|
err: io.ErrUnexpectedEOF,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "full",
|
name: "full",
|
||||||
|
@ -82,7 +82,7 @@ var jpegTests = []struct {
|
||||||
{0xff, 0xd8, 'l', 'e', 'n', 'g', 't', 'h', 0xff, 0xd9},
|
{0xff, 0xd8, 'l', 'e', 'n', 'g', 't', 'h', 0xff, 0xd9},
|
||||||
{0xff, 0xd8, 's', 'p', 'r', 'e', 'a', 'd', 0xff, 0xd9},
|
{0xff, 0xd8, 's', 'p', 'r', 'e', 'a', 'd', 0xff, 0xd9},
|
||||||
},
|
},
|
||||||
err: io.ErrUnexpectedEOF,
|
err: io.ErrUnexpectedEOF,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "full delayed",
|
name: "full delayed",
|
||||||
|
@ -101,7 +101,7 @@ var jpegTests = []struct {
|
||||||
{0xff, 0xd8, 'l', 'e', 'n', 'g', 't', 'h', 0xff, 0xd9},
|
{0xff, 0xd8, 'l', 'e', 'n', 'g', 't', 'h', 0xff, 0xd9},
|
||||||
{0xff, 0xd8, 's', 'p', 'r', 'e', 'a', 'd', 0xff, 0xd9},
|
{0xff, 0xd8, 's', 'p', 'r', 'e', 'a', 'd', 0xff, 0xd9},
|
||||||
},
|
},
|
||||||
err: io.ErrUnexpectedEOF,
|
err: io.ErrUnexpectedEOF,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ const (
|
||||||
H264SID = 27
|
H264SID = 27
|
||||||
H265SID = 36
|
H265SID = 36
|
||||||
MJPEGSID = 136
|
MJPEGSID = 136
|
||||||
JPEGSID = 137
|
JPEGSID = 137
|
||||||
PCMSID = 192
|
PCMSID = 192
|
||||||
ADPCMSID = 193
|
ADPCMSID = 193
|
||||||
)
|
)
|
||||||
|
|
|
@ -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 <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"
|
||||||
|
"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
|
||||||
|
}
|
|
@ -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 <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 (
|
||||||
|
"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
|
||||||
|
}
|
|
@ -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
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
|
@ -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:
|
||||||
|
@ -212,18 +222,34 @@ type Config struct {
|
||||||
// localhost:6970. MPEGT-TS packetization is used.
|
// localhost:6970. MPEGT-TS packetization is used.
|
||||||
Outputs []uint8
|
Outputs []uint8
|
||||||
|
|
||||||
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.
|
||||||
RBWriteTimeout uint // The ringbuffer write timeout in seconds.
|
RBStartElementSize uint // The starting element size of the ring buffer from which element size will increase to accomodate frames.
|
||||||
RecPeriod float64 // How many seconds to record at a time.
|
RBWriteTimeout uint // The ringbuffer write timeout in seconds.
|
||||||
Rotation uint // Rotation defines the video rotation angle in degrees Raspivid input.
|
RecPeriod float64 // How many seconds to record at a time.
|
||||||
RTMPURL string // RTMPURL specifies the Rtmp output destination URL. This must be defined if RTMP is to be used as an output.
|
Rotation uint // Rotation defines the video rotation angle in degrees Raspivid input.
|
||||||
RTPAddress string // RTPAddress defines the RTP output destination.
|
RTMPURL string // RTMPURL specifies the Rtmp output destination URL. This must be defined if RTMP is to be used as an output.
|
||||||
SampleRate uint // Samples a second (Hz).
|
RTPAddress string // RTPAddress defines the RTP output destination.
|
||||||
Saturation int
|
SampleRate uint // Samples a second (Hz).
|
||||||
Suppress bool // Holds logger suppression state.
|
Saturation int
|
||||||
VBRBitrate uint // VBRBitrate describes maximal variable bitrate.
|
|
||||||
|
// 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
|
// VBRQuality describes the general quality of video from the GeoVision camera
|
||||||
// under variable bitrate. VBRQuality can be one 5 consts defined:
|
// under variable bitrate. VBRQuality can be one 5 consts defined:
|
||||||
|
|
|
@ -35,28 +35,34 @@ import (
|
||||||
|
|
||||||
type dumbLogger struct{}
|
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{}
|
||||||
|
|
||||||
want := Config{
|
want := Config{
|
||||||
Logger: dl,
|
Logger: dl,
|
||||||
Input: defaultInput,
|
Input: defaultInput,
|
||||||
Outputs: []uint8{defaultOutput},
|
Outputs: []uint8{defaultOutput},
|
||||||
InputCodec: defaultInputCodec,
|
InputCodec: defaultInputCodec,
|
||||||
RTPAddress: defaultRTPAddr,
|
RTPAddress: defaultRTPAddr,
|
||||||
CameraIP: defaultCameraIP,
|
CameraIP: defaultCameraIP,
|
||||||
BurstPeriod: defaultBurstPeriod,
|
BurstPeriod: defaultBurstPeriod,
|
||||||
MinFrames: defaultMinFrames,
|
MinFrames: defaultMinFrames,
|
||||||
FrameRate: defaultFrameRate,
|
FrameRate: defaultFrameRate,
|
||||||
ClipDuration: defaultClipDuration,
|
ClipDuration: defaultClipDuration,
|
||||||
PSITime: defaultPSITime,
|
PSITime: defaultPSITime,
|
||||||
FileFPS: defaultFileFPS,
|
FileFPS: defaultFileFPS,
|
||||||
RBCapacity: defaultRBCapacity,
|
RBCapacity: defaultRBCapacity,
|
||||||
RBWriteTimeout: defaultRBWriteTimeout,
|
RBStartElementSize: defaultRBStartElementSize,
|
||||||
MinFPS: defaultMinFPS,
|
RBWriteTimeout: defaultRBWriteTimeout,
|
||||||
|
MinFPS: defaultMinFPS,
|
||||||
}
|
}
|
||||||
|
|
||||||
got := Config{Logger: dl}
|
got := Config{Logger: dl}
|
||||||
|
|
|
@ -37,64 +37,69 @@ 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"
|
||||||
KeyBitrate = "Bitrate"
|
KeyBitrate = "Bitrate"
|
||||||
KeyBrightness = "Brightness"
|
KeyBrightness = "Brightness"
|
||||||
KeyBurstPeriod = "BurstPeriod"
|
KeyBurstPeriod = "BurstPeriod"
|
||||||
KeyCameraChan = "CameraChan"
|
KeyCameraChan = "CameraChan"
|
||||||
KeyCameraIP = "CameraIP"
|
KeyCameraIP = "CameraIP"
|
||||||
KeyCBR = "CBR"
|
KeyCBR = "CBR"
|
||||||
KeyClipDuration = "ClipDuration"
|
KeyClipDuration = "ClipDuration"
|
||||||
KeyChannels = "Channels"
|
KeyChannels = "Channels"
|
||||||
KeyExposure = "Exposure"
|
KeyExposure = "Exposure"
|
||||||
KeyFileFPS = "FileFPS"
|
KeyFileFPS = "FileFPS"
|
||||||
KeyFilters = "Filters"
|
KeyFilters = "Filters"
|
||||||
KeyFrameRate = "FrameRate"
|
KeyFrameRate = "FrameRate"
|
||||||
KeyHeight = "Height"
|
KeyHeight = "Height"
|
||||||
KeyHorizontalFlip = "HorizontalFlip"
|
KeyHorizontalFlip = "HorizontalFlip"
|
||||||
KeyHTTPAddress = "HTTPAddress"
|
KeyHTTPAddress = "HTTPAddress"
|
||||||
KeyInput = "Input"
|
KeyInput = "Input"
|
||||||
KeyInputCodec = "InputCodec"
|
KeyInputCodec = "InputCodec"
|
||||||
KeyInputPath = "InputPath"
|
KeyInputPath = "InputPath"
|
||||||
KeyLogging = "logging"
|
KeyLogging = "logging"
|
||||||
KeyLoop = "Loop"
|
KeyLoop = "Loop"
|
||||||
KeyMinFPS = "MinFPS"
|
KeyMinFPS = "MinFPS"
|
||||||
KeyMinFrames = "MinFrames"
|
KeyMinFrames = "MinFrames"
|
||||||
KeyMode = "mode"
|
KeyMode = "mode"
|
||||||
KeyMotionDownscaling = "MotionDownscaling"
|
KeyMotionDownscaling = "MotionDownscaling"
|
||||||
KeyMotionHistory = "MotionHistory"
|
KeyMotionHistory = "MotionHistory"
|
||||||
KeyMotionInterval = "MotionInterval"
|
KeyMotionInterval = "MotionInterval"
|
||||||
KeyMotionKernel = "MotionKernel"
|
KeyMotionKernel = "MotionKernel"
|
||||||
KeyMotionMinArea = "MotionMinArea"
|
KeyMotionMinArea = "MotionMinArea"
|
||||||
KeyMotionPadding = "MotionPadding"
|
KeyMotionPadding = "MotionPadding"
|
||||||
KeyMotionPixels = "MotionPixels"
|
KeyMotionPixels = "MotionPixels"
|
||||||
KeyMotionThreshold = "MotionThreshold"
|
KeyMotionThreshold = "MotionThreshold"
|
||||||
KeyOutput = "Output"
|
KeyOutput = "Output"
|
||||||
KeyOutputPath = "OutputPath"
|
KeyOutputPath = "OutputPath"
|
||||||
KeyOutputs = "Outputs"
|
KeyOutputs = "Outputs"
|
||||||
KeyPSITime = "PSITime"
|
KeyPSITime = "PSITime"
|
||||||
KeyQuantization = "Quantization"
|
KeyQuantization = "Quantization"
|
||||||
KeyRBCapacity = "RBCapacity"
|
KeyRBCapacity = "RBCapacity"
|
||||||
KeyRBWriteTimeout = "RBWriteTimeout"
|
KeyRBStartElementSize = "RBStartElementSize"
|
||||||
KeyRecPeriod = "RecPeriod"
|
KeyRBWriteTimeout = "RBWriteTimeout"
|
||||||
KeyRotation = "Rotation"
|
KeyRecPeriod = "RecPeriod"
|
||||||
KeyRTMPURL = "RTMPURL"
|
KeyRotation = "Rotation"
|
||||||
KeyRTPAddress = "RTPAddress"
|
KeyRTMPURL = "RTMPURL"
|
||||||
KeySampleRate = "SampleRate"
|
KeyRTPAddress = "RTPAddress"
|
||||||
KeySaturation = "Saturation"
|
KeySampleRate = "SampleRate"
|
||||||
KeySuppress = "Suppress"
|
KeySaturation = "Saturation"
|
||||||
KeyVBRBitrate = "VBRBitrate"
|
KeySnapQuality = "SnapQuality"
|
||||||
KeyVBRQuality = "VBRQuality"
|
KeySuppress = "Suppress"
|
||||||
KeyVerticalFlip = "VerticalFlip"
|
KeyTimelapseDuration = "TimelapseDuration"
|
||||||
KeyWidth = "Width"
|
KeyTimelapseInterval = "TimelapseInterval"
|
||||||
|
KeyVBRBitrate = "VBRBitrate"
|
||||||
|
KeyVBRQuality = "VBRQuality"
|
||||||
|
KeyVerticalFlip = "VerticalFlip"
|
||||||
|
KeyWidth = "Width"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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"
|
||||||
|
@ -117,8 +122,9 @@ const (
|
||||||
defaultFileFPS = 0
|
defaultFileFPS = 0
|
||||||
|
|
||||||
// Ring buffer defaults.
|
// Ring buffer defaults.
|
||||||
defaultRBCapacity = 50000000 // => 50MB
|
defaultRBCapacity = 50000000 // => 50MB
|
||||||
defaultRBWriteTimeout = 5 // Seconds.
|
defaultRBStartElementSize = 1000 // bytes
|
||||||
|
defaultRBWriteTimeout = 5 // Seconds.
|
||||||
|
|
||||||
// Motion filter parameter defaults.
|
// Motion filter parameter defaults.
|
||||||
defaultMinFPS = 1.0
|
defaultMinFPS = 1.0
|
||||||
|
@ -264,24 +270,25 @@ 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,
|
||||||
"rtsp": InputRTSP,
|
"raspistill": InputRaspistill,
|
||||||
"v4l": InputV4L,
|
"rtsp": InputRTSP,
|
||||||
"file": InputFile,
|
"v4l": InputV4L,
|
||||||
"audio": InputAudio,
|
"file": InputFile,
|
||||||
|
"audio": InputAudio,
|
||||||
},
|
},
|
||||||
c,
|
c,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
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: 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,
|
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