From 20bf962fa31eece2ca1354d7af8bd89fed176e1c Mon Sep 17 00:00:00 2001 From: Saxon Date: Tue, 29 Oct 2019 19:02:55 +1030 Subject: [PATCH 01/21] revid: added AVDevice interface --- revid/inputs.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/revid/inputs.go b/revid/inputs.go index c1576019..66706e03 100644 --- a/revid/inputs.go +++ b/revid/inputs.go @@ -53,6 +53,13 @@ const ( ipCamPass = "admin" ) +type AVDevice interface { + io.Reader + Start() error + Stop() error + Set() error +} + // startRaspivid sets up things for input from raspivid i.e. starts // a raspivid process and pipes it's data output. func (r *Revid) startRaspivid() (func() error, error) { From 924858c1c0527d3409f638063243fa00792a6a4c Mon Sep 17 00:00:00 2001 From: Saxon Date: Fri, 1 Nov 2019 21:45:46 +1030 Subject: [PATCH 02/21] revid: added raspivid.go file to hold Raspivid implementation of AVDevice interface Wrote consts for default values, wrote global errors, wrote multiError type (might move) wrote Set method. --- revid/config.go | 38 ++++++------ revid/config_test.go | 8 +-- revid/inputs.go | 2 +- revid/raspivid.go | 145 +++++++++++++++++++++++++++++++++++++++++++ revid/revid.go | 36 +++++------ revid/revid_test.go | 14 ++--- 6 files changed, 194 insertions(+), 49 deletions(-) create mode 100644 revid/raspivid.go diff --git a/revid/config.go b/revid/config.go index 7be75475..619e8be6 100644 --- a/revid/config.go +++ b/revid/config.go @@ -82,19 +82,19 @@ const ( NothingDefined = iota // Input/Output. - File - - // Inputs. - Raspivid - V4L - RTSP - Audio + InputFile + InputRaspivid + InputV4L + InputRTSP + InputAudio // Outputs. - RTMP - RTP - HTTP - MPEGTS + OutputAudio + OutputRTMP + OutputRTP + OutputHTTP + OutputMPEGTS + OutputFile // Codecs. H264 @@ -105,8 +105,8 @@ const ( // Default config settings const ( // General revid defaults. - defaultInput = Raspivid - defaultOutput = HTTP + defaultInput = InputRaspivid + defaultOutput = OutputHTTP defaultFrameRate = 25 defaultWriteRate = 25 defaultTimeout = 0 @@ -327,7 +327,7 @@ func (c *Config) Validate() error { } switch c.Input { - case Raspivid, V4L, File, Audio, RTSP: + case InputRaspivid, InputV4L, InputFile, InputAudio, InputRTSP: case NothingDefined: c.Logger.Log(logger.Info, pkg+"no input type defined, defaulting", "input", defaultInput) c.Input = defaultInput @@ -339,7 +339,7 @@ func (c *Config) Validate() error { case codecutil.H264, codecutil.MJPEG, codecutil.PCM, codecutil.ADPCM: default: switch c.Input { - case Audio: + case OutputAudio: c.Logger.Log(logger.Info, pkg+"input is audio but no codec defined, defaulting", "inputCodec", defaultAudioInputCodec) c.InputCodec = defaultAudioInputCodec default: @@ -358,8 +358,8 @@ func (c *Config) Validate() error { var haveRTMPOut bool for i, o := range c.Outputs { switch o { - case File: - case RTMP: + case OutputFile: + case OutputRTMP: haveRTMPOut = true if c.Bitrate == 0 { c.Bitrate = defaultBitrate @@ -367,11 +367,11 @@ func (c *Config) Validate() error { c.Quantization = 0 if c.RTMPURL == "" { c.Logger.Log(logger.Info, pkg+"no RTMP URL: falling back to HTTP") - c.Outputs[i] = HTTP + c.Outputs[i] = OutputHTTP haveRTMPOut = false } fallthrough - case HTTP, RTP: + case OutputHTTP, OutputRTP: if !haveRTMPOut { c.Bitrate = 0 if c.Quantization == 0 { diff --git a/revid/config_test.go b/revid/config_test.go index eb248c93..6f121078 100644 --- a/revid/config_test.go +++ b/revid/config_test.go @@ -41,7 +41,7 @@ func TestValidate(t *testing.T) { err error }{ { - in: Config{Outputs: []uint8{HTTP}, Logger: &dumbLogger{}}, + in: Config{Outputs: []uint8{OutputHTTP}, Logger: &dumbLogger{}}, check: func(c Config) error { if c.Bitrate != 0 { return fmt.Errorf("did not get expected bitrate. Got: %v, Want: %v", c.Bitrate, 0) @@ -53,7 +53,7 @@ func TestValidate(t *testing.T) { }, }, { - in: Config{Outputs: []uint8{RTMP}, RTMPURL: "dummURL", Logger: &dumbLogger{}}, + in: Config{Outputs: []uint8{OutputRTMP}, RTMPURL: "dummURL", Logger: &dumbLogger{}}, check: func(c Config) error { if c.Bitrate != defaultBitrate { return fmt.Errorf("did not get expected bitrate. Got: %v, Want: %v", c.Bitrate, defaultBitrate) @@ -65,12 +65,12 @@ func TestValidate(t *testing.T) { }, }, { - in: Config{Outputs: []uint8{HTTP}, Quantization: 50, Logger: &dumbLogger{}}, + in: Config{Outputs: []uint8{OutputHTTP}, Quantization: 50, Logger: &dumbLogger{}}, check: func(c Config) error { return nil }, err: errInvalidQuantization, }, { - in: Config{Outputs: []uint8{HTTP}, Quantization: 20, Logger: &dumbLogger{}}, + in: Config{Outputs: []uint8{OutputHTTP}, Quantization: 20, Logger: &dumbLogger{}}, check: func(c Config) error { if c.Bitrate != 0 { return fmt.Errorf("did not get expected bitrate. Got: %v, Want: %v", c.Bitrate, 0) diff --git a/revid/inputs.go b/revid/inputs.go index 66706e03..775cf8ce 100644 --- a/revid/inputs.go +++ b/revid/inputs.go @@ -55,9 +55,9 @@ const ( type AVDevice interface { io.Reader + Set(c Config) error Start() error Stop() error - Set() error } // startRaspivid sets up things for input from raspivid i.e. starts diff --git a/revid/raspivid.go b/revid/raspivid.go new file mode 100644 index 00000000..f467b727 --- /dev/null +++ b/revid/raspivid.go @@ -0,0 +1,145 @@ +/* +DESCRIPTION + raspivid.go provides an implementation of the AVDevice interface for raspivid. + +AUTHORS + Saxon A. Nelson-Milton + +LICENSE + Copyright (C) 2017-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 revid + +import ( + "errors" + "fmt" + + "bitbucket.org/ausocean/av/codec/codecutil" +) + +// Raspivid AVDevice configuration defaults. +const ( + raspividDefaultCodec = codecutil.H264 + raspividDefaultRotation = 0 + raspividDefaultWidth = 1280 + raspividDefaultHeight = 720 + raspividDefaultBrightness = 50 + raspividDefaultSaturation = 0 + raspividDefaultExposure = "auto" + raspividDefaultAutoWhiteBalance = "auto" + raspividDefaultMinFrames = 100 + raspividDefaultQuantization = 30 + raspividDefaultBitrate = 48000 + raspividDefaultFramerate = 25 +) + +type Raspivid struct { + c Config +} + +var ( + errBadCodec = errors.New("codec bad or unset, defaulting") + errBadRotation = errors.New("rotation bad or unset, defaulting") + errBadWidth = errors.New("width bad or unset, defaulting") + errBadHeight = errors.New("height bad or unset, defaulting") + errBadFrameRate = errors.New("framerate bad or unset, defaulting") + errBadBitrate = errors.New("bitrate bad or unset, defaulting") + errBadMinFrames = errors.New("min frames bad or unset, defaulting") + errBadSaturation = errors.New("saturation bad or unset, defaulting") + errBadBrightness = errors.New("brightness bad or unset, defaulting") + errBadExposure = errors.New("exposure bad or unset, defaulting") + errBadAutoWhiteBalance = errors.New("auto white balance bad or unset, defaulting") + errBadQuantization = errors.New("quantization bad or unset, defaulting") +) + +type multiError []error + +func (me multiError) Error() string { + return fmt.Sprintf("%v", me) +} + +func (r *Raspivid) Set(c Config) error { + var errs []error + switch c.InputCodec { + case codecutil.H264, codecutil.MJPEG: + default: + c.InputCodec = raspividDefaultCodec + errs = append(errs, errBadCodec) + } + + if c.Rotation > 359 { + c.Rotation = raspividDefaultRotation + errs = append(errs, errBadRotation) + } + + if c.Width == 0 { + c.Width = raspividDefaultWidth + errs = append(errs, errBadWidth) + } + + if c.Height == 0 { + c.Height = raspividDefaultHeight + errs = append(errs, errBadHeight) + } + + if c.FrameRate == 0 { + c.FrameRate = raspividDefaultFramerate + errs = append(errs, errBadFrameRate) + } + + if c.VBR { + c.Bitrate = 0 + if c.Quantization < 10 || c.Quantization > 40 { + errs = append(errs, errBadQuantization) + c.Quantization = raspividDefaultQuantization + } + } else { + c.Quantization = 0 + if c.Bitrate <= 0 { + errs = append(errs, errBadBitrate) + c.Bitrate = raspividDefaultBitrate + } + } + + if c.MinFrames <= 0 { + errs = append(errs, errBadMinFrames) + c.MinFrames = raspividDefaultMinFrames + } + + if c.Brightness <= 0 || c.Brightness > 100 { + errs = append(errs, errBadBrightness) + c.Brightness = raspividDefaultBrightness + } + + if c.Saturation < -100 || c.Saturation > 100 { + errs = append(errs, errBadSaturation) + c.Saturation = raspividDefaultSaturation + } + + if c.Exposure == "" || !stringInSlice(c.Exposure, ExposureModes[:]) { + errs = append(errs, errBadExposure) + c.Exposure = raspividDefaultExposure + } + + if c.AutoWhiteBalance == "" || !stringInSlice(c.AutoWhiteBalance, AutoWhiteBalanceModes[:]) { + errs = append(errs, errBadAutoWhiteBalance) + c.AutoWhiteBalance = raspividDefaultAutoWhiteBalance + } + + r.c = c + return multiError(errs) +} diff --git a/revid/revid.go b/revid/revid.go index 0fbf2fe6..fd6239d3 100644 --- a/revid/revid.go +++ b/revid/revid.go @@ -168,7 +168,7 @@ func (r *Revid) reset(config Config) error { var encOptions []func(*mts.Encoder) error switch r.config.Input { - case Raspivid: + case InputRaspivid: switch r.config.InputCodec { case codecutil.H264: st = mts.EncodeH264 @@ -178,9 +178,9 @@ func (r *Revid) reset(config Config) error { default: panic("unknown input codec for raspivid input") } - case File, V4L: + case InputFile, InputV4L: st = mts.EncodeH264 - case RTSP: + case InputRTSP: switch r.config.InputCodec { case codecutil.H265: st = mts.EncodeH265 @@ -192,7 +192,7 @@ func (r *Revid) reset(config Config) error { default: panic("unknown input codec for RTSP input") } - case Audio: + case InputAudio: st = mts.EncodeAudio default: panic("unknown input type") @@ -246,7 +246,7 @@ func (r *Revid) setupPipeline(mtsEnc func(dst io.WriteCloser, rate float64) (io. var w io.WriteCloser for _, out := range r.config.Outputs { switch out { - case HTTP: + case OutputHTTP: w = newMtsSender( newHttpSender(r.ns, r.config.Logger.Log), r.config.Logger.Log, @@ -254,19 +254,19 @@ func (r *Revid) setupPipeline(mtsEnc func(dst io.WriteCloser, rate float64) (io. r.config.ClipDuration, ) mtsSenders = append(mtsSenders, w) - case RTP: + case OutputRTP: w, err := newRtpSender(r.config.RTPAddress, r.config.Logger.Log, r.config.FrameRate) if err != nil { r.config.Logger.Log(logger.Warning, pkg+"rtp connect error", "error", err.Error()) } mtsSenders = append(mtsSenders, w) - case File: + case OutputFile: w, err := newFileSender(r.config.OutputPath) if err != nil { return err } mtsSenders = append(mtsSenders, w) - case RTMP: + case OutputRTMP: w, err := newRtmpSender( r.config.RTMPURL, rtmpConnectionTimeout, @@ -305,7 +305,7 @@ func (r *Revid) setupPipeline(mtsEnc func(dst io.WriteCloser, rate float64) (io. r.encoders = multiWriter(encoders...) switch r.config.Input { - case Raspivid: + case InputRaspivid: r.setupInput = r.startRaspivid switch r.config.InputCodec { case codecutil.H264: @@ -313,12 +313,12 @@ func (r *Revid) setupPipeline(mtsEnc func(dst io.WriteCloser, rate float64) (io. case codecutil.MJPEG: r.lexTo = mjpeg.Lex } - case V4L: + case InputV4L: r.setupInput = r.startV4L r.lexTo = h264.Lex - case File: + case InputFile: r.setupInput = r.setupInputForFile - case RTSP: + case InputRTSP: r.setupInput = r.startRTSPCamera switch r.config.InputCodec { case codecutil.H264: @@ -328,7 +328,7 @@ func (r *Revid) setupPipeline(mtsEnc func(dst io.WriteCloser, rate float64) (io. case codecutil.MJPEG: panic("not implemented") } - case Audio: + case InputAudio: r.setupInput = r.startAudioDevice r.lexTo = codecutil.NewByteLexer(&r.config.ChunkSize).Lex } @@ -423,7 +423,7 @@ func (r *Revid) Update(vars map[string]string) error { for key, value := range vars { switch key { case "Input": - v, ok := map[string]uint8{"raspivid": Raspivid, "rtsp": RTSP}[strings.ToLower(value)] + v, ok := map[string]uint8{"raspivid": InputRaspivid, "rtsp": InputRTSP}[strings.ToLower(value)] if !ok { r.config.Logger.Log(logger.Warning, pkg+"invalid input var", "value", value) break @@ -463,13 +463,13 @@ func (r *Revid) Update(vars map[string]string) error { for i, output := range outputs { switch output { case "File": - r.config.Outputs[i] = File + r.config.Outputs[i] = OutputFile case "Http": - r.config.Outputs[i] = HTTP + r.config.Outputs[i] = OutputHTTP case "Rtmp": - r.config.Outputs[i] = RTMP + r.config.Outputs[i] = OutputRTMP case "Rtp": - r.config.Outputs[i] = RTP + r.config.Outputs[i] = OutputRTP default: r.config.Logger.Log(logger.Warning, pkg+"invalid output param", "value", value) continue diff --git a/revid/revid_test.go b/revid/revid_test.go index 8ab2e62f..a7e91323 100644 --- a/revid/revid_test.go +++ b/revid/revid_test.go @@ -58,7 +58,7 @@ func TestRaspivid(t *testing.T) { var c Config c.Logger = &logger - c.Input = Raspivid + c.Input = InputRaspivid rv, err := New(c, ns) if err != nil { @@ -148,7 +148,7 @@ func TestResetEncoderSenderSetup(t *testing.T) { encoders []encoder }{ { - outputs: []uint8{HTTP}, + outputs: []uint8{OutputHTTP}, encoders: []encoder{ { encoderType: mtsEncoderStr, @@ -157,7 +157,7 @@ func TestResetEncoderSenderSetup(t *testing.T) { }, }, { - outputs: []uint8{RTMP}, + outputs: []uint8{OutputRTMP}, encoders: []encoder{ { encoderType: flvEncoderStr, @@ -166,7 +166,7 @@ func TestResetEncoderSenderSetup(t *testing.T) { }, }, { - outputs: []uint8{RTP}, + outputs: []uint8{OutputRTP}, encoders: []encoder{ { encoderType: mtsEncoderStr, @@ -175,7 +175,7 @@ func TestResetEncoderSenderSetup(t *testing.T) { }, }, { - outputs: []uint8{HTTP, RTMP}, + outputs: []uint8{OutputHTTP, OutputRTMP}, encoders: []encoder{ { encoderType: mtsEncoderStr, @@ -188,7 +188,7 @@ func TestResetEncoderSenderSetup(t *testing.T) { }, }, { - outputs: []uint8{HTTP, RTP, RTMP}, + outputs: []uint8{OutputHTTP, OutputRTP, OutputRTMP}, encoders: []encoder{ { encoderType: mtsEncoderStr, @@ -201,7 +201,7 @@ func TestResetEncoderSenderSetup(t *testing.T) { }, }, { - outputs: []uint8{RTP, RTMP}, + outputs: []uint8{OutputRTP, OutputRTMP}, encoders: []encoder{ { encoderType: mtsEncoderStr, From b554c2820ab516f65226540e8bd1fc019823cadf Mon Sep 17 00:00:00 2001 From: Saxon Date: Sat, 2 Nov 2019 10:31:40 +1030 Subject: [PATCH 03/21] revid: wrote implementation of Start method for Raspivid implementation --- revid/raspivid.go | 76 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 71 insertions(+), 5 deletions(-) diff --git a/revid/raspivid.go b/revid/raspivid.go index f467b727..27b2c223 100644 --- a/revid/raspivid.go +++ b/revid/raspivid.go @@ -27,8 +27,12 @@ package revid import ( "errors" "fmt" + "io" + "os/exec" + "strings" "bitbucket.org/ausocean/av/codec/codecutil" + "bitbucket.org/ausocean/utils/logger" ) // Raspivid AVDevice configuration defaults. @@ -47,10 +51,6 @@ const ( raspividDefaultFramerate = 25 ) -type Raspivid struct { - c Config -} - var ( errBadCodec = errors.New("codec bad or unset, defaulting") errBadRotation = errors.New("rotation bad or unset, defaulting") @@ -66,6 +66,12 @@ var ( errBadQuantization = errors.New("quantization bad or unset, defaulting") ) +type Raspivid struct { + cfg Config + cmd *exec.Cmd + out io.ReadCloser +} + type multiError []error func (me multiError) Error() string { @@ -140,6 +146,66 @@ func (r *Raspivid) Set(c Config) error { c.AutoWhiteBalance = raspividDefaultAutoWhiteBalance } - r.c = c + r.cfg = c return multiError(errs) } + +func (r *Raspivid) Start() error { + const disabled = "0" + args := []string{ + "--output", "-", + "--nopreview", + "--timeout", disabled, + "--width", fmt.Sprint(r.cfg.Width), + "--height", fmt.Sprint(r.cfg.Height), + "--bitrate", fmt.Sprint(r.cfg.Bitrate * 1000), // Convert from kbps to bps. + "--framerate", fmt.Sprint(r.cfg.FrameRate), + "--rotation", fmt.Sprint(r.cfg.Rotation), + "--brightness", fmt.Sprint(r.cfg.Brightness), + "--saturation", fmt.Sprint(r.cfg.Saturation), + "--exposure", fmt.Sprint(r.cfg.Exposure), + "--awb", fmt.Sprint(r.cfg.AutoWhiteBalance), + } + + if r.cfg.FlipHorizontal { + args = append(args, "--hflip") + } + + if r.cfg.FlipVertical { + args = append(args, "--vflip") + } + if r.cfg.FlipHorizontal { + args = append(args, "--hflip") + } + + switch r.cfg.InputCodec { + default: + return fmt.Errorf("revid: invalid input codec: %v", r.cfg.InputCodec) + case codecutil.H264: + args = append(args, + "--codec", "H264", + "--inline", + "--intra", fmt.Sprint(r.cfg.MinFrames), + ) + if r.cfg.VBR { + args = append(args, "-qp", fmt.Sprint(r.cfg.Quantization)) + } + case codecutil.MJPEG: + args = append(args, "--codec", "MJPEG") + } + r.cfg.Logger.Log(logger.Info, pkg+"raspivid args", "raspividArgs", strings.Join(args, " ")) + r.cmd = exec.Command("raspivid", args...) + + var err error + r.out, err = r.cmd.StdoutPipe() + if err != nil { + return fmt.Errorf("could not pipe command output: %w", err) + } + + err = r.cmd.Start() + if err != nil { + return fmt.Errorf("could not start raspivid command: %w", err) + } + + return nil +} From b2a9dbf17d2dd17ca99c60c603728616449b6425 Mon Sep 17 00:00:00 2001 From: Saxon Date: Sat, 2 Nov 2019 10:36:55 +1030 Subject: [PATCH 04/21] revid/raspivid.go: wrote Read implementatino for io.Reader interface --- revid/raspivid.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/revid/raspivid.go b/revid/raspivid.go index 27b2c223..f5ff0e75 100644 --- a/revid/raspivid.go +++ b/revid/raspivid.go @@ -209,3 +209,7 @@ func (r *Raspivid) Start() error { return nil } + +func (r *Raspivid) Read(p []byte) (int, error) { + return r.out.Read(p) +} From 82903681eeb4260ce5ec4749654887ddab5cf24f Mon Sep 17 00:00:00 2001 From: Saxon Date: Sat, 2 Nov 2019 10:42:56 +1030 Subject: [PATCH 05/21] revid/raspivid.go: wrote Stop implementation for Raspivid AVDevice implementation --- revid/raspivid.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/revid/raspivid.go b/revid/raspivid.go index f5ff0e75..d28f521f 100644 --- a/revid/raspivid.go +++ b/revid/raspivid.go @@ -213,3 +213,14 @@ func (r *Raspivid) Start() error { func (r *Raspivid) Read(p []byte) (int, error) { return r.out.Read(p) } + +func (r *Raspivid) Stop() error { + if r.cmd == nil || r.cmd.Process == nil { + return errors.New("raspivid process was never started") + } + err := r.cmd.Process.Kill() + if err != nil { + return fmt.Errorf("could not kill raspivid process: %w", err) + } + return nil +} From d5aa968a27075e3d1a324be260e02f4acdf1e9af Mon Sep 17 00:00:00 2001 From: Saxon Date: Sat, 2 Nov 2019 10:54:50 +1030 Subject: [PATCH 06/21] revid/geovision.go: created file geovision.go to hold geovision implementation of AVDevice and wrote Set function --- revid/geovision.go | 62 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 revid/geovision.go diff --git a/revid/geovision.go b/revid/geovision.go new file mode 100644 index 00000000..bc24c478 --- /dev/null +++ b/revid/geovision.go @@ -0,0 +1,62 @@ +/* +DESCRIPTION + geovision.go + +AUTHORS + Saxon A. Nelson-Milton + +LICENSE + Copyright (C) 2017-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 revid + +import () + +type GeoVision struct {} + +func (g *GeoVision) Set(c Config) error { + err := gvctrl.Set( + r.config.CameraIP, + gvctrl.Channel(r.config.CameraChan), + gvctrl.CodecOut( + map[uint8]gvctrl.Codec{ + codecutil.H264: gvctrl.CodecH264, + codecutil.H265: gvctrl.CodecH265, + codecutil.MJPEG: gvctrl.CodecMJPEG, + }[r.config.InputCodec], + ), + gvctrl.Height(int(r.config.Height)), + gvctrl.FrameRate(int(r.config.FrameRate)), + gvctrl.VariableBitrate(r.config.VBR), + gvctrl.VBRQuality( + map[quality]gvctrl.Quality{ + qualityStandard: gvctrl.QualityStandard, + qualityFair: gvctrl.QualityFair, + qualityGood: gvctrl.QualityGood, + qualityGreat: gvctrl.QualityGreat, + qualityExcellent: gvctrl.QualityExcellent, + }[r.config.VBRQuality], + ), + gvctrl.VBRBitrate(r.config.VBRBitrate), + gvctrl.CBRBitrate(int(r.config.Bitrate)), + gvctrl.Refresh(float64(r.config.MinFrames)/float64(r.config.FrameRate)), + ) + if err != nil { + return nil, fmt.Errorf("could not set IPCamera settings: %w", err) + } + return nil +} From bea4d46c72c2e630e866852bb3c4735a32840e79 Mon Sep 17 00:00:00 2001 From: Saxon Date: Mon, 4 Nov 2019 15:47:18 +1030 Subject: [PATCH 07/21] revid/geovision.go: added geovision.go file and started geovision implementation of AVDevice interface --- revid/geovision.go | 111 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 96 insertions(+), 15 deletions(-) diff --git a/revid/geovision.go b/revid/geovision.go index bc24c478..f801dc50 100644 --- a/revid/geovision.go +++ b/revid/geovision.go @@ -24,24 +24,105 @@ LICENSE package revid -import () +import ( + "errors" + "fmt" -type GeoVision struct {} + "bitbucket.org/ausocean/av/codec/codecutil" + "bitbucket.org/ausocean/av/input/gvctrl" +) + +const ( + gvDefaultCameraIP = "192.168.1.50" + gvDefaultCodec = codecutil.H264 + gvDefaultHeight = 720 + gvDefaultFrameRate = 25 + gvDefaultBitrate = 400 + gvDefaultVBRBitrate = 400 + gvDefaultMinFrames = 100 + gvDefaultVBRQuality = "standard" +) + +var ( + errGVBadCameraIP = errors.New("camera IP bad or unset, defaulting") + errGVBadCodec = errors.New("codec bad or unset, defaulting") + errGVBadFrameRate = errors.New("frame rate bad or unset, defaulting") + errGVBadBitrate = errors.New("bitrate bad or unset, defaulting") + errGVBadVBRQuality = errors.New("VBR quality bad or unset, defaulting") + errGVBadHeight = errors.New("height bad or unset, defaulting") + errGVBadMinFrames = errors.New("min frames bad or unset, defaulting") +) + +type GeoVision struct { + cfg Config +} func (g *GeoVision) Set(c Config) error { - err := gvctrl.Set( - r.config.CameraIP, - gvctrl.Channel(r.config.CameraChan), + var errs multiError + if c.CameraIP == "" { + errs = append(errs, errGVBadCameraIP) + c.CameraIP = gvDefaultCameraIP + } + + switch c.InputCodec { + case codecutil.H264, codecutil.H265, codecutil.MJPEG: + default: + errs = append(errs, errGVBadCodec) + c.InputCodec = gvDefaultCodec + } + + if c.Height <= 0 { + errs = append(errs, errGVBadHeight) + c.Height = gvDefaultHeight + } + + if c.FrameRate <= 0 { + errs = append(errs, errGVBadFrameRate) + c.FrameRate = gvDefaultFrameRate + } + + if c.Bitrate <= 0 { + errs = append(errs, errGVBadBitrate) + c.Bitrate = gvDefaultBitrate + } + + if c.MinFrames <= 0 { + errs = append(errs, errGVBadMinFrames) + c.MinFrames = gvDefaultMinFrames + } + + switch c.VBRQuality { + case qualityStandard, qualityFair, qualityGood, qualityGreat, qualityExcellent: + default: + errs = append(errs, errGVBadVBRQuality) + c.VBRQuality = defaultVBRQuality + } + + if c.VBRBitrate <= 0 { + errs = append(errs, errGVBadVBRQuality) + c.VBRBitrate = defaultVBRBitrate + } + + if c.CameraChan != 1 && c.CameraChan != 2 { + errs = append(errs, errGVBadVBRQuality) + c.CameraChan = defaultCameraChan + } + + g.cfg = c + + err := gvctrl.Set( + g.cfg.CameraIP, + gvctrl.Channel(g.cfg.CameraChan), gvctrl.CodecOut( map[uint8]gvctrl.Codec{ codecutil.H264: gvctrl.CodecH264, codecutil.H265: gvctrl.CodecH265, codecutil.MJPEG: gvctrl.CodecMJPEG, - }[r.config.InputCodec], + }[g.cfg.InputCodec], ), - gvctrl.Height(int(r.config.Height)), - gvctrl.FrameRate(int(r.config.FrameRate)), - gvctrl.VariableBitrate(r.config.VBR), + gvctrl.Height(int(g.cfg.Height)), + gvctrl.FrameRate(int(g.cfg.FrameRate)), + gvctrl.VariableBitrate(g.cfg.VBR), gvctrl.VBRQuality( map[quality]gvctrl.Quality{ qualityStandard: gvctrl.QualityStandard, @@ -49,14 +130,14 @@ func (g *GeoVision) Set(c Config) error { qualityGood: gvctrl.QualityGood, qualityGreat: gvctrl.QualityGreat, qualityExcellent: gvctrl.QualityExcellent, - }[r.config.VBRQuality], + }[g.cfg.VBRQuality], ), - gvctrl.VBRBitrate(r.config.VBRBitrate), - gvctrl.CBRBitrate(int(r.config.Bitrate)), - gvctrl.Refresh(float64(r.config.MinFrames)/float64(r.config.FrameRate)), + gvctrl.VBRBitrate(g.cfg.VBRBitrate), + gvctrl.CBRBitrate(int(g.cfg.Bitrate)), + gvctrl.Refresh(float64(g.cfg.MinFrames)/float64(g.cfg.FrameRate)), ) if err != nil { - return nil, fmt.Errorf("could not set IPCamera settings: %w", err) + return fmt.Errorf("could not set IPCamera settings: %w", err) } - return nil + return multiError(errs) } From 1497f4a57533d7c2ec9d2fa618f204dd67928031 Mon Sep 17 00:00:00 2001 From: Saxon Date: Mon, 4 Nov 2019 19:12:13 +1030 Subject: [PATCH 08/21] revid/geovision.go: wrote Start method implementation --- revid/geovision.go | 102 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/revid/geovision.go b/revid/geovision.go index f801dc50..de029e7c 100644 --- a/revid/geovision.go +++ b/revid/geovision.go @@ -27,9 +27,14 @@ package revid import ( "errors" "fmt" + "time" "bitbucket.org/ausocean/av/codec/codecutil" "bitbucket.org/ausocean/av/input/gvctrl" + "bitbucket.org/ausocean/av/protocol/rtcp" + "bitbucket.org/ausocean/av/protocol/rtp" + "bitbucket.org/ausocean/av/protocol/rtsp" + "bitbucket.org/ausocean/utils/logger" ) const ( @@ -55,8 +60,11 @@ var ( type GeoVision struct { cfg Config + log Logger } +func NewGeovision(l Logger) *GeoVision { return &GeoVision{log: l} } + func (g *GeoVision) Set(c Config) error { var errs multiError if c.CameraIP == "" { @@ -139,5 +147,99 @@ func (g *GeoVision) Set(c Config) error { if err != nil { return fmt.Errorf("could not set IPCamera settings: %w", err) } + + // Give the camera some time to change it's configuration. + const setupDelay = 5 * time.Second + time.Sleep(setupDelay) + return multiError(errs) } + +func (g *GeoVision) Start() error { + rtspClt, local, remote, err := rtsp.NewClient("rtsp://" + ipCamUser + ":" + ipCamPass + "@" + g.cfg.CameraIP + ":8554/" + "CH002.sdp") + if err != nil { + return fmt.Errorf("could not create RTSP client: %w", err) + } + + g.log.Log(logger.Info, pkg+"created RTSP client") + + resp, err := rtspClt.Options() + if err != nil { + return fmt.Errorf("options request unsuccessful: %w", err) + } + g.log.Log(logger.Debug, pkg+"RTSP OPTIONS response", "response", resp.String()) + + resp, err = rtspClt.Describe() + if err != nil { + return fmt.Errorf("describe request unsuccessful: %w", err) + } + g.log.Log(logger.Debug, pkg+"RTSP DESCRIBE response", "response", resp.String()) + + resp, err = rtspClt.Setup("track1", fmt.Sprintf("RTP/AVP;unicast;client_port=%d-%d", rtpPort, rtcpPort)) + if err != nil { + return fmt.Errorf("setup request unsuccessful: %w", err) + } + g.log.Log(logger.Debug, pkg+"RTSP SETUP response", "response", resp.String()) + + rtpCltAddr, rtcpCltAddr, rtcpSvrAddr, err := formAddrs(local, remote, *resp) + if err != nil { + return fmt.Errorf("could not format addresses: %w", err) + } + + g.log.Log(logger.Info, pkg+"RTSP session setup complete") + + rtpClt, err := rtp.NewClient(rtpCltAddr) + if err != nil { + return fmt.Errorf("could not create RTP client: %w", err) + } + + rtcpClt, err := rtcp.NewClient(rtcpCltAddr, rtcpSvrAddr, rtpClt, g.log.Log) + if err != nil { + return fmt.Errorf("could not create RTCP client: %w", err) + } + + g.log.Log(logger.Info, pkg+"RTCP and RTP clients created") + + // Check errors from RTCP client until it has stopped running. + go func() { + for { + err, ok := <-rtcpClt.Err() + if ok { + g.log.Log(logger.Warning, pkg+"RTCP error", "error", err.Error()) + } else { + return + } + } + }() + + // Start the RTCP client. + rtcpClt.Start() + + g.log.Log(logger.Info, pkg+"RTCP client started") + + g.log.Log(logger.Info, pkg+"started input processor") + + resp, err = rtspClt.Play() + if err != nil { + return fmt.Errorf("play request unsuccessful: %w", err) + } + g.log.Log(logger.Debug, pkg+"RTSP server PLAY response", "response", resp.String()) + g.log.Log(logger.Info, pkg+"play requested, now receiving stream") + + return nil +} + +/* +err := rtpClt.Close() +if err != nil { + return fmt.Errorf("could not close RTP client: %w", err) +} + +err = rtspClt.Close() +if err != nil { + return fmt.Errorf("could not close RTSP client: %w", err) +} + +rtcpClt.Stop() + +g.log.Log(logger.Info, pkg+"RTP, RTSP and RTCP clients stopped and closed")*/ From ce8dc9a4b3e843c9f4e3869e917fc623fcc641d6 Mon Sep 17 00:00:00 2001 From: Saxon Date: Mon, 4 Nov 2019 19:27:05 +1030 Subject: [PATCH 09/21] revid/geovision.go: wrote Stop method implementation --- revid/geovision.go | 68 ++++++++++++++++++++++++++-------------------- revid/raspivid.go | 2 +- 2 files changed, 40 insertions(+), 30 deletions(-) diff --git a/revid/geovision.go b/revid/geovision.go index de029e7c..708d8e51 100644 --- a/revid/geovision.go +++ b/revid/geovision.go @@ -1,12 +1,13 @@ /* DESCRIPTION - geovision.go + geovision.go provides an implementation of the AVDevice interface for the + GeoVision IP camera. AUTHORS Saxon A. Nelson-Milton LICENSE - Copyright (C) 2017-2019 the Australian Ocean Lab (AusOcean) + 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 @@ -27,6 +28,7 @@ package revid import ( "errors" "fmt" + "net" "time" "bitbucket.org/ausocean/av/codec/codecutil" @@ -59,8 +61,11 @@ var ( ) type GeoVision struct { - cfg Config - log Logger + cfg Config + log Logger + rtpClt *rtp.Client + rtspClt *rtsp.Client + rtcpClt *rtcp.Client } func NewGeovision(l Logger) *GeoVision { return &GeoVision{log: l} } @@ -156,26 +161,31 @@ func (g *GeoVision) Set(c Config) error { } func (g *GeoVision) Start() error { - rtspClt, local, remote, err := rtsp.NewClient("rtsp://" + ipCamUser + ":" + ipCamPass + "@" + g.cfg.CameraIP + ":8554/" + "CH002.sdp") + var ( + local, remote *net.TCPAddr + err error + ) + + g.rtspClt, local, remote, err = rtsp.NewClient("rtsp://" + ipCamUser + ":" + ipCamPass + "@" + g.cfg.CameraIP + ":8554/" + "CH002.sdp") if err != nil { return fmt.Errorf("could not create RTSP client: %w", err) } g.log.Log(logger.Info, pkg+"created RTSP client") - resp, err := rtspClt.Options() + resp, err := g.rtspClt.Options() if err != nil { return fmt.Errorf("options request unsuccessful: %w", err) } g.log.Log(logger.Debug, pkg+"RTSP OPTIONS response", "response", resp.String()) - resp, err = rtspClt.Describe() + resp, err = g.rtspClt.Describe() if err != nil { return fmt.Errorf("describe request unsuccessful: %w", err) } g.log.Log(logger.Debug, pkg+"RTSP DESCRIBE response", "response", resp.String()) - resp, err = rtspClt.Setup("track1", fmt.Sprintf("RTP/AVP;unicast;client_port=%d-%d", rtpPort, rtcpPort)) + resp, err = g.rtspClt.Setup("track1", fmt.Sprintf("RTP/AVP;unicast;client_port=%d-%d", rtpPort, rtcpPort)) if err != nil { return fmt.Errorf("setup request unsuccessful: %w", err) } @@ -188,12 +198,12 @@ func (g *GeoVision) Start() error { g.log.Log(logger.Info, pkg+"RTSP session setup complete") - rtpClt, err := rtp.NewClient(rtpCltAddr) + g.rtpClt, err = rtp.NewClient(rtpCltAddr) if err != nil { return fmt.Errorf("could not create RTP client: %w", err) } - rtcpClt, err := rtcp.NewClient(rtcpCltAddr, rtcpSvrAddr, rtpClt, g.log.Log) + g.rtcpClt, err = rtcp.NewClient(rtcpCltAddr, rtcpSvrAddr, g.rtpClt, g.log.Log) if err != nil { return fmt.Errorf("could not create RTCP client: %w", err) } @@ -203,7 +213,7 @@ func (g *GeoVision) Start() error { // Check errors from RTCP client until it has stopped running. go func() { for { - err, ok := <-rtcpClt.Err() + err, ok := <-g.rtcpClt.Err() if ok { g.log.Log(logger.Warning, pkg+"RTCP error", "error", err.Error()) } else { @@ -213,13 +223,10 @@ func (g *GeoVision) Start() error { }() // Start the RTCP client. - rtcpClt.Start() - + g.rtcpClt.Start() g.log.Log(logger.Info, pkg+"RTCP client started") - g.log.Log(logger.Info, pkg+"started input processor") - - resp, err = rtspClt.Play() + resp, err = g.rtspClt.Play() if err != nil { return fmt.Errorf("play request unsuccessful: %w", err) } @@ -229,17 +236,20 @@ func (g *GeoVision) Start() error { return nil } -/* -err := rtpClt.Close() -if err != nil { - return fmt.Errorf("could not close RTP client: %w", err) +func (g *GeoVision) Stop() error { + err := g.rtpClt.Close() + if err != nil { + return fmt.Errorf("could not close RTP client: %w", err) + } + + err = g.rtspClt.Close() + if err != nil { + return fmt.Errorf("could not close RTSP client: %w", err) + } + + g.rtcpClt.Stop() + + g.log.Log(logger.Info, pkg+"RTP, RTSP and RTCP clients stopped and closed") + + return nil } - -err = rtspClt.Close() -if err != nil { - return fmt.Errorf("could not close RTSP client: %w", err) -} - -rtcpClt.Stop() - -g.log.Log(logger.Info, pkg+"RTP, RTSP and RTCP clients stopped and closed")*/ diff --git a/revid/raspivid.go b/revid/raspivid.go index d28f521f..8e57202e 100644 --- a/revid/raspivid.go +++ b/revid/raspivid.go @@ -6,7 +6,7 @@ AUTHORS Saxon A. Nelson-Milton LICENSE - Copyright (C) 2017-2019 the Australian Ocean Lab (AusOcean) + 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 From 5afad9c5aa547b83445c36262053f29b00180125 Mon Sep 17 00:00:00 2001 From: Saxon Date: Mon, 4 Nov 2019 19:43:02 +1030 Subject: [PATCH 10/21] revid/geovision.go: wrote Read method implementation --- revid/geovision.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/revid/geovision.go b/revid/geovision.go index 708d8e51..f0582316 100644 --- a/revid/geovision.go +++ b/revid/geovision.go @@ -253,3 +253,7 @@ func (g *GeoVision) Stop() error { return nil } + +func (g *GeoVision) Read(p []byte) (int, error) { + return g.rtpClt.Read(p) +} From 8302e959d9bb316042e2d73de7cdd5bcd8ff9ccb Mon Sep 17 00:00:00 2001 From: Saxon Date: Tue, 5 Nov 2019 10:57:12 +1030 Subject: [PATCH 11/21] revid/webcam.go: started writing implementation of AVDevice for webcams --- revid/webcam.go | 80 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 revid/webcam.go diff --git a/revid/webcam.go b/revid/webcam.go new file mode 100644 index 00000000..99704783 --- /dev/null +++ b/revid/webcam.go @@ -0,0 +1,80 @@ +/* +DESCRIPTION + webcam.go provides an implementation of AVDevice for webcams. + +AUTHORS + Saxon A. Nelson-Milton + +LICENSE + Copyright (C) 2019 the Australian Ocean Lab (AusOcean) + + It is free software: you can redistribute it and/or modify them + under the terms of the GNU General Public License as published by the + Free Software Foundation, either version 3 of the License, or (at your + option) any later version. + + It is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + for more details. + + You should have received a copy of the GNU General Public License + in gpl.txt. If not, see http://www.gnu.org/licenses. +*/ + +package revid + +import ( + "errors" + "io" +) + +const ( + defaultWebcamInputPath = "/dev/video0" + defaultWebcamFrameRate = 25 + defaultWebcamBitrate = 400 + defaultWebcamWidth = 1280 + defaultWebcamHeight = 720 +) + +var ( + errWebcamBadFrameRate = errors.New("frame rate bad or unset, defaulting") + errWebcamBadBitrate = errors.New("bitrate bad or unset, defaulting") + errWebcamWidth = errors.New("width bad or unset, defaulting") + errWebcamHeight = errors.New("height bad or unset, defaulting") +) + +type Webcam struct { + out io.ReadCloser + log Logger + cfg Config +} + +func NewWebcam(l Logger) *Webcam { + return &Webcam{log: l} +} + +func (w *Webcam) Set(c Config) error { + var errs []error + if c.Width == 0 { + errs = append(errs, errBadWidth) + c.Width = raspividDefaultWidth + } + + if c.Height == 0 { + errs = append(errs, errBadHeight) + c.Height = raspividDefaultHeight + } + + if c.FrameRate == 0 { + errs = append(errs, errBadFrameRate) + c.FrameRate = raspividDefaultFramerate + } + + if c.Bitrate <= 0 { + errs = append(errs, errBadBitrate) + c.Bitrate = raspividDefaultBitrate + } + w.cfg = c + return multiError(errs) +} From 81d168a277dbfbf78843dde4a55392aa76e442b3 Mon Sep 17 00:00:00 2001 From: Saxon Date: Tue, 5 Nov 2019 11:56:34 +1030 Subject: [PATCH 12/21] revid/webcam.go: wrote implementations of Start, Stop and Read methods --- revid/webcam.go | 54 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/revid/webcam.go b/revid/webcam.go index 99704783..b3ccb1c5 100644 --- a/revid/webcam.go +++ b/revid/webcam.go @@ -26,7 +26,12 @@ package revid import ( "errors" + "fmt" "io" + "os/exec" + "strings" + + "bitbucket.org/ausocean/utils/logger" ) const ( @@ -48,6 +53,7 @@ type Webcam struct { out io.ReadCloser log Logger cfg Config + cmd *exec.Cmd } func NewWebcam(l Logger) *Webcam { @@ -78,3 +84,51 @@ func (w *Webcam) Set(c Config) error { w.cfg = c return multiError(errs) } + +func (w *Webcam) Start() error { + args := []string{ + "-i", w.cfg.InputPath, + "-f", "h264", + "-r", fmt.Sprint(w.cfg.FrameRate), + } + + br := w.cfg.Bitrate * 1000 + args = append(args, + "-b:v", fmt.Sprint(br), + "-maxrate", fmt.Sprint(br), + "-bufsize", fmt.Sprint(br/2), + "-s", fmt.Sprintf("%dx%d", w.cfg.Width, w.cfg.Height), + "-", + ) + + w.log.Log(logger.Info, pkg+"ffmpeg args", "args", strings.Join(args, " ")) + w.cmd = exec.Command("ffmpeg", args...) + + var err error + w.out, err = w.cmd.StdoutPipe() + if err != nil { + return fmt.Errorf("failed to create pipe: %w", err) + } + + err = w.cmd.Start() + if err != nil { + return fmt.Errorf("failed to start ffmpeg: %w", err) + } + + return nil +} + +func (w *Webcam) Stop() error { + if w.cmd == nil || w.cmd.Process == nil { + return errors.New("raspivid process was never started") + } + err := w.cmd.Process.Kill() + if err != nil { + return fmt.Errorf("could not kill raspivid process: %w", err) + } + return w.out.Close() +} + +func (w *Webcam) Read(p []byte) (int, error) { + return w.out.Read(p) +} From 50c7fe139bf8d7ca8cc35350fab1037dd34d9c71 Mon Sep 17 00:00:00 2001 From: Saxon Date: Tue, 5 Nov 2019 12:36:27 +1030 Subject: [PATCH 13/21] revid/file.go: wrote AVDevice implementation for file containing audio or video media --- revid/file.go | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 revid/file.go diff --git a/revid/file.go b/revid/file.go new file mode 100644 index 00000000..61eae8d1 --- /dev/null +++ b/revid/file.go @@ -0,0 +1,54 @@ +/* +DESCRIPTION + file.go provides an implementation of the AVDevice interface for media files. + +AUTHORS + Saxon A. Nelson-Milton + +LICENSE + Copyright (C) 2019 the Australian Ocean Lab (AusOcean) + + It is free software: you can redistribute it and/or modify them + under the terms of the GNU General Public License as published by the + Free Software Foundation, either version 3 of the License, or (at your + option) any later version. + + It is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + for more details. + + You should have received a copy of the GNU General Public License + in gpl.txt. If not, see http://www.gnu.org/licenses. +*/ + +package revid + +import ( + "fmt" + "io" + "os" +) + +type AVFile struct { + f io.ReadCloser + cfg Config +} + +func NewAVFile() *AVFile { return &AVFile{} } +func (m *AVFile) Stop() error { return m.f.Close() } +func (m *AVFile) Read(p []byte) (int, error) { return m.f.Read(p) } + +func (m *AVFile) Set(c Config) error { + m.cfg = c + return nil +} + +func (m *AVFile) Start() error { + var err error + m.f, err = os.Open(m.cfg.InputPath) + if err != nil { + return fmt.Errorf("could not open media file: %w", err) + } + return nil +} From a6aef125fd0d0ea135ab7d32b3f05d3bc81f7592 Mon Sep 17 00:00:00 2001 From: Saxon Date: Tue, 5 Nov 2019 20:14:04 +1030 Subject: [PATCH 14/21] revid: cleaned up AVDevice implementations and added documentation to them --- revid/file.go | 23 ++++++++++++-- revid/geovision.go | 50 +++++++++++++++++++++--------- revid/inputs.go | 23 ++++++++++++++ revid/raspivid.go | 77 +++++++++++++++++++++++++++------------------- revid/webcam.go | 25 ++++++++++++--- 5 files changed, 143 insertions(+), 55 deletions(-) diff --git a/revid/file.go b/revid/file.go index 61eae8d1..d19ef69f 100644 --- a/revid/file.go +++ b/revid/file.go @@ -25,25 +25,30 @@ LICENSE package revid import ( + "errors" "fmt" "io" "os" ) +// AVFile is an implementation of the AVDevice interface for a file containg +// audio or video data. type AVFile struct { f io.ReadCloser cfg Config } -func NewAVFile() *AVFile { return &AVFile{} } -func (m *AVFile) Stop() error { return m.f.Close() } -func (m *AVFile) Read(p []byte) (int, error) { return m.f.Read(p) } +// NewAVFile returns a new AVFile. +func NewAVFile() *AVFile { return &AVFile{} } +// Set simply sets the AVFile's config to the passed config. func (m *AVFile) Set(c Config) error { m.cfg = c return nil } +// Start will open the file at the location of the InputPath field of the +// config struct. func (m *AVFile) Start() error { var err error m.f, err = os.Open(m.cfg.InputPath) @@ -52,3 +57,15 @@ func (m *AVFile) Start() error { } return nil } + +// Stop will close the file such that any further reads will fail. +func (m *AVFile) Stop() error { return m.f.Close() } + +// Read implements io.Reader. If start has not been called, or Start has been +// called and Stop has since been called, an error is returned. +func (m *AVFile) Read(p []byte) (int, error) { + if m.f != nil { + return m.f.Read(p) + } + return 0, errors.New("AV file is closed") +} diff --git a/revid/geovision.go b/revid/geovision.go index f0582316..713ebd97 100644 --- a/revid/geovision.go +++ b/revid/geovision.go @@ -39,17 +39,19 @@ import ( "bitbucket.org/ausocean/utils/logger" ) +// Configuration defaults. const ( - gvDefaultCameraIP = "192.168.1.50" - gvDefaultCodec = codecutil.H264 - gvDefaultHeight = 720 - gvDefaultFrameRate = 25 - gvDefaultBitrate = 400 - gvDefaultVBRBitrate = 400 - gvDefaultMinFrames = 100 - gvDefaultVBRQuality = "standard" + defaultGVCameraIP = "192.168.1.50" + defaultGVCodec = codecutil.H264 + defaultGVHeight = 720 + defaultGVFrameRate = 25 + defaultGVBitrate = 400 + defaultGVVBRBitrate = 400 + defaultGVMinFrames = 100 + defaultGVVBRQuality = "standard" ) +// Configuration field errors. var ( errGVBadCameraIP = errors.New("camera IP bad or unset, defaulting") errGVBadCodec = errors.New("codec bad or unset, defaulting") @@ -60,6 +62,9 @@ var ( errGVBadMinFrames = errors.New("min frames bad or unset, defaulting") ) +// GeoVision is an implementation of the AVDevice interface for a GeoVision +// IP camera. This has been designed to implement the GV-BX4700-8F in particular. +// Any other models are untested. type GeoVision struct { cfg Config log Logger @@ -68,40 +73,45 @@ type GeoVision struct { rtcpClt *rtcp.Client } +// NewGeoVision returns a new GeoVision. func NewGeovision(l Logger) *GeoVision { return &GeoVision{log: l} } +// Set will take a Config struct, check the validity of the relevant fields +// and then performs any configuration necessary using gvctrl to control the +// GeoVision web interface. If fields are not valid, an error is added to the +// multiError and a default value is used for that particular field. func (g *GeoVision) Set(c Config) error { var errs multiError if c.CameraIP == "" { errs = append(errs, errGVBadCameraIP) - c.CameraIP = gvDefaultCameraIP + c.CameraIP = defaultGVCameraIP } switch c.InputCodec { case codecutil.H264, codecutil.H265, codecutil.MJPEG: default: errs = append(errs, errGVBadCodec) - c.InputCodec = gvDefaultCodec + c.InputCodec = defaultGVCodec } if c.Height <= 0 { errs = append(errs, errGVBadHeight) - c.Height = gvDefaultHeight + c.Height = defaultGVHeight } if c.FrameRate <= 0 { errs = append(errs, errGVBadFrameRate) - c.FrameRate = gvDefaultFrameRate + c.FrameRate = defaultGVFrameRate } if c.Bitrate <= 0 { errs = append(errs, errGVBadBitrate) - c.Bitrate = gvDefaultBitrate + c.Bitrate = defaultGVBitrate } if c.MinFrames <= 0 { errs = append(errs, errGVBadMinFrames) - c.MinFrames = gvDefaultMinFrames + c.MinFrames = defaultGVMinFrames } switch c.VBRQuality { @@ -160,6 +170,9 @@ func (g *GeoVision) Set(c Config) error { return multiError(errs) } +// Start uses an RTSP client to communicate with the GeoVision RTSP server and +// request a stream that is then received by an RTP client, from which packets +// can be read from using the Read method. func (g *GeoVision) Start() error { var ( local, remote *net.TCPAddr @@ -236,6 +249,8 @@ func (g *GeoVision) Start() error { return nil } +// Stop will close the RTSP, RTCP, and RTP connections and in turn end the +// stream from the GeoVision. Future reads using Read will result in error. func (g *GeoVision) Stop() error { err := g.rtpClt.Close() if err != nil { @@ -254,6 +269,11 @@ func (g *GeoVision) Stop() error { return nil } +// Read implements io.Reader. If the GeoVision has not been started an error is +// returned. func (g *GeoVision) Read(p []byte) (int, error) { - return g.rtpClt.Read(p) + if g.rtpClt != nil { + return g.rtpClt.Read(p) + } + return 0, errors.New("cannot read, GeoVision not streaming") } diff --git a/revid/inputs.go b/revid/inputs.go index 775cf8ce..abe50bba 100644 --- a/revid/inputs.go +++ b/revid/inputs.go @@ -53,13 +53,36 @@ const ( ipCamPass = "admin" ) +// AVDevice describes a configurable audio or video device from which media data +// can be obtained. AVDevice implements io.Reader. type AVDevice interface { io.Reader + + // Set allows for configuration of the AVDevice using a Config struct. All, + // some or none of the fields of the Config struct may be used for configuration + // by an implementation. An implementation should specify what fields are + // considered. Set(c Config) error + + // Start will start the AVDevice capturing media data; after which the Read + // method may be called to obtain the data. The format of the data may differ + // and should be specified by the implementation. Start() error + + // Stop will stop the AVDevice from capturing media data. From this point + // Reads will no longer be successful. Stop() error } +// multiError implements the built in error interface. multiError is used here +// to collect multi errors during validation of configruation parameters for o +// AVDevices. +type multiError []error + +func (me multiError) Error() string { + return fmt.Sprintf("%v", me) +} + // startRaspivid sets up things for input from raspivid i.e. starts // a raspivid process and pipes it's data output. func (r *Revid) startRaspivid() (func() error, error) { diff --git a/revid/raspivid.go b/revid/raspivid.go index 8e57202e..43f94cf3 100644 --- a/revid/raspivid.go +++ b/revid/raspivid.go @@ -35,22 +35,23 @@ import ( "bitbucket.org/ausocean/utils/logger" ) -// Raspivid AVDevice configuration defaults. +// Raspivid configuration defaults. const ( - raspividDefaultCodec = codecutil.H264 - raspividDefaultRotation = 0 - raspividDefaultWidth = 1280 - raspividDefaultHeight = 720 - raspividDefaultBrightness = 50 - raspividDefaultSaturation = 0 - raspividDefaultExposure = "auto" - raspividDefaultAutoWhiteBalance = "auto" - raspividDefaultMinFrames = 100 - raspividDefaultQuantization = 30 - raspividDefaultBitrate = 48000 - raspividDefaultFramerate = 25 + defaultRaspividCodec = codecutil.H264 + defaultRaspividRotation = 0 + defaultRaspividWidth = 1280 + defaultRaspividHeight = 720 + defaultRaspividBrightness = 50 + defaultRaspividSaturation = 0 + defaultRaspividExposure = "auto" + defaultRaspividAutoWhiteBalance = "auto" + defaultRaspividMinFrames = 100 + defaultRaspividQuantization = 30 + defaultRaspividBitrate = 48000 + defaultRaspividFramerate = 25 ) +// Configuration errors. var ( errBadCodec = errors.New("codec bad or unset, defaulting") errBadRotation = errors.New("rotation bad or unset, defaulting") @@ -66,44 +67,47 @@ var ( errBadQuantization = errors.New("quantization bad or unset, defaulting") ) +// Raspivid is an implementation of AVDevice that provides control over the +// raspivid command to allow reading of data from a Raspberry Pi camera. type Raspivid struct { cfg Config cmd *exec.Cmd out io.ReadCloser + log Logger } -type multiError []error - -func (me multiError) Error() string { - return fmt.Sprintf("%v", me) -} +// NewRaspivid returns a new Raspivid. +func NewRaspivid(l Logger) *Raspivid { return &Raspivid{log: l} } +// Set will take a Config struct, check the validity of the relevant fields +// and then performs any configuration necessary. If fields are not valid, +// an error is added to the multiError and a default value is used. func (r *Raspivid) Set(c Config) error { var errs []error switch c.InputCodec { case codecutil.H264, codecutil.MJPEG: default: - c.InputCodec = raspividDefaultCodec + c.InputCodec = defaultRaspividCodec errs = append(errs, errBadCodec) } if c.Rotation > 359 { - c.Rotation = raspividDefaultRotation + c.Rotation = defaultRaspividRotation errs = append(errs, errBadRotation) } if c.Width == 0 { - c.Width = raspividDefaultWidth + c.Width = defaultRaspividWidth errs = append(errs, errBadWidth) } if c.Height == 0 { - c.Height = raspividDefaultHeight + c.Height = defaultRaspividHeight errs = append(errs, errBadHeight) } if c.FrameRate == 0 { - c.FrameRate = raspividDefaultFramerate + c.FrameRate = defaultRaspividFramerate errs = append(errs, errBadFrameRate) } @@ -111,45 +115,48 @@ func (r *Raspivid) Set(c Config) error { c.Bitrate = 0 if c.Quantization < 10 || c.Quantization > 40 { errs = append(errs, errBadQuantization) - c.Quantization = raspividDefaultQuantization + c.Quantization = defaultRaspividQuantization } } else { c.Quantization = 0 if c.Bitrate <= 0 { errs = append(errs, errBadBitrate) - c.Bitrate = raspividDefaultBitrate + c.Bitrate = defaultRaspividBitrate } } if c.MinFrames <= 0 { errs = append(errs, errBadMinFrames) - c.MinFrames = raspividDefaultMinFrames + c.MinFrames = defaultRaspividMinFrames } if c.Brightness <= 0 || c.Brightness > 100 { errs = append(errs, errBadBrightness) - c.Brightness = raspividDefaultBrightness + c.Brightness = defaultRaspividBrightness } if c.Saturation < -100 || c.Saturation > 100 { errs = append(errs, errBadSaturation) - c.Saturation = raspividDefaultSaturation + c.Saturation = defaultRaspividSaturation } if c.Exposure == "" || !stringInSlice(c.Exposure, ExposureModes[:]) { errs = append(errs, errBadExposure) - c.Exposure = raspividDefaultExposure + c.Exposure = defaultRaspividExposure } if c.AutoWhiteBalance == "" || !stringInSlice(c.AutoWhiteBalance, AutoWhiteBalanceModes[:]) { errs = append(errs, errBadAutoWhiteBalance) - c.AutoWhiteBalance = raspividDefaultAutoWhiteBalance + c.AutoWhiteBalance = defaultRaspividAutoWhiteBalance } r.cfg = c return multiError(errs) } +// Start will prepare the arguments for the raspivid command using the +// configuration set using the Set method then call the raspivid command, +// piping the video output from which the Read method will read from. func (r *Raspivid) Start() error { const disabled = "0" args := []string{ @@ -210,10 +217,16 @@ func (r *Raspivid) Start() error { return nil } +// Read implements io.Reader. Calling read before Start has been called will +// result in 0 bytes read and an error. func (r *Raspivid) Read(p []byte) (int, error) { - return r.out.Read(p) + if r.out != nil { + return r.out.Read(p) + } + return 0, errors.New("cannot read, raspivid has not started") } +// Stop will terminate the raspivid process and close the output pipe. func (r *Raspivid) Stop() error { if r.cmd == nil || r.cmd.Process == nil { return errors.New("raspivid process was never started") @@ -222,5 +235,5 @@ func (r *Raspivid) Stop() error { if err != nil { return fmt.Errorf("could not kill raspivid process: %w", err) } - return nil + return r.out.Close() } diff --git a/revid/webcam.go b/revid/webcam.go index b3ccb1c5..218bb031 100644 --- a/revid/webcam.go +++ b/revid/webcam.go @@ -34,6 +34,7 @@ import ( "bitbucket.org/ausocean/utils/logger" ) +// Configuration defaults. const ( defaultWebcamInputPath = "/dev/video0" defaultWebcamFrameRate = 25 @@ -42,6 +43,7 @@ const ( defaultWebcamHeight = 720 ) +// Configuration field errors. var ( errWebcamBadFrameRate = errors.New("frame rate bad or unset, defaulting") errWebcamBadBitrate = errors.New("bitrate bad or unset, defaulting") @@ -49,6 +51,8 @@ var ( errWebcamHeight = errors.New("height bad or unset, defaulting") ) +// Webcam is an implementation of the AVDevice interface for a Webcam. Webcam +// uses an ffmpeg process to pipe the video data from the webcam. type Webcam struct { out io.ReadCloser log Logger @@ -56,35 +60,41 @@ type Webcam struct { cmd *exec.Cmd } +// NewWebcam returns a new Webcam. func NewWebcam(l Logger) *Webcam { return &Webcam{log: l} } +// Set will validate the relevant fields of the given Config struct and assign +// the struct to the Webcam's Config. If fields are not valid, an error is +// added to the multiError and a default value is used. func (w *Webcam) Set(c Config) error { var errs []error if c.Width == 0 { errs = append(errs, errBadWidth) - c.Width = raspividDefaultWidth + c.Width = defaultWebcamWidth } if c.Height == 0 { errs = append(errs, errBadHeight) - c.Height = raspividDefaultHeight + c.Height = defaultWebcamHeight } if c.FrameRate == 0 { errs = append(errs, errBadFrameRate) - c.FrameRate = raspividDefaultFramerate + c.FrameRate = defaultWebcamFrameRate } if c.Bitrate <= 0 { errs = append(errs, errBadBitrate) - c.Bitrate = raspividDefaultBitrate + c.Bitrate = defaultWebcamBitrate } w.cfg = c return multiError(errs) } +// Start will build the required arguments for ffmpeg and then execute the +// command, piping video output where we can read using the Read method. func (w *Webcam) Start() error { args := []string{ "-i", w.cfg.InputPath, @@ -118,6 +128,7 @@ func (w *Webcam) Start() error { return nil } +// Stop will kill the ffmpeg process and close the output pipe. func (w *Webcam) Stop() error { if w.cmd == nil || w.cmd.Process == nil { return errors.New("raspivid process was never started") @@ -129,6 +140,10 @@ func (w *Webcam) Stop() error { return w.out.Close() } +// Read implements io.Reader. If the pipe is nil a read error is returned. func (w *Webcam) Read(p []byte) (int, error) { - return w.out.Read(p) + if w.out != nil { + return w.out.Read(p) + } + return 0, errors.New("webcam not streaming") } From 7dc15d80953eaf961c7d8bf14a3f7b309e7f4e5a Mon Sep 17 00:00:00 2001 From: Saxon Date: Tue, 5 Nov 2019 20:22:05 +1030 Subject: [PATCH 15/21] revid-cli: updated input and output const names --- cmd/revid-cli/main.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/cmd/revid-cli/main.go b/cmd/revid-cli/main.go index ae5e76af..c76343e4 100644 --- a/cmd/revid-cli/main.go +++ b/cmd/revid-cli/main.go @@ -179,15 +179,15 @@ func handleFlags() revid.Config { switch *inputPtr { case "Raspivid": - cfg.Input = revid.Raspivid + cfg.Input = revid.InputRaspivid case "v4l": - cfg.Input = revid.V4L + cfg.Input = revid.InputV4L case "File": - cfg.Input = revid.File + cfg.Input = revid.InputFile case "Audio": - cfg.Input = revid.Audio + cfg.Input = revid.InputAudio case "RTSP": - cfg.Input = revid.RTSP + cfg.Input = revid.InputRTSP case "": default: log.Log(logger.Error, pkg+"bad input argument") @@ -214,13 +214,13 @@ func handleFlags() revid.Config { for _, o := range outputs { switch o { case "File": - cfg.Outputs = append(cfg.Outputs, revid.File) + cfg.Outputs = append(cfg.Outputs, revid.OutputFile) case "Http": - cfg.Outputs = append(cfg.Outputs, revid.HTTP) + cfg.Outputs = append(cfg.Outputs, revid.OutputHTTP) case "Rtmp": - cfg.Outputs = append(cfg.Outputs, revid.RTMP) + cfg.Outputs = append(cfg.Outputs, revid.OutputRTMP) case "Rtp": - cfg.Outputs = append(cfg.Outputs, revid.RTP) + cfg.Outputs = append(cfg.Outputs, revid.OutputRTP) case "": default: log.Log(logger.Error, pkg+"bad output argument", "arg", o) From bc7f450c5f3105b5e40557c744a8f2d14c72d84f Mon Sep 17 00:00:00 2001 From: Saxon Date: Wed, 6 Nov 2019 08:48:11 +1030 Subject: [PATCH 16/21] revid: fixed AVDevice comment --- revid/inputs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/revid/inputs.go b/revid/inputs.go index abe50bba..bae92032 100644 --- a/revid/inputs.go +++ b/revid/inputs.go @@ -54,7 +54,7 @@ const ( ) // AVDevice describes a configurable audio or video device from which media data -// can be obtained. AVDevice implements io.Reader. +// can be obtained. AVDevice is an io.Reader. type AVDevice interface { io.Reader From a02ea397d54dd5de6bf94877133f4fdc999e3be0 Mon Sep 17 00:00:00 2001 From: Saxon Date: Wed, 6 Nov 2019 10:45:48 +1030 Subject: [PATCH 17/21] revid/geovision.go: fixed GeoVision constructor name, NewGeovision->NewGeoVision --- revid/geovision.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/revid/geovision.go b/revid/geovision.go index 713ebd97..6c11d1f9 100644 --- a/revid/geovision.go +++ b/revid/geovision.go @@ -74,7 +74,7 @@ type GeoVision struct { } // NewGeoVision returns a new GeoVision. -func NewGeovision(l Logger) *GeoVision { return &GeoVision{log: l} } +func NewGeoVision(l Logger) *GeoVision { return &GeoVision{log: l} } // Set will take a Config struct, check the validity of the relevant fields // and then performs any configuration necessary using gvctrl to control the From 9a93e92b509e2d49e35e28d935183ed55380159c Mon Sep 17 00:00:00 2001 From: Saxon Date: Wed, 6 Nov 2019 11:31:12 +1030 Subject: [PATCH 18/21] revid/webcam.go: fixed error messages in Webcam.Stop --- revid/webcam.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/revid/webcam.go b/revid/webcam.go index 218bb031..34dceb41 100644 --- a/revid/webcam.go +++ b/revid/webcam.go @@ -131,11 +131,11 @@ func (w *Webcam) Start() error { // Stop will kill the ffmpeg process and close the output pipe. func (w *Webcam) Stop() error { if w.cmd == nil || w.cmd.Process == nil { - return errors.New("raspivid process was never started") + return errors.New("ffmpeg process was never started") } err := w.cmd.Process.Kill() if err != nil { - return fmt.Errorf("could not kill raspivid process: %w", err) + return fmt.Errorf("could not kill ffmpeg process: %w", err) } return w.out.Close() } From 57d73a8d0ae5b6bba49252e60b0c76250cd1be0c Mon Sep 17 00:00:00 2001 From: Saxon Date: Wed, 6 Nov 2019 17:27:10 +1030 Subject: [PATCH 19/21] created av/device package and sub packages raspivid, geovision, webcam and file av/device/device.go now contains the AVDevice interface and implementations of this interface, namely, raspivid, geovision, webcam and file are contained in the packages av/device/raspivid, av/device/geovision, av/device/webcam and av/device/file respctively. config.go and testing was also moved to a new package called config.go in order to remove would be circular dependency between AVDevice implementations and revid. Modifications were made elsewhere expecting config.Config to be part of the revid package. --- cmd/revid-cli/main.go | 31 +- device/device.go | 64 ++++ {revid => device/file}/file.go | 8 +- {revid => device/geovision}/geovision.go | 108 +++++-- .../geovision}/gvctrl/gvctrl-cli/main.go | 2 +- {input => device/geovision}/gvctrl/gvctrl.go | 0 .../geovision}/gvctrl/gvctrl_test.go | 0 {input => device/geovision}/gvctrl/request.go | 0 {input => device/geovision}/gvctrl/utils.go | 0 {revid => device/raspivid}/raspivid.go | 61 +++- {revid => device/webcam}/webcam.go | 45 +-- go.mod | 5 +- go.sum | 49 ++- revid/audio_linux.go | 32 +- revid/{ => config}/config.go | 27 +- revid/{ => config}/config_test.go | 2 +- revid/inputs.go | 168 +++++----- revid/revid.go | 288 +++++++++--------- revid/revid_test.go | 21 +- 19 files changed, 549 insertions(+), 362 deletions(-) create mode 100644 device/device.go rename {revid => device/file}/file.go (93%) rename {revid => device/geovision}/geovision.go (72%) rename {input => device/geovision}/gvctrl/gvctrl-cli/main.go (98%) rename {input => device/geovision}/gvctrl/gvctrl.go (100%) rename {input => device/geovision}/gvctrl/gvctrl_test.go (100%) rename {input => device/geovision}/gvctrl/request.go (100%) rename {input => device/geovision}/gvctrl/utils.go (100%) rename {revid => device/raspivid}/raspivid.go (86%) rename {revid => device/webcam}/webcam.go (78%) rename revid/{ => config}/config.go (97%) rename revid/{ => config}/config_test.go (99%) diff --git a/cmd/revid-cli/main.go b/cmd/revid-cli/main.go index c76343e4..8a0fbe01 100644 --- a/cmd/revid-cli/main.go +++ b/cmd/revid-cli/main.go @@ -1,6 +1,6 @@ /* NAME - revid-cli - command line interface for Revid. + revid-cli - command line interface for revid. DESCRIPTION See Readme.md @@ -42,6 +42,7 @@ import ( "bitbucket.org/ausocean/av/container/mts" "bitbucket.org/ausocean/av/container/mts/meta" "bitbucket.org/ausocean/av/revid" + "bitbucket.org/ausocean/av/revid/config" "bitbucket.org/ausocean/iot/pi/netsender" "bitbucket.org/ausocean/iot/pi/sds" "bitbucket.org/ausocean/iot/pi/smartlogger" @@ -102,8 +103,8 @@ func main() { // handleFlags parses command line flags and returns a revid configuration // based on them. -func handleFlags() revid.Config { - var cfg revid.Config +func handleFlags() config.Config { + var cfg config.Config var ( cpuprofile = flag.String("cpuprofile", "", "write cpu profile to `file`") @@ -128,8 +129,8 @@ func handleFlags() revid.Config { rotationPtr = flag.Uint("Rotation", 0, "Rotate video output. (0-359 degrees)") brightnessPtr = flag.Uint("Brightness", 50, "Set brightness. (0-100) ") saturationPtr = flag.Int("Saturation", 0, "Set Saturation. (100-100)") - exposurePtr = flag.String("Exposure", "auto", "Set exposure mode. ("+strings.Join(revid.ExposureModes[:], ",")+")") - autoWhiteBalancePtr = flag.String("Awb", "auto", "Set automatic white balance mode. ("+strings.Join(revid.AutoWhiteBalanceModes[:], ",")+")") + exposurePtr = flag.String("Exposure", "auto", "Set exposure mode. ("+strings.Join(config.ExposureModes[:], ",")+")") + autoWhiteBalancePtr = flag.String("Awb", "auto", "Set automatic white balance mode. ("+strings.Join(config.AutoWhiteBalanceModes[:], ",")+")") // Audio specific flags. sampleRatePtr = flag.Int("SampleRate", 48000, "Sample rate of recorded audio") @@ -179,15 +180,15 @@ func handleFlags() revid.Config { switch *inputPtr { case "Raspivid": - cfg.Input = revid.InputRaspivid + cfg.Input = config.InputRaspivid case "v4l": - cfg.Input = revid.InputV4L + cfg.Input = config.InputV4L case "File": - cfg.Input = revid.InputFile + cfg.Input = config.InputFile case "Audio": - cfg.Input = revid.InputAudio + cfg.Input = config.InputAudio case "RTSP": - cfg.Input = revid.InputRTSP + cfg.Input = config.InputRTSP case "": default: log.Log(logger.Error, pkg+"bad input argument") @@ -214,13 +215,13 @@ func handleFlags() revid.Config { for _, o := range outputs { switch o { case "File": - cfg.Outputs = append(cfg.Outputs, revid.OutputFile) + cfg.Outputs = append(cfg.Outputs, config.OutputFile) case "Http": - cfg.Outputs = append(cfg.Outputs, revid.OutputHTTP) + cfg.Outputs = append(cfg.Outputs, config.OutputHTTP) case "Rtmp": - cfg.Outputs = append(cfg.Outputs, revid.OutputRTMP) + cfg.Outputs = append(cfg.Outputs, config.OutputRTMP) case "Rtp": - cfg.Outputs = append(cfg.Outputs, revid.OutputRTP) + cfg.Outputs = append(cfg.Outputs, config.OutputRTP) case "": default: log.Log(logger.Error, pkg+"bad output argument", "arg", o) @@ -258,7 +259,7 @@ func handleFlags() revid.Config { } // initialize then run the main NetSender client -func run(cfg revid.Config) { +func run(cfg config.Config) { log.Log(logger.Info, pkg+"running in NetSender mode") var rv *revid.Revid diff --git a/device/device.go b/device/device.go new file mode 100644 index 00000000..1801be20 --- /dev/null +++ b/device/device.go @@ -0,0 +1,64 @@ +/* +DESCRIPTION + device.go provides AVDevice, and interface that describes a configurable + audio or video device that can be started and stopped from which data may + be obtained. + +AUTHORS + Saxon A. Nelson-Milton + +LICENSE + Copyright (C) 2019 the Australian Ocean Lab (AusOcean) + + It is free software: you can redistribute it and/or modify them + under the terms of the GNU General Public License as published by the + Free Software Foundation, either version 3 of the License, or (at your + option) any later version. + + It is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + for more details. + + You should have received a copy of the GNU General Public License + in gpl.txt. If not, see http://www.gnu.org/licenses. +*/ + +package device + +import ( + "fmt" + "io" + + "bitbucket.org/ausocean/av/revid/config" +) + +// AVDevice describes a configurable audio or video device from which media data +// can be obtained. AVDevice is an io.Reader. +type AVDevice interface { + io.Reader + + // Set allows for configuration of the AVDevice using a Config struct. All, + // some or none of the fields of the Config struct may be used for configuration + // by an implementation. An implementation should specify what fields are + // considered. + Set(c config.Config) error + + // Start will start the AVDevice capturing media data; after which the Read + // method may be called to obtain the data. The format of the data may differ + // and should be specified by the implementation. + Start() error + + // Stop will stop the AVDevice from capturing media data. From this point + // Reads will no longer be successful. + Stop() error +} + +// multiError implements the built in error interface. multiError is used here +// to collect multi errors during validation of configruation parameters for o +// AVDevices. +type MultiError []error + +func (me MultiError) Error() string { + return fmt.Sprintf("%v", me) +} diff --git a/revid/file.go b/device/file/file.go similarity index 93% rename from revid/file.go rename to device/file/file.go index d19ef69f..8f23c408 100644 --- a/revid/file.go +++ b/device/file/file.go @@ -22,27 +22,29 @@ LICENSE in gpl.txt. If not, see http://www.gnu.org/licenses. */ -package revid +package file import ( "errors" "fmt" "io" "os" + + "bitbucket.org/ausocean/av/revid/config" ) // AVFile is an implementation of the AVDevice interface for a file containg // audio or video data. type AVFile struct { f io.ReadCloser - cfg Config + cfg config.Config } // NewAVFile returns a new AVFile. func NewAVFile() *AVFile { return &AVFile{} } // Set simply sets the AVFile's config to the passed config. -func (m *AVFile) Set(c Config) error { +func (m *AVFile) Set(c config.Config) error { m.cfg = c return nil } diff --git a/revid/geovision.go b/device/geovision/geovision.go similarity index 72% rename from revid/geovision.go rename to device/geovision/geovision.go index 6c11d1f9..6eaf6949 100644 --- a/revid/geovision.go +++ b/device/geovision/geovision.go @@ -23,32 +23,53 @@ LICENSE in gpl.txt. If not, see http://www.gnu.org/licenses. */ -package revid +package geovision import ( "errors" "fmt" "net" + "strconv" + "strings" "time" "bitbucket.org/ausocean/av/codec/codecutil" - "bitbucket.org/ausocean/av/input/gvctrl" + "bitbucket.org/ausocean/av/device" + "bitbucket.org/ausocean/av/device/geovision/gvctrl" "bitbucket.org/ausocean/av/protocol/rtcp" "bitbucket.org/ausocean/av/protocol/rtp" "bitbucket.org/ausocean/av/protocol/rtsp" + "bitbucket.org/ausocean/av/revid/config" "bitbucket.org/ausocean/utils/logger" ) +// Indicate package when logging. +const pkg = "geovision: " + +// Constants for real time clients. +const ( + rtpPort = 60000 + rtcpPort = 60001 + defaultServerRTCPPort = 17301 +) + +// TODO: remove this when gvctrl has configurable user and pass. +const ( + ipCamUser = "admin" + ipCamPass = "admin" +) + // Configuration defaults. const ( - defaultGVCameraIP = "192.168.1.50" - defaultGVCodec = codecutil.H264 - defaultGVHeight = 720 - defaultGVFrameRate = 25 - defaultGVBitrate = 400 - defaultGVVBRBitrate = 400 - defaultGVMinFrames = 100 - defaultGVVBRQuality = "standard" + defaultCameraIP = "192.168.1.50" + defaultCodec = codecutil.H264 + defaultHeight = 720 + defaultFrameRate = 25 + defaultBitrate = 400 + defaultVBRBitrate = 400 + defaultMinFrames = 100 + defaultVBRQuality = config.QualityStandard + defaultCameraChan = 2 ) // Configuration field errors. @@ -66,56 +87,56 @@ var ( // IP camera. This has been designed to implement the GV-BX4700-8F in particular. // Any other models are untested. type GeoVision struct { - cfg Config - log Logger + cfg config.Config + log config.Logger rtpClt *rtp.Client rtspClt *rtsp.Client rtcpClt *rtcp.Client } // NewGeoVision returns a new GeoVision. -func NewGeoVision(l Logger) *GeoVision { return &GeoVision{log: l} } +func NewGeoVision(l config.Logger) *GeoVision { return &GeoVision{log: l} } // Set will take a Config struct, check the validity of the relevant fields // and then performs any configuration necessary using gvctrl to control the // GeoVision web interface. If fields are not valid, an error is added to the // multiError and a default value is used for that particular field. -func (g *GeoVision) Set(c Config) error { - var errs multiError +func (g *GeoVision) Set(c config.Config) error { + var errs device.MultiError if c.CameraIP == "" { errs = append(errs, errGVBadCameraIP) - c.CameraIP = defaultGVCameraIP + c.CameraIP = defaultCameraIP } switch c.InputCodec { case codecutil.H264, codecutil.H265, codecutil.MJPEG: default: errs = append(errs, errGVBadCodec) - c.InputCodec = defaultGVCodec + c.InputCodec = defaultCodec } if c.Height <= 0 { errs = append(errs, errGVBadHeight) - c.Height = defaultGVHeight + c.Height = defaultHeight } if c.FrameRate <= 0 { errs = append(errs, errGVBadFrameRate) - c.FrameRate = defaultGVFrameRate + c.FrameRate = defaultFrameRate } if c.Bitrate <= 0 { errs = append(errs, errGVBadBitrate) - c.Bitrate = defaultGVBitrate + c.Bitrate = defaultBitrate } if c.MinFrames <= 0 { errs = append(errs, errGVBadMinFrames) - c.MinFrames = defaultGVMinFrames + c.MinFrames = defaultMinFrames } switch c.VBRQuality { - case qualityStandard, qualityFair, qualityGood, qualityGreat, qualityExcellent: + case config.QualityStandard, config.QualityFair, config.QualityGood, config.QualityGreat, config.QualityExcellent: default: errs = append(errs, errGVBadVBRQuality) c.VBRQuality = defaultVBRQuality @@ -147,12 +168,12 @@ func (g *GeoVision) Set(c Config) error { gvctrl.FrameRate(int(g.cfg.FrameRate)), gvctrl.VariableBitrate(g.cfg.VBR), gvctrl.VBRQuality( - map[quality]gvctrl.Quality{ - qualityStandard: gvctrl.QualityStandard, - qualityFair: gvctrl.QualityFair, - qualityGood: gvctrl.QualityGood, - qualityGreat: gvctrl.QualityGreat, - qualityExcellent: gvctrl.QualityExcellent, + map[config.Quality]gvctrl.Quality{ + config.QualityStandard: gvctrl.QualityStandard, + config.QualityFair: gvctrl.QualityFair, + config.QualityGood: gvctrl.QualityGood, + config.QualityGreat: gvctrl.QualityGreat, + config.QualityExcellent: gvctrl.QualityExcellent, }[g.cfg.VBRQuality], ), gvctrl.VBRBitrate(g.cfg.VBRBitrate), @@ -167,7 +188,7 @@ func (g *GeoVision) Set(c Config) error { const setupDelay = 5 * time.Second time.Sleep(setupDelay) - return multiError(errs) + return errs } // Start uses an RTSP client to communicate with the GeoVision RTSP server and @@ -277,3 +298,32 @@ func (g *GeoVision) Read(p []byte) (int, error) { } return 0, errors.New("cannot read, GeoVision not streaming") } + +// formAddrs is a helper function to form the addresses for the RTP client, +// RTCP client, and the RTSP server's RTCP addr using the local, remote addresses +// of the RTSP conn, and the SETUP method response. +func formAddrs(local, remote *net.TCPAddr, setupResp rtsp.Response) (rtpCltAddr, rtcpCltAddr, rtcpSvrAddr string, err error) { + svrRTCPPort, err := parseSvrRTCPPort(setupResp) + if err != nil { + return "", "", "", err + } + rtpCltAddr = strings.Split(local.String(), ":")[0] + ":" + strconv.Itoa(rtpPort) + rtcpCltAddr = strings.Split(local.String(), ":")[0] + ":" + strconv.Itoa(rtcpPort) + rtcpSvrAddr = strings.Split(remote.String(), ":")[0] + ":" + strconv.Itoa(svrRTCPPort) + return +} + +// parseServerRTCPPort is a helper function to get the RTSP server's RTCP port. +func parseSvrRTCPPort(resp rtsp.Response) (int, error) { + transport := resp.Header.Get("Transport") + for _, p := range strings.Split(transport, ";") { + if strings.Contains(p, "server_port") { + port, err := strconv.Atoi(strings.Split(p, "-")[1]) + if err != nil { + return 0, err + } + return port, nil + } + } + return 0, errors.New("SETUP response did not provide RTCP port") +} diff --git a/input/gvctrl/gvctrl-cli/main.go b/device/geovision/gvctrl/gvctrl-cli/main.go similarity index 98% rename from input/gvctrl/gvctrl-cli/main.go rename to device/geovision/gvctrl/gvctrl-cli/main.go index 8c7d5d18..8e5eb942 100644 --- a/input/gvctrl/gvctrl-cli/main.go +++ b/device/geovision/gvctrl/gvctrl-cli/main.go @@ -31,7 +31,7 @@ import ( "fmt" "strconv" - "bitbucket.org/ausocean/av/input/gvctrl" + "bitbucket.org/ausocean/av/device/geovision/gvctrl" ) func main() { diff --git a/input/gvctrl/gvctrl.go b/device/geovision/gvctrl/gvctrl.go similarity index 100% rename from input/gvctrl/gvctrl.go rename to device/geovision/gvctrl/gvctrl.go diff --git a/input/gvctrl/gvctrl_test.go b/device/geovision/gvctrl/gvctrl_test.go similarity index 100% rename from input/gvctrl/gvctrl_test.go rename to device/geovision/gvctrl/gvctrl_test.go diff --git a/input/gvctrl/request.go b/device/geovision/gvctrl/request.go similarity index 100% rename from input/gvctrl/request.go rename to device/geovision/gvctrl/request.go diff --git a/input/gvctrl/utils.go b/device/geovision/gvctrl/utils.go similarity index 100% rename from input/gvctrl/utils.go rename to device/geovision/gvctrl/utils.go diff --git a/revid/raspivid.go b/device/raspivid/raspivid.go similarity index 86% rename from revid/raspivid.go rename to device/raspivid/raspivid.go index 43f94cf3..59f1a7e9 100644 --- a/revid/raspivid.go +++ b/device/raspivid/raspivid.go @@ -22,7 +22,7 @@ LICENSE in gpl.txt. If not, see http://www.gnu.org/licenses. */ -package revid +package raspivid import ( "errors" @@ -32,9 +32,14 @@ import ( "strings" "bitbucket.org/ausocean/av/codec/codecutil" + "bitbucket.org/ausocean/av/device" + "bitbucket.org/ausocean/av/revid/config" "bitbucket.org/ausocean/utils/logger" ) +// To indicate package when logging. +const pkg = "raspivid: " + // Raspivid configuration defaults. const ( defaultRaspividCodec = codecutil.H264 @@ -67,23 +72,53 @@ var ( errBadQuantization = errors.New("quantization bad or unset, defaulting") ) +// Possible modes for raspivid --exposure parameter. +var ExposureModes = [...]string{ + "auto", + "night", + "nightpreview", + "backlight", + "spotlight", + "sports", + "snow", + "beach", + "verylong", + "fixedfps", + "antishake", + "fireworks", +} + +// Possible modes for raspivid --awb parameter. +var AutoWhiteBalanceModes = [...]string{ + "off", + "auto", + "sun", + "cloud", + "shade", + "tungsten", + "fluorescent", + "incandescent", + "flash", + "horizon", +} + // Raspivid is an implementation of AVDevice that provides control over the // raspivid command to allow reading of data from a Raspberry Pi camera. type Raspivid struct { - cfg Config + cfg config.Config cmd *exec.Cmd out io.ReadCloser - log Logger + log config.Logger } // NewRaspivid returns a new Raspivid. -func NewRaspivid(l Logger) *Raspivid { return &Raspivid{log: l} } +func NewRaspivid(l config.Logger) *Raspivid { return &Raspivid{log: l} } // Set will take a Config struct, check the validity of the relevant fields // and then performs any configuration necessary. If fields are not valid, // an error is added to the multiError and a default value is used. -func (r *Raspivid) Set(c Config) error { - var errs []error +func (r *Raspivid) Set(c config.Config) error { + var errs device.MultiError switch c.InputCodec { case codecutil.H264, codecutil.MJPEG: default: @@ -140,7 +175,7 @@ func (r *Raspivid) Set(c Config) error { c.Saturation = defaultRaspividSaturation } - if c.Exposure == "" || !stringInSlice(c.Exposure, ExposureModes[:]) { + if c.Exposure == "" || !stringInSlice(c.Exposure, config.ExposureModes[:]) { errs = append(errs, errBadExposure) c.Exposure = defaultRaspividExposure } @@ -151,7 +186,7 @@ func (r *Raspivid) Set(c Config) error { } r.cfg = c - return multiError(errs) + return errs } // Start will prepare the arguments for the raspivid command using the @@ -237,3 +272,13 @@ func (r *Raspivid) Stop() error { } return r.out.Close() } + +// stringInSlice returns true if want is in slice. +func stringInSlice(want string, slice []string) bool { + for _, s := range slice { + if s == want { + return true + } + } + return false +} diff --git a/revid/webcam.go b/device/webcam/webcam.go similarity index 78% rename from revid/webcam.go rename to device/webcam/webcam.go index 34dceb41..13a1ad32 100644 --- a/revid/webcam.go +++ b/device/webcam/webcam.go @@ -22,7 +22,7 @@ LICENSE in gpl.txt. If not, see http://www.gnu.org/licenses. */ -package revid +package webcam import ( "errors" @@ -31,66 +31,71 @@ import ( "os/exec" "strings" + "bitbucket.org/ausocean/av/device" + "bitbucket.org/ausocean/av/revid/config" "bitbucket.org/ausocean/utils/logger" ) +// Used to indicate package in logging. +const pkg = "webcam: " + // Configuration defaults. const ( - defaultWebcamInputPath = "/dev/video0" - defaultWebcamFrameRate = 25 - defaultWebcamBitrate = 400 - defaultWebcamWidth = 1280 - defaultWebcamHeight = 720 + defaultInputPath = "/dev/video0" + defaultFrameRate = 25 + defaultBitrate = 400 + defaultWidth = 1280 + defaultHeight = 720 ) // Configuration field errors. var ( - errWebcamBadFrameRate = errors.New("frame rate bad or unset, defaulting") - errWebcamBadBitrate = errors.New("bitrate bad or unset, defaulting") - errWebcamWidth = errors.New("width bad or unset, defaulting") - errWebcamHeight = errors.New("height bad or unset, defaulting") + errBadFrameRate = errors.New("frame rate bad or unset, defaulting") + errBadBitrate = errors.New("bitrate bad or unset, defaulting") + errBadWidth = errors.New("width bad or unset, defaulting") + errBadHeight = errors.New("height bad or unset, defaulting") ) // Webcam is an implementation of the AVDevice interface for a Webcam. Webcam // uses an ffmpeg process to pipe the video data from the webcam. type Webcam struct { out io.ReadCloser - log Logger - cfg Config + log config.Logger + cfg config.Config cmd *exec.Cmd } // NewWebcam returns a new Webcam. -func NewWebcam(l Logger) *Webcam { +func NewWebcam(l config.Logger) *Webcam { return &Webcam{log: l} } // Set will validate the relevant fields of the given Config struct and assign // the struct to the Webcam's Config. If fields are not valid, an error is // added to the multiError and a default value is used. -func (w *Webcam) Set(c Config) error { - var errs []error +func (w *Webcam) Set(c config.Config) error { + var errs device.MultiError if c.Width == 0 { errs = append(errs, errBadWidth) - c.Width = defaultWebcamWidth + c.Width = defaultWidth } if c.Height == 0 { errs = append(errs, errBadHeight) - c.Height = defaultWebcamHeight + c.Height = defaultHeight } if c.FrameRate == 0 { errs = append(errs, errBadFrameRate) - c.FrameRate = defaultWebcamFrameRate + c.FrameRate = defaultFrameRate } if c.Bitrate <= 0 { errs = append(errs, errBadBitrate) - c.Bitrate = defaultWebcamBitrate + c.Bitrate = defaultBitrate } w.cfg = c - return multiError(errs) + return errs } // Start will build the required arguments for ffmpeg and then execute the diff --git a/go.mod b/go.mod index 922e40a9..1b29004b 100644 --- a/go.mod +++ b/go.mod @@ -4,15 +4,16 @@ go 1.13 require ( bitbucket.org/ausocean/iot v1.2.7 - bitbucket.org/ausocean/test v0.0.0-20190821085226-7a524f2344ba + bitbucket.org/ausocean/test v0.0.0-20190821085226-7a524f2344ba // indirect bitbucket.org/ausocean/utils v1.2.10 github.com/BurntSushi/toml v0.3.1 // indirect 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/go-delve/delve v1.3.2 github.com/mewkiz/flac v1.0.5 github.com/pkg/errors v0.8.1 github.com/yobert/alsa v0.0.0-20180630182551-d38d89fa843e - gocv.io/x/gocv v0.21.0 + gocv.io/x/gocv v0.21.0 // indirect gopkg.in/yaml.v2 v2.2.2 // indirect ) diff --git a/go.sum b/go.sum index 231895b1..1770d71f 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,6 @@ -bitbucket.org/ausocean/av v0.0.0-20190416003121-6ee286e98874/go.mod h1:DxZEprrNNQ2slHKAQVUHryDaWc5CbjxyHAvomhzg+AE= -bitbucket.org/ausocean/av/input/gvctrl v0.0.0-20191017223116-ce6c12cce8cd h1:L99pvZZtdy3v54ym6GswYi8SOGgz+4Tr8hiIOI+nSiQ= -bitbucket.org/ausocean/av/input/gvctrl v0.0.0-20191017223116-ce6c12cce8cd/go.mod h1:Hg522DOVaj23J7CIxknCxmNsLGdg1iZ+Td1FDcTOdLQ= -bitbucket.org/ausocean/iot v1.2.4/go.mod h1:5HVLgPHccW2PxS7WDUQO6sKWMgk3Vfze/7d5bHs8EWU= -bitbucket.org/ausocean/iot v1.2.6 h1:KAAY1KZDbyOpoKajT1dM8BawupHiW9hUOelseSV1Ptc= -bitbucket.org/ausocean/iot v1.2.6/go.mod h1:71AYHh8yGZ8XyzDBskwIWMF+8E8ORagXpXE24wlhoE0= bitbucket.org/ausocean/iot v1.2.7 h1:dZgrmVtuXnzHgybDthn0bYgAJms9euTONXBsqsx9g5M= bitbucket.org/ausocean/iot v1.2.7/go.mod h1:aAWgPo2f8sD2OPmxae1E5/iD9+tKY/iW4pcQMQXUvHM= bitbucket.org/ausocean/test v0.0.0-20190821085226-7a524f2344ba/go.mod h1:MbKtu9Pu8l3hiVGX6ep8S1VwAVY5uCbifCFOYsm914w= -bitbucket.org/ausocean/utils v0.0.0-20190408050157-66d3b4d4041e/go.mod h1:uXzX9z3PLemyURTMWRhVI8uLhPX4uuvaaO85v2hcob8= -bitbucket.org/ausocean/utils v1.2.6/go.mod h1:uXzX9z3PLemyURTMWRhVI8uLhPX4uuvaaO85v2hcob8= -bitbucket.org/ausocean/utils v1.2.8 h1:hyxAIqYBqjqCguG+6A/kKyrAihyeUt2LziZg6CH0gLU= -bitbucket.org/ausocean/utils v1.2.8/go.mod h1:uXzX9z3PLemyURTMWRhVI8uLhPX4uuvaaO85v2hcob8= -bitbucket.org/ausocean/utils v1.2.9 h1:g45C6KCNvCLOGFv+ZnmDbQOOdnwpIsvzuNOD141CTVI= bitbucket.org/ausocean/utils v1.2.9/go.mod h1:uXzX9z3PLemyURTMWRhVI8uLhPX4uuvaaO85v2hcob8= bitbucket.org/ausocean/utils v1.2.10 h1:JTS7n+K+0o/FQFWKjdGgA1ElZ4TQu9aHX3wTJXOayXw= bitbucket.org/ausocean/utils v1.2.10/go.mod h1:uXzX9z3PLemyURTMWRhVI8uLhPX4uuvaaO85v2hcob8= @@ -24,28 +13,50 @@ github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMx github.com/adrianmo/go-nmea v1.1.1-0.20190109062325-c448653979f7/go.mod h1:HHPxPAm2kmev+61qmkZh7xgZF/7qHtSpsWppip2Ipv8= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/cosiner/argv v0.0.0-20170225145430-13bacc38a0a5/go.mod h1:p/NrK5tF6ICIly4qwEDsf6VDirFiWWz0FenfYBwJaKQ= +github.com/cpuguy83/go-md2man v1.0.8/go.mod h1:N6JayAiVKtlHSnuTCeuLSQVs75hb8q+dYQLjr7cDsKY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/go-audio/aiff v0.0.0-20180403003018-6c3a8a6aff12/go.mod h1:AMSAp6W1zd0koOdX6QDgGIuBDTUvLa2SLQtm7d9eM3c= github.com/go-audio/audio v0.0.0-20180206231410-b697a35b5608/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs= github.com/go-audio/audio v0.0.0-20181013203223-7b2a6ca21480 h1:4sGU+UABMMsRJyD+Y2yzMYxq0GJFUsRRESI0P1gZ2ig= github.com/go-audio/audio v0.0.0-20181013203223-7b2a6ca21480/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs= 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/go-delve/delve v1.3.2 h1:K8VjV+Q2YnBYlPq0ctjrvc9h7h03wXszlszzfGW5Tog= +github.com/go-delve/delve v1.3.2/go.mod h1:LLw6qJfIsRK9WcwV2IRRqsdlgrqzOeuGrQOCOIhDpt8= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 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/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/mattetti/audio v0.0.0-20180912171649-01576cde1f21 h1:Hc1iKlyxNHp3CV59G2E/qabUkHvEwOIJxDK0CJ7CRjA= github.com/mattetti/audio v0.0.0-20180912171649-01576cde1f21/go.mod h1:LlQmBGkOuV/SKzEDXBPKauvN2UqCgzXO2XjecTGj40s= +github.com/mattn/go-colorable v0.0.0-20170327083344-ded68f7a9561/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mewkiz/flac v1.0.5 h1:dHGW/2kf+/KZ2GGqSVayNEhL9pluKn/rr/h/QqD9Ogc= github.com/mewkiz/flac v1.0.5/go.mod h1:EHZNU32dMF6alpurYyKHDLYpW1lYpBZ5WrXi/VuNIGs= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/peterh/liner v0.0.0-20170317030525-88609521dc4b/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v0.0.0-20170413231811-06b906832ed0/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday v0.0.0-20180428102519-11635eb403ff/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/sirupsen/logrus v0.0.0-20180523074243-ea8897e79973/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +github.com/spf13/cobra v0.0.0-20170417170307-b6cb39589372/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/pflag v0.0.0-20170417173400-9e4c21054fa1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= @@ -53,6 +64,7 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/yobert/alsa v0.0.0-20180630182551-d38d89fa843e h1:3NIzz7weXhh3NToPgbtlQtKiVgerEaG4/nY2skGoGG0= github.com/yobert/alsa v0.0.0-20180630182551-d38d89fa843e/go.mod h1:CaowXBWOiSGWEpBBV8LoVnQTVPV4ycyviC9IBLj8dRw= github.com/yryz/ds18b20 v0.0.0-20180211073435-3cf383a40624/go.mod h1:MqFju5qeLDFh+S9PqxYT7TEla8xeW7bgGr/69q3oki0= +go.starlark.net v0.0.0-20190702223751-32f345186213/go.mod h1:c1/X6cHgvdXj6pUlmWKMkuqRnW4K8x2vwt6JAaaircg= go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= @@ -60,12 +72,25 @@ go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/ go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -gocv.io/x/gocv v0.21.0 h1:dVjagrupZrfCRY0qPEaYWgoNMRpBel6GYDH4mvQOK8Y= gocv.io/x/gocv v0.21.0/go.mod h1:Rar2PS6DV+T4FL+PM535EImD/h13hGVaHhnCu1xarBs= +golang.org/x/arch v0.0.0-20171004143515-077ac972c2e4/go.mod h1:cYlCBUl1MsqxdiKgmc4uh7TxZfWSFLOGSRR090WDxt8= +golang.org/x/crypto v0.0.0-20180614174826-fd5f17ee7299/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190305064518-30e92a19ae4a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20181120060634-fc4f04983f62/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= 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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/revid/audio_linux.go b/revid/audio_linux.go index 831ef077..ad3a3e5d 100644 --- a/revid/audio_linux.go +++ b/revid/audio_linux.go @@ -34,40 +34,40 @@ import ( func (r *Revid) startAudioDevice() (func() error, error) { // Create audio device. ac := &audio.Config{ - SampleRate: r.config.SampleRate, - Channels: r.config.Channels, - RecPeriod: r.config.RecPeriod, - BitDepth: r.config.BitDepth, - Codec: r.config.InputCodec, + SampleRate: r.cfg.SampleRate, + Channels: r.cfg.Channels, + RecPeriod: r.cfg.RecPeriod, + BitDepth: r.cfg.BitDepth, + Codec: r.cfg.InputCodec, } - mts.Meta.Add("sampleRate", strconv.Itoa(r.config.SampleRate)) - mts.Meta.Add("channels", strconv.Itoa(r.config.Channels)) - mts.Meta.Add("period", fmt.Sprintf("%.6f", r.config.RecPeriod)) - mts.Meta.Add("bitDepth", strconv.Itoa(r.config.BitDepth)) - switch r.config.InputCodec { + mts.Meta.Add("sampleRate", strconv.Itoa(r.cfg.SampleRate)) + mts.Meta.Add("channels", strconv.Itoa(r.cfg.Channels)) + mts.Meta.Add("period", fmt.Sprintf("%.6f", r.cfg.RecPeriod)) + mts.Meta.Add("bitDepth", strconv.Itoa(r.cfg.BitDepth)) + switch r.cfg.InputCodec { case codecutil.PCM: mts.Meta.Add("codec", "pcm") case codecutil.ADPCM: mts.Meta.Add("codec", "adpcm") default: - r.config.Logger.Log(logger.Fatal, pkg+"no audio codec set in config") + r.cfg.Logger.Log(logger.Fatal, pkg+"no audio codec set in config") } - ai, err := audio.NewDevice(ac, r.config.Logger) + ai, err := audio.NewDevice(ac, r.cfg.Logger) if err != nil { - r.config.Logger.Log(logger.Fatal, pkg+"failed to create audio device", "error", err.Error()) + r.cfg.Logger.Log(logger.Fatal, pkg+"failed to create audio device", "error", err.Error()) } // Start audio device err = ai.Start() if err != nil { - r.config.Logger.Log(logger.Fatal, pkg+"failed to start audio device", "error", err.Error()) + r.cfg.Logger.Log(logger.Fatal, pkg+"failed to start audio device", "error", err.Error()) } // Process output from audio device. - r.config.ChunkSize = ai.ChunkSize() + r.cfg.ChunkSize = ai.ChunkSize() r.wg.Add(1) - go r.processFrom(ai, time.Duration(float64(time.Second)/r.config.WriteRate)) + go r.processFrom(ai, time.Duration(float64(time.Second)/r.cfg.WriteRate)) return func() error { ai.Stop() return nil diff --git a/revid/config.go b/revid/config/config.go similarity index 97% rename from revid/config.go rename to revid/config/config.go index 619e8be6..22dd3543 100644 --- a/revid/config.go +++ b/revid/config/config.go @@ -23,7 +23,7 @@ LICENSE along with revid in gpl.txt. If not, see http://www.gnu.org/licenses. */ -package revid +package config import ( "errors" @@ -63,17 +63,24 @@ var AutoWhiteBalanceModes = [...]string{ "horizon", } +const pkg = "config: " + +type Logger interface { + SetLevel(int8) + Log(level int8, message string, params ...interface{}) +} + // quality represents video quality. -type quality int +type Quality int // The different video qualities that can be used for variable bitrate when // using the GeoVision camera. const ( - qualityStandard quality = iota - qualityFair - qualityGood - qualityGreat - qualityExcellent + QualityStandard Quality = iota + QualityFair + QualityGood + QualityGreat + QualityExcellent ) // Enums to define inputs, outputs and codecs. @@ -115,7 +122,7 @@ const ( defaultRtpAddr = "localhost:6970" defaultCameraIP = "192.168.1.50" defaultVBR = false - defaultVBRQuality = qualityStandard + defaultVBRQuality = QualityStandard defaultBurstPeriod = 10 // Seconds defaultVBRBitrate = 500 // kbps defaultCameraChan = 2 @@ -236,7 +243,7 @@ type Config struct { // VBRQuality describes the general quality of video from the GeoVision camera // under variable bitrate. VBRQuality can be one 5 consts defined: // qualityStandard, qualityFair, qualityGood, qualityGreat and qualityExcellent. - VBRQuality quality + VBRQuality Quality // VBRBitrate describes maximal bitrate for the GeoVision camera when under // variable bitrate. @@ -518,7 +525,7 @@ func (c *Config) Validate() error { } switch c.VBRQuality { - case qualityStandard, qualityFair, qualityGood, qualityGreat, qualityExcellent: + case QualityStandard, QualityFair, QualityGood, QualityGreat, QualityExcellent: default: c.Logger.Log(logger.Info, pkg+"VBRQuality bad or unset, defaulting", "VBRQuality", defaultVBRQuality) c.VBRQuality = defaultVBRQuality diff --git a/revid/config_test.go b/revid/config/config_test.go similarity index 99% rename from revid/config_test.go rename to revid/config/config_test.go index 6f121078..90973c25 100644 --- a/revid/config_test.go +++ b/revid/config/config_test.go @@ -22,7 +22,7 @@ LICENSE in gpl.txt. If not, see http://www.gnu.org/licenses. */ -package revid +package config import ( "fmt" diff --git a/revid/inputs.go b/revid/inputs.go index bae92032..a31e0201 100644 --- a/revid/inputs.go +++ b/revid/inputs.go @@ -40,10 +40,11 @@ import ( "time" "bitbucket.org/ausocean/av/codec/codecutil" - "bitbucket.org/ausocean/av/input/gvctrl" + "bitbucket.org/ausocean/av/device/geovision/gvctrl" "bitbucket.org/ausocean/av/protocol/rtcp" "bitbucket.org/ausocean/av/protocol/rtp" "bitbucket.org/ausocean/av/protocol/rtsp" + "bitbucket.org/ausocean/av/revid/config" "bitbucket.org/ausocean/utils/logger" ) @@ -53,84 +54,61 @@ const ( ipCamPass = "admin" ) -// AVDevice describes a configurable audio or video device from which media data -// can be obtained. AVDevice is an io.Reader. -type AVDevice interface { - io.Reader - - // Set allows for configuration of the AVDevice using a Config struct. All, - // some or none of the fields of the Config struct may be used for configuration - // by an implementation. An implementation should specify what fields are - // considered. - Set(c Config) error - - // Start will start the AVDevice capturing media data; after which the Read - // method may be called to obtain the data. The format of the data may differ - // and should be specified by the implementation. - Start() error - - // Stop will stop the AVDevice from capturing media data. From this point - // Reads will no longer be successful. - Stop() error -} - -// multiError implements the built in error interface. multiError is used here -// to collect multi errors during validation of configruation parameters for o -// AVDevices. -type multiError []error - -func (me multiError) Error() string { - return fmt.Sprintf("%v", me) -} +// Constants for real time clients. +const ( + rtpPort = 60000 + rtcpPort = 60001 + defaultServerRTCPPort = 17301 +) // startRaspivid sets up things for input from raspivid i.e. starts // a raspivid process and pipes it's data output. func (r *Revid) startRaspivid() (func() error, error) { - r.config.Logger.Log(logger.Info, pkg+"starting raspivid") + r.cfg.Logger.Log(logger.Info, pkg+"starting raspivid") const disabled = "0" args := []string{ "--output", "-", "--nopreview", "--timeout", disabled, - "--width", fmt.Sprint(r.config.Width), - "--height", fmt.Sprint(r.config.Height), - "--bitrate", fmt.Sprint(r.config.Bitrate * 1000), // Convert from kbps to bps. - "--framerate", fmt.Sprint(r.config.FrameRate), - "--rotation", fmt.Sprint(r.config.Rotation), - "--brightness", fmt.Sprint(r.config.Brightness), - "--saturation", fmt.Sprint(r.config.Saturation), - "--exposure", fmt.Sprint(r.config.Exposure), - "--awb", fmt.Sprint(r.config.AutoWhiteBalance), + "--width", fmt.Sprint(r.cfg.Width), + "--height", fmt.Sprint(r.cfg.Height), + "--bitrate", fmt.Sprint(r.cfg.Bitrate * 1000), // Convert from kbps to bps. + "--framerate", fmt.Sprint(r.cfg.FrameRate), + "--rotation", fmt.Sprint(r.cfg.Rotation), + "--brightness", fmt.Sprint(r.cfg.Brightness), + "--saturation", fmt.Sprint(r.cfg.Saturation), + "--exposure", fmt.Sprint(r.cfg.Exposure), + "--awb", fmt.Sprint(r.cfg.AutoWhiteBalance), } - if r.config.FlipHorizontal { + if r.cfg.FlipHorizontal { args = append(args, "--hflip") } - if r.config.FlipVertical { + if r.cfg.FlipVertical { args = append(args, "--vflip") } - if r.config.FlipHorizontal { + if r.cfg.FlipHorizontal { args = append(args, "--hflip") } - switch r.config.InputCodec { + switch r.cfg.InputCodec { default: - return nil, fmt.Errorf("revid: invalid input codec: %v", r.config.InputCodec) + return nil, fmt.Errorf("revid: invalid input codec: %v", r.cfg.InputCodec) case codecutil.H264: args = append(args, "--codec", "H264", "--inline", - "--intra", fmt.Sprint(r.config.MinFrames), + "--intra", fmt.Sprint(r.cfg.MinFrames), ) - if r.config.VBR { - args = append(args, "-qp", fmt.Sprint(r.config.Quantization)) + if r.cfg.VBR { + args = append(args, "-qp", fmt.Sprint(r.cfg.Quantization)) } case codecutil.MJPEG: args = append(args, "--codec", "MJPEG") } - r.config.Logger.Log(logger.Info, pkg+"raspivid args", "raspividArgs", strings.Join(args, " ")) + r.cfg.Logger.Log(logger.Info, pkg+"raspivid args", "raspividArgs", strings.Join(args, " ")) r.cmd = exec.Command("raspivid", args...) stdout, err := r.cmd.StdoutPipe() @@ -151,28 +129,28 @@ func (r *Revid) startRaspivid() (func() error, error) { func (r *Revid) startV4L() (func() error, error) { const defaultVideo = "/dev/video0" - r.config.Logger.Log(logger.Info, pkg+"starting webcam") - if r.config.InputPath == "" { - r.config.Logger.Log(logger.Info, pkg+"using default video device", "device", defaultVideo) - r.config.InputPath = defaultVideo + r.cfg.Logger.Log(logger.Info, pkg+"starting webcam") + if r.cfg.InputPath == "" { + r.cfg.Logger.Log(logger.Info, pkg+"using default video device", "device", defaultVideo) + r.cfg.InputPath = defaultVideo } args := []string{ - "-i", r.config.InputPath, + "-i", r.cfg.InputPath, "-f", "h264", - "-r", fmt.Sprint(r.config.FrameRate), + "-r", fmt.Sprint(r.cfg.FrameRate), } - br := r.config.Bitrate * 1000 + br := r.cfg.Bitrate * 1000 args = append(args, "-b:v", fmt.Sprint(br), "-maxrate", fmt.Sprint(br), "-bufsize", fmt.Sprint(br/2), - "-s", fmt.Sprintf("%dx%d", r.config.Width, r.config.Height), + "-s", fmt.Sprintf("%dx%d", r.cfg.Width, r.cfg.Height), "-", ) - r.config.Logger.Log(logger.Info, pkg+"ffmpeg args", "args", strings.Join(args, " ")) + r.cfg.Logger.Log(logger.Info, pkg+"ffmpeg args", "args", strings.Join(args, " ")) r.cmd = exec.Command("ffmpeg", args...) stdout, err := r.cmd.StdoutPipe() @@ -182,7 +160,7 @@ func (r *Revid) startV4L() (func() error, error) { err = r.cmd.Start() if err != nil { - r.config.Logger.Log(logger.Fatal, pkg+"cannot start webcam", "error", err.Error()) + r.cfg.Logger.Log(logger.Fatal, pkg+"cannot start webcam", "error", err.Error()) return nil, nil } @@ -194,9 +172,9 @@ func (r *Revid) startV4L() (func() error, error) { // setupInputForFile sets up input from file and starts the revid.processFrom // routine. func (r *Revid) setupInputForFile() (func() error, error) { - f, err := os.Open(r.config.InputPath) + f, err := os.Open(r.cfg.InputPath) if err != nil { - r.config.Logger.Log(logger.Error, err.Error()) + r.cfg.Logger.Log(logger.Error, err.Error()) r.Stop() return nil, err } @@ -214,92 +192,92 @@ func (r *Revid) setupInputForFile() (func() error, error) { // TODO(saxon): this function should really be startGeoVision. It's much too // specific to be called startRTSPCamera. func (r *Revid) startRTSPCamera() (func() error, error) { - r.config.Logger.Log(logger.Info, pkg+"starting geovision...") + r.cfg.Logger.Log(logger.Info, pkg+"starting geovision...") err := gvctrl.Set( - r.config.CameraIP, - gvctrl.Channel(r.config.CameraChan), + r.cfg.CameraIP, + gvctrl.Channel(r.cfg.CameraChan), gvctrl.CodecOut( map[uint8]gvctrl.Codec{ codecutil.H264: gvctrl.CodecH264, codecutil.H265: gvctrl.CodecH265, codecutil.MJPEG: gvctrl.CodecMJPEG, - }[r.config.InputCodec], + }[r.cfg.InputCodec], ), - gvctrl.Height(int(r.config.Height)), - gvctrl.FrameRate(int(r.config.FrameRate)), - gvctrl.VariableBitrate(r.config.VBR), + gvctrl.Height(int(r.cfg.Height)), + gvctrl.FrameRate(int(r.cfg.FrameRate)), + gvctrl.VariableBitrate(r.cfg.VBR), gvctrl.VBRQuality( - map[quality]gvctrl.Quality{ - qualityStandard: gvctrl.QualityStandard, - qualityFair: gvctrl.QualityFair, - qualityGood: gvctrl.QualityGood, - qualityGreat: gvctrl.QualityGreat, - qualityExcellent: gvctrl.QualityExcellent, - }[r.config.VBRQuality], + map[config.Quality]gvctrl.Quality{ + config.QualityStandard: gvctrl.QualityStandard, + config.QualityFair: gvctrl.QualityFair, + config.QualityGood: gvctrl.QualityGood, + config.QualityGreat: gvctrl.QualityGreat, + config.QualityExcellent: gvctrl.QualityExcellent, + }[r.cfg.VBRQuality], ), - gvctrl.VBRBitrate(r.config.VBRBitrate), - gvctrl.CBRBitrate(int(r.config.Bitrate)), - gvctrl.Refresh(float64(r.config.MinFrames)/float64(r.config.FrameRate)), + gvctrl.VBRBitrate(r.cfg.VBRBitrate), + gvctrl.CBRBitrate(int(r.cfg.Bitrate)), + gvctrl.Refresh(float64(r.cfg.MinFrames)/float64(r.cfg.FrameRate)), ) if err != nil { return nil, fmt.Errorf("could not set IPCamera settings: %w", err) } - r.config.Logger.Log(logger.Info, pkg+"completed geovision configuration") + r.cfg.Logger.Log(logger.Info, pkg+"completed geovision configuration") time.Sleep(5 * time.Second) - rtspClt, local, remote, err := rtsp.NewClient("rtsp://" + ipCamUser + ":" + ipCamPass + "@" + r.config.CameraIP + ":8554/" + "CH002.sdp") + rtspClt, local, remote, err := rtsp.NewClient("rtsp://" + ipCamUser + ":" + ipCamPass + "@" + r.cfg.CameraIP + ":8554/" + "CH002.sdp") if err != nil { return nil, err } - r.config.Logger.Log(logger.Info, pkg+"created RTSP client") + r.cfg.Logger.Log(logger.Info, pkg+"created RTSP client") resp, err := rtspClt.Options() if err != nil { return nil, err } - r.config.Logger.Log(logger.Debug, pkg+"RTSP OPTIONS response", "response", resp.String()) + r.cfg.Logger.Log(logger.Debug, pkg+"RTSP OPTIONS response", "response", resp.String()) resp, err = rtspClt.Describe() if err != nil { return nil, err } - r.config.Logger.Log(logger.Debug, pkg+"RTSP DESCRIBE response", "response", resp.String()) + r.cfg.Logger.Log(logger.Debug, pkg+"RTSP DESCRIBE response", "response", resp.String()) resp, err = rtspClt.Setup("track1", fmt.Sprintf("RTP/AVP;unicast;client_port=%d-%d", rtpPort, rtcpPort)) if err != nil { return nil, err } - r.config.Logger.Log(logger.Debug, pkg+"RTSP SETUP response", "response", resp.String()) + r.cfg.Logger.Log(logger.Debug, pkg+"RTSP SETUP response", "response", resp.String()) rtpCltAddr, rtcpCltAddr, rtcpSvrAddr, err := formAddrs(local, remote, *resp) if err != nil { return nil, err } - r.config.Logger.Log(logger.Info, pkg+"RTSP session setup complete") + r.cfg.Logger.Log(logger.Info, pkg+"RTSP session setup complete") rtpClt, err := rtp.NewClient(rtpCltAddr) if err != nil { return nil, err } - rtcpClt, err := rtcp.NewClient(rtcpCltAddr, rtcpSvrAddr, rtpClt, r.config.Logger.Log) + rtcpClt, err := rtcp.NewClient(rtcpCltAddr, rtcpSvrAddr, rtpClt, r.cfg.Logger.Log) if err != nil { return nil, err } - r.config.Logger.Log(logger.Info, pkg+"RTCP and RTP clients created") + r.cfg.Logger.Log(logger.Info, pkg+"RTCP and RTP clients created") // Check errors from RTCP client until it has stopped running. go func() { for { err, ok := <-rtcpClt.Err() if ok { - r.config.Logger.Log(logger.Warning, pkg+"RTCP error", "error", err.Error()) + r.cfg.Logger.Log(logger.Warning, pkg+"RTCP error", "error", err.Error()) } else { return } @@ -309,20 +287,20 @@ func (r *Revid) startRTSPCamera() (func() error, error) { // Start the RTCP client. rtcpClt.Start() - r.config.Logger.Log(logger.Info, pkg+"RTCP client started") + r.cfg.Logger.Log(logger.Info, pkg+"RTCP client started") // Start reading data from the RTP client. r.wg.Add(1) - go r.processFrom(rtpClt, time.Second/time.Duration(r.config.FrameRate)) + go r.processFrom(rtpClt, time.Second/time.Duration(r.cfg.FrameRate)) - r.config.Logger.Log(logger.Info, pkg+"started input processor") + r.cfg.Logger.Log(logger.Info, pkg+"started input processor") resp, err = rtspClt.Play() if err != nil { return nil, err } - r.config.Logger.Log(logger.Debug, pkg+"RTSP server PLAY response", "response", resp.String()) - r.config.Logger.Log(logger.Info, pkg+"play requested, now receiving stream") + r.cfg.Logger.Log(logger.Debug, pkg+"RTSP server PLAY response", "response", resp.String()) + r.cfg.Logger.Log(logger.Info, pkg+"play requested, now receiving stream") return func() error { err := rtpClt.Close() @@ -337,7 +315,7 @@ func (r *Revid) startRTSPCamera() (func() error, error) { rtcpClt.Stop() - r.config.Logger.Log(logger.Info, pkg+"RTP, RTSP and RTCP clients stopped and closed") + r.cfg.Logger.Log(logger.Info, pkg+"RTP, RTSP and RTCP clients stopped and closed") return nil }, nil } @@ -375,6 +353,6 @@ func parseSvrRTCPPort(resp rtsp.Response) (int, error) { // then send individual access units to revid's encoders. func (r *Revid) processFrom(read io.Reader, delay time.Duration) { r.err <- r.lexTo(r.encoders, read, delay) - r.config.Logger.Log(logger.Info, pkg+"finished lexing") + r.cfg.Logger.Log(logger.Info, pkg+"finished lexing") r.wg.Done() } diff --git a/revid/revid.go b/revid/revid.go index fd6239d3..1f7c213f 100644 --- a/revid/revid.go +++ b/revid/revid.go @@ -44,24 +44,32 @@ import ( "bitbucket.org/ausocean/av/codec/mjpeg" "bitbucket.org/ausocean/av/container/flv" "bitbucket.org/ausocean/av/container/mts" + "bitbucket.org/ausocean/av/revid/config" "bitbucket.org/ausocean/iot/pi/netsender" "bitbucket.org/ausocean/utils/ioext" "bitbucket.org/ausocean/utils/logger" "bitbucket.org/ausocean/utils/ring" ) +// Ring buffer defaults. +const ( + // MTS ring buffer defaults. + defaultMTSRBSize = 1000 + defaultMTSRBElementSize = 100000 + defaultMTSRBWriteTimeout = 5 + + // RTMP ring buffer defaults. + defaultRTMPRBSize = 1000 + defaultRTMPRBElementSize = 300000 + defaultRTMPRBWriteTimeout = 5 +) + // RTMP connection properties. const ( rtmpConnectionMaxTries = 5 rtmpConnectionTimeout = 10 ) -const ( - rtpPort = 60000 - rtcpPort = 60001 - defaultServerRTCPPort = 17301 -) - const pkg = "revid: " type Logger interface { @@ -76,7 +84,7 @@ type Revid struct { // For historical reasons it also handles logging. // FIXME(kortschak): The relationship of concerns // in config/ns is weird. - config Config + cfg config.Config // ns holds the netsender.Sender responsible for HTTP. ns *netsender.Sender @@ -116,13 +124,13 @@ type Revid struct { // New returns a pointer to a new Revid with the desired configuration, and/or // an error if construction of the new instance was not successful. -func New(c Config, ns *netsender.Sender) (*Revid, error) { +func New(c config.Config, ns *netsender.Sender) (*Revid, error) { r := Revid{ns: ns, err: make(chan error)} err := r.setConfig(c) if err != nil { return nil, fmt.Errorf("could not set config, failed with error: %v", err) } - r.config.Logger.SetLevel(c.LogLevel) + r.cfg.Logger.SetLevel(c.LogLevel) go r.handleErrors() return &r, nil } @@ -130,8 +138,8 @@ func New(c Config, ns *netsender.Sender) (*Revid, error) { // Config returns a copy of revids current config. // // Config is not safe for concurrent use. -func (r *Revid) Config() Config { - return r.config +func (r *Revid) Config() config.Config { + return r.cfg } // TODO(Saxon): put more thought into error severity and how to handle these. @@ -139,7 +147,7 @@ func (r *Revid) handleErrors() { for { err := <-r.err if err != nil { - r.config.Logger.Log(logger.Error, pkg+"async error", "error", err.Error()) + r.cfg.Logger.Log(logger.Error, pkg+"async error", "error", err.Error()) } } } @@ -154,45 +162,45 @@ func (r *Revid) Bitrate() int { // reset swaps the current config of a Revid with the passed // configuration; checking validity and returning errors if not valid. It then // sets up the data pipeline accordingly to this configuration. -func (r *Revid) reset(config Config) error { - err := r.setConfig(config) +func (r *Revid) reset(c config.Config) error { + err := r.setConfig(c) if err != nil { return err } - r.config.Logger.SetLevel(config.LogLevel) + r.cfg.Logger.SetLevel(c.LogLevel) err = r.setupPipeline( func(dst io.WriteCloser, fps float64) (io.WriteCloser, error) { var st int var encOptions []func(*mts.Encoder) error - switch r.config.Input { - case InputRaspivid: - switch r.config.InputCodec { + switch r.cfg.Input { + case config.InputRaspivid: + switch r.cfg.InputCodec { case codecutil.H264: st = mts.EncodeH264 case codecutil.MJPEG: st = mts.EncodeMJPEG - encOptions = append(encOptions, mts.PacketBasedPSI(int(r.config.MinFrames))) + encOptions = append(encOptions, mts.PacketBasedPSI(int(r.cfg.MinFrames))) default: panic("unknown input codec for raspivid input") } - case InputFile, InputV4L: + case config.InputFile, config.InputV4L: st = mts.EncodeH264 - case InputRTSP: - switch r.config.InputCodec { + case config.InputRTSP: + switch r.cfg.InputCodec { case codecutil.H265: st = mts.EncodeH265 case codecutil.H264: st = mts.EncodeH264 case codecutil.MJPEG: st = mts.EncodeMJPEG - encOptions = append(encOptions, mts.PacketBasedPSI(int(r.config.MinFrames))) + encOptions = append(encOptions, mts.PacketBasedPSI(int(r.cfg.MinFrames))) default: panic("unknown input codec for RTSP input") } - case InputAudio: + case config.InputAudio: st = mts.EncodeAudio default: panic("unknown input type") @@ -215,13 +223,13 @@ func (r *Revid) reset(config Config) error { // setConfig takes a config, checks it's validity and then replaces the current // revid config. -func (r *Revid) setConfig(config Config) error { - r.config.Logger = config.Logger +func (r *Revid) setConfig(config config.Config) error { + r.cfg.Logger = config.Logger err := config.Validate() if err != nil { return errors.New("Config struct is bad: " + err.Error()) } - r.config = config + r.cfg = config return nil } @@ -244,38 +252,38 @@ func (r *Revid) setupPipeline(mtsEnc func(dst io.WriteCloser, rate float64) (io. // to mtsSenders if the output requires MPEGTS encoding, or flvSenders if the // output requires FLV encoding. var w io.WriteCloser - for _, out := range r.config.Outputs { + for _, out := range r.cfg.Outputs { switch out { - case OutputHTTP: + case config.OutputHTTP: w = newMtsSender( - newHttpSender(r.ns, r.config.Logger.Log), - r.config.Logger.Log, - ring.NewBuffer(r.config.MTSRBSize, r.config.MTSRBElementSize, time.Duration(r.config.MTSRBWriteTimeout)*time.Second), - r.config.ClipDuration, + newHttpSender(r.ns, r.cfg.Logger.Log), + r.cfg.Logger.Log, + ring.NewBuffer(r.cfg.MTSRBSize, r.cfg.MTSRBElementSize, time.Duration(r.cfg.MTSRBWriteTimeout)*time.Second), + r.cfg.ClipDuration, ) mtsSenders = append(mtsSenders, w) - case OutputRTP: - w, err := newRtpSender(r.config.RTPAddress, r.config.Logger.Log, r.config.FrameRate) + case config.OutputRTP: + w, err := newRtpSender(r.cfg.RTPAddress, r.cfg.Logger.Log, r.cfg.FrameRate) if err != nil { - r.config.Logger.Log(logger.Warning, pkg+"rtp connect error", "error", err.Error()) + r.cfg.Logger.Log(logger.Warning, pkg+"rtp connect error", "error", err.Error()) } mtsSenders = append(mtsSenders, w) - case OutputFile: - w, err := newFileSender(r.config.OutputPath) + case config.OutputFile: + w, err := newFileSender(r.cfg.OutputPath) if err != nil { return err } mtsSenders = append(mtsSenders, w) - case OutputRTMP: + case config.OutputRTMP: w, err := newRtmpSender( - r.config.RTMPURL, + r.cfg.RTMPURL, rtmpConnectionTimeout, rtmpConnectionMaxTries, - ring.NewBuffer(r.config.RTMPRBSize, r.config.RTMPRBElementSize, time.Duration(r.config.RTMPRBWriteTimeout)*time.Second), - r.config.Logger.Log, + ring.NewBuffer(r.cfg.RTMPRBSize, r.cfg.RTMPRBElementSize, time.Duration(r.cfg.RTMPRBWriteTimeout)*time.Second), + r.cfg.Logger.Log, ) if err != nil { - r.config.Logger.Log(logger.Warning, pkg+"rtmp connect error", "error", err.Error()) + r.cfg.Logger.Log(logger.Warning, pkg+"rtmp connect error", "error", err.Error()) } flvSenders = append(flvSenders, w) } @@ -286,7 +294,7 @@ func (r *Revid) setupPipeline(mtsEnc func(dst io.WriteCloser, rate float64) (io. // as a destination. if len(mtsSenders) != 0 { mw := multiWriter(mtsSenders...) - e, _ := mtsEnc(mw, r.config.WriteRate) + e, _ := mtsEnc(mw, r.cfg.WriteRate) encoders = append(encoders, e) } @@ -295,7 +303,7 @@ func (r *Revid) setupPipeline(mtsEnc func(dst io.WriteCloser, rate float64) (io. // as a destination. if len(flvSenders) != 0 { mw := multiWriter(flvSenders...) - e, err := flvEnc(mw, int(r.config.FrameRate)) + e, err := flvEnc(mw, int(r.cfg.FrameRate)) if err != nil { return err } @@ -304,23 +312,23 @@ func (r *Revid) setupPipeline(mtsEnc func(dst io.WriteCloser, rate float64) (io. r.encoders = multiWriter(encoders...) - switch r.config.Input { - case InputRaspivid: + switch r.cfg.Input { + case config.InputRaspivid: r.setupInput = r.startRaspivid - switch r.config.InputCodec { + switch r.cfg.InputCodec { case codecutil.H264: r.lexTo = h264.Lex case codecutil.MJPEG: r.lexTo = mjpeg.Lex } - case InputV4L: + case config.InputV4L: r.setupInput = r.startV4L r.lexTo = h264.Lex - case InputFile: + case config.InputFile: r.setupInput = r.setupInputForFile - case InputRTSP: + case config.InputRTSP: r.setupInput = r.startRTSPCamera - switch r.config.InputCodec { + switch r.cfg.InputCodec { case codecutil.H264: r.lexTo = h264.NewExtractor().Extract case codecutil.H265: @@ -328,9 +336,9 @@ func (r *Revid) setupPipeline(mtsEnc func(dst io.WriteCloser, rate float64) (io. case codecutil.MJPEG: panic("not implemented") } - case InputAudio: + case config.InputAudio: r.setupInput = r.startAudioDevice - r.lexTo = codecutil.NewByteLexer(&r.config.ChunkSize).Lex + r.lexTo = codecutil.NewByteLexer(&r.cfg.ChunkSize).Lex } return nil @@ -342,15 +350,15 @@ func (r *Revid) setupPipeline(mtsEnc func(dst io.WriteCloser, rate float64) (io. // Start is safe for concurrent use. func (r *Revid) Start() error { if r.IsRunning() { - r.config.Logger.Log(logger.Warning, pkg+"start called, but revid already running") + r.cfg.Logger.Log(logger.Warning, pkg+"start called, but revid already running") return nil } r.mu.Lock() defer r.mu.Unlock() - r.config.Logger.Log(logger.Info, pkg+"starting Revid") - err := r.reset(r.config) + r.cfg.Logger.Log(logger.Info, pkg+"starting Revid") + err := r.reset(r.cfg) if err != nil { r.Stop() return err @@ -369,7 +377,7 @@ func (r *Revid) Start() error { // Stop is safe for concurrent use. func (r *Revid) Stop() { if !r.IsRunning() { - r.config.Logger.Log(logger.Warning, pkg+"stop called but revid isn't running") + r.cfg.Logger.Log(logger.Warning, pkg+"stop called but revid isn't running") return } @@ -379,26 +387,26 @@ func (r *Revid) Stop() { if r.closeInput != nil { err := r.closeInput() if err != nil { - r.config.Logger.Log(logger.Error, pkg+"could not close input", "error", err.Error()) + r.cfg.Logger.Log(logger.Error, pkg+"could not close input", "error", err.Error()) } } - r.config.Logger.Log(logger.Info, pkg+"closing pipeline") + r.cfg.Logger.Log(logger.Info, pkg+"closing pipeline") err := r.encoders.Close() if err != nil { - r.config.Logger.Log(logger.Error, pkg+"failed to close pipeline", "error", err.Error()) + r.cfg.Logger.Log(logger.Error, pkg+"failed to close pipeline", "error", err.Error()) } - r.config.Logger.Log(logger.Info, pkg+"closed pipeline") + r.cfg.Logger.Log(logger.Info, pkg+"closed pipeline") if r.cmd != nil && r.cmd.Process != nil { - r.config.Logger.Log(logger.Info, pkg+"killing input proccess") + r.cfg.Logger.Log(logger.Info, pkg+"killing input proccess") r.cmd.Process.Kill() } - r.config.Logger.Log(logger.Info, pkg+"waiting for routines to close") + r.cfg.Logger.Log(logger.Info, pkg+"waiting for routines to close") r.wg.Wait() - r.config.Logger.Log(logger.Info, pkg+"revid stopped") + r.cfg.Logger.Log(logger.Info, pkg+"revid stopped") r.running = false } @@ -423,239 +431,239 @@ func (r *Revid) Update(vars map[string]string) error { for key, value := range vars { switch key { case "Input": - v, ok := map[string]uint8{"raspivid": InputRaspivid, "rtsp": InputRTSP}[strings.ToLower(value)] + v, ok := map[string]uint8{"raspivid": config.InputRaspivid, "rtsp": config.InputRTSP}[strings.ToLower(value)] if !ok { - r.config.Logger.Log(logger.Warning, pkg+"invalid input var", "value", value) + r.cfg.Logger.Log(logger.Warning, pkg+"invalid input var", "value", value) break } - r.config.Input = v + r.cfg.Input = v case "Saturation": s, err := strconv.ParseInt(value, 10, 0) if err != nil { - r.config.Logger.Log(logger.Warning, pkg+"invalid saturation param", "value", value) + r.cfg.Logger.Log(logger.Warning, pkg+"invalid saturation param", "value", value) break } - r.config.Saturation = int(s) + r.cfg.Saturation = int(s) case "Brightness": b, err := strconv.ParseUint(value, 10, 0) if err != nil { - r.config.Logger.Log(logger.Warning, pkg+"invalid brightness param", "value", value) + r.cfg.Logger.Log(logger.Warning, pkg+"invalid brightness param", "value", value) break } - r.config.Brightness = uint(b) + r.cfg.Brightness = uint(b) case "Exposure": - r.config.Exposure = value + r.cfg.Exposure = value case "AutoWhiteBalance": - r.config.AutoWhiteBalance = value + r.cfg.AutoWhiteBalance = value case "InputCodec": switch value { case "H264": - r.config.InputCodec = codecutil.H264 + r.cfg.InputCodec = codecutil.H264 case "MJPEG": - r.config.InputCodec = codecutil.MJPEG + r.cfg.InputCodec = codecutil.MJPEG default: - r.config.Logger.Log(logger.Warning, pkg+"invalid InputCodec variable value", "value", value) + r.cfg.Logger.Log(logger.Warning, pkg+"invalid InputCodec variable value", "value", value) } case "Output": outputs := strings.Split(value, ",") - r.config.Outputs = make([]uint8, len(outputs)) + r.cfg.Outputs = make([]uint8, len(outputs)) for i, output := range outputs { switch output { case "File": - r.config.Outputs[i] = OutputFile + r.cfg.Outputs[i] = config.OutputFile case "Http": - r.config.Outputs[i] = OutputHTTP + r.cfg.Outputs[i] = config.OutputHTTP case "Rtmp": - r.config.Outputs[i] = OutputRTMP + r.cfg.Outputs[i] = config.OutputRTMP case "Rtp": - r.config.Outputs[i] = OutputRTP + r.cfg.Outputs[i] = config.OutputRTP default: - r.config.Logger.Log(logger.Warning, pkg+"invalid output param", "value", value) + r.cfg.Logger.Log(logger.Warning, pkg+"invalid output param", "value", value) continue } } case "RtmpUrl": - r.config.RTMPURL = value + r.cfg.RTMPURL = value case "RtpAddress": - r.config.RTPAddress = value + r.cfg.RTPAddress = value case "Bitrate": v, err := strconv.ParseUint(value, 10, 0) if err != nil { - r.config.Logger.Log(logger.Warning, pkg+"invalid framerate param", "value", value) + r.cfg.Logger.Log(logger.Warning, pkg+"invalid framerate param", "value", value) break } - r.config.Bitrate = uint(v) + r.cfg.Bitrate = uint(v) case "OutputPath": - r.config.OutputPath = value + r.cfg.OutputPath = value case "InputPath": - r.config.InputPath = value + r.cfg.InputPath = value case "Height": h, err := strconv.ParseUint(value, 10, 0) if err != nil { - r.config.Logger.Log(logger.Warning, pkg+"invalid height param", "value", value) + r.cfg.Logger.Log(logger.Warning, pkg+"invalid height param", "value", value) break } - r.config.Height = uint(h) + r.cfg.Height = uint(h) case "Width": w, err := strconv.ParseUint(value, 10, 0) if err != nil { - r.config.Logger.Log(logger.Warning, pkg+"invalid width param", "value", value) + r.cfg.Logger.Log(logger.Warning, pkg+"invalid width param", "value", value) break } - r.config.Width = uint(w) + r.cfg.Width = uint(w) case "FrameRate": v, err := strconv.ParseUint(value, 10, 0) if err != nil { - r.config.Logger.Log(logger.Warning, pkg+"invalid framerate param", "value", value) + r.cfg.Logger.Log(logger.Warning, pkg+"invalid framerate param", "value", value) break } - r.config.FrameRate = uint(v) + r.cfg.FrameRate = uint(v) case "Rotation": v, err := strconv.ParseUint(value, 10, 0) if err != nil || v > 359 { - r.config.Logger.Log(logger.Warning, pkg+"invalid rotation param", "value", value) + r.cfg.Logger.Log(logger.Warning, pkg+"invalid rotation param", "value", value) break } - r.config.Rotation = uint(v) + r.cfg.Rotation = uint(v) case "HttpAddress": - r.config.HTTPAddress = value + r.cfg.HTTPAddress = value case "Quantization": v, err := strconv.Atoi(value) if err != nil { - r.config.Logger.Log(logger.Warning, pkg+"invalid quantization param", "value", v) + r.cfg.Logger.Log(logger.Warning, pkg+"invalid quantization param", "value", v) break } - r.config.Quantization = uint(v) + r.cfg.Quantization = uint(v) case "MinFrames": v, err := strconv.Atoi(value) if err != nil { - r.config.Logger.Log(logger.Warning, pkg+"invalid MinFrames param", "value", value) + r.cfg.Logger.Log(logger.Warning, pkg+"invalid MinFrames param", "value", value) break } - r.config.MinFrames = uint(v) + r.cfg.MinFrames = uint(v) case "ClipDuration": v, err := strconv.Atoi(value) if err != nil { - r.config.Logger.Log(logger.Warning, pkg+"invalid ClipDuration param", "value", value) + r.cfg.Logger.Log(logger.Warning, pkg+"invalid ClipDuration param", "value", value) break } - r.config.ClipDuration = time.Duration(v) * time.Second + r.cfg.ClipDuration = time.Duration(v) * time.Second case "HorizontalFlip": switch strings.ToLower(value) { case "true": - r.config.FlipHorizontal = true + r.cfg.FlipHorizontal = true case "false": - r.config.FlipHorizontal = false + r.cfg.FlipHorizontal = false default: - r.config.Logger.Log(logger.Warning, pkg+"invalid HorizontalFlip param", "value", value) + r.cfg.Logger.Log(logger.Warning, pkg+"invalid HorizontalFlip param", "value", value) } case "VerticalFlip": switch strings.ToLower(value) { case "true": - r.config.FlipVertical = true + r.cfg.FlipVertical = true case "false": - r.config.FlipVertical = false + r.cfg.FlipVertical = false default: - r.config.Logger.Log(logger.Warning, pkg+"invalid VerticalFlip param", "value", value) + r.cfg.Logger.Log(logger.Warning, pkg+"invalid VerticalFlip param", "value", value) } case "BurstPeriod": v, err := strconv.ParseUint(value, 10, 0) if err != nil { - r.config.Logger.Log(logger.Warning, pkg+"invalid BurstPeriod param", "value", value) + r.cfg.Logger.Log(logger.Warning, pkg+"invalid BurstPeriod param", "value", value) break } - r.config.BurstPeriod = uint(v) + r.cfg.BurstPeriod = uint(v) case "Logging": switch value { case "Debug": - r.config.LogLevel = logger.Debug + r.cfg.LogLevel = logger.Debug case "Info": - r.config.LogLevel = logger.Info + r.cfg.LogLevel = logger.Info case "Warning": - r.config.LogLevel = logger.Warning + r.cfg.LogLevel = logger.Warning case "Error": - r.config.LogLevel = logger.Error + r.cfg.LogLevel = logger.Error case "Fatal": - r.config.LogLevel = logger.Fatal + r.cfg.LogLevel = logger.Fatal default: - r.config.Logger.Log(logger.Warning, pkg+"invalid Logging param", "value", value) + r.cfg.Logger.Log(logger.Warning, pkg+"invalid Logging param", "value", value) } case "RTMPRBSize": v, err := strconv.Atoi(value) if err != nil || v < 0 { - r.config.Logger.Log(logger.Warning, pkg+"invalid RTMPRBSize var", "value", value) + r.cfg.Logger.Log(logger.Warning, pkg+"invalid RTMPRBSize var", "value", value) break } - r.config.RTMPRBSize = v + r.cfg.RTMPRBSize = v case "RTMPRBElementSize": v, err := strconv.Atoi(value) if err != nil || v < 0 { - r.config.Logger.Log(logger.Warning, pkg+"invalid RTMPRBElementSize var", "value", value) + r.cfg.Logger.Log(logger.Warning, pkg+"invalid RTMPRBElementSize var", "value", value) break } - r.config.RTMPRBElementSize = v + r.cfg.RTMPRBElementSize = v case "RTMPRBWriteTimeout": v, err := strconv.Atoi(value) if err != nil || v <= 0 { - r.config.Logger.Log(logger.Warning, pkg+"invalid RTMPRBWriteTimeout var", "value", value) + r.cfg.Logger.Log(logger.Warning, pkg+"invalid RTMPRBWriteTimeout var", "value", value) break } - r.config.RTMPRBWriteTimeout = v + r.cfg.RTMPRBWriteTimeout = v case "MTSRBSize": v, err := strconv.Atoi(value) if err != nil || v < 0 { - r.config.Logger.Log(logger.Warning, pkg+"invalid MTSRBSize var", "value", value) + r.cfg.Logger.Log(logger.Warning, pkg+"invalid MTSRBSize var", "value", value) break } - r.config.MTSRBSize = v + r.cfg.MTSRBSize = v case "MTSRBElementSize": v, err := strconv.Atoi(value) if err != nil || v < 0 { - r.config.Logger.Log(logger.Warning, pkg+"invalid MTSRBElementSize var", "value", value) + r.cfg.Logger.Log(logger.Warning, pkg+"invalid MTSRBElementSize var", "value", value) break } - r.config.MTSRBElementSize = v + r.cfg.MTSRBElementSize = v case "MTSRBWriteTimeout": v, err := strconv.Atoi(value) if err != nil || v <= 0 { - r.config.Logger.Log(logger.Warning, pkg+"invalid MTSRBWriteTimeout var", "value", value) + r.cfg.Logger.Log(logger.Warning, pkg+"invalid MTSRBWriteTimeout var", "value", value) break } - r.config.MTSRBWriteTimeout = v + r.cfg.MTSRBWriteTimeout = v case "VBR": v, ok := map[string]bool{"true": true, "false": false}[strings.ToLower(value)] if !ok { - r.config.Logger.Log(logger.Warning, pkg+"invalid VBR var", "value", value) + r.cfg.Logger.Log(logger.Warning, pkg+"invalid VBR var", "value", value) break } - r.config.VBR = v + r.cfg.VBR = v case "VBRQuality": - v, ok := map[string]quality{"standard": qualityStandard, "fair": qualityFair, "good": qualityGood, "great": qualityGreat, "excellent": qualityExcellent}[strings.ToLower(value)] + 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.config.Logger.Log(logger.Warning, pkg+"invalid VBRQuality var", "value", value) + r.cfg.Logger.Log(logger.Warning, pkg+"invalid VBRQuality var", "value", value) break } - r.config.VBRQuality = v + r.cfg.VBRQuality = v case "VBRBitrate": v, err := strconv.Atoi(value) if err != nil || v <= 0 { - r.config.Logger.Log(logger.Warning, pkg+"invalid VBRBitrate var", "value", value) + r.cfg.Logger.Log(logger.Warning, pkg+"invalid VBRBitrate var", "value", value) break } - r.config.VBRBitrate = v + r.cfg.VBRBitrate = v case "CameraChan": v, err := strconv.Atoi(value) if err != nil || (v != 1 && v != 2) { - r.config.Logger.Log(logger.Warning, pkg+"invalid CameraChan var", "value", value) + r.cfg.Logger.Log(logger.Warning, pkg+"invalid CameraChan var", "value", value) break } - r.config.CameraChan = v + r.cfg.CameraChan = v } } - r.config.Logger.Log(logger.Info, pkg+"revid config changed", "config", fmt.Sprintf("%+v", r.config)) + r.cfg.Logger.Log(logger.Info, pkg+"revid config changed", "config", fmt.Sprintf("%+v", r.cfg)) return nil } diff --git a/revid/revid_test.go b/revid/revid_test.go index a7e91323..3fbdb7f7 100644 --- a/revid/revid_test.go +++ b/revid/revid_test.go @@ -35,6 +35,7 @@ import ( "runtime" "testing" + "bitbucket.org/ausocean/av/revid/config" "bitbucket.org/ausocean/iot/pi/netsender" ) @@ -56,9 +57,9 @@ func TestRaspivid(t *testing.T) { t.Errorf("netsender.New failed with error %v", err) } - var c Config + var c config.Config c.Logger = &logger - c.Input = InputRaspivid + c.Input = config.InputRaspivid rv, err := New(c, ns) if err != nil { @@ -148,7 +149,7 @@ func TestResetEncoderSenderSetup(t *testing.T) { encoders []encoder }{ { - outputs: []uint8{OutputHTTP}, + outputs: []uint8{config.OutputHTTP}, encoders: []encoder{ { encoderType: mtsEncoderStr, @@ -157,7 +158,7 @@ func TestResetEncoderSenderSetup(t *testing.T) { }, }, { - outputs: []uint8{OutputRTMP}, + outputs: []uint8{config.OutputRTMP}, encoders: []encoder{ { encoderType: flvEncoderStr, @@ -166,7 +167,7 @@ func TestResetEncoderSenderSetup(t *testing.T) { }, }, { - outputs: []uint8{OutputRTP}, + outputs: []uint8{config.OutputRTP}, encoders: []encoder{ { encoderType: mtsEncoderStr, @@ -175,7 +176,7 @@ func TestResetEncoderSenderSetup(t *testing.T) { }, }, { - outputs: []uint8{OutputHTTP, OutputRTMP}, + outputs: []uint8{config.OutputHTTP, config.OutputRTMP}, encoders: []encoder{ { encoderType: mtsEncoderStr, @@ -188,7 +189,7 @@ func TestResetEncoderSenderSetup(t *testing.T) { }, }, { - outputs: []uint8{OutputHTTP, OutputRTP, OutputRTMP}, + outputs: []uint8{config.OutputHTTP, config.OutputRTP, config.OutputRTMP}, encoders: []encoder{ { encoderType: mtsEncoderStr, @@ -201,7 +202,7 @@ func TestResetEncoderSenderSetup(t *testing.T) { }, }, { - outputs: []uint8{OutputRTP, OutputRTMP}, + outputs: []uint8{config.OutputRTP, config.OutputRTMP}, encoders: []encoder{ { encoderType: mtsEncoderStr, @@ -215,7 +216,7 @@ func TestResetEncoderSenderSetup(t *testing.T) { }, } - rv, err := New(Config{Logger: &testLogger{}}, nil) + rv, err := New(config.Config{Logger: &testLogger{}}, nil) if err != nil { t.Fatalf("unexpected err: %v", err) } @@ -224,7 +225,7 @@ func TestResetEncoderSenderSetup(t *testing.T) { for testNum, test := range tests { // Create a new config and reset revid with it. const dummyURL = "rtmp://dummy" - c := Config{Logger: &testLogger{}, Outputs: test.outputs, RTMPURL: dummyURL} + c := config.Config{Logger: &testLogger{}, Outputs: test.outputs, RTMPURL: dummyURL} err := rv.setConfig(c) if err != nil { t.Fatalf("unexpected error: %v for test %v", err, testNum) From 61b1059205779c37871afb1e76b1fc5a29a6a71e Mon Sep 17 00:00:00 2001 From: Saxon Date: Wed, 6 Nov 2019 17:47:14 +1030 Subject: [PATCH 20/21] device/device.go: fixed file header --- device/device.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/device/device.go b/device/device.go index 1801be20..c66c45e3 100644 --- a/device/device.go +++ b/device/device.go @@ -1,6 +1,6 @@ /* DESCRIPTION - device.go provides AVDevice, and interface that describes a configurable + device.go provides AVDevice, an interface that describes a configurable audio or video device that can be started and stopped from which data may be obtained. From 9314e0d32e1fe644c9a1839eba135613b9e3d991 Mon Sep 17 00:00:00 2001 From: Saxon Date: Wed, 6 Nov 2019 21:26:39 +1030 Subject: [PATCH 21/21] revid/config/config.go: fixed constant names in comments for Input and Outputs fields of Config --- revid/config/config.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/revid/config/config.go b/revid/config/config.go index 22dd3543..a8f43463 100644 --- a/revid/config/config.go +++ b/revid/config/config.go @@ -169,13 +169,13 @@ type Config struct { // Input defines the input data source. // // Valid values are defined by enums: - // Raspivid: + // InputRaspivid: // Read data from a Raspberry Pi Camera. - // V4l: + // InputV4l: // Read from webcam. - // File: + // InputFile: // Location must be specified in InputPath field. - // RTSP: + // InputRTSP: // CameraIP should also be defined. Input uint8 @@ -187,16 +187,16 @@ type Config struct { // Outputs define the outputs we wish to output data too. // // Valid outputs are defined by enums: - // File: + // OutputFile: // Location must be defined by the OutputPath field. MPEG-TS packetization // is used. - // HTTP: + // OutputHTTP: // Destination is defined by the sh field located in /etc/netsender.conf. // MPEGT-TS packetization is used. - // RTMP: + // OutputRTMP: // Destination URL must be defined in the RtmpUrl field. FLV packetization // is used. - // RTP: + // OutputRTP: // Destination is defined by RtpAddr field, otherwise it will default to // localhost:6970. MPEGT-TS packetization is used. Outputs []uint8