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) +}