From e56455f7d02bb1c995290b70d0369d625022d2aa Mon Sep 17 00:00:00 2001 From: Saxon Date: Fri, 11 Oct 2019 20:16:21 +1030 Subject: [PATCH 01/19] input/gvctrl: added Set function with functional options for controlling GeoVision Added a file called gvctrl.go which holds all exported functions, including Set, and the options available for use with Set. This file also holds important consts and the settings struct. Also added a file called request.go, which houses 3 functions that are in charge of creating HTTP requests, firstly to get the log in page from which a log in request body can be generated, then to submit the generated log in request body, and then to submit the settings. Finally a utils.go file has been added to house a few helper functions. --- input/gvctrl/go.mod | 3 + input/gvctrl/gvctrl.go | 225 ++++++++++++++++++++++++++++++++++++++++ input/gvctrl/request.go | 210 +++++++++++++++++++++++++++++++++++++ input/gvctrl/utils.go | 54 ++++++++++ 4 files changed, 492 insertions(+) create mode 100644 input/gvctrl/go.mod create mode 100644 input/gvctrl/gvctrl.go create mode 100644 input/gvctrl/request.go create mode 100644 input/gvctrl/utils.go diff --git a/input/gvctrl/go.mod b/input/gvctrl/go.mod new file mode 100644 index 00000000..1ffc4877 --- /dev/null +++ b/input/gvctrl/go.mod @@ -0,0 +1,3 @@ +module bitbucket.org/ausocean/av/input/gvctrl + +go 1.12 diff --git a/input/gvctrl/gvctrl.go b/input/gvctrl/gvctrl.go new file mode 100644 index 00000000..21b6e583 --- /dev/null +++ b/input/gvctrl/gvctrl.go @@ -0,0 +1,225 @@ +/* +DESCRIPTION + gvctrl.go provides a programmatic interface for controlling the HTTP based + server hosted by GeoVision cameras for settings control. + +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 gvctrl + +import ( + "fmt" + "math/rand" + "net/http" + "net/http/cookiejar" + "strconv" + "time" +) + +type codec string + +const ( + codecH265 codec = "28" + codecH264 codec = "10" + codecMJPEG codec = "4" +) + +type quality string + +const ( + qualityStandard quality = "0" + qualityFair quality = "1" + qualityGood quality = "2" + qualityGreat quality = "3" + qualityExcellent quality = "4" +) + +const ( + defaultCodec = codecH264 + defaultRes = "6400360" // 360p + defaultFrameRate = "25000" // 25 fps + defaultVBR = "0" // Variable bitrate off + defaultQuality = qualityGood + defaultBitRate = "512000" // 512 kbps (lowest with 360p) + defaultRefresh = "2000" // 2 seconds +) + +type settings struct { + codec codec + res string + frameRate string + vbr string + quality quality + bitRate string + refresh string +} + +func newSettings() settings { + return settings{ + codec: defaultCodec, + res: defaultRes, + frameRate: defaultFrameRate, + vbr: defaultVBR, + quality: defaultQuality, + bitRate: defaultBitRate, + refresh: defaultRefresh, + } +} + +type option func(s settings) error + +func Set(host string, options ...option) error { + // Randomly generate an ID our client will use. + const ( + minID = 10000 + maxID = 99999 + ) + rand.Seed(time.Now().UTC().UnixNano()) + id := strconv.Itoa(maxID + rand.Intn(maxID-minID)) + + // Create a client with a cookie jar. + jar, err := cookiejar.New(nil) + if err != nil { + return fmt.Errorf("could not create cookie jar, failed with error: %v", err) + } + + client := &http.Client{ + Timeout: time.Duration(5 * time.Second), + Jar: jar, + } + + // Get the request body required for log in. + body, err := genLogIn(client, id, host) + if err != nil { + return fmt.Errorf("could not generate log in request data: %v", err) + } + + // Log in using generated log in request body. + err = logIn(client, id, host, body) + if err != nil { + return fmt.Errorf("could not logIn: %v", err) + } + + // Apply the options to the settings specified by the user. + s := newSettings() + for _, op := range options { + err = op(s) + if err != nil { + return fmt.Errorf("could not action option: %v", err) + } + } + + // Submit the settings to the server. + err = submitSettings(client, id, host, s) + if err != nil { + return fmt.Errorf("could not submit settings: %v", err) + } + return nil +} + +func Codec(c codec) option { + return func(s settings) error { + switch c { + case codecH265, codecH264, codecMJPEG: + s.codec = c + return nil + default: + return fmt.Errorf("unknown codec: %v", c) + } + } +} + +func Height(h int) option { + return func(s settings) error { + v, ok := map[int]string{256: "4480256", 360: "6400360", 720: "12800720"}[h] + if !ok { + return fmt.Errorf("invalid display height: %d", h) + } + s.res = v + return nil + } +} + +func FrameRate(f int) option { + return func(s settings) error { + if 1 > f || f > 30 { + return fmt.Errorf("invalid frame rate: %d", f) + } + s.frameRate = strconv.Itoa(f * 1000) + return nil + } +} + +func VariableBitRate(b bool) option { + return func(s settings) error { + s.vbr = "0" + if b { + s.vbr = "1" + } + return nil + } +} + +func Quality(q quality) option { + return func(s settings) error { + switch q { + case qualityStandard, qualityFair, qualityGood, qualityGreat, qualityExcellent: + s.quality = q + return nil + default: + return fmt.Errorf("invalid quality: %v", q) + } + } +} + +func BitRate(r int) option { + return func(s settings) error { + var ( + vbrRates = []int{2000, 4000, 6000, 8000, 10000, 12000, 14000, 16000, 18000, 20000} + cbrRates256 = []int{128, 256, 512, 1024} + cbrRates360 = []int{512, 1024, 2048, 3072} + cbrRates720 = []int{1024, 2048, 4096, 6144} + ) + + if s.vbr == "1" { + s.bitRate = convRate(r, vbrRates) + return nil + } + + switch s.res { + case "12800720": + s.bitRate = convRate(r, cbrRates720) + case "6400360": + s.bitRate = convRate(r, cbrRates360) + case "4480256": + s.bitRate = convRate(r, cbrRates256) + default: + panic("bad resolution") + } + return nil + } +} + +func Refresh(r int) option { + return func(settings) error { + return nil + } +} diff --git a/input/gvctrl/request.go b/input/gvctrl/request.go new file mode 100644 index 00000000..5e26ffba --- /dev/null +++ b/input/gvctrl/request.go @@ -0,0 +1,210 @@ +/* +DESCRIPTION + request.go provides unexported functionality for creating and sending requests + required to configure settings of the GeoVision camera. + +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 gvctrl + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "regexp" + "strconv" +) + +const ( + baseURL = "http://192.168.1.50" + loginPageURL = baseURL + "/ssi.cgi/login.htm" + loggedInURL = baseURL + "/LoginPC.cgi" + settingsURL = baseURL + "/VideoSetting.cgi" +) + +const ( + user = "admin" + pass = "admin" +) + +func genLogIn(c *http.Client, id, host string) (string, error) { + req, err := http.NewRequest("GET", loginPageURL, nil) + if err != nil { + return "", fmt.Errorf("can't create GET request for log in page: %v", err) + } + + req.Header.Set("Connection", "keep-alive") + req.Header.Set("Cache-Control", "max-age=0") + req.Header.Set("Upgrade-Insecure-Requests", "1") + req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36") + req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3") + req.Header.Set("Accept-Encoding", "gzip, deflate") + req.Header.Set("Accept-Language", "en-GB,en-US;q=0.9,en;q=0.8") + req.Header.Set("Cookie", "CLIENT_ID="+id) + + resp, err := c.Do(req) + if err != nil { + return "", fmt.Errorf("could not do GET request for log in page: %v", err) + } + + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("could not read response of GET request for log in page: %v", err) + } + + // Find the CC values in the source of the response. + // These are used in calculation of the md5 hashes for the form submitted at + // log in. + var cc [2]string + for i := range cc { + regStr := "cc" + strconv.Itoa(i+1) + "=\".{4}\"" + exp := regexp.MustCompile(regStr).FindString(string(body)) + cc[i] = exp[5 : len(exp)-1] + } + + var data url.Values + data.Set("grp", "-1") + data.Set("username", "") + data.Set("password", "") + data.Set("Apply", "Apply") + data.Set("umd5", md5Hex(cc[0]+user+cc[1])) + data.Set("pmd5", md5Hex(cc[1]+pass+cc[0])) + data.Set("browser", "1") + data.Set("is_check_OCX_OK", "0") + + return data.Encode(), nil +} + +func logIn(c *http.Client, id, host, b string) error { + req, err := http.NewRequest("POST", loggedInURL, bytes.NewBuffer([]byte(b))) + if err != nil { + return fmt.Errorf("could not create log in request: %v", err) + } + + req.Header.Set("Connection", "keep-alive") + req.Header.Set("Content-Length", "142") + req.Header.Set("Cache-Control", "max-age=0") + req.Header.Set("Origin", "http://192.168.1.50") + req.Header.Set("Upgrade-Insecure-Requests", "1") + req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36") + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3") + req.Header.Set("Referer", "http://192.168.1.50/ssi.cgi/Login.htm") + req.Header.Set("Accept-Encoding", "gzip, deflate") + req.Header.Set("Accept-Language", "en-GB,en-US;q=0.9,en;q=0.8") + req.Header.Set("Cookie", "CLIENT_ID="+id+"; CLIENT_ID="+id) + + _, err = c.Do(req) + if err != nil { + return fmt.Errorf("could not do log in request: %v", err) + } + + return nil +} + +func submitSettings(c *http.Client, id, host string, s settings) error { + var f url.Values + f.Set("dwConnType", "5") + f.Set("mpeg_type", "10") + f.Set("dwflicker_hz", "0") + f.Set("szResolution", s.res) + f.Set("dwFrameRate", s.frameRate) + + if s.codec == codecMJPEG { + f.Set("vbr_enable", "1") + f.Set("dwVbrQuality", s.vbr) + f.Set("vbrmaxbitrate", "750000") + } else { + switch s.vbr { + case "0": + f.Set("vbr_enable", "0") + f.Set("max_bit_rate", s.bitRate) + case "1": + f.Set("vbr_enable", "1") + f.Set("dwVbrQuality", s.vbr) + f.Set("vbrmaxbitrate", s.bitRate) + default: + panic("invalid vbrEnable parameter") + } + + f.Set("custom_rate_control_type", "0") + f.Set("custom_bitrate", "512000") + f.Set("custom_qp_init", "25") + f.Set("custom_qp_min", "10") + f.Set("custom_qp_max", "40") + } + + f.Set("gop_N", s.refresh) + + if s.codec == codecH264 || s.codec == codecH265 { + f.Set("dwEncProfile", "3") + f.Set("dwEncLevel", "31") + f.Set("dwEntropy", "0") + } + + f.Set("u8PreAlarmBuf", "1") + f.Set("u32PostAlarmBuf2Disk", "1") + f.Set("u8SplitInterval", "5") + f.Set("bEnableIO", "1") + f.Set("bEbIoIn", "1") + f.Set("bEbIoIn1", "1") + f.Set("bOSDFontSize", "0") + f.Set("bEnableOSDCameraName", "1") + f.Set("bCamNamePos", "2") + f.Set("bEnableOSDDate", "1") + f.Set("bDatePos", "2") + f.Set("bEnableOSDTime", "1") + f.Set("bTimePos", "2") + f.Set("szOsdCamName", "Camera") + f.Set("u16PostAlarmBuf", "1") + f.Set("dwCameraId", "1") // Channel=1 => cameraID=0 and chanel=2 => cameraID=1 + f.Set("LangCode", "undefined") + f.Set("Recflag", "0") + f.Set("submit", "Apply") + fBytes := []byte(f.Encode()) + + req, err := http.NewRequest("POST", settingsURL, bytes.NewReader(fBytes)) + if err != nil { + return fmt.Errorf("could not create settings submit request: %v", err) + } + + req.Header.Set("Connection", "keep-alive") + req.Header.Set("Content-Length", strconv.Itoa(len(fBytes))) + req.Header.Set("Cache-Control", "max-age=0") + req.Header.Set("Origin", "http://192.168.1.50") + req.Header.Set("Upgrade-Insecure-Requests", "1") + req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36") + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3") + req.Header.Set("Referer", "http://192.168.1.50/ssi.cgi/VideoSettingSub.htm?cam=2") + req.Header.Set("Accept-Encoding", "gzip, deflate") + req.Header.Set("Accept-Language", "en-GB,en-US;q=0.9,en;q=0.8") + req.Header.Set("Cookie", "CLIENT_ID="+id) + + // NB: not capturing error, as we always get one here for some reason. + // TODO: figure out why. Does not affect submission. + c.Do(req) + + return nil +} diff --git a/input/gvctrl/utils.go b/input/gvctrl/utils.go new file mode 100644 index 00000000..21986341 --- /dev/null +++ b/input/gvctrl/utils.go @@ -0,0 +1,54 @@ +/* +DESCRIPTION + utils.go provides helper functions for functionality found in both gvctrl.go + request.go. + +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 gvctrl + +import ( + "crypto/md5" + "encoding/hex" + "math" + "strconv" + "strings" +) + +func md5Hex(s string) string { + h := md5.New() + h.Write([]byte(s)) + return strings.ToUpper(hex.EncodeToString(h.Sum(nil))) +} + +func closestValIdx(v int, l []int) int { + var idx int + for i := range l { + if math.Abs(float64(l[i]-v)) < math.Abs(float64(l[idx]-v)) { + idx = i + } + } + return idx +} + +func convRate(v int, l []int) string { + return strconv.Itoa(l[closestValIdx(v, l)] * 1000) +} From bab1a510b137942c506da2f6680c1effeada9d7b Mon Sep 17 00:00:00 2001 From: Saxon Date: Sat, 12 Oct 2019 15:39:27 +1030 Subject: [PATCH 02/19] input/gvctrl/gvctrl.go: renamed convRate function to closestValStr to generalise and finished Refresh option function --- input/gvctrl/gvctrl.go | 24 ++++++++++++++++++------ input/gvctrl/utils.go | 2 +- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/input/gvctrl/gvctrl.go b/input/gvctrl/gvctrl.go index 21b6e583..49065d82 100644 --- a/input/gvctrl/gvctrl.go +++ b/input/gvctrl/gvctrl.go @@ -190,6 +190,7 @@ func Quality(q quality) option { } } +// TODO: add check of r func BitRate(r int) option { return func(s settings) error { var ( @@ -200,17 +201,17 @@ func BitRate(r int) option { ) if s.vbr == "1" { - s.bitRate = convRate(r, vbrRates) + s.bitRate = closestValStr(r, vbrRates) return nil } switch s.res { case "12800720": - s.bitRate = convRate(r, cbrRates720) + s.bitRate = closestValStr(r, cbrRates720) case "6400360": - s.bitRate = convRate(r, cbrRates360) + s.bitRate = closestValStr(r, cbrRates360) case "4480256": - s.bitRate = convRate(r, cbrRates256) + s.bitRate = closestValStr(r, cbrRates256) default: panic("bad resolution") } @@ -218,8 +219,19 @@ func BitRate(r int) option { } } -func Refresh(r int) option { - return func(settings) error { +func Refresh(r float64) option { + return func(s settings) error { + const ( + maxRefreshPeriod = 5 + minRefreshPeriod = .25 + ) + + if minRefreshPeriod > r || r > maxRefreshPeriod { + return fmt.Errorf("invalid refresh period: %g", r) + } + + refOptions := []int{250, 500, 1000, 1500, 2000, 2500, 3000, 3500, 4000, 4500, 5000} + s.refresh = closestValStr(int(r*1000), refOptions) return nil } } diff --git a/input/gvctrl/utils.go b/input/gvctrl/utils.go index 21986341..ddd4a838 100644 --- a/input/gvctrl/utils.go +++ b/input/gvctrl/utils.go @@ -49,6 +49,6 @@ func closestValIdx(v int, l []int) int { return idx } -func convRate(v int, l []int) string { +func closestValStr(v int, l []int) string { return strconv.Itoa(l[closestValIdx(v, l)] * 1000) } From dc466b039bfcc7de98b99709eb56a14453fcdc28 Mon Sep 17 00:00:00 2001 From: Saxon Date: Sat, 12 Oct 2019 15:52:35 +1030 Subject: [PATCH 03/19] input/gvctrl/gvctrl.go: created consts for resolution strings --- input/gvctrl/gvctrl.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/input/gvctrl/gvctrl.go b/input/gvctrl/gvctrl.go index 49065d82..01d7fc56 100644 --- a/input/gvctrl/gvctrl.go +++ b/input/gvctrl/gvctrl.go @@ -52,6 +52,12 @@ const ( qualityExcellent quality = "4" ) +const ( + res256 = "4480256" + res360 = "6400360" + res720 = "12800720" +) + const ( defaultCodec = codecH264 defaultRes = "6400360" // 360p @@ -149,7 +155,7 @@ func Codec(c codec) option { func Height(h int) option { return func(s settings) error { - v, ok := map[int]string{256: "4480256", 360: "6400360", 720: "12800720"}[h] + v, ok := map[int]string{256: res256, 360: res360, 720: res720}[h] if !ok { return fmt.Errorf("invalid display height: %d", h) } @@ -206,11 +212,11 @@ func BitRate(r int) option { } switch s.res { - case "12800720": + case res720: s.bitRate = closestValStr(r, cbrRates720) - case "6400360": + case res360: s.bitRate = closestValStr(r, cbrRates360) - case "4480256": + case res256: s.bitRate = closestValStr(r, cbrRates256) default: panic("bad resolution") From 366226bb29f32efc2e51f46e0ba3739ebd54337c Mon Sep 17 00:00:00 2001 From: Saxon Date: Sat, 12 Oct 2019 16:48:35 +1030 Subject: [PATCH 04/19] input/gvctrl: changed name of closestValStr back to convRate as can only be used by Bitrate options function --- input/gvctrl/gvctrl.go | 10 +++++----- input/gvctrl/utils.go | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/input/gvctrl/gvctrl.go b/input/gvctrl/gvctrl.go index 01d7fc56..19d98101 100644 --- a/input/gvctrl/gvctrl.go +++ b/input/gvctrl/gvctrl.go @@ -207,17 +207,17 @@ func BitRate(r int) option { ) if s.vbr == "1" { - s.bitRate = closestValStr(r, vbrRates) + s.bitRate = convRate(r, vbrRates) return nil } switch s.res { case res720: - s.bitRate = closestValStr(r, cbrRates720) + s.bitRate = convRate(r, cbrRates720) case res360: - s.bitRate = closestValStr(r, cbrRates360) + s.bitRate = convRate(r, cbrRates360) case res256: - s.bitRate = closestValStr(r, cbrRates256) + s.bitRate = convRate(r, cbrRates256) default: panic("bad resolution") } @@ -237,7 +237,7 @@ func Refresh(r float64) option { } refOptions := []int{250, 500, 1000, 1500, 2000, 2500, 3000, 3500, 4000, 4500, 5000} - s.refresh = closestValStr(int(r*1000), refOptions) + s.refresh = strconv.Itoa(refOptions[closestValIdx(int(r*1000), refOptions)]) return nil } } diff --git a/input/gvctrl/utils.go b/input/gvctrl/utils.go index ddd4a838..21986341 100644 --- a/input/gvctrl/utils.go +++ b/input/gvctrl/utils.go @@ -49,6 +49,6 @@ func closestValIdx(v int, l []int) int { return idx } -func closestValStr(v int, l []int) string { +func convRate(v int, l []int) string { return strconv.Itoa(l[closestValIdx(v, l)] * 1000) } From caa46939b48b119e04714fc9e4b309ad940fd6f9 Mon Sep 17 00:00:00 2001 From: Saxon Date: Sat, 12 Oct 2019 16:58:15 +1030 Subject: [PATCH 05/19] input/gvctrl/gvctrl_test.go: added gvctrl_test.go file and wrote test for closestValIdx --- input/gvctrl/gvctrl_test.go | 78 +++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 input/gvctrl/gvctrl_test.go diff --git a/input/gvctrl/gvctrl_test.go b/input/gvctrl/gvctrl_test.go new file mode 100644 index 00000000..15569cf3 --- /dev/null +++ b/input/gvctrl/gvctrl_test.go @@ -0,0 +1,78 @@ +/* +DESCRIPTION + gvctrl_test.go provides tests of functionality in the gvctrl package. + +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 gvctrl + +import "testing" + +func TestClosestValIdx(t *testing.T) { + tests := []struct { + l []int + v int + want int + }{ + { + l: []int{2, 5, 8, 11, 14}, + v: 6, + want: 1, + }, + { + l: []int{2, 5, 8, 11, 14}, + v: 12, + want: 3, + }, + { + l: []int{2, 5, 8, 11, 14}, + v: 13, + want: 4, + }, + { + l: []int{2, 5, 8, 11, 14}, + v: 0, + want: 0, + }, + { + l: []int{2, 5, 8, 11, 14}, + v: 17, + want: 4, + }, + { + l: []int{2, 5, 8, 11, 15}, + v: 13, + want: 3, + }, + { + l: []int{}, + v: 17, + want: 0, + }, + } + + for i, test := range tests { + got := closestValIdx(test.v, test.l) + if got != test.want { + t.Errorf("did not get expected result for test: %d\nGot: %v\nWant: %v", i, got, test.want) + } + } +} From de915bcb009c44726b70509d90db12bb0803b957 Mon Sep 17 00:00:00 2001 From: Saxon Date: Sat, 12 Oct 2019 17:05:44 +1030 Subject: [PATCH 06/19] input/gvctrl/gvctrl_test.go: added test for convRate --- input/gvctrl/gvctrl_test.go | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/input/gvctrl/gvctrl_test.go b/input/gvctrl/gvctrl_test.go index 15569cf3..d0dca01e 100644 --- a/input/gvctrl/gvctrl_test.go +++ b/input/gvctrl/gvctrl_test.go @@ -76,3 +76,34 @@ func TestClosestValIdx(t *testing.T) { } } } + +func TestConvRate(t *testing.T) { + tests := []struct { + l []int + v int + want string + }{ + { + l: []int{512, 1024, 2048, 3072}, + v: 1400, + want: "1024000", + }, + { + l: []int{512, 1024, 2048, 3072}, + v: 1900, + want: "2048000", + }, + { + l: []int{512, 1024, 2048, 3072}, + v: 4000, + want: "3072000", + }, + } + + for i, test := range tests { + got := convRate(test.v, test.l) + if got != test.want { + t.Errorf("did not get expected result for test: %d\nGot: %v\nWant: %v", i, got, test.want) + } + } +} From ad83376cd6f9879ccd091e56fe37639a7ab97563 Mon Sep 17 00:00:00 2001 From: Saxon Date: Sat, 12 Oct 2019 17:27:09 +1030 Subject: [PATCH 07/19] input/gvctrl/gvctrl_test.go: added test for Height option function and fixed how option functions work with the settings struct --- input/gvctrl/gvctrl.go | 44 ++++++++++++++++++------------------- input/gvctrl/gvctrl_test.go | 43 +++++++++++++++++++++++++++++++++++- 2 files changed, 64 insertions(+), 23 deletions(-) diff --git a/input/gvctrl/gvctrl.go b/input/gvctrl/gvctrl.go index 19d98101..7d42102e 100644 --- a/input/gvctrl/gvctrl.go +++ b/input/gvctrl/gvctrl.go @@ -90,7 +90,7 @@ func newSettings() settings { } } -type option func(s settings) error +type option func(s settings) (settings, error) func Set(host string, options ...option) error { // Randomly generate an ID our client will use. @@ -127,7 +127,7 @@ func Set(host string, options ...option) error { // Apply the options to the settings specified by the user. s := newSettings() for _, op := range options { - err = op(s) + s, err = op(s) if err != nil { return fmt.Errorf("could not action option: %v", err) } @@ -142,63 +142,63 @@ func Set(host string, options ...option) error { } func Codec(c codec) option { - return func(s settings) error { + return func(s settings) (settings, error) { switch c { case codecH265, codecH264, codecMJPEG: s.codec = c - return nil + return s, nil default: - return fmt.Errorf("unknown codec: %v", c) + return s, fmt.Errorf("unknown codec: %v", c) } } } func Height(h int) option { - return func(s settings) error { + return func(s settings) (settings, error) { v, ok := map[int]string{256: res256, 360: res360, 720: res720}[h] if !ok { - return fmt.Errorf("invalid display height: %d", h) + return s, fmt.Errorf("invalid display height: %d", h) } s.res = v - return nil + return s, nil } } func FrameRate(f int) option { - return func(s settings) error { + return func(s settings) (settings, error) { if 1 > f || f > 30 { - return fmt.Errorf("invalid frame rate: %d", f) + return s, fmt.Errorf("invalid frame rate: %d", f) } s.frameRate = strconv.Itoa(f * 1000) - return nil + return s, nil } } func VariableBitRate(b bool) option { - return func(s settings) error { + return func(s settings) (settings, error) { s.vbr = "0" if b { s.vbr = "1" } - return nil + return s, nil } } func Quality(q quality) option { - return func(s settings) error { + return func(s settings) (settings, error) { switch q { case qualityStandard, qualityFair, qualityGood, qualityGreat, qualityExcellent: s.quality = q - return nil + return s, nil default: - return fmt.Errorf("invalid quality: %v", q) + return s, fmt.Errorf("invalid quality: %v", q) } } } // TODO: add check of r func BitRate(r int) option { - return func(s settings) error { + return func(s settings) (settings, error) { var ( vbrRates = []int{2000, 4000, 6000, 8000, 10000, 12000, 14000, 16000, 18000, 20000} cbrRates256 = []int{128, 256, 512, 1024} @@ -208,7 +208,7 @@ func BitRate(r int) option { if s.vbr == "1" { s.bitRate = convRate(r, vbrRates) - return nil + return s, nil } switch s.res { @@ -221,23 +221,23 @@ func BitRate(r int) option { default: panic("bad resolution") } - return nil + return s, nil } } func Refresh(r float64) option { - return func(s settings) error { + return func(s settings) (settings, error) { const ( maxRefreshPeriod = 5 minRefreshPeriod = .25 ) if minRefreshPeriod > r || r > maxRefreshPeriod { - return fmt.Errorf("invalid refresh period: %g", r) + return s, fmt.Errorf("invalid refresh period: %g", r) } refOptions := []int{250, 500, 1000, 1500, 2000, 2500, 3000, 3500, 4000, 4500, 5000} s.refresh = strconv.Itoa(refOptions[closestValIdx(int(r*1000), refOptions)]) - return nil + return s, nil } } diff --git a/input/gvctrl/gvctrl_test.go b/input/gvctrl/gvctrl_test.go index d0dca01e..8a4a0dbb 100644 --- a/input/gvctrl/gvctrl_test.go +++ b/input/gvctrl/gvctrl_test.go @@ -24,7 +24,10 @@ LICENSE package gvctrl -import "testing" +import ( + "errors" + "testing" +) func TestClosestValIdx(t *testing.T) { tests := []struct { @@ -107,3 +110,41 @@ func TestConvRate(t *testing.T) { } } } + +func TestHeight(t *testing.T) { + tests := []struct { + h int + want settings + err error + }{ + { + h: 256, + want: settings{res: "4480256"}, + }, + { + h: 360, + want: settings{res: "6400360"}, + }, + { + h: 720, + want: settings{res: "12800720"}, + }, + { + h: 500, + want: settings{}, + err: errors.New(""), + }, + } + + for i, test := range tests { + s := settings{} + got, err := Height(test.h)(s) + if test.err == nil && err != nil || test.err != nil && err == nil { + t.Errorf("did not get expected error: %v", test.err) + } + + if got != test.want { + t.Errorf("did not get expected result for test: %d\nGot: %v\nWant: %v", i, got, test.want) + } + } +} From 9e4ff72c486bc962f83018cbba9a41d9afbf039d Mon Sep 17 00:00:00 2001 From: Saxon Date: Sat, 12 Oct 2019 17:54:21 +1030 Subject: [PATCH 08/19] input/gvctrl/gvctrl_test.go: added test for BitRate option function --- input/gvctrl/gvctrl_test.go | 80 +++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/input/gvctrl/gvctrl_test.go b/input/gvctrl/gvctrl_test.go index 8a4a0dbb..906334fc 100644 --- a/input/gvctrl/gvctrl_test.go +++ b/input/gvctrl/gvctrl_test.go @@ -148,3 +148,83 @@ func TestHeight(t *testing.T) { } } } + +func TestBitrate(t *testing.T) { + tests := []struct { + r int + in settings + want settings + }{ + { + r: 7000, + in: settings{vbr: "1"}, + want: settings{ + vbr: "1", + bitRate: "6000000", + }, + }, + { + r: 7500, + in: settings{vbr: "1"}, + want: settings{ + vbr: "1", + bitRate: "8000000", + }, + }, + { + r: 600, + in: settings{ + vbr: "0", + res: res256, + }, + want: settings{ + vbr: "0", + res: res256, + bitRate: "512000", + }, + }, + { + r: 100, + in: settings{ + vbr: "0", + res: res256, + }, + want: settings{ + vbr: "0", + res: res256, + bitRate: "128000", + }, + }, + { + r: 2048, + in: settings{ + vbr: "0", + res: res360, + }, + want: settings{ + vbr: "0", + res: res360, + bitRate: "2048000", + }, + }, + { + r: 500, + in: settings{ + vbr: "0", + res: res720, + }, + want: settings{ + vbr: "0", + res: res720, + bitRate: "1024000", + }, + }, + } + + for i, test := range tests { + got, _ := BitRate(test.r)(test.in) + if got != test.want { + t.Errorf("did not get expected result for test: %d\nGot: %v\nWant: %v", i, got, test.want) + } + } +} From 7639c7bb74ff72c4ee7362e003e51fee332213bc Mon Sep 17 00:00:00 2001 From: Saxon Date: Sat, 12 Oct 2019 18:07:32 +1030 Subject: [PATCH 09/19] input/gvctrl/gvctrl_test.go: added test for Refresh option function --- input/gvctrl/gvctrl_test.go | 39 +++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/input/gvctrl/gvctrl_test.go b/input/gvctrl/gvctrl_test.go index 906334fc..0c1f362b 100644 --- a/input/gvctrl/gvctrl_test.go +++ b/input/gvctrl/gvctrl_test.go @@ -228,3 +228,42 @@ func TestBitrate(t *testing.T) { } } } + +func TestRefresh(t *testing.T) { + tests := []struct { + r float64 + want settings + err error + }{ + { + r: 1.6, + want: settings{refresh: "1500"}, + }, + { + r: 2.4, + want: settings{refresh: "2500"}, + }, + { + r: 0, + want: settings{}, + err: errors.New(""), + }, + { + r: 6, + want: settings{}, + err: errors.New(""), + }, + } + + for i, test := range tests { + s := settings{} + got, err := Refresh(test.r)(s) + if test.err == nil && err != nil || test.err != nil && err == nil { + t.Errorf("did not get expected error: %v", test.err) + } + + if got != test.want { + t.Errorf("did not get expected result for test: %d\nGot: %v\nWant: %v", i, got, test.want) + } + } +} From afe136a51dd6bd26af1802ef7dffa31260f3ba74 Mon Sep 17 00:00:00 2001 From: Saxon Date: Sun, 13 Oct 2019 12:42:31 +1030 Subject: [PATCH 10/19] input/gvctrl/gvctrl_test.go: added TestPopulateForm to test construction of settings form and fixed bugs --- input/gvctrl/gvctrl_test.go | 50 ++++++++++++++++++++++++ input/gvctrl/request.go | 61 +---------------------------- input/gvctrl/utils.go | 76 +++++++++++++++++++++++++++++++++++++ 3 files changed, 127 insertions(+), 60 deletions(-) diff --git a/input/gvctrl/gvctrl_test.go b/input/gvctrl/gvctrl_test.go index 0c1f362b..7314e5a6 100644 --- a/input/gvctrl/gvctrl_test.go +++ b/input/gvctrl/gvctrl_test.go @@ -26,6 +26,8 @@ package gvctrl import ( "errors" + "net/url" + "reflect" "testing" ) @@ -267,3 +269,51 @@ func TestRefresh(t *testing.T) { } } } + +func TestPopulateForm(t *testing.T) { + tests := []struct { + in settings + want string + }{ + { + in: newSettings(), + want: "dwConnType=5&mpeg_type=10&dwflicker_hz=0&szResolution=6400360&dwFrameRate=25000&vbr_enable=0&max_bit_rate=512000&custom_rate_control_type=0&custom_bitrate=0&custom_qp_init=25&custom_qp_min=10&custom_qp_max=40&gop_N=2000&dwEncProfile=1&dwEncLevel=31&dwEntropy=0&u8PreAlarmBuf=1&u32PostAlarmBuf2Disk=1&u8SplitInterval=5&bEnableIO=1&bEbIoIn=1&bEbIoIn1=1&bOSDFontSize=0&bEnableOSDCameraName=1&bCamNamePos=2&bEnableOSDDate=1&bDatePos=2&bEnableOSDTime=1&bTimePos=2&szOsdCamName=Camera&u16PostAlarmBuf=1&dwCameraId=1&LangCode=undefined&Recflag=0&submit=Apply", + }, + { + in: settings{ + codec: codecH265, + res: defaultRes, + frameRate: defaultFrameRate, + vbr: defaultVBR, + quality: defaultQuality, + bitRate: defaultBitRate, + refresh: defaultRefresh, + }, + want: "dwConnType=5&mpeg_type=28&dwflicker_hz=0&szResolution=6400360&dwFrameRate=25000&vbr_enable=0&max_bit_rate=512000&custom_rate_control_type=0&custom_bitrate=0&custom_qp_init=25&custom_qp_min=10&custom_qp_max=40&gop_N=2000&dwEncProfile=1&dwEncLevel=31&dwEntropy=0&u8PreAlarmBuf=1&u32PostAlarmBuf2Disk=1&u8SplitInterval=5&bEnableIO=1&bEbIoIn=1&bEbIoIn1=1&bOSDFontSize=0&bEnableOSDCameraName=1&bCamNamePos=2&bEnableOSDDate=1&bDatePos=2&bEnableOSDTime=1&bTimePos=2&szOsdCamName=Camera&u16PostAlarmBuf=1&dwCameraId=1&LangCode=undefined&Recflag=0&submit=Apply", + }, + { + in: settings{ + codec: codecMJPEG, + res: defaultRes, + frameRate: defaultFrameRate, + vbr: defaultVBR, + quality: defaultQuality, + bitRate: defaultBitRate, + refresh: defaultRefresh, + }, + want: "dwConnType=5&mpeg_type=4&dwflicker_hz=0&szResolution=6400360&dwFrameRate=25000&vbr_enable=1&dwVbrQuality=2&vbrmaxbitrate=500000&custom_qp_init=25&gop_N=1500&u8PreAlarmBuf=1&u32PostAlarmBuf2Disk=1&u8SplitInterval=5&bEnableIO=1&bEbIoIn=1&bEbIoIn1=1&bOSDFontSize=0&bEnableOSDCameraName=1&bCamNamePos=2&bEnableOSDDate=1&bDatePos=2&bEnableOSDTime=1&bTimePos=2&szOsdCamName=Camera&u16PostAlarmBuf=1&dwCameraId=1&LangCode=undefined&Recflag=0&submit=Apply", + }, + } + + for i, test := range tests { + got := populateForm(test.in) + want, err := url.ParseQuery(test.want) + if err != nil { + t.Fatalf("should not have got error: %v parsing want string for test: %d", err, i) + } + + if !reflect.DeepEqual(got, want) { + t.Errorf("did not get expected result for test: %d\nGot: %v\nWant: %v", i, got, want) + } + } +} diff --git a/input/gvctrl/request.go b/input/gvctrl/request.go index 5e26ffba..288e898e 100644 --- a/input/gvctrl/request.go +++ b/input/gvctrl/request.go @@ -124,66 +124,7 @@ func logIn(c *http.Client, id, host, b string) error { } func submitSettings(c *http.Client, id, host string, s settings) error { - var f url.Values - f.Set("dwConnType", "5") - f.Set("mpeg_type", "10") - f.Set("dwflicker_hz", "0") - f.Set("szResolution", s.res) - f.Set("dwFrameRate", s.frameRate) - - if s.codec == codecMJPEG { - f.Set("vbr_enable", "1") - f.Set("dwVbrQuality", s.vbr) - f.Set("vbrmaxbitrate", "750000") - } else { - switch s.vbr { - case "0": - f.Set("vbr_enable", "0") - f.Set("max_bit_rate", s.bitRate) - case "1": - f.Set("vbr_enable", "1") - f.Set("dwVbrQuality", s.vbr) - f.Set("vbrmaxbitrate", s.bitRate) - default: - panic("invalid vbrEnable parameter") - } - - f.Set("custom_rate_control_type", "0") - f.Set("custom_bitrate", "512000") - f.Set("custom_qp_init", "25") - f.Set("custom_qp_min", "10") - f.Set("custom_qp_max", "40") - } - - f.Set("gop_N", s.refresh) - - if s.codec == codecH264 || s.codec == codecH265 { - f.Set("dwEncProfile", "3") - f.Set("dwEncLevel", "31") - f.Set("dwEntropy", "0") - } - - f.Set("u8PreAlarmBuf", "1") - f.Set("u32PostAlarmBuf2Disk", "1") - f.Set("u8SplitInterval", "5") - f.Set("bEnableIO", "1") - f.Set("bEbIoIn", "1") - f.Set("bEbIoIn1", "1") - f.Set("bOSDFontSize", "0") - f.Set("bEnableOSDCameraName", "1") - f.Set("bCamNamePos", "2") - f.Set("bEnableOSDDate", "1") - f.Set("bDatePos", "2") - f.Set("bEnableOSDTime", "1") - f.Set("bTimePos", "2") - f.Set("szOsdCamName", "Camera") - f.Set("u16PostAlarmBuf", "1") - f.Set("dwCameraId", "1") // Channel=1 => cameraID=0 and chanel=2 => cameraID=1 - f.Set("LangCode", "undefined") - f.Set("Recflag", "0") - f.Set("submit", "Apply") - fBytes := []byte(f.Encode()) - + fBytes := []byte(populateForm(s).Encode()) req, err := http.NewRequest("POST", settingsURL, bytes.NewReader(fBytes)) if err != nil { return fmt.Errorf("could not create settings submit request: %v", err) diff --git a/input/gvctrl/utils.go b/input/gvctrl/utils.go index 21986341..078438ed 100644 --- a/input/gvctrl/utils.go +++ b/input/gvctrl/utils.go @@ -29,6 +29,7 @@ import ( "crypto/md5" "encoding/hex" "math" + "net/url" "strconv" "strings" ) @@ -52,3 +53,78 @@ func closestValIdx(v int, l []int) int { func convRate(v int, l []int) string { return strconv.Itoa(l[closestValIdx(v, l)] * 1000) } + +func populateForm(s settings) url.Values { + f := url.Values{} + f.Set("dwConnType", "5") + f.Set("mpeg_type", string(s.codec)) + f.Set("dwflicker_hz", "0") + f.Set("szResolution", s.res) + f.Set("dwFrameRate", s.frameRate) + f.Set("custom_qp_init", "25") + + if s.codec == codecMJPEG { + f.Set("vbr_enable", "1") + f.Set("dwVbrQuality", string(s.quality)) + + switch s.res { + case res256: + f.Set("vbrmaxbitrate", "250000") + case res360: + f.Set("vbrmaxbitrate", "500000") + case res720: + f.Set("vbrmaxbitrate", "750000") + default: + panic("invalid resolution") + } + } else { + switch s.vbr { + case "0": + f.Set("vbr_enable", "0") + f.Set("max_bit_rate", s.bitRate) + case "1": + f.Set("vbr_enable", "1") + f.Set("dwVbrQuality", s.vbr) + f.Set("vbrmaxbitrate", s.bitRate) + default: + panic("invalid vbrEnable parameter") + } + + f.Set("custom_rate_control_type", "0") + f.Set("custom_bitrate", "0") + f.Set("custom_qp_min", "10") + f.Set("custom_qp_max", "40") + } + + f.Set("gop_N", s.refresh) + if s.codec == codecMJPEG { + f.Set("gop_N", "1500") + } + + if s.codec == codecH264 || s.codec == codecH265 { + f.Set("dwEncProfile", "1") + f.Set("dwEncLevel", "31") + f.Set("dwEntropy", "0") + } + + f.Set("u8PreAlarmBuf", "1") + f.Set("u32PostAlarmBuf2Disk", "1") + f.Set("u8SplitInterval", "5") + f.Set("bEnableIO", "1") + f.Set("bEbIoIn", "1") + f.Set("bEbIoIn1", "1") + f.Set("bOSDFontSize", "0") + f.Set("bEnableOSDCameraName", "1") + f.Set("bCamNamePos", "2") + f.Set("bEnableOSDDate", "1") + f.Set("bDatePos", "2") + f.Set("bEnableOSDTime", "1") + f.Set("bTimePos", "2") + f.Set("szOsdCamName", "Camera") + f.Set("u16PostAlarmBuf", "1") + f.Set("dwCameraId", "1") // Channel=1 => cameraID=0 and chanel=2 => cameraID=1 + f.Set("LangCode", "undefined") + f.Set("Recflag", "0") + f.Set("submit", "Apply") + return f +} From cda7093c21951f5ca6a6ea59b6e2629a1ca32783 Mon Sep 17 00:00:00 2001 From: Saxon Date: Sun, 13 Oct 2019 18:11:49 +1030 Subject: [PATCH 11/19] input/gvctrl: added another test case to TestPopulateForm and fixed potential bug regarding bitrate defaults --- input/gvctrl/gvctrl.go | 73 ++++++++++++++------------- input/gvctrl/gvctrl_test.go | 99 ++++++++++++++++++++++++------------- input/gvctrl/utils.go | 6 +-- 3 files changed, 108 insertions(+), 70 deletions(-) diff --git a/input/gvctrl/gvctrl.go b/input/gvctrl/gvctrl.go index 7d42102e..5a60f6c3 100644 --- a/input/gvctrl/gvctrl.go +++ b/input/gvctrl/gvctrl.go @@ -59,34 +59,37 @@ const ( ) const ( - defaultCodec = codecH264 - defaultRes = "6400360" // 360p - defaultFrameRate = "25000" // 25 fps - defaultVBR = "0" // Variable bitrate off - defaultQuality = qualityGood - defaultBitRate = "512000" // 512 kbps (lowest with 360p) - defaultRefresh = "2000" // 2 seconds + defaultCodec = codecH264 + defaultRes = "6400360" // 360p + defaultFrameRate = "25000" // 25 fps + defaultVBR = "0" // Variable bitrate off + defaultQuality = qualityGood + defaultVBRBitRate = "250000" // 512 kbps (lowest with 360p) + defaultCBRBitRate = "512000" + defaultRefresh = "2000" // 2 seconds ) type settings struct { - codec codec - res string - frameRate string - vbr string - quality quality - bitRate string - refresh string + codec codec + res string + frameRate string + vbr string + quality quality + vbrBitRate string + cbrBitRate string + refresh string } func newSettings() settings { return settings{ - codec: defaultCodec, - res: defaultRes, - frameRate: defaultFrameRate, - vbr: defaultVBR, - quality: defaultQuality, - bitRate: defaultBitRate, - refresh: defaultRefresh, + codec: defaultCodec, + res: defaultRes, + frameRate: defaultFrameRate, + vbr: defaultVBR, + quality: defaultQuality, + vbrBitRate: defaultVBRBitRate, + cbrBitRate: defaultCBRBitRate, + refresh: defaultRefresh, } } @@ -184,7 +187,7 @@ func VariableBitRate(b bool) option { } } -func Quality(q quality) option { +func VBRQuality(q quality) option { return func(s settings) (settings, error) { switch q { case qualityStandard, qualityFair, qualityGood, qualityGreat, qualityExcellent: @@ -196,28 +199,32 @@ func Quality(q quality) option { } } -// TODO: add check of r -func BitRate(r int) option { +func VBRBitRate(r int) option { + return func(s settings) (settings, error) { + var vbrRates = []int{250, 500, 750, 1000, 1250, 1500, 1750, 2000, 2250, 2500} + if s.vbr == "1" { + s.vbrBitRate = convRate(r, vbrRates) + return s, nil + } + return s, nil + } +} + +func CBRBitRate(r int) option { return func(s settings) (settings, error) { var ( - vbrRates = []int{2000, 4000, 6000, 8000, 10000, 12000, 14000, 16000, 18000, 20000} cbrRates256 = []int{128, 256, 512, 1024} cbrRates360 = []int{512, 1024, 2048, 3072} cbrRates720 = []int{1024, 2048, 4096, 6144} ) - if s.vbr == "1" { - s.bitRate = convRate(r, vbrRates) - return s, nil - } - switch s.res { case res720: - s.bitRate = convRate(r, cbrRates720) + s.cbrBitRate = convRate(r, cbrRates720) case res360: - s.bitRate = convRate(r, cbrRates360) + s.cbrBitRate = convRate(r, cbrRates360) case res256: - s.bitRate = convRate(r, cbrRates256) + s.cbrBitRate = convRate(r, cbrRates256) default: panic("bad resolution") } diff --git a/input/gvctrl/gvctrl_test.go b/input/gvctrl/gvctrl_test.go index 7314e5a6..e11ca244 100644 --- a/input/gvctrl/gvctrl_test.go +++ b/input/gvctrl/gvctrl_test.go @@ -151,28 +151,44 @@ func TestHeight(t *testing.T) { } } -func TestBitrate(t *testing.T) { +func TestVBRBitrate(t *testing.T) { tests := []struct { r int in settings want settings }{ { - r: 7000, + r: 300, in: settings{vbr: "1"}, want: settings{ - vbr: "1", - bitRate: "6000000", + vbr: "1", + vbrBitRate: "250000", }, }, { - r: 7500, + r: 400, in: settings{vbr: "1"}, want: settings{ - vbr: "1", - bitRate: "8000000", + vbr: "1", + vbrBitRate: "500000", }, }, + } + + for i, test := range tests { + got, _ := VBRBitRate(test.r)(test.in) + if got != test.want { + t.Errorf("did not get expected result for test: %d\nGot: %v\nWant: %v", i, got, test.want) + } + } +} + +func TestCBRBitrate(t *testing.T) { + tests := []struct { + r int + in settings + want settings + }{ { r: 600, in: settings{ @@ -180,9 +196,9 @@ func TestBitrate(t *testing.T) { res: res256, }, want: settings{ - vbr: "0", - res: res256, - bitRate: "512000", + vbr: "0", + res: res256, + cbrBitRate: "512000", }, }, { @@ -192,9 +208,9 @@ func TestBitrate(t *testing.T) { res: res256, }, want: settings{ - vbr: "0", - res: res256, - bitRate: "128000", + vbr: "0", + res: res256, + cbrBitRate: "128000", }, }, { @@ -204,9 +220,9 @@ func TestBitrate(t *testing.T) { res: res360, }, want: settings{ - vbr: "0", - res: res360, - bitRate: "2048000", + vbr: "0", + res: res360, + cbrBitRate: "2048000", }, }, { @@ -216,15 +232,15 @@ func TestBitrate(t *testing.T) { res: res720, }, want: settings{ - vbr: "0", - res: res720, - bitRate: "1024000", + vbr: "0", + res: res720, + cbrBitRate: "1024000", }, }, } for i, test := range tests { - got, _ := BitRate(test.r)(test.in) + got, _ := CBRBitRate(test.r)(test.in) if got != test.want { t.Errorf("did not get expected result for test: %d\nGot: %v\nWant: %v", i, got, test.want) } @@ -281,28 +297,43 @@ func TestPopulateForm(t *testing.T) { }, { in: settings{ - codec: codecH265, - res: defaultRes, - frameRate: defaultFrameRate, - vbr: defaultVBR, - quality: defaultQuality, - bitRate: defaultBitRate, - refresh: defaultRefresh, + codec: codecH265, + res: defaultRes, + frameRate: defaultFrameRate, + vbr: defaultVBR, + quality: defaultQuality, + vbrBitRate: defaultVBRBitRate, + cbrBitRate: defaultCBRBitRate, + refresh: defaultRefresh, }, want: "dwConnType=5&mpeg_type=28&dwflicker_hz=0&szResolution=6400360&dwFrameRate=25000&vbr_enable=0&max_bit_rate=512000&custom_rate_control_type=0&custom_bitrate=0&custom_qp_init=25&custom_qp_min=10&custom_qp_max=40&gop_N=2000&dwEncProfile=1&dwEncLevel=31&dwEntropy=0&u8PreAlarmBuf=1&u32PostAlarmBuf2Disk=1&u8SplitInterval=5&bEnableIO=1&bEbIoIn=1&bEbIoIn1=1&bOSDFontSize=0&bEnableOSDCameraName=1&bCamNamePos=2&bEnableOSDDate=1&bDatePos=2&bEnableOSDTime=1&bTimePos=2&szOsdCamName=Camera&u16PostAlarmBuf=1&dwCameraId=1&LangCode=undefined&Recflag=0&submit=Apply", }, { in: settings{ - codec: codecMJPEG, - res: defaultRes, - frameRate: defaultFrameRate, - vbr: defaultVBR, - quality: defaultQuality, - bitRate: defaultBitRate, - refresh: defaultRefresh, + codec: codecMJPEG, + res: defaultRes, + frameRate: defaultFrameRate, + vbr: defaultVBR, + quality: defaultQuality, + vbrBitRate: defaultVBRBitRate, + cbrBitRate: defaultCBRBitRate, + refresh: defaultRefresh, }, want: "dwConnType=5&mpeg_type=4&dwflicker_hz=0&szResolution=6400360&dwFrameRate=25000&vbr_enable=1&dwVbrQuality=2&vbrmaxbitrate=500000&custom_qp_init=25&gop_N=1500&u8PreAlarmBuf=1&u32PostAlarmBuf2Disk=1&u8SplitInterval=5&bEnableIO=1&bEbIoIn=1&bEbIoIn1=1&bOSDFontSize=0&bEnableOSDCameraName=1&bCamNamePos=2&bEnableOSDDate=1&bDatePos=2&bEnableOSDTime=1&bTimePos=2&szOsdCamName=Camera&u16PostAlarmBuf=1&dwCameraId=1&LangCode=undefined&Recflag=0&submit=Apply", }, + { + in: settings{ + codec: codecH264, + res: defaultRes, + frameRate: defaultFrameRate, + vbr: "1", + quality: defaultQuality, + vbrBitRate: defaultVBRBitRate, + cbrBitRate: defaultCBRBitRate, + refresh: defaultRefresh, + }, + want: "dwConnType=5&mpeg_type=10&dwflicker_hz=0&szResolution=6400360&dwFrameRate=25000&vbr_enable=1&dwVbrQuality=2&vbrmaxbitrate=250000&custom_rate_control_type=0&custom_bitrate=0&custom_qp_init=25&custom_qp_min=10&custom_qp_max=40&gop_N=2000&dwEncProfile=1&dwEncLevel=31&dwEntropy=0&u8PreAlarmBuf=1&u32PostAlarmBuf2Disk=1&u8SplitInterval=5&bEnableIO=1&bEbIoIn=1&bEbIoIn1=1&bOSDFontSize=0&bEnableOSDCameraName=1&bCamNamePos=2&bEnableOSDDate=1&bDatePos=2&bEnableOSDTime=1&bTimePos=2&szOsdCamName=Camera&u16PostAlarmBuf=1&dwCameraId=1&LangCode=undefined&Recflag=0&submit=Apply", + }, } for i, test := range tests { diff --git a/input/gvctrl/utils.go b/input/gvctrl/utils.go index 078438ed..74b527d5 100644 --- a/input/gvctrl/utils.go +++ b/input/gvctrl/utils.go @@ -81,11 +81,11 @@ func populateForm(s settings) url.Values { switch s.vbr { case "0": f.Set("vbr_enable", "0") - f.Set("max_bit_rate", s.bitRate) + f.Set("max_bit_rate", s.cbrBitRate) case "1": f.Set("vbr_enable", "1") - f.Set("dwVbrQuality", s.vbr) - f.Set("vbrmaxbitrate", s.bitRate) + f.Set("dwVbrQuality", string(s.quality)) + f.Set("vbrmaxbitrate", s.vbrBitRate) default: panic("invalid vbrEnable parameter") } From b3c3b842c834394c935fbcf23068cf467825dab8 Mon Sep 17 00:00:00 2001 From: Saxon Date: Sun, 13 Oct 2019 18:25:36 +1030 Subject: [PATCH 12/19] input/gvctrl: moved unexported stuff from gvctrl.go to be more friendly to API users --- input/gvctrl/gvctrl.go | 89 ++++++++++--------------------------- input/gvctrl/gvctrl_test.go | 6 +-- input/gvctrl/utils.go | 47 ++++++++++++++++++-- 3 files changed, 71 insertions(+), 71 deletions(-) diff --git a/input/gvctrl/gvctrl.go b/input/gvctrl/gvctrl.go index 5a60f6c3..818cf1cd 100644 --- a/input/gvctrl/gvctrl.go +++ b/input/gvctrl/gvctrl.go @@ -34,65 +34,6 @@ import ( "time" ) -type codec string - -const ( - codecH265 codec = "28" - codecH264 codec = "10" - codecMJPEG codec = "4" -) - -type quality string - -const ( - qualityStandard quality = "0" - qualityFair quality = "1" - qualityGood quality = "2" - qualityGreat quality = "3" - qualityExcellent quality = "4" -) - -const ( - res256 = "4480256" - res360 = "6400360" - res720 = "12800720" -) - -const ( - defaultCodec = codecH264 - defaultRes = "6400360" // 360p - defaultFrameRate = "25000" // 25 fps - defaultVBR = "0" // Variable bitrate off - defaultQuality = qualityGood - defaultVBRBitRate = "250000" // 512 kbps (lowest with 360p) - defaultCBRBitRate = "512000" - defaultRefresh = "2000" // 2 seconds -) - -type settings struct { - codec codec - res string - frameRate string - vbr string - quality quality - vbrBitRate string - cbrBitRate string - refresh string -} - -func newSettings() settings { - return settings{ - codec: defaultCodec, - res: defaultRes, - frameRate: defaultFrameRate, - vbr: defaultVBR, - quality: defaultQuality, - vbrBitRate: defaultVBRBitRate, - cbrBitRate: defaultCBRBitRate, - refresh: defaultRefresh, - } -} - type option func(s settings) (settings, error) func Set(host string, options ...option) error { @@ -144,14 +85,22 @@ func Set(host string, options ...option) error { return nil } -func Codec(c codec) option { +type Codec string + +const ( + CodecH265 Codec = "28" + CodecH264 Codec = "10" + CodecMJPEG Codec = "4" +) + +func CodecOut(c Codec) option { return func(s settings) (settings, error) { switch c { - case codecH265, codecH264, codecMJPEG: + case CodecH265, CodecH264, CodecMJPEG: s.codec = c return s, nil default: - return s, fmt.Errorf("unknown codec: %v", c) + return s, fmt.Errorf("unknown Codec: %v", c) } } } @@ -187,14 +136,24 @@ func VariableBitRate(b bool) option { } } -func VBRQuality(q quality) option { +type Quality string + +const ( + QualityStandard Quality = "0" + QualityFair Quality = "1" + QualityGood Quality = "2" + QualityGreat Quality = "3" + QualityExcellent Quality = "4" +) + +func VBRQuality(q Quality) option { return func(s settings) (settings, error) { switch q { - case qualityStandard, qualityFair, qualityGood, qualityGreat, qualityExcellent: + case QualityStandard, QualityFair, QualityGood, QualityGreat, QualityExcellent: s.quality = q return s, nil default: - return s, fmt.Errorf("invalid quality: %v", q) + return s, fmt.Errorf("invalid Quality: %v", q) } } } diff --git a/input/gvctrl/gvctrl_test.go b/input/gvctrl/gvctrl_test.go index e11ca244..92bbb5a8 100644 --- a/input/gvctrl/gvctrl_test.go +++ b/input/gvctrl/gvctrl_test.go @@ -297,7 +297,7 @@ func TestPopulateForm(t *testing.T) { }, { in: settings{ - codec: codecH265, + codec: CodecH265, res: defaultRes, frameRate: defaultFrameRate, vbr: defaultVBR, @@ -310,7 +310,7 @@ func TestPopulateForm(t *testing.T) { }, { in: settings{ - codec: codecMJPEG, + codec: CodecMJPEG, res: defaultRes, frameRate: defaultFrameRate, vbr: defaultVBR, @@ -323,7 +323,7 @@ func TestPopulateForm(t *testing.T) { }, { in: settings{ - codec: codecH264, + codec: CodecH264, res: defaultRes, frameRate: defaultFrameRate, vbr: "1", diff --git a/input/gvctrl/utils.go b/input/gvctrl/utils.go index 74b527d5..1ca158da 100644 --- a/input/gvctrl/utils.go +++ b/input/gvctrl/utils.go @@ -34,6 +34,47 @@ import ( "strings" ) +const ( + res256 = "4480256" + res360 = "6400360" + res720 = "12800720" +) + +const ( + defaultCodec = CodecH264 + defaultRes = "6400360" // 360p + defaultFrameRate = "25000" // 25 fps + defaultVBR = "0" // Variable bitrate off + defaultQuality = QualityGood + defaultVBRBitRate = "250000" // 512 kbps (lowest with 360p) + defaultCBRBitRate = "512000" + defaultRefresh = "2000" // 2 seconds +) + +type settings struct { + codec Codec + res string + frameRate string + vbr string + quality Quality + vbrBitRate string + cbrBitRate string + refresh string +} + +func newSettings() settings { + return settings{ + codec: defaultCodec, + res: defaultRes, + frameRate: defaultFrameRate, + vbr: defaultVBR, + quality: defaultQuality, + vbrBitRate: defaultVBRBitRate, + cbrBitRate: defaultCBRBitRate, + refresh: defaultRefresh, + } +} + func md5Hex(s string) string { h := md5.New() h.Write([]byte(s)) @@ -63,7 +104,7 @@ func populateForm(s settings) url.Values { f.Set("dwFrameRate", s.frameRate) f.Set("custom_qp_init", "25") - if s.codec == codecMJPEG { + if s.codec == CodecMJPEG { f.Set("vbr_enable", "1") f.Set("dwVbrQuality", string(s.quality)) @@ -97,11 +138,11 @@ func populateForm(s settings) url.Values { } f.Set("gop_N", s.refresh) - if s.codec == codecMJPEG { + if s.codec == CodecMJPEG { f.Set("gop_N", "1500") } - if s.codec == codecH264 || s.codec == codecH265 { + if s.codec == CodecH264 || s.codec == CodecH265 { f.Set("dwEncProfile", "1") f.Set("dwEncLevel", "31") f.Set("dwEntropy", "0") From ab71c2cb06f490b50678bec7aaa498a6f440788b Mon Sep 17 00:00:00 2001 From: Saxon Date: Sun, 13 Oct 2019 21:47:50 +1030 Subject: [PATCH 13/19] input/gvctrl/gvctrl-cli/main.go: added sub package gvctrl-cli under gvctrl gvctrl-cli provides a command line interface for controlling the geovision camera. Good for testing purposes. --- input/gvctrl/gvctrl-cli/main.go | 96 +++++++++++++++++++++++++++++++++ input/gvctrl/gvctrl.go | 32 +++++------ input/gvctrl/request.go | 21 ++++---- 3 files changed, 123 insertions(+), 26 deletions(-) create mode 100644 input/gvctrl/gvctrl-cli/main.go diff --git a/input/gvctrl/gvctrl-cli/main.go b/input/gvctrl/gvctrl-cli/main.go new file mode 100644 index 00000000..489c6bbc --- /dev/null +++ b/input/gvctrl/gvctrl-cli/main.go @@ -0,0 +1,96 @@ +/* +DESCRIPTION + See package description. + +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 main + +import ( + "flag" + "fmt" + "strconv" + + "bitbucket.org/ausocean/av/input/gvctrl" +) + +func main() { + var ( + hostPtr = flag.String("host", "192.168.1.50", "IP of GeoVision camera.") + codecPtr = flag.String("codec", "", "h264, h265 or mjpeg") + heightPtr = flag.Int("height", 0, "256, 360 or 720") + fpsPtr = flag.Int("fps", 0, "Frame rate in frames per second.") + vbrPtr = flag.Bool("vbr", false, "If true, variable bitrate.") + vbrQualityPtr = flag.Int("quality", -1, "General quality under variable bitrate, 0 to 4 inclusive.") + vbrRatePtr = flag.Int("vbr-rate", 0, "Variable bitrate maximal bitrate in kbps.") + cbrRatePtr = flag.Int("cbr-rate", 0, "Constant bitrate, bitrate in kbps.") + refreshPtr = flag.Float64("refresh", 0, "Inter refresh period in seconds.") + ) + flag.Parse() + + var options []gvctrl.Option + + if *codecPtr != "" { + var c gvctrl.Codec + switch *codecPtr { + case "h264": + c = gvctrl.CodecH264 + case "h265": + c = gvctrl.CodecH265 + case "mjpeg": + c = gvctrl.CodecMJPEG + default: + panic(fmt.Sprintf("invalid codec: %s", *codecPtr)) + } + options = append(options, gvctrl.CodecOut(c)) + } + + if *heightPtr != 0 { + options = append(options, gvctrl.Height(*heightPtr)) + } + + if *fpsPtr != 0 { + options = append(options, gvctrl.FrameRate(*fpsPtr)) + } + + options = append(options, gvctrl.VariableBitRate(*vbrPtr)) + + if *vbrQualityPtr != -1 { + options = append(options, gvctrl.VBRQuality(gvctrl.Quality(strconv.Itoa(*vbrQualityPtr)))) + } + + if *vbrRatePtr != 0 { + options = append(options, gvctrl.VBRBitRate(*vbrRatePtr)) + } + + if *cbrRatePtr != 0 { + options = append(options, gvctrl.CBRBitRate(*cbrRatePtr)) + } + + if *refreshPtr != 0 { + options = append(options, gvctrl.Refresh(*refreshPtr)) + } + + err := gvctrl.Set(*hostPtr, options...) + if err != nil { + panic(fmt.Sprintf("error from gvctrl.Set: %v", err)) + } +} diff --git a/input/gvctrl/gvctrl.go b/input/gvctrl/gvctrl.go index 818cf1cd..45685154 100644 --- a/input/gvctrl/gvctrl.go +++ b/input/gvctrl/gvctrl.go @@ -12,7 +12,7 @@ LICENSE 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. + 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 @@ -34,9 +34,9 @@ import ( "time" ) -type option func(s settings) (settings, error) +type Option func(s settings) (settings, error) -func Set(host string, options ...option) error { +func Set(host string, options ...Option) error { // Randomly generate an ID our client will use. const ( minID = 10000 @@ -73,7 +73,7 @@ func Set(host string, options ...option) error { for _, op := range options { s, err = op(s) if err != nil { - return fmt.Errorf("could not action option: %v", err) + return fmt.Errorf("could not action Option: %v", err) } } @@ -93,7 +93,7 @@ const ( CodecMJPEG Codec = "4" ) -func CodecOut(c Codec) option { +func CodecOut(c Codec) Option { return func(s settings) (settings, error) { switch c { case CodecH265, CodecH264, CodecMJPEG: @@ -105,7 +105,7 @@ func CodecOut(c Codec) option { } } -func Height(h int) option { +func Height(h int) Option { return func(s settings) (settings, error) { v, ok := map[int]string{256: res256, 360: res360, 720: res720}[h] if !ok { @@ -116,7 +116,7 @@ func Height(h int) option { } } -func FrameRate(f int) option { +func FrameRate(f int) Option { return func(s settings) (settings, error) { if 1 > f || f > 30 { return s, fmt.Errorf("invalid frame rate: %d", f) @@ -126,7 +126,7 @@ func FrameRate(f int) option { } } -func VariableBitRate(b bool) option { +func VariableBitRate(b bool) Option { return func(s settings) (settings, error) { s.vbr = "0" if b { @@ -139,14 +139,14 @@ func VariableBitRate(b bool) option { type Quality string const ( - QualityStandard Quality = "0" - QualityFair Quality = "1" + QualityStandard Quality = "4" + QualityFair Quality = "3" QualityGood Quality = "2" - QualityGreat Quality = "3" - QualityExcellent Quality = "4" + QualityGreat Quality = "1" + QualityExcellent Quality = "0" ) -func VBRQuality(q Quality) option { +func VBRQuality(q Quality) Option { return func(s settings) (settings, error) { switch q { case QualityStandard, QualityFair, QualityGood, QualityGreat, QualityExcellent: @@ -158,7 +158,7 @@ func VBRQuality(q Quality) option { } } -func VBRBitRate(r int) option { +func VBRBitRate(r int) Option { return func(s settings) (settings, error) { var vbrRates = []int{250, 500, 750, 1000, 1250, 1500, 1750, 2000, 2250, 2500} if s.vbr == "1" { @@ -169,7 +169,7 @@ func VBRBitRate(r int) option { } } -func CBRBitRate(r int) option { +func CBRBitRate(r int) Option { return func(s settings) (settings, error) { var ( cbrRates256 = []int{128, 256, 512, 1024} @@ -191,7 +191,7 @@ func CBRBitRate(r int) option { } } -func Refresh(r float64) option { +func Refresh(r float64) Option { return func(s settings) (settings, error) { const ( maxRefreshPeriod = 5 diff --git a/input/gvctrl/request.go b/input/gvctrl/request.go index 288e898e..807df136 100644 --- a/input/gvctrl/request.go +++ b/input/gvctrl/request.go @@ -42,6 +42,7 @@ const ( settingsURL = baseURL + "/VideoSetting.cgi" ) +// TODO: make this configurable. const ( user = "admin" pass = "admin" @@ -83,17 +84,17 @@ func genLogIn(c *http.Client, id, host string) (string, error) { cc[i] = exp[5 : len(exp)-1] } - var data url.Values - data.Set("grp", "-1") - data.Set("username", "") - data.Set("password", "") - data.Set("Apply", "Apply") - data.Set("umd5", md5Hex(cc[0]+user+cc[1])) - data.Set("pmd5", md5Hex(cc[1]+pass+cc[0])) - data.Set("browser", "1") - data.Set("is_check_OCX_OK", "0") + f := url.Values{} + f.Set("grp", "-1") + f.Set("username", "") + f.Set("password", "") + f.Set("Apply", "Apply") + f.Set("umd5", md5Hex(cc[0]+user+cc[1])) + f.Set("pmd5", md5Hex(cc[1]+pass+cc[0])) + f.Set("browser", "1") + f.Set("is_check_OCX_OK", "0") - return data.Encode(), nil + return f.Encode(), nil } func logIn(c *http.Client, id, host, b string) error { From b86ff77996106b5649ae7eb45964e664260ef8f4 Mon Sep 17 00:00:00 2001 From: Saxon Date: Mon, 14 Oct 2019 09:56:43 +1030 Subject: [PATCH 14/19] input/gvctrl/gvctrl.go: wrote documentation in gvctrl.go and renamed BitRate to Bitrate --- input/gvctrl/gvctrl-cli/main.go | 6 +-- input/gvctrl/gvctrl.go | 72 ++++++++++++++++++++++++++++----- input/gvctrl/gvctrl_test.go | 28 ++++++------- input/gvctrl/utils.go | 16 ++++---- 4 files changed, 88 insertions(+), 34 deletions(-) diff --git a/input/gvctrl/gvctrl-cli/main.go b/input/gvctrl/gvctrl-cli/main.go index 489c6bbc..9963a0c1 100644 --- a/input/gvctrl/gvctrl-cli/main.go +++ b/input/gvctrl/gvctrl-cli/main.go @@ -71,18 +71,18 @@ func main() { options = append(options, gvctrl.FrameRate(*fpsPtr)) } - options = append(options, gvctrl.VariableBitRate(*vbrPtr)) + options = append(options, gvctrl.VariableBitrate(*vbrPtr)) if *vbrQualityPtr != -1 { options = append(options, gvctrl.VBRQuality(gvctrl.Quality(strconv.Itoa(*vbrQualityPtr)))) } if *vbrRatePtr != 0 { - options = append(options, gvctrl.VBRBitRate(*vbrRatePtr)) + options = append(options, gvctrl.VBRBitrate(*vbrRatePtr)) } if *cbrRatePtr != 0 { - options = append(options, gvctrl.CBRBitRate(*cbrRatePtr)) + options = append(options, gvctrl.CBRBitrate(*cbrRatePtr)) } if *refreshPtr != 0 { diff --git a/input/gvctrl/gvctrl.go b/input/gvctrl/gvctrl.go index 45685154..b27c805d 100644 --- a/input/gvctrl/gvctrl.go +++ b/input/gvctrl/gvctrl.go @@ -1,7 +1,9 @@ /* DESCRIPTION - gvctrl.go provides a programmatic interface for controlling the HTTP based - server hosted by GeoVision cameras for settings control. + gvctrl.go provides exported functionality of a basic API to allow programmatic + control over the GeoVision camera (namely the GV-BX4700) through the HTTP + server used for settings control. See package documentation for further + information on the API. AUTHORS Saxon A. Nelson-Milton @@ -23,6 +25,13 @@ LICENSE in gpl.txt. If not, see http://www.gnu.org/licenses. */ +// Package gvctrl provides a basic API for programmatic control of the +// web based interface provided by GeoVision cameras. This API has been +// developed and tested only with the GV-BX4700, and therefore may not work +// with other models without further evolution. +// +// Settings on a GeoVision camera are updated using the Set function. One or +// more option functions may be provided to control camera function. package gvctrl import ( @@ -34,8 +43,23 @@ import ( "time" ) +// Option describes a function that will apply an option to the passed s. type Option func(s settings) (settings, error) +// Set will log in to the camera at host and submit a form of settings. The +// settings form is populated with values influenced by the optional options +// passed. Available options are defined below this function. +// +// The following defaults are applied to each configurable parameter if not +// influenced by the passed options: +// codec: H264 +// resolution: 640x360 +// framerate: 25 +// variable bitrate: off +// variable bitrate quality: good +// vbr bitrate: 250 kbps +// cbr bitrate: 512 kbps +// refresh: 2 seconds func Set(host string, options ...Option) error { // Randomly generate an ID our client will use. const ( @@ -85,14 +109,18 @@ func Set(host string, options ...Option) error { return nil } +// Codec is a video codec. type Codec string +// The avilable codecs that may be selected using CodecOut below. const ( CodecH265 Codec = "28" CodecH264 Codec = "10" CodecMJPEG Codec = "4" ) +// CodecOut will set the video codec outputted by the camera. The available +// codec options are listed above as consts. func CodecOut(c Codec) Option { return func(s settings) (settings, error) { switch c { @@ -105,6 +133,8 @@ func CodecOut(c Codec) Option { } } +// Height will set the height component of the video resolution. Available +// heights are 256, 360 and 720. func Height(h int) Option { return func(s settings) (settings, error) { v, ok := map[int]string{256: res256, 360: res360, 720: res720}[h] @@ -116,6 +146,8 @@ func Height(h int) Option { } } +// FrameRate will set the frame rate of the video. This value is defined in +// units of frames per second, and must be between 1 and 30 inclusive. func FrameRate(f int) Option { return func(s settings) (settings, error) { if 1 > f || f > 30 { @@ -126,7 +158,9 @@ func FrameRate(f int) Option { } } -func VariableBitRate(b bool) Option { +// VariableBitrate with b set true will turn on variable bitrate video and +// with b set false will turn off variable bitrate (resulting in constant bitrate). +func VariableBitrate(b bool) Option { return func(s settings) (settings, error) { s.vbr = "0" if b { @@ -136,8 +170,11 @@ func VariableBitRate(b bool) Option { } } +// Quality defines an average quality of video from the camera. type Quality string +// The available video qualities under variable bitrate. NB: it is not known +// what bitrates these correspond to. const ( QualityStandard Quality = "4" QualityFair Quality = "3" @@ -146,6 +183,8 @@ const ( QualityExcellent Quality = "0" ) +// VBRQuality will set the average quality of video under variable bitrate. +// The quality may be chosen from standard to excellent, as defined above. func VBRQuality(q Quality) Option { return func(s settings) (settings, error) { switch q { @@ -158,18 +197,30 @@ func VBRQuality(q Quality) Option { } } -func VBRBitRate(r int) Option { +// VBRBitrate will set the maximal bitrate when the camera is set to variable +// bitrate. The possible values of maximal bitrate in kbps are predefined (by +// the camera) as: 250, 500, 750, 1000, 1250, 1500, 1750, 2000, 2250 and 2500. +// If the passed rate does not match one of these values, the closest value is +// selected. +func VBRBitrate(r int) Option { return func(s settings) (settings, error) { var vbrRates = []int{250, 500, 750, 1000, 1250, 1500, 1750, 2000, 2250, 2500} if s.vbr == "1" { - s.vbrBitRate = convRate(r, vbrRates) + s.vbrBitrate = convRate(r, vbrRates) return s, nil } return s, nil } } -func CBRBitRate(r int) Option { +// CBRBitrate will select the bitrate when the camera is set to constant bitrate. +// The possible values of bitrate are predefined for each resolution as follows: +// 256p: 128, 256, 512, 1024 +// 360p: 512, 1024, 2048, 3072 +// 720p: 1024, 2048, 4096, 6144 +// If the passed rate does not align with one of these values, the closest +// value is selected. +func CBRBitrate(r int) Option { return func(s settings) (settings, error) { var ( cbrRates256 = []int{128, 256, 512, 1024} @@ -179,11 +230,11 @@ func CBRBitRate(r int) Option { switch s.res { case res720: - s.cbrBitRate = convRate(r, cbrRates720) + s.cbrBitrate = convRate(r, cbrRates720) case res360: - s.cbrBitRate = convRate(r, cbrRates360) + s.cbrBitrate = convRate(r, cbrRates360) case res256: - s.cbrBitRate = convRate(r, cbrRates256) + s.cbrBitrate = convRate(r, cbrRates256) default: panic("bad resolution") } @@ -191,6 +242,9 @@ func CBRBitRate(r int) Option { } } +// Refresh will set the intra refresh period. The passed value is in seconds and +// must be between .25 and 5 inclusive. The value will be rounded to the nearest +// value divisible by .25 seconds. func Refresh(r float64) Option { return func(s settings) (settings, error) { const ( diff --git a/input/gvctrl/gvctrl_test.go b/input/gvctrl/gvctrl_test.go index 92bbb5a8..d15ed9be 100644 --- a/input/gvctrl/gvctrl_test.go +++ b/input/gvctrl/gvctrl_test.go @@ -162,7 +162,7 @@ func TestVBRBitrate(t *testing.T) { in: settings{vbr: "1"}, want: settings{ vbr: "1", - vbrBitRate: "250000", + vbrBitrate: "250000", }, }, { @@ -170,13 +170,13 @@ func TestVBRBitrate(t *testing.T) { in: settings{vbr: "1"}, want: settings{ vbr: "1", - vbrBitRate: "500000", + vbrBitrate: "500000", }, }, } for i, test := range tests { - got, _ := VBRBitRate(test.r)(test.in) + got, _ := VBRBitrate(test.r)(test.in) if got != test.want { t.Errorf("did not get expected result for test: %d\nGot: %v\nWant: %v", i, got, test.want) } @@ -198,7 +198,7 @@ func TestCBRBitrate(t *testing.T) { want: settings{ vbr: "0", res: res256, - cbrBitRate: "512000", + cbrBitrate: "512000", }, }, { @@ -210,7 +210,7 @@ func TestCBRBitrate(t *testing.T) { want: settings{ vbr: "0", res: res256, - cbrBitRate: "128000", + cbrBitrate: "128000", }, }, { @@ -222,7 +222,7 @@ func TestCBRBitrate(t *testing.T) { want: settings{ vbr: "0", res: res360, - cbrBitRate: "2048000", + cbrBitrate: "2048000", }, }, { @@ -234,13 +234,13 @@ func TestCBRBitrate(t *testing.T) { want: settings{ vbr: "0", res: res720, - cbrBitRate: "1024000", + cbrBitrate: "1024000", }, }, } for i, test := range tests { - got, _ := CBRBitRate(test.r)(test.in) + got, _ := CBRBitrate(test.r)(test.in) if got != test.want { t.Errorf("did not get expected result for test: %d\nGot: %v\nWant: %v", i, got, test.want) } @@ -302,8 +302,8 @@ func TestPopulateForm(t *testing.T) { frameRate: defaultFrameRate, vbr: defaultVBR, quality: defaultQuality, - vbrBitRate: defaultVBRBitRate, - cbrBitRate: defaultCBRBitRate, + vbrBitrate: defaultVBRBitrate, + cbrBitrate: defaultCBRBitrate, refresh: defaultRefresh, }, want: "dwConnType=5&mpeg_type=28&dwflicker_hz=0&szResolution=6400360&dwFrameRate=25000&vbr_enable=0&max_bit_rate=512000&custom_rate_control_type=0&custom_bitrate=0&custom_qp_init=25&custom_qp_min=10&custom_qp_max=40&gop_N=2000&dwEncProfile=1&dwEncLevel=31&dwEntropy=0&u8PreAlarmBuf=1&u32PostAlarmBuf2Disk=1&u8SplitInterval=5&bEnableIO=1&bEbIoIn=1&bEbIoIn1=1&bOSDFontSize=0&bEnableOSDCameraName=1&bCamNamePos=2&bEnableOSDDate=1&bDatePos=2&bEnableOSDTime=1&bTimePos=2&szOsdCamName=Camera&u16PostAlarmBuf=1&dwCameraId=1&LangCode=undefined&Recflag=0&submit=Apply", @@ -315,8 +315,8 @@ func TestPopulateForm(t *testing.T) { frameRate: defaultFrameRate, vbr: defaultVBR, quality: defaultQuality, - vbrBitRate: defaultVBRBitRate, - cbrBitRate: defaultCBRBitRate, + vbrBitrate: defaultVBRBitrate, + cbrBitrate: defaultCBRBitrate, refresh: defaultRefresh, }, want: "dwConnType=5&mpeg_type=4&dwflicker_hz=0&szResolution=6400360&dwFrameRate=25000&vbr_enable=1&dwVbrQuality=2&vbrmaxbitrate=500000&custom_qp_init=25&gop_N=1500&u8PreAlarmBuf=1&u32PostAlarmBuf2Disk=1&u8SplitInterval=5&bEnableIO=1&bEbIoIn=1&bEbIoIn1=1&bOSDFontSize=0&bEnableOSDCameraName=1&bCamNamePos=2&bEnableOSDDate=1&bDatePos=2&bEnableOSDTime=1&bTimePos=2&szOsdCamName=Camera&u16PostAlarmBuf=1&dwCameraId=1&LangCode=undefined&Recflag=0&submit=Apply", @@ -328,8 +328,8 @@ func TestPopulateForm(t *testing.T) { frameRate: defaultFrameRate, vbr: "1", quality: defaultQuality, - vbrBitRate: defaultVBRBitRate, - cbrBitRate: defaultCBRBitRate, + vbrBitrate: defaultVBRBitrate, + cbrBitrate: defaultCBRBitrate, refresh: defaultRefresh, }, want: "dwConnType=5&mpeg_type=10&dwflicker_hz=0&szResolution=6400360&dwFrameRate=25000&vbr_enable=1&dwVbrQuality=2&vbrmaxbitrate=250000&custom_rate_control_type=0&custom_bitrate=0&custom_qp_init=25&custom_qp_min=10&custom_qp_max=40&gop_N=2000&dwEncProfile=1&dwEncLevel=31&dwEntropy=0&u8PreAlarmBuf=1&u32PostAlarmBuf2Disk=1&u8SplitInterval=5&bEnableIO=1&bEbIoIn=1&bEbIoIn1=1&bOSDFontSize=0&bEnableOSDCameraName=1&bCamNamePos=2&bEnableOSDDate=1&bDatePos=2&bEnableOSDTime=1&bTimePos=2&szOsdCamName=Camera&u16PostAlarmBuf=1&dwCameraId=1&LangCode=undefined&Recflag=0&submit=Apply", diff --git a/input/gvctrl/utils.go b/input/gvctrl/utils.go index 1ca158da..1c2775fa 100644 --- a/input/gvctrl/utils.go +++ b/input/gvctrl/utils.go @@ -46,8 +46,8 @@ const ( defaultFrameRate = "25000" // 25 fps defaultVBR = "0" // Variable bitrate off defaultQuality = QualityGood - defaultVBRBitRate = "250000" // 512 kbps (lowest with 360p) - defaultCBRBitRate = "512000" + defaultVBRBitrate = "250000" // 512 kbps (lowest with 360p) + defaultCBRBitrate = "512000" defaultRefresh = "2000" // 2 seconds ) @@ -57,8 +57,8 @@ type settings struct { frameRate string vbr string quality Quality - vbrBitRate string - cbrBitRate string + vbrBitrate string + cbrBitrate string refresh string } @@ -69,8 +69,8 @@ func newSettings() settings { frameRate: defaultFrameRate, vbr: defaultVBR, quality: defaultQuality, - vbrBitRate: defaultVBRBitRate, - cbrBitRate: defaultCBRBitRate, + vbrBitrate: defaultVBRBitrate, + cbrBitrate: defaultCBRBitrate, refresh: defaultRefresh, } } @@ -122,11 +122,11 @@ func populateForm(s settings) url.Values { switch s.vbr { case "0": f.Set("vbr_enable", "0") - f.Set("max_bit_rate", s.cbrBitRate) + f.Set("max_bit_rate", s.cbrBitrate) case "1": f.Set("vbr_enable", "1") f.Set("dwVbrQuality", string(s.quality)) - f.Set("vbrmaxbitrate", s.vbrBitRate) + f.Set("vbrmaxbitrate", s.vbrBitrate) default: panic("invalid vbrEnable parameter") } From 5b176134897728581e07f7b6773e322c7549a45a Mon Sep 17 00:00:00 2001 From: Saxon Date: Mon, 14 Oct 2019 10:37:41 +1030 Subject: [PATCH 15/19] input/gvctrl/utils.go: wrote documentation for utils.go --- input/gvctrl/utils.go | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/input/gvctrl/utils.go b/input/gvctrl/utils.go index 1c2775fa..c33cc919 100644 --- a/input/gvctrl/utils.go +++ b/input/gvctrl/utils.go @@ -1,7 +1,7 @@ /* DESCRIPTION - utils.go provides helper functions for functionality found in both gvctrl.go - request.go. + utils.go provides general constants, structs and helper functions for use in + this package. AUTHORS Saxon A. Nelson-Milton @@ -34,12 +34,15 @@ import ( "strings" ) +// The strings used in encoding the settings form to indicate resultion. const ( - res256 = "4480256" - res360 = "6400360" - res720 = "12800720" + res256 = "4480256" // 480x256 + res360 = "6400360" // 640x360 + res720 = "12800720" // 1280x720 ) +// Default values for fields in the settings struct when the newSettings +// constructor is used. const ( defaultCodec = CodecH264 defaultRes = "6400360" // 360p @@ -51,6 +54,8 @@ const ( defaultRefresh = "2000" // 2 seconds ) +// settings holds string representations required by the settings form for each +// of the parameters configurable through this API. type settings struct { codec Codec res string @@ -62,6 +67,7 @@ type settings struct { refresh string } +// newSetting will return a settings with default values. func newSettings() settings { return settings{ codec: defaultCodec, @@ -75,12 +81,15 @@ func newSettings() settings { } } +// md5Hex returns the md5 hex string of s with alphanumerics as upper case. func md5Hex(s string) string { h := md5.New() h.Write([]byte(s)) return strings.ToUpper(hex.EncodeToString(h.Sum(nil))) } +// closestValIdx will return the index of the value in l that is closest to the +// value v. func closestValIdx(v int, l []int) int { var idx int for i := range l { @@ -91,10 +100,14 @@ func closestValIdx(v int, l []int) int { return idx } +// convRate is used to firstly find a value in l closest to the bitrate v (in +// kbps), convert from kbps to bps, and the convert to string. func convRate(v int, l []int) string { return strconv.Itoa(l[closestValIdx(v, l)] * 1000) } +// populateForm will populate the settings form using the passed settings struct +// s and return as a url.Values. func populateForm(s settings) url.Values { f := url.Values{} f.Set("dwConnType", "5") From c726571399ab083b4c71eec4bd998d6548c554b5 Mon Sep 17 00:00:00 2001 From: Saxon Date: Mon, 14 Oct 2019 11:05:11 +1030 Subject: [PATCH 16/19] input/gvctrl/request.go: added documentation to request.go --- input/gvctrl/request.go | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/input/gvctrl/request.go b/input/gvctrl/request.go index 807df136..d4b6f530 100644 --- a/input/gvctrl/request.go +++ b/input/gvctrl/request.go @@ -35,11 +35,11 @@ import ( "strconv" ) +// Relevant sub directories. const ( - baseURL = "http://192.168.1.50" - loginPageURL = baseURL + "/ssi.cgi/login.htm" - loggedInURL = baseURL + "/LoginPC.cgi" - settingsURL = baseURL + "/VideoSetting.cgi" + loginSubDir = "/ssi.cgi/login.htm" // Used to get log in page. + loggedInSubDir = "/LoginPC.cgi" // Used to submit log in. + settingsSubDir = "/VideoSetting.cgi" // Used to submit settings. ) // TODO: make this configurable. @@ -48,8 +48,11 @@ const ( pass = "admin" ) +// genLogIn gets the log in page and extracts the randomly generated cc values +// from which (as well as username and password) two hashes are generated. +// The generated hex is encoded into a url encoded form and returned as a string. func genLogIn(c *http.Client, id, host string) (string, error) { - req, err := http.NewRequest("GET", loginPageURL, nil) + req, err := http.NewRequest("GET", "https://"+host+loginSubDir, nil) if err != nil { return "", fmt.Errorf("can't create GET request for log in page: %v", err) } @@ -97,8 +100,9 @@ func genLogIn(c *http.Client, id, host string) (string, error) { return f.Encode(), nil } +// logIn will submit the form b generated by genLogIn. func logIn(c *http.Client, id, host, b string) error { - req, err := http.NewRequest("POST", loggedInURL, bytes.NewBuffer([]byte(b))) + req, err := http.NewRequest("POST", "http://"+host+loggedInSubDir, bytes.NewBuffer([]byte(b))) if err != nil { return fmt.Errorf("could not create log in request: %v", err) } @@ -106,12 +110,12 @@ func logIn(c *http.Client, id, host, b string) error { req.Header.Set("Connection", "keep-alive") req.Header.Set("Content-Length", "142") req.Header.Set("Cache-Control", "max-age=0") - req.Header.Set("Origin", "http://192.168.1.50") + req.Header.Set("Origin", "http://"+host) req.Header.Set("Upgrade-Insecure-Requests", "1") req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36") req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3") - req.Header.Set("Referer", "http://192.168.1.50/ssi.cgi/Login.htm") + req.Header.Set("Referer", "http://"+host+"/ssi.cgi/Login.htm") req.Header.Set("Accept-Encoding", "gzip, deflate") req.Header.Set("Accept-Language", "en-GB,en-US;q=0.9,en;q=0.8") req.Header.Set("Cookie", "CLIENT_ID="+id+"; CLIENT_ID="+id) @@ -124,9 +128,11 @@ func logIn(c *http.Client, id, host, b string) error { return nil } +// submitSettings will populate a url encoded form using s and submit the +// settings to the server. func submitSettings(c *http.Client, id, host string, s settings) error { fBytes := []byte(populateForm(s).Encode()) - req, err := http.NewRequest("POST", settingsURL, bytes.NewReader(fBytes)) + req, err := http.NewRequest("POST", "http://"+host+settingsSubDir, bytes.NewReader(fBytes)) if err != nil { return fmt.Errorf("could not create settings submit request: %v", err) } @@ -134,12 +140,12 @@ func submitSettings(c *http.Client, id, host string, s settings) error { req.Header.Set("Connection", "keep-alive") req.Header.Set("Content-Length", strconv.Itoa(len(fBytes))) req.Header.Set("Cache-Control", "max-age=0") - req.Header.Set("Origin", "http://192.168.1.50") + req.Header.Set("Origin", "http://"+host) req.Header.Set("Upgrade-Insecure-Requests", "1") req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36") req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3") - req.Header.Set("Referer", "http://192.168.1.50/ssi.cgi/VideoSettingSub.htm?cam=2") + req.Header.Set("Referer", "http://"+host+"/ssi.cgi/VideoSettingSub.htm?cam=2") req.Header.Set("Accept-Encoding", "gzip, deflate") req.Header.Set("Accept-Language", "en-GB,en-US;q=0.9,en;q=0.8") req.Header.Set("Cookie", "CLIENT_ID="+id) From ea7bba7823856ad121eedc0b858d59d61feb4475 Mon Sep 17 00:00:00 2001 From: Saxon Date: Mon, 14 Oct 2019 11:07:27 +1030 Subject: [PATCH 17/19] input/gvctrl/gvctrl-cli/main.go: added package documentation --- input/gvctrl/gvctrl-cli/main.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/input/gvctrl/gvctrl-cli/main.go b/input/gvctrl/gvctrl-cli/main.go index 9963a0c1..8c7d5d18 100644 --- a/input/gvctrl/gvctrl-cli/main.go +++ b/input/gvctrl/gvctrl-cli/main.go @@ -22,6 +22,8 @@ LICENSE in gpl.txt. If not, see http://www.gnu.org/licenses. */ +// Package gvctrl-cli provides a command line interface for controlling GeoVision +// camera settings using the gvctrl package. package main import ( From 0111ba706dc261e6cd1131a1d7934c440accf15f Mon Sep 17 00:00:00 2001 From: Saxon Date: Mon, 14 Oct 2019 16:05:38 +1030 Subject: [PATCH 18/19] input/gvctrl: using login or Login rather than logIn or LogIn --- input/gvctrl/gvctrl.go | 6 +++--- input/gvctrl/request.go | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/input/gvctrl/gvctrl.go b/input/gvctrl/gvctrl.go index b27c805d..51fbfae1 100644 --- a/input/gvctrl/gvctrl.go +++ b/input/gvctrl/gvctrl.go @@ -81,15 +81,15 @@ func Set(host string, options ...Option) error { } // Get the request body required for log in. - body, err := genLogIn(client, id, host) + body, err := getLogin(client, id, host) if err != nil { return fmt.Errorf("could not generate log in request data: %v", err) } // Log in using generated log in request body. - err = logIn(client, id, host, body) + err = login(client, id, host, body) if err != nil { - return fmt.Errorf("could not logIn: %v", err) + return fmt.Errorf("could not login: %v", err) } // Apply the options to the settings specified by the user. diff --git a/input/gvctrl/request.go b/input/gvctrl/request.go index d4b6f530..662cff32 100644 --- a/input/gvctrl/request.go +++ b/input/gvctrl/request.go @@ -48,10 +48,10 @@ const ( pass = "admin" ) -// genLogIn gets the log in page and extracts the randomly generated cc values +// getLogin gets the log in page and extracts the randomly generated cc values // from which (as well as username and password) two hashes are generated. // The generated hex is encoded into a url encoded form and returned as a string. -func genLogIn(c *http.Client, id, host string) (string, error) { +func getLogin(c *http.Client, id, host string) (string, error) { req, err := http.NewRequest("GET", "https://"+host+loginSubDir, nil) if err != nil { return "", fmt.Errorf("can't create GET request for log in page: %v", err) @@ -100,8 +100,8 @@ func genLogIn(c *http.Client, id, host string) (string, error) { return f.Encode(), nil } -// logIn will submit the form b generated by genLogIn. -func logIn(c *http.Client, id, host, b string) error { +// login will submit the form b generated by genLogin. +func login(c *http.Client, id, host, b string) error { req, err := http.NewRequest("POST", "http://"+host+loggedInSubDir, bytes.NewBuffer([]byte(b))) if err != nil { return fmt.Errorf("could not create log in request: %v", err) From 239db674a341ca777c4ed8e8de3c089ab9039eeb Mon Sep 17 00:00:00 2001 From: Saxon Date: Mon, 14 Oct 2019 16:07:15 +1030 Subject: [PATCH 19/19] input/gvctrl: using log-in rather than log in in comments --- input/gvctrl/gvctrl.go | 8 ++++---- input/gvctrl/request.go | 18 +++++++++--------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/input/gvctrl/gvctrl.go b/input/gvctrl/gvctrl.go index 51fbfae1..79d260f1 100644 --- a/input/gvctrl/gvctrl.go +++ b/input/gvctrl/gvctrl.go @@ -46,7 +46,7 @@ import ( // Option describes a function that will apply an option to the passed s. type Option func(s settings) (settings, error) -// Set will log in to the camera at host and submit a form of settings. The +// Set will log-in to the camera at host and submit a form of settings. The // settings form is populated with values influenced by the optional options // passed. Available options are defined below this function. // @@ -80,13 +80,13 @@ func Set(host string, options ...Option) error { Jar: jar, } - // Get the request body required for log in. + // Get the request body required for log-in. body, err := getLogin(client, id, host) if err != nil { - return fmt.Errorf("could not generate log in request data: %v", err) + return fmt.Errorf("could not generate log-in request data: %v", err) } - // Log in using generated log in request body. + // Log in using generated log-in request body. err = login(client, id, host, body) if err != nil { return fmt.Errorf("could not login: %v", err) diff --git a/input/gvctrl/request.go b/input/gvctrl/request.go index 662cff32..3dd72b93 100644 --- a/input/gvctrl/request.go +++ b/input/gvctrl/request.go @@ -37,8 +37,8 @@ import ( // Relevant sub directories. const ( - loginSubDir = "/ssi.cgi/login.htm" // Used to get log in page. - loggedInSubDir = "/LoginPC.cgi" // Used to submit log in. + loginSubDir = "/ssi.cgi/login.htm" // Used to get log-in page. + loggedInSubDir = "/LoginPC.cgi" // Used to submit log-in. settingsSubDir = "/VideoSetting.cgi" // Used to submit settings. ) @@ -48,13 +48,13 @@ const ( pass = "admin" ) -// getLogin gets the log in page and extracts the randomly generated cc values +// getLogin gets the log-in page and extracts the randomly generated cc values // from which (as well as username and password) two hashes are generated. // The generated hex is encoded into a url encoded form and returned as a string. func getLogin(c *http.Client, id, host string) (string, error) { req, err := http.NewRequest("GET", "https://"+host+loginSubDir, nil) if err != nil { - return "", fmt.Errorf("can't create GET request for log in page: %v", err) + return "", fmt.Errorf("can't create GET request for log-in page: %v", err) } req.Header.Set("Connection", "keep-alive") @@ -68,18 +68,18 @@ func getLogin(c *http.Client, id, host string) (string, error) { resp, err := c.Do(req) if err != nil { - return "", fmt.Errorf("could not do GET request for log in page: %v", err) + return "", fmt.Errorf("could not do GET request for log-in page: %v", err) } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { - return "", fmt.Errorf("could not read response of GET request for log in page: %v", err) + return "", fmt.Errorf("could not read response of GET request for log-in page: %v", err) } // Find the CC values in the source of the response. // These are used in calculation of the md5 hashes for the form submitted at - // log in. + // log-in. var cc [2]string for i := range cc { regStr := "cc" + strconv.Itoa(i+1) + "=\".{4}\"" @@ -104,7 +104,7 @@ func getLogin(c *http.Client, id, host string) (string, error) { func login(c *http.Client, id, host, b string) error { req, err := http.NewRequest("POST", "http://"+host+loggedInSubDir, bytes.NewBuffer([]byte(b))) if err != nil { - return fmt.Errorf("could not create log in request: %v", err) + return fmt.Errorf("could not create log-in request: %v", err) } req.Header.Set("Connection", "keep-alive") @@ -122,7 +122,7 @@ func login(c *http.Client, id, host, b string) error { _, err = c.Do(req) if err != nil { - return fmt.Errorf("could not do log in request: %v", err) + return fmt.Errorf("could not do log-in request: %v", err) } return nil