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.
This commit is contained in:
Saxon 2019-10-11 20:16:21 +10:30
parent 8e3f173162
commit e56455f7d0
4 changed files with 492 additions and 0 deletions

3
input/gvctrl/go.mod Normal file
View File

@ -0,0 +1,3 @@
module bitbucket.org/ausocean/av/input/gvctrl
go 1.12

225
input/gvctrl/gvctrl.go Normal file
View File

@ -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 <saxon@ausocean.org>
LICENSE
Copyright (C) 2019 the Australian Ocean Lab (AusOcean)
It is free software: you can redistribute it and/or modify them
under the terms of the GNU General Public License as published by the
Free Software Foundation, either version 3 of the License, or (at your
option) any later version.
It is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
for more details.
You should have received a copy of the GNU General Public License
in gpl.txt. If not, see http://www.gnu.org/licenses.
*/
package 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
}
}

210
input/gvctrl/request.go Normal file
View File

@ -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 <saxon@ausocean.org>
LICENSE
Copyright (C) 2019 the Australian Ocean Lab (AusOcean)
It is free software: you can redistribute it and/or modify them
under the terms of the GNU General Public License as published by the
Free Software Foundation, either version 3 of the License, or (at your
option) any later version.
It is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
for more details.
You should have received a copy of the GNU General Public License
in gpl.txt. If not, see http://www.gnu.org/licenses.
*/
package 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
}

54
input/gvctrl/utils.go Normal file
View File

@ -0,0 +1,54 @@
/*
DESCRIPTION
utils.go provides helper functions for functionality found in both gvctrl.go
request.go.
AUTHORS
Saxon A. Nelson-Milton <saxon@ausocean.org>
LICENSE
Copyright (C) 2019 the Australian Ocean Lab (AusOcean)
It is free software: you can redistribute it and/or modify them
under the terms of the GNU General Public License as published by the
Free Software Foundation, either version 3 of the License, or (at your
option) any later version.
It is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
for more details.
You should have received a copy of the GNU General Public License
in gpl.txt. If not, see http://www.gnu.org/licenses.
*/
package 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)
}