927 lines
20 KiB
Go
927 lines
20 KiB
Go
// backend\onvif_resolutions.go
|
|
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/sha1"
|
|
"crypto/tls"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"encoding/xml"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
)
|
|
|
|
const onvifResolutionCacheMaxAge = 24 * time.Hour
|
|
const onvifResolutionFetchTimeout = 7 * time.Second
|
|
|
|
type onvifResolutionCacheEntry struct {
|
|
Resolutions []string
|
|
CheckedAt time.Time
|
|
CheckError string
|
|
}
|
|
|
|
type onvifCameraConnection struct {
|
|
Host string
|
|
Port string
|
|
Username string
|
|
Password string
|
|
SkipTLSVerify bool
|
|
}
|
|
|
|
func (s *Server) getCachedOnvifCameraResolutions(
|
|
ctx context.Context,
|
|
cameraDevice milestoneCameraPatchDevice,
|
|
skipTLSVerify bool,
|
|
) ([]string, error) {
|
|
host := strings.TrimSpace(cameraDevice.IPAddress)
|
|
port := strings.TrimSpace(cameraDevice.MilestonePort)
|
|
if host == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
if port == "" {
|
|
port = "80"
|
|
}
|
|
|
|
hardwareID := strings.TrimSpace(cameraDevice.MilestoneHardwareID)
|
|
cacheReady := hardwareID != ""
|
|
if cacheReady {
|
|
if err := s.ensureOnvifResolutionCacheTable(ctx); err != nil {
|
|
cacheReady = false
|
|
log.Printf("ONVIF resolution cache table could not be ensured: %v", err)
|
|
}
|
|
}
|
|
|
|
var cachedEntry onvifResolutionCacheEntry
|
|
var cached bool
|
|
if cacheReady {
|
|
entry, ok, err := s.getOnvifResolutionCache(
|
|
ctx,
|
|
hardwareID,
|
|
host,
|
|
port,
|
|
)
|
|
if err != nil {
|
|
log.Printf("ONVIF resolution cache could not be read: %v", err)
|
|
} else {
|
|
cachedEntry = entry
|
|
cached = ok
|
|
}
|
|
}
|
|
|
|
if cached &&
|
|
len(cachedEntry.Resolutions) > 0 &&
|
|
time.Since(cachedEntry.CheckedAt) <= onvifResolutionCacheMaxAge {
|
|
return cachedEntry.Resolutions, nil
|
|
}
|
|
|
|
username, password := s.onvifCredentials(ctx)
|
|
fetchCtx, cancel := context.WithTimeout(ctx, onvifResolutionFetchTimeout)
|
|
defer cancel()
|
|
|
|
resolutions, err := fetchOnvifVideoEncoderResolutions(fetchCtx, onvifCameraConnection{
|
|
Host: host,
|
|
Port: port,
|
|
Username: username,
|
|
Password: password,
|
|
SkipTLSVerify: skipTLSVerify,
|
|
})
|
|
if err != nil {
|
|
if cached && len(cachedEntry.Resolutions) > 0 {
|
|
return cachedEntry.Resolutions, nil
|
|
}
|
|
|
|
if cacheReady {
|
|
if cacheErr := s.upsertOnvifResolutionCache(
|
|
ctx,
|
|
hardwareID,
|
|
host,
|
|
port,
|
|
nil,
|
|
err.Error(),
|
|
); cacheErr != nil {
|
|
log.Printf("ONVIF resolution cache error could not be stored: %v", cacheErr)
|
|
}
|
|
}
|
|
|
|
return nil, err
|
|
}
|
|
|
|
if cacheReady {
|
|
if err := s.upsertOnvifResolutionCache(
|
|
ctx,
|
|
hardwareID,
|
|
host,
|
|
port,
|
|
resolutions,
|
|
"",
|
|
); err != nil {
|
|
log.Printf("ONVIF resolution cache could not be stored: %v", err)
|
|
}
|
|
}
|
|
|
|
return resolutions, nil
|
|
}
|
|
|
|
func (s *Server) onvifCredentials(ctx context.Context) (string, string) {
|
|
envUsername := strings.TrimSpace(os.Getenv("ONVIF_USERNAME"))
|
|
envPassword := strings.TrimSpace(os.Getenv("ONVIF_PASSWORD"))
|
|
if envUsername != "" || envPassword != "" {
|
|
return envUsername, envPassword
|
|
}
|
|
|
|
credentials, err := s.getMilestoneCredentials(ctx)
|
|
if err != nil {
|
|
return "", ""
|
|
}
|
|
|
|
return strings.TrimSpace(credentials.Username), strings.TrimSpace(credentials.Password)
|
|
}
|
|
|
|
func (s *Server) ensureOnvifResolutionCacheTable(ctx context.Context) error {
|
|
_, err := s.db.Exec(
|
|
ctx,
|
|
`
|
|
CREATE TABLE IF NOT EXISTS onvif_resolution_cache (
|
|
milestone_hardware_id TEXT PRIMARY KEY,
|
|
host TEXT NOT NULL DEFAULT '',
|
|
port TEXT NOT NULL DEFAULT '',
|
|
resolutions JSONB NOT NULL DEFAULT '[]'::JSONB,
|
|
check_error TEXT NOT NULL DEFAULT '',
|
|
checked_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
)
|
|
`,
|
|
)
|
|
return err
|
|
}
|
|
|
|
func (s *Server) getOnvifResolutionCache(
|
|
ctx context.Context,
|
|
hardwareID string,
|
|
host string,
|
|
port string,
|
|
) (onvifResolutionCacheEntry, bool, error) {
|
|
var entry onvifResolutionCacheEntry
|
|
var rawResolutions []byte
|
|
|
|
err := s.db.QueryRow(
|
|
ctx,
|
|
`
|
|
SELECT resolutions, checked_at, check_error
|
|
FROM onvif_resolution_cache
|
|
WHERE milestone_hardware_id = $1
|
|
AND host = $2
|
|
AND port = $3
|
|
LIMIT 1
|
|
`,
|
|
strings.TrimSpace(hardwareID),
|
|
strings.TrimSpace(host),
|
|
strings.TrimSpace(port),
|
|
).Scan(
|
|
&rawResolutions,
|
|
&entry.CheckedAt,
|
|
&entry.CheckError,
|
|
)
|
|
if errors.Is(err, pgxErrNoRows()) {
|
|
return entry, false, nil
|
|
}
|
|
if err != nil {
|
|
return entry, false, err
|
|
}
|
|
|
|
if err := json.Unmarshal(rawResolutions, &entry.Resolutions); err != nil {
|
|
return entry, false, err
|
|
}
|
|
|
|
entry.Resolutions = sortMilestoneResolutionValues(entry.Resolutions)
|
|
|
|
return entry, true, nil
|
|
}
|
|
|
|
func (s *Server) upsertOnvifResolutionCache(
|
|
ctx context.Context,
|
|
hardwareID string,
|
|
host string,
|
|
port string,
|
|
resolutions []string,
|
|
checkError string,
|
|
) error {
|
|
resolutions = sortMilestoneResolutionValues(resolutions)
|
|
|
|
rawResolutions, err := json.Marshal(resolutions)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = s.db.Exec(
|
|
ctx,
|
|
`
|
|
INSERT INTO onvif_resolution_cache (
|
|
milestone_hardware_id,
|
|
host,
|
|
port,
|
|
resolutions,
|
|
check_error,
|
|
checked_at,
|
|
updated_at
|
|
)
|
|
VALUES ($1, $2, $3, $4::JSONB, $5, now(), now())
|
|
ON CONFLICT (milestone_hardware_id)
|
|
DO UPDATE SET
|
|
host = EXCLUDED.host,
|
|
port = EXCLUDED.port,
|
|
resolutions = EXCLUDED.resolutions,
|
|
check_error = EXCLUDED.check_error,
|
|
checked_at = EXCLUDED.checked_at,
|
|
updated_at = now()
|
|
`,
|
|
strings.TrimSpace(hardwareID),
|
|
strings.TrimSpace(host),
|
|
strings.TrimSpace(port),
|
|
string(rawResolutions),
|
|
strings.TrimSpace(checkError),
|
|
)
|
|
return err
|
|
}
|
|
|
|
func fetchOnvifVideoEncoderResolutions(
|
|
ctx context.Context,
|
|
connection onvifCameraConnection,
|
|
) ([]string, error) {
|
|
client := onvifHTTPClient(connection.SkipTLSVerify)
|
|
var lastErr error
|
|
|
|
for _, deviceEndpoint := range onvifDeviceServiceCandidates(connection.Host, connection.Port) {
|
|
mediaXAddr, err := onvifGetMediaServiceXAddr(
|
|
ctx,
|
|
client,
|
|
deviceEndpoint,
|
|
connection,
|
|
)
|
|
if err != nil {
|
|
lastErr = err
|
|
continue
|
|
}
|
|
|
|
if mediaXAddr == "" {
|
|
mediaXAddr = deviceEndpoint
|
|
}
|
|
mediaXAddr = onvifResolveEndpoint(deviceEndpoint, mediaXAddr)
|
|
|
|
resolutions, err := onvifGetVideoEncoderConfigurationOptions(
|
|
ctx,
|
|
client,
|
|
mediaXAddr,
|
|
"",
|
|
connection,
|
|
)
|
|
if err == nil && len(resolutions) > 0 {
|
|
return resolutions, nil
|
|
}
|
|
if err != nil {
|
|
lastErr = err
|
|
}
|
|
|
|
profileTokens, profileErr := onvifGetProfileTokens(
|
|
ctx,
|
|
client,
|
|
mediaXAddr,
|
|
connection,
|
|
)
|
|
if profileErr != nil {
|
|
lastErr = profileErr
|
|
continue
|
|
}
|
|
|
|
merged := []string{}
|
|
for _, profileToken := range profileTokens {
|
|
profileResolutions, err := onvifGetVideoEncoderConfigurationOptions(
|
|
ctx,
|
|
client,
|
|
mediaXAddr,
|
|
profileToken,
|
|
connection,
|
|
)
|
|
if err != nil {
|
|
lastErr = err
|
|
continue
|
|
}
|
|
merged = append(merged, profileResolutions...)
|
|
}
|
|
|
|
merged = sortMilestoneResolutionValues(merged)
|
|
if len(merged) > 0 {
|
|
return merged, nil
|
|
}
|
|
}
|
|
|
|
if lastErr != nil {
|
|
return nil, lastErr
|
|
}
|
|
|
|
return nil, fmt.Errorf("ONVIF returned no video encoder resolution options")
|
|
}
|
|
|
|
func onvifGetMediaServiceXAddr(
|
|
ctx context.Context,
|
|
client *http.Client,
|
|
deviceEndpoint string,
|
|
connection onvifCameraConnection,
|
|
) (string, error) {
|
|
body := `<tds:GetCapabilities xmlns:tds="http://www.onvif.org/ver10/device/wsdl">` +
|
|
`<tds:Category>Media</tds:Category>` +
|
|
`</tds:GetCapabilities>`
|
|
|
|
responseBody, err := onvifSOAPRequest(
|
|
ctx,
|
|
client,
|
|
deviceEndpoint,
|
|
"http://www.onvif.org/ver10/device/wsdl/GetCapabilities",
|
|
body,
|
|
connection,
|
|
)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
xaddrs := xmlTextValuesByLocalName(responseBody, "XAddr")
|
|
for _, xaddr := range xaddrs {
|
|
if strings.Contains(strings.ToLower(xaddr), "media") {
|
|
return strings.TrimSpace(xaddr), nil
|
|
}
|
|
}
|
|
if len(xaddrs) > 0 {
|
|
return strings.TrimSpace(xaddrs[0]), nil
|
|
}
|
|
|
|
return "", fmt.Errorf("ONVIF media service XAddr is missing")
|
|
}
|
|
|
|
func onvifGetProfileTokens(
|
|
ctx context.Context,
|
|
client *http.Client,
|
|
mediaEndpoint string,
|
|
connection onvifCameraConnection,
|
|
) ([]string, error) {
|
|
body := `<trt:GetProfiles xmlns:trt="http://www.onvif.org/ver10/media/wsdl"/>`
|
|
|
|
responseBody, err := onvifSOAPRequest(
|
|
ctx,
|
|
client,
|
|
mediaEndpoint,
|
|
"http://www.onvif.org/ver10/media/wsdl/GetProfiles",
|
|
body,
|
|
connection,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return xmlProfileTokens(responseBody), nil
|
|
}
|
|
|
|
func onvifGetVideoEncoderConfigurationOptions(
|
|
ctx context.Context,
|
|
client *http.Client,
|
|
mediaEndpoint string,
|
|
profileToken string,
|
|
connection onvifCameraConnection,
|
|
) ([]string, error) {
|
|
body := `<trt:GetVideoEncoderConfigurationOptions xmlns:trt="http://www.onvif.org/ver10/media/wsdl">`
|
|
if strings.TrimSpace(profileToken) != "" {
|
|
body += `<trt:ProfileToken>` + xmlText(profileToken) + `</trt:ProfileToken>`
|
|
}
|
|
body += `</trt:GetVideoEncoderConfigurationOptions>`
|
|
|
|
responseBody, err := onvifSOAPRequest(
|
|
ctx,
|
|
client,
|
|
mediaEndpoint,
|
|
"http://www.onvif.org/ver10/media/wsdl/GetVideoEncoderConfigurationOptions",
|
|
body,
|
|
connection,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return xmlResolutionValues(responseBody), nil
|
|
}
|
|
|
|
func onvifSOAPRequest(
|
|
ctx context.Context,
|
|
client *http.Client,
|
|
endpoint string,
|
|
action string,
|
|
body string,
|
|
connection onvifCameraConnection,
|
|
) ([]byte, error) {
|
|
var lastErr error
|
|
|
|
for _, soap12 := range []bool{true, false} {
|
|
responseBody, err := onvifSOAPRequestWithVersion(
|
|
ctx,
|
|
client,
|
|
endpoint,
|
|
action,
|
|
body,
|
|
connection,
|
|
soap12,
|
|
)
|
|
if err == nil {
|
|
return responseBody, nil
|
|
}
|
|
lastErr = err
|
|
}
|
|
|
|
return nil, lastErr
|
|
}
|
|
|
|
func onvifSOAPRequestWithVersion(
|
|
ctx context.Context,
|
|
client *http.Client,
|
|
endpoint string,
|
|
action string,
|
|
body string,
|
|
connection onvifCameraConnection,
|
|
soap12 bool,
|
|
) ([]byte, error) {
|
|
envelopeNS := "http://www.w3.org/2003/05/soap-envelope"
|
|
if !soap12 {
|
|
envelopeNS = "http://schemas.xmlsoap.org/soap/envelope/"
|
|
}
|
|
|
|
envelope := `<?xml version="1.0" encoding="UTF-8"?>` +
|
|
`<s:Envelope xmlns:s="` + envelopeNS + `">` +
|
|
`<s:Header>` + onvifSecurityHeader(connection.Username, connection.Password) + `</s:Header>` +
|
|
`<s:Body>` + body + `</s:Body>` +
|
|
`</s:Envelope>`
|
|
|
|
request, err := http.NewRequestWithContext(
|
|
ctx,
|
|
http.MethodPost,
|
|
endpoint,
|
|
strings.NewReader(envelope),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if soap12 {
|
|
request.Header.Set("Content-Type", `application/soap+xml; charset=utf-8; action="`+action+`"`)
|
|
} else {
|
|
request.Header.Set("Content-Type", "text/xml; charset=utf-8")
|
|
request.Header.Set("SOAPAction", `"`+action+`"`)
|
|
}
|
|
request.Header.Set("Accept", "application/soap+xml, text/xml, application/xml")
|
|
|
|
response, err := client.Do(request)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer response.Body.Close()
|
|
|
|
responseBody, readErr := io.ReadAll(response.Body)
|
|
if readErr != nil {
|
|
return nil, readErr
|
|
}
|
|
|
|
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
|
return nil, fmt.Errorf(
|
|
"ONVIF request failed: endpoint=%s status=%d body=%s",
|
|
endpoint,
|
|
response.StatusCode,
|
|
truncateMilestoneLogBody(responseBody),
|
|
)
|
|
}
|
|
|
|
if faultText := xmlSOAPFaultText(responseBody); faultText != "" {
|
|
return nil, fmt.Errorf("ONVIF SOAP fault: %s", faultText)
|
|
}
|
|
|
|
return responseBody, nil
|
|
}
|
|
|
|
func onvifSecurityHeader(username string, password string) string {
|
|
username = strings.TrimSpace(username)
|
|
password = strings.TrimSpace(password)
|
|
if username == "" && password == "" {
|
|
return ""
|
|
}
|
|
|
|
nonce := make([]byte, 20)
|
|
if _, err := rand.Read(nonce); err != nil {
|
|
return ""
|
|
}
|
|
|
|
created := time.Now().UTC().Format("2006-01-02T15:04:05Z")
|
|
hash := sha1.New()
|
|
hash.Write(nonce)
|
|
hash.Write([]byte(created))
|
|
hash.Write([]byte(password))
|
|
digest := base64.StdEncoding.EncodeToString(hash.Sum(nil))
|
|
encodedNonce := base64.StdEncoding.EncodeToString(nonce)
|
|
|
|
return `<wsse:Security s:mustUnderstand="1" ` +
|
|
`xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" ` +
|
|
`xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">` +
|
|
`<wsse:UsernameToken>` +
|
|
`<wsse:Username>` + xmlText(username) + `</wsse:Username>` +
|
|
`<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">` +
|
|
xmlText(digest) +
|
|
`</wsse:Password>` +
|
|
`<wsse:Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">` +
|
|
xmlText(encodedNonce) +
|
|
`</wsse:Nonce>` +
|
|
`<wsu:Created>` + xmlText(created) + `</wsu:Created>` +
|
|
`</wsse:UsernameToken>` +
|
|
`</wsse:Security>`
|
|
}
|
|
|
|
func onvifDeviceServiceCandidates(host string, port string) []string {
|
|
host = strings.TrimSpace(host)
|
|
port = strings.TrimSpace(port)
|
|
|
|
if parsedHost, parsedPort := parseOnvifHostAndPort(host); parsedHost != "" {
|
|
host = parsedHost
|
|
if port == "" {
|
|
port = parsedPort
|
|
}
|
|
}
|
|
|
|
if port == "" {
|
|
port = "80"
|
|
}
|
|
|
|
ports := []string{port}
|
|
if port != "80" {
|
|
ports = append(ports, "80")
|
|
}
|
|
|
|
candidates := []string{}
|
|
seen := map[string]struct{}{}
|
|
|
|
for _, candidatePort := range ports {
|
|
schemes := []string{"http"}
|
|
if candidatePort == "443" {
|
|
schemes = []string{"https", "http"}
|
|
}
|
|
|
|
for _, scheme := range schemes {
|
|
candidate := scheme + "://" + net.JoinHostPort(host, candidatePort) + "/onvif/device_service"
|
|
if _, exists := seen[candidate]; exists {
|
|
continue
|
|
}
|
|
seen[candidate] = struct{}{}
|
|
candidates = append(candidates, candidate)
|
|
}
|
|
}
|
|
|
|
return candidates
|
|
}
|
|
|
|
func parseOnvifHostAndPort(value string) (string, string) {
|
|
value = strings.TrimSpace(value)
|
|
if value == "" {
|
|
return "", ""
|
|
}
|
|
|
|
if strings.Contains(value, "://") {
|
|
parsedURL, err := url.Parse(value)
|
|
if err == nil && parsedURL.Hostname() != "" {
|
|
return strings.TrimSpace(parsedURL.Hostname()), strings.TrimSpace(parsedURL.Port())
|
|
}
|
|
}
|
|
|
|
parsedURL, err := url.Parse("//" + value)
|
|
if err == nil && parsedURL.Hostname() != "" {
|
|
return strings.TrimSpace(parsedURL.Hostname()), strings.TrimSpace(parsedURL.Port())
|
|
}
|
|
|
|
return value, ""
|
|
}
|
|
|
|
func onvifResolveEndpoint(baseEndpoint string, rawEndpoint string) string {
|
|
rawEndpoint = strings.TrimSpace(rawEndpoint)
|
|
if rawEndpoint == "" {
|
|
return baseEndpoint
|
|
}
|
|
|
|
parsedEndpoint, err := url.Parse(rawEndpoint)
|
|
if err == nil && parsedEndpoint.IsAbs() {
|
|
return rawEndpoint
|
|
}
|
|
|
|
baseURL, err := url.Parse(baseEndpoint)
|
|
if err != nil {
|
|
return rawEndpoint
|
|
}
|
|
|
|
relativeURL, err := url.Parse(rawEndpoint)
|
|
if err != nil {
|
|
return rawEndpoint
|
|
}
|
|
|
|
return baseURL.ResolveReference(relativeURL).String()
|
|
}
|
|
|
|
func onvifHTTPClient(skipTLSVerify bool) *http.Client {
|
|
client := &http.Client{
|
|
Timeout: onvifResolutionFetchTimeout,
|
|
}
|
|
|
|
if skipTLSVerify {
|
|
client.Transport = &http.Transport{
|
|
TLSClientConfig: &tls.Config{
|
|
InsecureSkipVerify: true,
|
|
},
|
|
}
|
|
}
|
|
|
|
return client
|
|
}
|
|
|
|
func xmlText(value string) string {
|
|
var buffer bytes.Buffer
|
|
_ = xml.EscapeText(&buffer, []byte(value))
|
|
return buffer.String()
|
|
}
|
|
|
|
func xmlTextValuesByLocalName(body []byte, localName string) []string {
|
|
decoder := xml.NewDecoder(bytes.NewReader(body))
|
|
values := []string{}
|
|
|
|
for {
|
|
token, err := decoder.Token()
|
|
if err != nil {
|
|
break
|
|
}
|
|
|
|
startElement, ok := token.(xml.StartElement)
|
|
if !ok || startElement.Name.Local != localName {
|
|
continue
|
|
}
|
|
|
|
var text string
|
|
if err := decoder.DecodeElement(&text, &startElement); err == nil {
|
|
text = strings.TrimSpace(text)
|
|
if text != "" {
|
|
values = append(values, text)
|
|
}
|
|
}
|
|
}
|
|
|
|
return values
|
|
}
|
|
|
|
func xmlProfileTokens(body []byte) []string {
|
|
decoder := xml.NewDecoder(bytes.NewReader(body))
|
|
tokens := []string{}
|
|
seen := map[string]struct{}{}
|
|
|
|
for {
|
|
token, err := decoder.Token()
|
|
if err != nil {
|
|
break
|
|
}
|
|
|
|
startElement, ok := token.(xml.StartElement)
|
|
if !ok || startElement.Name.Local != "Profiles" {
|
|
continue
|
|
}
|
|
|
|
for _, attr := range startElement.Attr {
|
|
if attr.Name.Local != "token" {
|
|
continue
|
|
}
|
|
|
|
value := strings.TrimSpace(attr.Value)
|
|
if value == "" {
|
|
continue
|
|
}
|
|
|
|
if _, exists := seen[value]; exists {
|
|
continue
|
|
}
|
|
seen[value] = struct{}{}
|
|
tokens = append(tokens, value)
|
|
}
|
|
}
|
|
|
|
return tokens
|
|
}
|
|
|
|
func xmlResolutionValues(body []byte) []string {
|
|
decoder := xml.NewDecoder(bytes.NewReader(body))
|
|
resolutions := []string{}
|
|
seen := map[string]struct{}{}
|
|
|
|
inResolution := false
|
|
width := 0
|
|
height := 0
|
|
|
|
addResolution := func() {
|
|
if width <= 0 || height <= 0 {
|
|
return
|
|
}
|
|
|
|
value := fmt.Sprintf("%dx%d", width, height)
|
|
if _, exists := seen[value]; exists {
|
|
return
|
|
}
|
|
seen[value] = struct{}{}
|
|
resolutions = append(resolutions, value)
|
|
}
|
|
|
|
for {
|
|
token, err := decoder.Token()
|
|
if err != nil {
|
|
break
|
|
}
|
|
|
|
switch typedToken := token.(type) {
|
|
case xml.StartElement:
|
|
switch typedToken.Name.Local {
|
|
case "ResolutionsAvailable", "Resolution":
|
|
inResolution = true
|
|
width = 0
|
|
height = 0
|
|
case "Width":
|
|
if !inResolution {
|
|
continue
|
|
}
|
|
var text string
|
|
if err := decoder.DecodeElement(&text, &typedToken); err == nil {
|
|
width, _ = strconv.Atoi(strings.TrimSpace(text))
|
|
}
|
|
case "Height":
|
|
if !inResolution {
|
|
continue
|
|
}
|
|
var text string
|
|
if err := decoder.DecodeElement(&text, &typedToken); err == nil {
|
|
height, _ = strconv.Atoi(strings.TrimSpace(text))
|
|
}
|
|
}
|
|
case xml.EndElement:
|
|
switch typedToken.Name.Local {
|
|
case "ResolutionsAvailable", "Resolution":
|
|
addResolution()
|
|
inResolution = false
|
|
width = 0
|
|
height = 0
|
|
}
|
|
}
|
|
}
|
|
|
|
return sortMilestoneResolutionValues(resolutions)
|
|
}
|
|
|
|
func xmlSOAPFaultText(body []byte) string {
|
|
for _, name := range []string{"Text", "Reason", "faultstring"} {
|
|
values := xmlTextValuesByLocalName(body, name)
|
|
if len(values) == 0 {
|
|
continue
|
|
}
|
|
return values[0]
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func applyOnvifAvailableResolutionsToMilestoneSettings(
|
|
settings map[string]any,
|
|
resolutions []string,
|
|
) {
|
|
if len(resolutions) == 0 {
|
|
return
|
|
}
|
|
|
|
rawStreams, ok := settings["stream"].([]any)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
for _, rawStream := range rawStreams {
|
|
streamMap, ok := rawStream.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
streamMap["availableResolutions"] = mergeMilestoneResolutionValues(
|
|
streamMap["availableResolutions"],
|
|
milestoneStringValue(streamMap["resolution"]),
|
|
resolutions,
|
|
)
|
|
}
|
|
}
|
|
|
|
func applyOnvifAvailableResolutionsToMilestoneActiveStreams(
|
|
activeStreams []any,
|
|
resolutions []string,
|
|
) {
|
|
if len(resolutions) == 0 {
|
|
return
|
|
}
|
|
|
|
for _, rawGroup := range activeStreams {
|
|
group, ok := rawGroup.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
rawSettings, ok := group["stream"].([]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
for _, rawSetting := range rawSettings {
|
|
setting, ok := rawSetting.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
setting["availableResolutions"] = mergeMilestoneResolutionValues(
|
|
setting["availableResolutions"],
|
|
milestoneStringValue(setting["resolution"]),
|
|
resolutions,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
func mergeMilestoneResolutionValues(existing any, current string, extra []string) []string {
|
|
values := []string{}
|
|
values = append(values, extractMilestoneResolutionValues(existing, 0)...)
|
|
values = append(values, current)
|
|
values = append(values, extra...)
|
|
return sortMilestoneResolutionValues(values)
|
|
}
|
|
|
|
func sortMilestoneResolutionValues(values []string) []string {
|
|
seen := map[string]struct{}{}
|
|
resolutions := []string{}
|
|
|
|
for _, value := range values {
|
|
resolution := normalizeMilestoneResolution(value)
|
|
if resolution == "" {
|
|
continue
|
|
}
|
|
|
|
if _, exists := seen[resolution]; exists {
|
|
continue
|
|
}
|
|
|
|
seen[resolution] = struct{}{}
|
|
resolutions = append(resolutions, resolution)
|
|
}
|
|
|
|
sort.SliceStable(resolutions, func(i int, j int) bool {
|
|
iWidth, iHeight := splitMilestoneResolution(resolutions[i])
|
|
jWidth, jHeight := splitMilestoneResolution(resolutions[j])
|
|
iPixels := iWidth * iHeight
|
|
jPixels := jWidth * jHeight
|
|
if iPixels == jPixels {
|
|
if iWidth == jWidth {
|
|
return iHeight < jHeight
|
|
}
|
|
return iWidth < jWidth
|
|
}
|
|
return iPixels < jPixels
|
|
})
|
|
|
|
return resolutions
|
|
}
|
|
|
|
func splitMilestoneResolution(value string) (int, int) {
|
|
parts := strings.Split(normalizeMilestoneResolution(value), "x")
|
|
if len(parts) != 2 {
|
|
return 0, 0
|
|
}
|
|
|
|
width, _ := strconv.Atoi(parts[0])
|
|
height, _ := strconv.Atoi(parts[1])
|
|
return width, height
|
|
}
|
|
|
|
func pgxErrNoRows() error {
|
|
return pgx.ErrNoRows
|
|
}
|