diff --git a/cmd/rv/main.go b/cmd/rv/main.go index 53d145f7..64680907 100644 --- a/cmd/rv/main.go +++ b/cmd/rv/main.go @@ -102,7 +102,7 @@ const ( defaultSleepTime = 60 // Seconds profilePath = "rv.prof" pkg = "rv: " - runPreDelay = 20 * time.Second + runPreDelay = 20 * time.Second ) // This is set to true if the 'profile' build tag is provided on build. @@ -136,7 +136,7 @@ func main() { var rv *revid.Revid log.Log(logger.Debug, "initialising netsender client") - ns, err := netsender.New(log, nil, readPin(rv), nil, config.TypeData) + ns, err := netsender.New(log, nil, readPin(rv), nil, createVarMap()) if err != nil { log.Log(logger.Fatal, pkg+"could not initialise netsender client: "+err.Error()) } @@ -146,9 +146,9 @@ func main() { if err != nil { log.Log(logger.Fatal, pkg+"could not initialise revid", "error", err.Error()) } - - // NB: Problems were encountered with communicating with RTSP inputs. When trying to - // connect it would fail due to timeout; as if things had not been set up quickly + + // NB: Problems were encountered with communicating with RTSP inputs. When trying to + // connect it would fail due to timeout; as if things had not been set up quickly // enough before revid tried to do things. This delay fixes this, but there is probably // a better way to solve this problem. time.Sleep(runPreDelay) @@ -235,6 +235,14 @@ func run(rv *revid.Revid, ns *netsender.Sender, l *logger.Logger, nl *netlogger. } } +func createVarMap() map[string]string { + var m map[string]string + for _, v := range config.Variables { + m[v.Name] = v.Type_ + } + return m +} + // profile opens a file to hold CPU profiling metrics and then starts the // CPU profiler. func profile(l *logger.Logger) { diff --git a/filter/vfps.go b/filter/vfps.go index 9a9b9a4e..8db649c2 100644 --- a/filter/vfps.go +++ b/filter/vfps.go @@ -41,7 +41,7 @@ type VariableFPS struct { } // NewVariableFPS returns a pointer to a new VariableFPS filter struct. -func NewVariableFPS(dst io.WriteCloser, minFPS float64, filter Filter) *VariableFPS { +func NewVariableFPS(dst io.WriteCloser, minFPS uint, filter Filter) *VariableFPS { frames := uint(25 / minFPS) return &VariableFPS{filter, dst, frames, 0} } diff --git a/go.mod b/go.mod index 9787c04a..0b9483f3 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/Comcast/gots v0.0.0-20190305015453-8d56e473f0f7 github.com/go-audio/audio v0.0.0-20181013203223-7b2a6ca21480 github.com/go-audio/wav v0.0.0-20181013172942-de841e69b884 + github.com/google/go-cmp v0.4.1 github.com/mewkiz/flac v1.0.5 github.com/pkg/errors v0.8.1 github.com/yobert/alsa v0.0.0-20180630182551-d38d89fa843e diff --git a/go.sum b/go.sum index f44fd02f..acbd8bbb 100644 --- a/go.sum +++ b/go.sum @@ -25,6 +25,8 @@ github.com/go-audio/audio v0.0.0-20181013203223-7b2a6ca21480/go.mod h1:6uAu0+H2l github.com/go-audio/wav v0.0.0-20181013172942-de841e69b884 h1:2TaXIaVA4ff/MHHezOj83tCypALTFAcXOImcFWNa3jw= github.com/go-audio/wav v0.0.0-20181013172942-de841e69b884/go.mod h1:UiqzUyfX0zs3pJ/DPyvS5v8sN6s5bXPUDDIVA5v8dks= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/google/go-cmp v0.4.1 h1:/exdXoGamhu5ONeUJH0deniYLWYvQwW66yvlfiiKTu0= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/jacobsa/go-serial v0.0.0-20180131005756-15cf729a72d4/go.mod h1:2RvX5ZjVtsznNZPEt4xwJXNJrM3VTZoQf7V6gk0ysvs= github.com/kidoman/embd v0.0.0-20170508013040-d3d8c0c5c68d/go.mod h1:ACKj9jnzOzj1lw2ETilpFGK7L9dtJhAzT7T1OhAGtRQ= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= @@ -62,6 +64,7 @@ golang.org/x/image v0.0.0-20200119044424-58c23975cae1 h1:5h3ngYt7+vXCDZCup/HkCQg golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/sys v0.0.0-20190913121621-c3b328c6e5a7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= diff --git a/revid/config/config.go b/revid/config/config.go index d6114137..fec73a7f 100644 --- a/revid/config/config.go +++ b/revid/config/config.go @@ -27,10 +27,8 @@ LICENSE package config import ( - "errors" "time" - "bitbucket.org/ausocean/av/codec/codecutil" "bitbucket.org/ausocean/utils/logger" ) @@ -65,33 +63,6 @@ const ( MJPEG ) -// Default config settings -const ( - // General revid defaults. - defaultInput = InputRaspivid - defaultOutput = OutputHTTP - defaultTimeout = 0 - defaultInputCodec = codecutil.H264 - defaultVerbosity = logger.Error - defaultRtpAddr = "localhost:6970" - defaultCameraIP = "192.168.1.50" - defaultBurstPeriod = 10 // Seconds - defaultMinFrames = 100 - defaultFrameRate = 25 - defaultWriteRate = 25 - defaultClipDuration = 0 - defaultAudioInputCodec = codecutil.ADPCM - defaultPSITime = 2 - defaultFileFPS = 0 - - // Ring buffer defaults. - defaultRBCapacity = 50000000 // => 50MB - defaultRBWriteTimeout = 5 // Seconds. - - // Motion filter parameter defaults. - defaultMinFPS = 1.0 -) - // Quality represents video quality. type Quality int @@ -257,10 +228,10 @@ type Config struct { Width uint // Width defines the input video width Raspivid input. Bitrate uint // Bitrate specifies the bitrate for constant bitrate in kbps. - HorizontalFlip bool // HorizontalFlip flips video horizontally for Raspivid input. - VerticalFlip bool // VerticalFlip flips video vertically for Raspivid input. - Filters []uint8 // Defines the methods of filtering to be used in between lexing and encoding. - PSITime uint // Sets the time between a packet being sent. + HorizontalFlip bool // HorizontalFlip flips video horizontally for Raspivid input. + VerticalFlip bool // VerticalFlip flips video vertically for Raspivid input. + Filters []uint // Defines the methods of filtering to be used in between lexing and encoding. + PSITime uint // Sets the time between a packet being sent. // Ring buffer parameters. RBCapacity uint // The number of bytes the ring buffer will occupy. @@ -268,9 +239,9 @@ type Config struct { // Motion filter parameters. // Some parameters can be used with any filter, while others can only be used by a few. - MinFPS float64 // The reduced framerate of the video when there is no motion. - MotionInterval uint // Sets the number of frames that are held before the filter is used (on the nth frame). - MotionDownscaling uint // Downscaling factor of frames used for motion detection. + MinFPS uint // The reduced framerate of the video when there is no motion. + MotionInterval uint // Sets the number of frames that are held before the filter is used (on the nth frame). + MotionDownscaling uint // Downscaling factor of frames used for motion detection. MotionMinArea float64 // Used to ignore small areas of motion detection (KNN & MOG only). MotionThreshold float64 // Intensity value that is considered motion. @@ -286,187 +257,32 @@ type Config struct { FileFPS uint } -// TypeData contains information about all of the variables that -// can be set over the web. It is a psuedo const. -var TypeData = map[string]string{ - "AutoWhiteBalance": "enum:off,auto,sun,cloud,shade,tungsten,fluorescent,incandescent,flash,horizon", - "BitDepth": "int", - "Bitrate": "uint", - "Brightness": "uint", - "BurstPeriod": "uint", - "CameraChan": "int", - "CameraIP": "string", - "CBR": "bool", - "ClipDuration": "uint", - "Exposure": "enum:auto,night,nightpreview,backlight,spotlight,sports,snow,beach,verylong,fixedfps,antishake,fireworks", - "FileFPS": "int", - "Filters": "enums:NoOp,MOG,VariableFPS,KNN,Difference,Basic", - "FrameRate": "uint", - "Height": "uint", - "HorizontalFlip": "bool", - "HTTPAddress": "string", - "Input": "enum:raspivid,rtsp,v4l,file,audio", - "InputCodec": "enum:H264,MJPEG,PCM,ADPCM", - "InputPath": "string", - "logging": "enum:Debug,Info,Warning,Error,Fatal", - "Loop": "bool", - "MinFPS": "float", - "MinFrames": "uint", - "mode": "enum:Normal,Paused,Burst,Loop", - "MotionDownscaling": "uint", - "MotionHistory": "uint", - "MotionInterval": "int", - "MotionKernel": "uint", - "MotionMinArea": "float", - "MotionPadding": "uint", - "MotionPixels": "int", - "MotionThreshold": "float", - "Output": "enum:File,Http,Rtmp,Rtp", - "OutputPath": "string", - "Outputs": "enums:File,Http,Rtmp,Rtp", - "Quantization": "uint", - "RBCapacity": "uint", - "RBWriteTimeout": "uint", - "Rotation": "uint", - "RTMPURL": "string", - "RTPAddress": "string", - "Saturation": "int", - "VBRBitrate": "int", - "VBRQuality": "enum:standard,fair,good,great,excellent", - "VerticalFlip": "bool", - "Width": "uint", -} - -// Validation errors. -var ( - errInvalidQuantization = errors.New("invalid quantization") -) - // Validate checks for any errors in the config fields and defaults settings // if particular parameters have not been defined. func (c *Config) Validate() error { - switch c.LogLevel { - case logger.Debug, logger.Info, logger.Warning, logger.Error, logger.Fatal: - default: - c.LogInvalidField("LogLevel", defaultVerbosity) - c.LogLevel = defaultVerbosity - } - - if c.CameraIP == "" { - c.LogInvalidField("CameraIP", defaultCameraIP) - c.CameraIP = defaultCameraIP - } - - switch c.Input { - case InputRaspivid, InputV4L, InputFile, InputAudio, InputRTSP: - case NothingDefined: - c.LogInvalidField("Input", defaultInput) - c.Input = defaultInput - default: - return errors.New("bad input type defined in config") - } - - switch c.InputCodec { - case codecutil.H264, codecutil.MJPEG, codecutil.PCM, codecutil.ADPCM: - default: - switch c.Input { - case OutputAudio: - c.LogInvalidField("InputCodec", defaultAudioInputCodec) - c.InputCodec = defaultAudioInputCodec - default: - c.LogInvalidField("InputCodec", defaultInputCodec) - c.InputCodec = defaultInputCodec + for _, v := range Variables { + if v.Validate != nil { + v.Validate(c) } } - - if c.Outputs == nil { - c.LogInvalidField("Outputs", defaultOutput) - c.Outputs = append(c.Outputs, defaultOutput) - } - - for i, o := range c.Outputs { - switch o { - case OutputFile, OutputHTTP, OutputRTP: - case OutputRTMP: - if c.RTMPURL == "" { - c.Logger.Log(logger.Info, "no RTMP URL: falling back to HTTP") - c.Outputs[i] = OutputHTTP - } - default: - return errors.New("bad output type defined in config") - } - } - - if c.BurstPeriod == 0 { - c.LogInvalidField("BurstPeriod", defaultBurstPeriod) - c.BurstPeriod = defaultBurstPeriod - } - - const maxMinFrames = 1000 - switch { - case c.MinFrames == 0: - c.LogInvalidField("MinFrames", defaultMinFrames) - c.MinFrames = defaultMinFrames - case c.MinFrames < 0: - return errors.New("refresh period is less than 0") - case c.MinFrames > maxMinFrames: - return errors.New("refresh period is greater than 1000") - } - - if c.FrameRate <= 0 { - c.LogInvalidField("FrameRate", defaultFrameRate) - c.FrameRate = defaultFrameRate - } - - if c.WriteRate <= 0 { - c.LogInvalidField("writeRate", defaultWriteRate) - c.WriteRate = defaultWriteRate - } - - if c.ClipDuration == 0 { - c.LogInvalidField("ClipDuration", defaultClipDuration) - c.ClipDuration = defaultClipDuration - } else if c.ClipDuration < 0 { - return errors.New("clip duration is less than 0") - } - - if c.RTPAddress == "" { - c.LogInvalidField("RTPAddress", defaultRtpAddr) - c.RTPAddress = defaultRtpAddr - } - - if c.RBWriteTimeout <= 0 { - c.Logger.Log(logger.Info, "RBWriteTimeout bad or unset, defaulting", "RBWriteTimeout", defaultRBWriteTimeout) - c.RBWriteTimeout = defaultRBWriteTimeout - } - - if c.RBCapacity <= 0 { - c.Logger.Log(logger.Info, "RBCapacity bad or unset, defaulting", "RBCapacity", defaultRBCapacity) - c.RBCapacity = defaultRBCapacity - } - - if c.PSITime <= 0 { - c.LogInvalidField("PSITime", defaultPSITime) - c.PSITime = defaultPSITime - } - if c.MinFPS <= 0 { - c.LogInvalidField("MinFPS", defaultMinFPS) - c.MinFPS = defaultMinFPS - } - - if c.FileFPS <= 0 || (c.FileFPS > 0 && c.Input != InputFile) { - c.LogInvalidField("FileFPS", defaultFileFPS) - c.FileFPS = defaultFileFPS - } - return nil } +// Update takes a map of configuration variable names and their corresponding +// values, parses the string values and converting into correct type, and then +// sets the config struct fields as appropriate. +func (c *Config) Update(vars map[string]string) { + for _, value := range Variables { + if v, ok := vars[value.Name]; ok && value.Update != nil { + value.Update(c, v) + } + } +} + func (c *Config) LogInvalidField(name string, def interface{}) { c.Logger.Log(logger.Info, name+" bad or unset, defaulting", name, def) } -// stringInSlice returns true if want is in slice. func stringInSlice(want string, slice []string) bool { for _, s := range slice { if s == want { diff --git a/revid/config/config_test.go b/revid/config/config_test.go new file mode 100644 index 00000000..36831eda --- /dev/null +++ b/revid/config/config_test.go @@ -0,0 +1,177 @@ +/* +DESCRIPTION + config_test.go provides testing for the Config struct methods (Validate and Update). + +AUTHORS + Saxon A. Nelson-Milton + +LICENSE + Copyright (C) 2020 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 + along with revid in gpl.txt. If not, see http://www.gnu.org/licenses. +*/ + +package config + +import ( + "testing" + "time" + + "bitbucket.org/ausocean/av/codec/codecutil" + "bitbucket.org/ausocean/utils/logger" + "github.com/google/go-cmp/cmp" +) + +type dumbLogger struct{} + +func (dl *dumbLogger) Log(l int8, m string, a ...interface{}) {} +func (dl *dumbLogger) SetLevel(l int8) {} + +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, + WriteRate: defaultWriteRate, + ClipDuration: defaultClipDuration, + PSITime: defaultPSITime, + FileFPS: defaultFileFPS, + RBCapacity: defaultRBCapacity, + RBWriteTimeout: defaultRBWriteTimeout, + MinFPS: defaultMinFPS, + } + + got := Config{Logger: dl} + err := (&got).Validate() + if err != nil { + t.Fatalf("did not expect error: %v", err) + } + + if !cmp.Equal(got, want) { + t.Errorf("configs not equal\nwant: %v\ngot: %v", want, got) + } +} + +func TestUpdate(t *testing.T) { + updateMap := map[string]string{ + "AutoWhiteBalance": "sun", + "BitDepth": "3", + "Bitrate": "200000", + "Brightness": "30", + "BurstPeriod": "10", + "CameraChan": "2", + "CameraIP": "192.168.1.5", + "CBR": "true", + "ClipDuration": "5", + "Exposure": "night", + "FileFPS": "30", + "Filters": "MOG", + "FrameRate": "30", + "Height": "300", + "HorizontalFlip": "true", + "HTTPAddress": "http://address", + "Input": "rtsp", + "InputCodec": "MJPEG", + "InputPath": "/inputpath", + "logging": "Error", + "Loop": "true", + "MinFPS": "30", + "MinFrames": "30", + "MotionDownscaling": "3", + "MotionHistory": "4", + "MotionInterval": "6", + "MotionKernel": "2", + "MotionMinArea": "9", + "MotionPadding": "8", + "MotionPixels": "100", + "MotionThreshold": "34", + "OutputPath": "/outputpath", + "Outputs": "Rtmp,Rtp", + "Quantization": "30", + "RBCapacity": "100000", + "RBWriteTimeout": "50", + "Rotation": "180", + "RTMPURL": "rtmp://url", + "RTPAddress": "ip:port", + "Saturation": "-10", + "VBRBitrate": "300000", + "VBRQuality": "excellent", + "VerticalFlip": "true", + "Width": "300", + } + + dl := &dumbLogger{} + + want := Config{ + Logger: dl, + AutoWhiteBalance: "sun", + BitDepth: 3, + Bitrate: 200000, + Brightness: 30, + BurstPeriod: 10, + CameraChan: 2, + CameraIP: "192.168.1.5", + CBR: true, + ClipDuration: 5 * time.Second, + Exposure: "night", + FileFPS: 30, + Filters: []uint{FilterMOG}, + FrameRate: 30, + Height: 300, + HorizontalFlip: true, + HTTPAddress: "http://address", + Input: InputRTSP, + InputCodec: codecutil.MJPEG, + InputPath: "/inputpath", + LogLevel: logger.Error, + Loop: true, + MinFPS: 30, + MinFrames: 30, + MotionDownscaling: 3, + MotionHistory: 4, + MotionInterval: 6, + MotionKernel: 2, + MotionMinArea: 9, + MotionPadding: 8, + MotionPixels: 100, + MotionThreshold: 34, + OutputPath: "/outputpath", + Outputs: []uint8{OutputRTMP, OutputRTP}, + Quantization: 30, + RBCapacity: 100000, + RBWriteTimeout: 50, + Rotation: 180, + RTMPURL: "rtmp://url", + RTPAddress: "ip:port", + Saturation: -10, + VBRBitrate: 300000, + VBRQuality: QualityExcellent, + VerticalFlip: true, + Width: 300, + } + + got := Config{Logger: dl} + got.Update(updateMap) + if !cmp.Equal(want, got) { + t.Errorf("configs not equal\nwant: %v\ngot: %v", want, got) + } +} diff --git a/revid/config/variables.go b/revid/config/variables.go new file mode 100644 index 00000000..c977c736 --- /dev/null +++ b/revid/config/variables.go @@ -0,0 +1,561 @@ +/* +DESCRIPTION + variables.go contains a list of structs that provide a variable Name, type in + a string format, a function for updating the variable in the Config struct + from a string, and finally, a validation function to check the validity of the + corresponding field value in the Config. + +AUTHORS + Saxon A. Nelson-Milton + +LICENSE + Copyright (C) 2020 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 + along with revid in gpl.txt. If not, see http://www.gnu.org/licenses. +*/ + +package config + +import ( + "fmt" + "strconv" + "strings" + "time" + + "bitbucket.org/ausocean/av/codec/codecutil" + "bitbucket.org/ausocean/utils/logger" +) + +// Default variable values. +const ( + // General revid defaults. + defaultInput = InputRaspivid + defaultOutput = OutputHTTP + defaultInputCodec = codecutil.H264 + defaultVerbosity = logger.Error + defaultRTPAddr = "localhost:6970" + defaultCameraIP = "192.168.1.50" + defaultBurstPeriod = 10 // Seconds + defaultMinFrames = 100 + defaultFrameRate = 25 + defaultWriteRate = 25 + defaultClipDuration = 0 + defaultAudioInputCodec = codecutil.ADPCM + defaultPSITime = 2 + defaultFileFPS = 0 + + // Ring buffer defaults. + defaultRBCapacity = 50000000 // => 50MB + defaultRBWriteTimeout = 5 // Seconds. + + // Motion filter parameter defaults. + defaultMinFPS = 1.0 +) + +var Variables = []struct { + Name string + Type_ string + Update func(*Config, string) + Validate func(*Config) +}{ + { + Name: "AutoWhiteBalance", + Type_: "enum:off,auto,sun,cloud,shade,tungsten,fluorescent,incandescent,flash,horizon", + Update: func(c *Config, v string) { c.AutoWhiteBalance = v }, + }, + { + Name: "BitDepth", + Type_: "uint", + Update: func(c *Config, v string) { c.BitDepth = parseUint("BitDepth", v, c) }, + }, + { + Name: "Bitrate", + Type_: "uint", + Update: func(c *Config, v string) { c.Bitrate = parseUint("Bitrate", v, c) }, + }, + { + Name: "Brightness", + Type_: "uint", + Update: func(c *Config, v string) { c.Brightness = parseUint("Brightness", v, c) }, + }, + { + Name: "BurstPeriod", + Type_: "uint", + Update: func(c *Config, v string) { c.BurstPeriod = parseUint("BurstPeriod", v, c) }, + Validate: func(c *Config) { + if c.BurstPeriod <= 0 { + c.LogInvalidField("BurstPeriod", defaultBurstPeriod) + c.BurstPeriod = defaultBurstPeriod + } + }, + }, + { + Name: "CameraChan", + Type_: "uint", + Update: func(c *Config, v string) { c.CameraChan = uint8(parseUint("CameraChan", v, c)) }, + }, + { + Name: "CameraIP", + Type_: "string", + Update: func(c *Config, v string) { c.CameraIP = v }, + Validate: func(c *Config) { + if c.CameraIP == "" { + c.LogInvalidField("CameraIP", defaultCameraIP) + c.CameraIP = defaultCameraIP + } + }, + }, + { + Name: "CBR", + Type_: "bool", + Update: func(c *Config, v string) { c.CBR = parseBool("CBR", v, c) }, + }, + { + Name: "ClipDuration", + Type_: "uint", + Update: func(c *Config, v string) { + _v, err := strconv.Atoi(v) + if err != nil { + c.Logger.Log(logger.Warning, "invalid ClipDuration param", "value", v) + } + c.ClipDuration = time.Duration(_v) * time.Second + }, + Validate: func(c *Config) { + if c.ClipDuration <= 0 { + c.LogInvalidField("ClipDuration", defaultClipDuration) + c.ClipDuration = defaultClipDuration + } + }, + }, + { + Name: "Exposure", + Type_: "enum:auto,night,nightpreview,backlight,spotlight,sports,snow,beach,verylong,fixedfps,antishake,fireworks", + Update: func(c *Config, v string) { c.Exposure = v }, + }, + { + Name: "FileFPS", + Type_: "uint", + Update: func(c *Config, v string) { c.FileFPS = parseUint("FileFPS", v, c) }, + Validate: func(c *Config) { + if c.FileFPS <= 0 || (c.FileFPS > 0 && c.Input != InputFile) { + c.LogInvalidField("FileFPS", defaultFileFPS) + c.FileFPS = defaultFileFPS + } + }, + }, + { + Name: "Filters", + Type_: "enums:NoOp,MOG,VariableFPS,KNN,Difference,Basic", + Update: func(c *Config, v string) { + filters := strings.Split(v, ",") + m := map[string]uint{"NoOp": FilterNoOp, "MOG": FilterMOG, "VariableFPS": FilterVariableFPS, "KNN": FilterKNN, "Difference": FilterDiff, "Basic": FilterBasic} + c.Filters = make([]uint, len(filters)) + for i, filter := range filters { + v, ok := m[filter] + if !ok { + c.Logger.Log(logger.Warning, "invalid Filters param", "value", v) + } + c.Filters[i] = uint(v) + } + }, + }, + { + Name: "FrameRate", + Type_: "uint", + Update: func(c *Config, v string) { c.FrameRate = parseUint("FrameRate", v, c) }, + Validate: func(c *Config) { + if c.FrameRate <= 0 || c.FrameRate > 60 { + c.LogInvalidField("FrameRate", defaultFrameRate) + c.FrameRate = defaultFrameRate + } + }, + }, + { + Name: "Height", + Type_: "uint", + Update: func(c *Config, v string) { c.Height = parseUint("Height", v, c) }, + }, + { + Name: "HorizontalFlip", + Type_: "bool", + Update: func(c *Config, v string) { c.HorizontalFlip = parseBool("HorizontalFlip", v, c) }, + }, + { + Name: "HTTPAddress", + Type_: "string", + Update: func(c *Config, v string) { c.HTTPAddress = v }, + }, + { + Name: "Input", + Type_: "enum:raspivid,rtsp,v4l,file,audio", + Update: func(c *Config, v string) { + c.Input = parseEnum( + "Input", + v, + map[string]uint8{ + "raspivid": InputRaspivid, + "rtsp": InputRTSP, + "v4l": InputV4L, + "file": InputFile, + "audio": InputAudio, + }, + c, + ) + }, + Validate: func(c *Config) { + switch c.Input { + case InputRaspivid, InputV4L, InputFile, InputAudio, InputRTSP: + default: + c.LogInvalidField("Input", defaultInput) + c.Input = defaultInput + } + }, + }, + { + Name: "InputCodec", + Type_: "enum:H264,H265,MJPEG,PCM,ADPCM", + Update: func(c *Config, v string) { + c.InputCodec = parseEnum( + "InputCodec", + v, + map[string]uint8{ + "h264": codecutil.H264, + "h265": codecutil.H265, + "mjpeg": codecutil.MJPEG, + "pcm": codecutil.PCM, + "adpcm": codecutil.ADPCM, + }, + c, + ) + }, + Validate: func(c *Config) { + switch c.InputCodec { + case codecutil.H264, codecutil.MJPEG, codecutil.PCM, codecutil.ADPCM: + default: + switch c.Input { + case OutputAudio: + c.LogInvalidField("InputCodec", defaultAudioInputCodec) + c.InputCodec = defaultAudioInputCodec + default: + c.LogInvalidField("InputCodec", defaultInputCodec) + c.InputCodec = defaultInputCodec + } + } + }, + }, + { + Name: "InputPath", + Type_: "string", + Update: func(c *Config, v string) { c.InputPath = v }, + }, + { + Name: "logging", + Type_: "enum:Debug,Info,Warning,Error,Fatal", + Update: func(c *Config, v string) { + switch v { + case "Debug": + c.LogLevel = logger.Debug + case "Info": + c.LogLevel = logger.Info + case "Warning": + c.LogLevel = logger.Warning + case "Error": + c.LogLevel = logger.Error + case "Fatal": + c.LogLevel = logger.Fatal + default: + c.Logger.Log(logger.Warning, "invalid Logging param", "value", v) + } + }, + Validate: func(c *Config) { + switch c.LogLevel { + case logger.Debug, logger.Info, logger.Warning, logger.Error, logger.Fatal: + default: + c.LogInvalidField("LogLevel", defaultVerbosity) + c.LogLevel = defaultVerbosity + } + }, + }, + { + Name: "Loop", + Type_: "bool", + Update: func(c *Config, v string) { c.Loop = parseBool("Loop", v, c) }, + }, + { + Name: "MinFPS", + Type_: "uint", + Update: func(c *Config, v string) { c.MinFPS = parseUint("MinFPS", v, c) }, + Validate: func(c *Config) { c.MinFPS = lessThanOrEqual("MinFPS", c.MinFPS, 0, c, defaultMinFPS) }, + }, + { + Name: "MinFrames", + Type_: "uint", + Update: func(c *Config, v string) { c.MinFrames = parseUint("MinFrames", v, c) }, + Validate: func(c *Config) { + const maxMinFrames = 1000 + if c.MinFrames <= 0 || c.MinFrames > maxMinFrames { + c.LogInvalidField("MinFrames", defaultMinFrames) + c.MinFrames = defaultMinFrames + } + }, + }, + { + Name: "mode", + Type_: "enum:Normal,Paused,Burst,Loop", + Update: func(c *Config, v string) { + c.Loop = false + if v == "Loop" { + c.Loop = true + } + }, + }, + { + Name: "MotionDownscaling", + Type_: "uint", + Update: func(c *Config, v string) { c.MotionDownscaling = parseUint("MotionDownscaling", v, c) }, + }, + { + Name: "MotionHistory", + Type_: "uint", + Update: func(c *Config, v string) { c.MotionHistory = parseUint("MotionHistory", v, c) }, + }, + { + Name: "MotionInterval", + Type_: "uint", + Update: func(c *Config, v string) { c.MotionInterval = parseUint("MotionInterval", v, c) }, + }, + { + Name: "MotionKernel", + Type_: "uint", + Update: func(c *Config, v string) { c.MotionKernel = parseUint("MotionKernel", v, c) }, + }, + { + Name: "MotionMinArea", + Type_: "float", + Update: func(c *Config, v string) { + f, err := strconv.ParseFloat(v, 64) + if err != nil { + c.Logger.Log(logger.Warning, "invalid MotionMinArea var", "value", v) + } + c.MotionMinArea = f + }, + }, + { + Name: "MotionPadding", + Type_: "uint", + Update: func(c *Config, v string) { c.MotionPadding = parseUint("MotionPadding", v, c) }, + }, + { + Name: "MotionPixels", + Type_: "uint", + Update: func(c *Config, v string) { c.MotionPixels = parseUint("MotionPixels", v, c) }, + }, + { + Name: "MotionThreshold", + Type_: "float", + Update: func(c *Config, v string) { + f, err := strconv.ParseFloat(v, 64) + if err != nil { + c.Logger.Log(logger.Warning, "invalid MotionThreshold var", "value", v) + } + c.MotionThreshold = f + }, + }, + { + Name: "Output", + Type_: "enum:File,HTTP,RTMP,RTP", + Update: func(c *Config, v string) { + c.Outputs = make([]uint8, 1) + switch strings.ToLower(v) { + case "file": + c.Outputs[0] = OutputFile + case "http": + c.Outputs[0] = OutputHTTP + case "rtmp": + c.Outputs[0] = OutputRTMP + case "rtp": + c.Outputs[0] = OutputRTP + default: + c.Logger.Log(logger.Warning, "invalid output param", "value", v) + } + }, + }, + { + Name: "OutputPath", + Type_: "string", + Update: func(c *Config, v string) { c.OutputPath = v }, + }, + { + Name: "Outputs", + Type_: "enums:File,HTTP,RTMP,RTP", + Update: func(c *Config, v string) { + outputs := strings.Split(v, ",") + c.Outputs = make([]uint8, len(outputs)) + for i, output := range outputs { + switch strings.ToLower(output) { + case "file": + c.Outputs[i] = OutputFile + case "http": + c.Outputs[i] = OutputHTTP + case "rtmp": + c.Outputs[i] = OutputRTMP + case "rtp": + c.Outputs[i] = OutputRTP + default: + c.Logger.Log(logger.Warning, "invalid outputs param", "value", v) + } + } + }, + Validate: func(c *Config) { + if c.Outputs == nil { + c.LogInvalidField("Outputs", defaultOutput) + c.Outputs = append(c.Outputs, defaultOutput) + } + }, + }, + { + Name: "PSITime", + Type_: "uint", + Update: func(c *Config, v string) { c.PSITime = parseUint("PSITime", v, c) }, + Validate: func(c *Config) { c.PSITime = lessThanOrEqual("PSITime", c.PSITime, 0, c, defaultPSITime) }, + }, + { + Name: "Quantization", + Type_: "uint", + Update: func(c *Config, v string) { c.Quantization = parseUint("Quantization", v, c) }, + }, + { + Name: "RBCapacity", + Type_: "uint", + Update: func(c *Config, v string) { c.RBCapacity = parseUint("RBCapacity", v, c) }, + Validate: func(c *Config) { c.RBCapacity = lessThanOrEqual("RBCapacity", c.RBCapacity, 0, c, defaultRBCapacity) }, + }, + { + Name: "RBWriteTimeout", + Type_: "uint", + Update: func(c *Config, v string) { c.RBWriteTimeout = parseUint("RBWriteTimeout", v, c) }, + Validate: func(c *Config) { + c.RBWriteTimeout = lessThanOrEqual("RBWriteTimeout", c.RBWriteTimeout, 0, c, defaultRBWriteTimeout) + }, + }, + { + Name: "Rotation", + Type_: "uint", + Update: func(c *Config, v string) { c.Rotation = parseUint("Rotation", v, c) }, + }, + { + Name: "RTMPURL", + Type_: "string", + Update: func(c *Config, v string) { c.RTMPURL = v }, + }, + { + Name: "RTPAddress", + Type_: "string", + Update: func(c *Config, v string) { c.RTPAddress = v }, + Validate: func(c *Config) { + if c.RTPAddress == "" { + c.LogInvalidField("RTPAddress", defaultRTPAddr) + c.RTPAddress = defaultRTPAddr + } + }, + }, + { + Name: "Saturation", + Type_: "int", + Update: func(c *Config, v string) { + _v, err := strconv.Atoi(v) + if err != nil { + c.Logger.Log(logger.Warning, "invalid saturation param", "value", v) + } + c.Saturation = _v + }, + }, + { + Name: "VBRBitrate", + Type_: "uint", + Update: func(c *Config, v string) { c.VBRBitrate = parseUint("VBRBitrate", v, c) }, + }, + { + Name: "VBRQuality", + Type_: "enum:standard,fair,good,great,excellent", + Update: func(c *Config, v string) { + c.VBRQuality = Quality(parseEnum( + "VBRQuality", + v, + map[string]uint8{ + "standard": uint8(QualityStandard), + "fair": uint8(QualityFair), + "good": uint8(QualityGood), + "great": uint8(QualityGreat), + "excellent": uint8(QualityExcellent), + }, + c, + )) + }, + }, + { + Name: "VerticalFlip", + Type_: "bool", + Update: func(c *Config, v string) { c.VerticalFlip = parseBool("VerticalFlip", v, c) }, + }, + { + Name: "Width", + Type_: "uint", + Update: func(c *Config, v string) { c.Width = parseUint("Width", v, c) }, + }, + { + Name: "WriteRate", + Type_: "uint", + Update: func(c *Config, v string) { c.WriteRate = float64(parseUint("WriteRate", v, c)) }, + Validate: func(c *Config) { + c.WriteRate = float64(lessThanOrEqual("WriteRate", uint(c.WriteRate), 0, c, defaultWriteRate)) + }, + }, +} + +func parseUint(n, v string, c *Config) uint { + _v, err := strconv.Atoi(v) + if err != nil { + c.Logger.Log(logger.Warning, fmt.Sprintf("invalid %s param", n), "value", v) + } + return uint(_v) +} + +func parseBool(n, v string, c *Config) (b bool) { + switch strings.ToLower(v) { + case "true": + b = true + case "false": + b = false + default: + c.Logger.Log(logger.Warning, fmt.Sprintf("invalid value for %s param", n), "value", v) + } + return +} + +func parseEnum(n, v string, enums map[string]uint8, c *Config) uint8 { + _v, ok := enums[strings.ToLower(v)] + if !ok { + c.Logger.Log(logger.Warning, fmt.Sprintf("invalid value for %s param", n), "value", v) + } + return _v +} + +func lessThanOrEqual(n string, v, cmp uint, c *Config, def uint) uint { + if v <= cmp { + c.LogInvalidField(n, def) + return def + } + return v +} diff --git a/revid/revid.go b/revid/revid.go index ab621d02..d0d981b4 100644 --- a/revid/revid.go +++ b/revid/revid.go @@ -33,8 +33,6 @@ import ( "errors" "fmt" "io" - "strconv" - "strings" "sync" "time" @@ -557,324 +555,7 @@ func (r *Revid) Update(vars map[string]string) error { defer r.mu.Unlock() //look through the vars and update revid where needed r.cfg.Logger.Log(logger.Debug, "checking vars from server", "vars", vars) - for key, value := range vars { - switch key { - case "Input": - v, ok := map[string]uint8{"raspivid": config.InputRaspivid, "rtsp": config.InputRTSP, "v4l": config.InputV4L, "file": config.InputFile}[strings.ToLower(value)] - if !ok { - r.cfg.Logger.Log(logger.Warning, "invalid input var", "value", value) - break - } - r.cfg.Input = v - case "Saturation": - s, err := strconv.Atoi(value) - if err != nil { - r.cfg.Logger.Log(logger.Warning, "invalid saturation param", "value", value) - break - } - r.cfg.Saturation = int(s) - case "Brightness": - b, err := strconv.Atoi(value) - if err != nil { - r.cfg.Logger.Log(logger.Warning, "invalid brightness param", "value", value) - break - } - r.cfg.Brightness = uint(b) - case "Exposure": - r.cfg.Exposure = value - case "AutoWhiteBalance": - r.cfg.AutoWhiteBalance = value - case "InputCodec": - switch value { - case "H264": - r.cfg.InputCodec = codecutil.H264 - case "MJPEG": - r.cfg.InputCodec = codecutil.MJPEG - default: - r.cfg.Logger.Log(logger.Warning, "invalid InputCodec variable value", "value", value) - } - case "Outputs": - outputs := strings.Split(value, ",") - r.cfg.Outputs = make([]uint8, len(outputs)) - - for i, output := range outputs { - switch output { - case "File": - r.cfg.Outputs[i] = config.OutputFile - case "Http": - r.cfg.Outputs[i] = config.OutputHTTP - case "Rtmp": - r.cfg.Outputs[i] = config.OutputRTMP - case "Rtp": - r.cfg.Outputs[i] = config.OutputRTP - default: - r.cfg.Logger.Log(logger.Warning, "invalid outputs param", "value", value) - continue - } - } - case "Output": - r.cfg.Outputs = make([]uint8, 1) - switch strings.ToLower(value) { - case "file": - r.cfg.Outputs[0] = config.OutputFile - case "http": - r.cfg.Outputs[0] = config.OutputHTTP - case "rtmp": - r.cfg.Outputs[0] = config.OutputRTMP - case "rtp": - r.cfg.Outputs[0] = config.OutputRTP - default: - r.cfg.Logger.Log(logger.Warning, "invalid output param", "value", value) - continue - } - - case "RTMPURL": - r.cfg.RTMPURL = value - case "RTPAddress": - r.cfg.RTPAddress = value - case "Bitrate": - v, err := strconv.Atoi(value) - if err != nil { - r.cfg.Logger.Log(logger.Warning, "invalid framerate param", "value", value) - break - } - r.cfg.Bitrate = uint(v) - case "OutputPath": - r.cfg.OutputPath = value - case "InputPath": - r.cfg.InputPath = value - case "Height": - h, err := strconv.Atoi(value) - if err != nil { - r.cfg.Logger.Log(logger.Warning, "invalid height param", "value", value) - break - } - r.cfg.Height = uint(h) - case "Width": - w, err := strconv.Atoi(value) - if err != nil { - r.cfg.Logger.Log(logger.Warning, "invalid width param", "value", value) - break - } - r.cfg.Width = uint(w) - case "FrameRate": - v, err := strconv.Atoi(value) - if err != nil { - r.cfg.Logger.Log(logger.Warning, "invalid framerate param", "value", value) - break - } - r.cfg.FrameRate = uint(v) - case "Rotation": - v, err := strconv.Atoi(value) - if err != nil || v > 359 { - r.cfg.Logger.Log(logger.Warning, "invalid rotation param", "value", value) - break - } - r.cfg.Rotation = uint(v) - case "HTTPAddress": - r.cfg.HTTPAddress = value - case "Quantization": - v, err := strconv.Atoi(value) - if err != nil { - r.cfg.Logger.Log(logger.Warning, "invalid quantization param", "value", v) - break - } - r.cfg.Quantization = uint(v) - case "MinFrames": - v, err := strconv.Atoi(value) - if err != nil { - r.cfg.Logger.Log(logger.Warning, "invalid MinFrames param", "value", value) - break - } - r.cfg.MinFrames = uint(v) - - case "ClipDuration": - v, err := strconv.Atoi(value) - if err != nil { - r.cfg.Logger.Log(logger.Warning, "invalid ClipDuration param", "value", value) - break - } - r.cfg.ClipDuration = time.Duration(v) * time.Second - - case "HorizontalFlip": - switch strings.ToLower(value) { - case "true": - r.cfg.HorizontalFlip = true - case "false": - r.cfg.HorizontalFlip = false - default: - r.cfg.Logger.Log(logger.Warning, "invalid HorizontalFlip param", "value", value) - } - case "VerticalFlip": - switch strings.ToLower(value) { - case "true": - r.cfg.VerticalFlip = true - case "false": - r.cfg.VerticalFlip = false - default: - r.cfg.Logger.Log(logger.Warning, "invalid VerticalFlip param", "value", value) - } - case "Filters": - filters := strings.Split(value, ",") - m := map[string]int{"NoOp": config.FilterNoOp, "MOG": config.FilterMOG, "VariableFPS": config.FilterVariableFPS, "KNN": config.FilterKNN, "Difference": config.FilterDiff, "Basic": config.FilterBasic} - r.cfg.Filters = make([]uint8, len(filters)) - for i, filter := range filters { - v, ok := m[filter] - if !ok { - r.cfg.Logger.Log(logger.Warning, "invalid Filters param", "value", value) - } - r.cfg.Filters[i] = uint8(v) - } - case "PSITime": - v, err := strconv.Atoi(value) - if err != nil || v < 0 { - r.cfg.Logger.Log(logger.Warning, "invalid PSITime var", "value", value) - break - } - r.cfg.PSITime = uint(v) - case "BurstPeriod": - v, err := strconv.Atoi(value) - if err != nil { - r.cfg.Logger.Log(logger.Warning, "invalid BurstPeriod param", "value", value) - break - } - r.cfg.BurstPeriod = uint(v) - case "logging": - switch value { - case "Debug": - r.cfg.LogLevel = logger.Debug - case "Info": - r.cfg.LogLevel = logger.Info - case "Warning": - r.cfg.LogLevel = logger.Warning - case "Error": - r.cfg.LogLevel = logger.Error - case "Fatal": - r.cfg.LogLevel = logger.Fatal - default: - r.cfg.Logger.Log(logger.Warning, "invalid Logging param", "value", value) - } - case "RBCapacity": - v, err := strconv.Atoi(value) - if err != nil || v < 0 { - r.cfg.Logger.Log(logger.Warning, "invalid RBCapacity var", "value", value) - break - } - r.cfg.RBCapacity = uint(v) - case "RBWriteTimeout": - v, err := strconv.Atoi(value) - if err != nil || v <= 0 { - r.cfg.Logger.Log(logger.Warning, "invalid RBWriteTimeout var", "value", value) - break - } - r.cfg.RBWriteTimeout = uint(v) - case "CBR": - v, ok := map[string]bool{"true": true, "false": false}[strings.ToLower(value)] - if !ok { - r.cfg.Logger.Log(logger.Warning, "invalid CBR var", "value", value) - break - } - r.cfg.CBR = v - case "CameraIP": - r.cfg.CameraIP = value - case "VBRQuality": - v, ok := map[string]config.Quality{"standard": config.QualityStandard, "fair": config.QualityFair, "good": config.QualityGood, "great": config.QualityGreat, "excellent": config.QualityExcellent}[strings.ToLower(value)] - if !ok { - r.cfg.Logger.Log(logger.Warning, "invalid VBRQuality var", "value", value) - break - } - r.cfg.VBRQuality = v - case "VBRBitrate": - v, err := strconv.Atoi(value) - if err != nil || v <= 0 { - r.cfg.Logger.Log(logger.Warning, "invalid VBRBitrate var", "value", value) - break - } - r.cfg.VBRBitrate = uint(v) - case "CameraChan": - v, err := strconv.Atoi(value) - if err != nil || (v != 1 && v != 2) { - r.cfg.Logger.Log(logger.Warning, "invalid CameraChan var", "value", value) - break - } - r.cfg.CameraChan = uint8(v) - case "MinFPS": - v, err := strconv.ParseFloat(value, 64) - if err != nil { - r.cfg.Logger.Log(logger.Warning, "invalid MinFPS var", "value", value) - break - } - r.cfg.MinFPS = v - case "MotionMinArea": - v, err := strconv.ParseFloat(value, 64) - if err != nil { - r.cfg.Logger.Log(logger.Warning, "invalid MotionMinArea var", "value", value) - break - } - r.cfg.MotionMinArea = v - case "MotionThreshold": - v, err := strconv.ParseFloat(value, 64) - if err != nil { - r.cfg.Logger.Log(logger.Warning, "invalid MotionThreshold var", "value", value) - break - } - r.cfg.MotionThreshold = v - case "MotionKernel": - v, err := strconv.Atoi(value) - if err != nil { - r.cfg.Logger.Log(logger.Warning, "invalid MotionKernel var", "value", value) - break - } - r.cfg.MotionKernel = uint(v) - case "MotionHistory": - v, err := strconv.Atoi(value) - if err != nil || v <= 0 { - r.cfg.Logger.Log(logger.Warning, "invalid MotionHistory var", "value", value) - break - } - r.cfg.MotionHistory = uint(v) - case "MotionPadding": - v, err := strconv.Atoi(value) - if err != nil || v <= 0 { - r.cfg.Logger.Log(logger.Warning, "invalid MotionPadding var", "value", value) - break - } - r.cfg.MotionPadding = uint(v) - case "MotionPixels": - v, err := strconv.Atoi(value) - if err != nil { - r.cfg.Logger.Log(logger.Warning, "invalid MotionPixels var", "value", value) - break - } - r.cfg.MotionPixels = uint(v) - case "MotionDownscaling": - v, err := strconv.Atoi(value) - if err != nil { - r.cfg.Logger.Log(logger.Warning, "invalid MotionDownscaling var", "value", value) - break - } - r.cfg.MotionDownscaling = uint(v) - case "MotionInterval": - v, err := strconv.Atoi(value) - if err != nil || v < 0 { - r.cfg.Logger.Log(logger.Warning, "invalid MotionInterval var", "value", value) - break - } - r.cfg.MotionInterval = uint(v) - case "FileFPS": - v, err := strconv.Atoi(value) - if err != nil { - r.cfg.Logger.Log(logger.Warning, "invalid FileFPS var", "value", value) - break - } - r.cfg.FileFPS = uint(v) - case "mode": - r.cfg.Loop = false - if value == "Loop" { - r.cfg.Loop = true - } - } - } + r.cfg.Update(vars) r.cfg.Logger.Log(logger.Info, "finished reconfig") r.cfg.Logger.Log(logger.Debug, "config changed", "config", r.cfg) return nil