mirror of
https://github.com/kerberos-io/agent.git
synced 2026-03-12 15:59:13 +00:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f29b952001 | ||
|
|
38247ac9f6 | ||
|
|
580f17028a | ||
|
|
48d933a561 | ||
|
|
0c70ab6158 | ||
|
|
839185dac8 | ||
|
|
ba6cdef9d5 | ||
|
|
bedb3c0d7f | ||
|
|
2539255940 | ||
|
|
24136f8b15 | ||
|
|
910bb3c079 | ||
|
|
47f4c19617 | ||
|
|
280a81809a | ||
|
|
59358acb30 | ||
|
|
ebd655ac73 | ||
|
|
6325e37aae | ||
|
|
ecabc47847 | ||
|
|
31cc3d8939 | ||
|
|
c71cb71d08 | ||
|
|
65a739ea75 | ||
|
|
410a62e9ef | ||
|
|
aa76dd1ec8 | ||
|
|
384448d123 | ||
|
|
414f74758c |
@@ -227,6 +227,10 @@ Next to attaching the configuration file, it is also possible to override the co
|
||||
| `AGENT_KERBEROSVAULT_DIRECTORY` | The directory, in the provider, where the recordings will be stored in. | "" |
|
||||
| `AGENT_DROPBOX_ACCESS_TOKEN` | The Access Token from your Dropbox app, that is used to leverage the Dropbox SDK. | "" |
|
||||
| `AGENT_DROPBOX_DIRECTORY` | The directory, in the provider, where the recordings will be stored in. | "" |
|
||||
| `AGENT_ENCRYPTION` | Enable 'true' or disable 'false' end-to-end encryption through MQTT (recordings will follow). | "false" |
|
||||
| `AGENT_ENCRYPTION_FINGERPRINT` | The fingerprint of the keypair (public/private keys), so you know which one to use. | "" |
|
||||
| `AGENT_ENCRYPTION_PRIVATE_KEY` | The private key (assymetric/RSA) to decryptand sign requests send over MQTT. | "" |
|
||||
| `AGENT_ENCRYPTION_SYMMETRIC_KEY` | The symmetric key (AES) to encrypt and decrypt request send over MQTT. | "" |
|
||||
|
||||
## Contribute with Codespaces
|
||||
|
||||
|
||||
2
machinery/.vscode/launch.json
vendored
2
machinery/.vscode/launch.json
vendored
@@ -10,7 +10,7 @@
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "main.go",
|
||||
"args": ["-action run"],
|
||||
"args": ["-action", "run"],
|
||||
"envFile": "${workspaceFolder}/.env",
|
||||
"buildFlags": "--tags dynamic",
|
||||
},
|
||||
|
||||
@@ -111,5 +111,6 @@
|
||||
"hub_key": "",
|
||||
"hub_private_key": "",
|
||||
"hub_site": "",
|
||||
"condition_uri": ""
|
||||
"condition_uri": "",
|
||||
"encryption": {}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,35 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/camera/onvif/gotopreset": {
|
||||
"post": {
|
||||
"description": "Will activate the desired ONVIF preset.",
|
||||
"tags": [
|
||||
"camera"
|
||||
],
|
||||
"summary": "Will activate the desired ONVIF preset.",
|
||||
"operationId": "camera-onvif-gotopreset",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "OnvifPreset",
|
||||
"name": "config",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.OnvifPreset"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.APIResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/camera/onvif/login": {
|
||||
"post": {
|
||||
"description": "Try to login into ONVIF supported camera.",
|
||||
@@ -112,6 +141,35 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/camera/onvif/presets": {
|
||||
"post": {
|
||||
"description": "Will return the ONVIF presets for the specific camera.",
|
||||
"tags": [
|
||||
"camera"
|
||||
],
|
||||
"summary": "Will return the ONVIF presets for the specific camera.",
|
||||
"operationId": "camera-onvif-presets",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "OnvifCredentials",
|
||||
"name": "config",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.OnvifCredentials"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.APIResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/camera/onvif/zoom": {
|
||||
"post": {
|
||||
"description": "Zooming in or out the camera.",
|
||||
@@ -317,8 +375,15 @@ const docTemplate = `{
|
||||
"models.APIResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"can_pan_tilt": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"can_zoom": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"data": {},
|
||||
"message": {}
|
||||
"message": {},
|
||||
"ptz_functions": {}
|
||||
}
|
||||
},
|
||||
"models.Authentication": {
|
||||
@@ -621,6 +686,17 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.OnvifPreset": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"onvif_credentials": {
|
||||
"$ref": "#/definitions/models.OnvifCredentials"
|
||||
},
|
||||
"preset": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.OnvifZoom": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -46,6 +46,35 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/camera/onvif/gotopreset": {
|
||||
"post": {
|
||||
"description": "Will activate the desired ONVIF preset.",
|
||||
"tags": [
|
||||
"camera"
|
||||
],
|
||||
"summary": "Will activate the desired ONVIF preset.",
|
||||
"operationId": "camera-onvif-gotopreset",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "OnvifPreset",
|
||||
"name": "config",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.OnvifPreset"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.APIResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/camera/onvif/login": {
|
||||
"post": {
|
||||
"description": "Try to login into ONVIF supported camera.",
|
||||
@@ -104,6 +133,35 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/camera/onvif/presets": {
|
||||
"post": {
|
||||
"description": "Will return the ONVIF presets for the specific camera.",
|
||||
"tags": [
|
||||
"camera"
|
||||
],
|
||||
"summary": "Will return the ONVIF presets for the specific camera.",
|
||||
"operationId": "camera-onvif-presets",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "OnvifCredentials",
|
||||
"name": "config",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.OnvifCredentials"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.APIResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/camera/onvif/zoom": {
|
||||
"post": {
|
||||
"description": "Zooming in or out the camera.",
|
||||
@@ -309,8 +367,15 @@
|
||||
"models.APIResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"can_pan_tilt": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"can_zoom": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"data": {},
|
||||
"message": {}
|
||||
"message": {},
|
||||
"ptz_functions": {}
|
||||
}
|
||||
},
|
||||
"models.Authentication": {
|
||||
@@ -613,6 +678,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.OnvifPreset": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"onvif_credentials": {
|
||||
"$ref": "#/definitions/models.OnvifCredentials"
|
||||
},
|
||||
"preset": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.OnvifZoom": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -2,8 +2,13 @@ basePath: /
|
||||
definitions:
|
||||
models.APIResponse:
|
||||
properties:
|
||||
can_pan_tilt:
|
||||
type: boolean
|
||||
can_zoom:
|
||||
type: boolean
|
||||
data: {}
|
||||
message: {}
|
||||
ptz_functions: {}
|
||||
type: object
|
||||
models.Authentication:
|
||||
properties:
|
||||
@@ -202,6 +207,13 @@ definitions:
|
||||
tilt:
|
||||
type: number
|
||||
type: object
|
||||
models.OnvifPreset:
|
||||
properties:
|
||||
onvif_credentials:
|
||||
$ref: '#/definitions/models.OnvifCredentials'
|
||||
preset:
|
||||
type: string
|
||||
type: object
|
||||
models.OnvifZoom:
|
||||
properties:
|
||||
onvif_credentials:
|
||||
@@ -310,6 +322,25 @@ paths:
|
||||
summary: Will return the ONVIF capabilities for the specific camera.
|
||||
tags:
|
||||
- camera
|
||||
/api/camera/onvif/gotopreset:
|
||||
post:
|
||||
description: Will activate the desired ONVIF preset.
|
||||
operationId: camera-onvif-gotopreset
|
||||
parameters:
|
||||
- description: OnvifPreset
|
||||
in: body
|
||||
name: config
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/models.OnvifPreset'
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIResponse'
|
||||
summary: Will activate the desired ONVIF preset.
|
||||
tags:
|
||||
- camera
|
||||
/api/camera/onvif/login:
|
||||
post:
|
||||
description: Try to login into ONVIF supported camera.
|
||||
@@ -348,6 +379,25 @@ paths:
|
||||
summary: Panning or/and tilting the camera.
|
||||
tags:
|
||||
- camera
|
||||
/api/camera/onvif/presets:
|
||||
post:
|
||||
description: Will return the ONVIF presets for the specific camera.
|
||||
operationId: camera-onvif-presets
|
||||
parameters:
|
||||
- description: OnvifCredentials
|
||||
in: body
|
||||
name: config
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/models.OnvifCredentials'
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIResponse'
|
||||
summary: Will return the ONVIF presets for the specific camera.
|
||||
tags:
|
||||
- camera
|
||||
/api/camera/onvif/zoom:
|
||||
post:
|
||||
description: Zooming in or out the camera.
|
||||
|
||||
@@ -3,7 +3,7 @@ module github.com/kerberos-io/agent/machinery
|
||||
go 1.19
|
||||
|
||||
// replace github.com/kerberos-io/joy4 v1.0.57 => ../../../../github.com/kerberos-io/joy4
|
||||
// replace github.com/kerberos-io/onvif v0.0.5 => ../../../../github.com/kerberos-io/onvif
|
||||
// replace github.com/kerberos-io/onvif v0.0.6 => ../../../../github.com/kerberos-io/onvif
|
||||
|
||||
require (
|
||||
github.com/InVisionApp/conjungo v1.1.0
|
||||
@@ -25,7 +25,7 @@ require (
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/kellydunn/golang-geo v0.7.0
|
||||
github.com/kerberos-io/joy4 v1.0.58
|
||||
github.com/kerberos-io/onvif v0.0.5
|
||||
github.com/kerberos-io/onvif v0.0.7
|
||||
github.com/minio/minio-go/v6 v6.0.57
|
||||
github.com/nsmith5/mjpeg v0.0.0-20200913181537-54b8ada0e53e
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||
|
||||
@@ -266,8 +266,8 @@ github.com/kellydunn/golang-geo v0.7.0 h1:A5j0/BvNgGwY6Yb6inXQxzYwlPHc6WVZR+Mrar
|
||||
github.com/kellydunn/golang-geo v0.7.0/go.mod h1:YYlQPJ+DPEzrHx8kT3oPHC/NjyvCCXE+IuKGKdrjrcU=
|
||||
github.com/kerberos-io/joy4 v1.0.58 h1:R8EECSF+bG7o2yHC6cX/lF77Z+bDVGl6OioLZ3+5MN4=
|
||||
github.com/kerberos-io/joy4 v1.0.58/go.mod h1:nZp4AjvKvTOXRrmDyAIOw+Da+JA5OcSo/JundGfOlFU=
|
||||
github.com/kerberos-io/onvif v0.0.5 h1:kq9mnHZkih9Jl4DyIJ4Rzt++Y3DDKy3nI8S2ESEfZ5w=
|
||||
github.com/kerberos-io/onvif v0.0.5/go.mod h1:Hr2dJOH2LM5SpYKk17gYZ1CMjhGhUl+QlT5kwYogrW0=
|
||||
github.com/kerberos-io/onvif v0.0.7 h1:LIrXjTH7G2W9DN69xZeJSB0uS3W1+C3huFO8kTqx7/A=
|
||||
github.com/kerberos-io/onvif v0.0.7/go.mod h1:Hr2dJOH2LM5SpYKk17gYZ1CMjhGhUl+QlT5kwYogrW0=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/klauspost/compress v1.15.0 h1:xqfchp4whNFxn5A4XFyyYtitiWI8Hy5EW59jEwcyL6U=
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"github.com/kerberos-io/agent/machinery/src/components"
|
||||
"github.com/kerberos-io/agent/machinery/src/log"
|
||||
"github.com/kerberos-io/agent/machinery/src/models"
|
||||
|
||||
configService "github.com/kerberos-io/agent/machinery/src/config"
|
||||
"github.com/kerberos-io/agent/machinery/src/routers"
|
||||
"github.com/kerberos-io/agent/machinery/src/utils"
|
||||
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
|
||||
@@ -92,10 +94,10 @@ func main() {
|
||||
configuration.Port = port
|
||||
|
||||
// Open this configuration either from Kerberos Agent or Kerberos Factory.
|
||||
components.OpenConfig(configDirectory, &configuration)
|
||||
configService.OpenConfig(configDirectory, &configuration)
|
||||
|
||||
// We will override the configuration with the environment variables
|
||||
components.OverrideWithEnvironmentVariables(&configuration)
|
||||
configService.OverrideWithEnvironmentVariables(&configuration)
|
||||
|
||||
// Printing final configuration
|
||||
utils.PrintConfiguration(&configuration)
|
||||
@@ -113,7 +115,7 @@ func main() {
|
||||
if configuration.Config.Key == "" {
|
||||
key := utils.RandStringBytesMaskImpr(30)
|
||||
configuration.Config.Key = key
|
||||
err := components.StoreConfig(configDirectory, configuration.Config)
|
||||
err := configService.StoreConfig(configDirectory, configuration.Config)
|
||||
if err == nil {
|
||||
log.Log.Info("Main: updated unique key for agent to: " + key)
|
||||
} else {
|
||||
|
||||
1
machinery/src/api/onvif.go
Normal file
1
machinery/src/api/onvif.go
Normal file
@@ -0,0 +1 @@
|
||||
package api
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
|
||||
func CleanupRecordingDirectory(configDirectory string, configuration *models.Configuration) {
|
||||
autoClean := configuration.Config.AutoClean
|
||||
if autoClean != "false" {
|
||||
if autoClean == "true" {
|
||||
maxSize := configuration.Config.MaxDirectorySize
|
||||
if maxSize == 0 {
|
||||
maxSize = 300
|
||||
|
||||
@@ -279,6 +279,8 @@ loop:
|
||||
onvifEnabled := "false"
|
||||
onvifZoom := "false"
|
||||
onvifPanTilt := "false"
|
||||
onvifPresets := "false"
|
||||
var onvifPresetsList []byte
|
||||
if config.Capture.IPCamera.ONVIFXAddr != "" {
|
||||
cameraConfiguration := configuration.Config.Capture.IPCamera
|
||||
device, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
|
||||
@@ -293,8 +295,34 @@ loop:
|
||||
if canPanTilt {
|
||||
onvifPanTilt = "true"
|
||||
}
|
||||
// Try to read out presets
|
||||
presets, err := onvif.GetPresetsFromDevice(device)
|
||||
if err == nil && len(presets) > 0 {
|
||||
onvifPresets = "true"
|
||||
onvifPresetsList, err = json.Marshal(presets)
|
||||
if err != nil {
|
||||
log.Log.Error("HandleHeartBeat: error while marshalling presets: " + err.Error())
|
||||
onvifPresetsList = []byte("[]")
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
log.Log.Error("HandleHeartBeat: error while getting presets: " + err.Error())
|
||||
} else {
|
||||
log.Log.Debug("HandleHeartBeat: no presets found.")
|
||||
}
|
||||
onvifPresetsList = []byte("[]")
|
||||
}
|
||||
} else {
|
||||
log.Log.Error("HandleHeartBeat: error while getting PTZ configurations: " + err.Error())
|
||||
onvifPresetsList = []byte("[]")
|
||||
}
|
||||
} else {
|
||||
log.Log.Error("HandleHeartBeat: error while connecting to ONVIF device: " + err.Error())
|
||||
onvifPresetsList = []byte("[]")
|
||||
}
|
||||
} else {
|
||||
log.Log.Debug("HandleHeartBeat: ONVIF is not enabled.")
|
||||
onvifPresetsList = []byte("[]")
|
||||
}
|
||||
|
||||
// Check if the agent is running inside a cluster (Kerberos Factory) or as
|
||||
@@ -339,6 +367,8 @@ loop:
|
||||
"onvif" : "%s",
|
||||
"onvif_zoom" : "%s",
|
||||
"onvif_pantilt" : "%s",
|
||||
"onvif_presets": "%s",
|
||||
"onvif_presets_list": %s,
|
||||
"cameraConnected": "%s",
|
||||
"numberoffiles" : "33",
|
||||
"timestamp" : 1564747908,
|
||||
@@ -346,7 +376,7 @@ loop:
|
||||
"docker" : true,
|
||||
"kios" : false,
|
||||
"raspberrypi" : false
|
||||
}`, config.Key, system.Version, system.CPUId, username, key, name, isEnterprise, system.Hostname, system.Architecture, system.TotalMemory, system.UsedMemory, system.FreeMemory, system.ProcessUsedMemory, macs, ips, "0", "0", "0", uptimeString, boottimeString, config.HubSite, onvifEnabled, onvifZoom, onvifPanTilt, cameraConnected)
|
||||
}`, config.Key, system.Version, system.CPUId, username, key, name, isEnterprise, system.Hostname, system.Architecture, system.TotalMemory, system.UsedMemory, system.FreeMemory, system.ProcessUsedMemory, macs, ips, "0", "0", "0", uptimeString, boottimeString, config.HubSite, onvifEnabled, onvifZoom, onvifPanTilt, onvifPresets, onvifPresetsList, cameraConnected)
|
||||
|
||||
var jsonStr = []byte(object)
|
||||
buffy := bytes.NewBuffer(jsonStr)
|
||||
@@ -428,19 +458,17 @@ func HandleLiveStreamSD(livestreamCursor *pubsub.QueueCursor, configuration *mod
|
||||
// Allocate frame
|
||||
frame := ffmpeg.AllocVideoFrame()
|
||||
|
||||
key := ""
|
||||
hubKey := ""
|
||||
if config.Cloud == "s3" && config.S3 != nil && config.S3.Publickey != "" {
|
||||
key = config.S3.Publickey
|
||||
hubKey = config.S3.Publickey
|
||||
} else if config.Cloud == "kstorage" && config.KStorage != nil && config.KStorage.CloudKey != "" {
|
||||
key = config.KStorage.CloudKey
|
||||
hubKey = config.KStorage.CloudKey
|
||||
}
|
||||
// This is the new way ;)
|
||||
if config.HubKey != "" {
|
||||
key = config.HubKey
|
||||
hubKey = config.HubKey
|
||||
}
|
||||
|
||||
topic := "kerberos/" + key + "/device/" + config.Key + "/live"
|
||||
|
||||
lastLivestreamRequest := int64(0)
|
||||
|
||||
var cursorError error
|
||||
@@ -461,7 +489,27 @@ func HandleLiveStreamSD(livestreamCursor *pubsub.QueueCursor, configuration *mod
|
||||
continue
|
||||
}
|
||||
log.Log.Info("HandleLiveStreamSD: Sending base64 encoded images to MQTT.")
|
||||
sendImage(frame, topic, mqttClient, pkt, decoder, decoderMutex)
|
||||
_, err := computervision.GetRawImage(frame, pkt, decoder, decoderMutex)
|
||||
if err == nil {
|
||||
bytes, _ := computervision.ImageToBytes(&frame.Image)
|
||||
encoded := base64.StdEncoding.EncodeToString(bytes)
|
||||
|
||||
valueMap := make(map[string]interface{})
|
||||
valueMap["image"] = encoded
|
||||
message := models.Message{
|
||||
Payload: models.Payload{
|
||||
Action: "receive-sd-stream",
|
||||
DeviceId: configuration.Config.Key,
|
||||
Value: valueMap,
|
||||
},
|
||||
}
|
||||
payload, err := models.PackageMQTTMessage(configuration, message)
|
||||
if err == nil {
|
||||
mqttClient.Publish("kerberos/hub/"+hubKey, 0, false, payload)
|
||||
} else {
|
||||
log.Log.Info("HandleRequestConfig: something went wrong while sending acknowledge config to hub: " + string(payload))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup the frame.
|
||||
@@ -475,15 +523,6 @@ func HandleLiveStreamSD(livestreamCursor *pubsub.QueueCursor, configuration *mod
|
||||
log.Log.Debug("HandleLiveStreamSD: finished")
|
||||
}
|
||||
|
||||
func sendImage(frame *ffmpeg.VideoFrame, topic string, mqttClient mqtt.Client, pkt av.Packet, decoder *ffmpeg.VideoDecoder, decoderMutex *sync.Mutex) {
|
||||
_, err := computervision.GetRawImage(frame, pkt, decoder, decoderMutex)
|
||||
if err == nil {
|
||||
bytes, _ := computervision.ImageToBytes(&frame.Image)
|
||||
encoded := base64.StdEncoding.EncodeToString(bytes)
|
||||
mqttClient.Publish(topic, 0, false, encoded)
|
||||
}
|
||||
}
|
||||
|
||||
func HandleLiveStreamHD(livestreamCursor *pubsub.QueueCursor, configuration *models.Configuration, communication *models.Communication, mqttClient mqtt.Client, codecs []av.CodecData, decoder *ffmpeg.VideoDecoder, decoderMutex *sync.Mutex) {
|
||||
|
||||
config := configuration.Config
|
||||
@@ -502,25 +541,23 @@ func HandleLiveStreamHD(livestreamCursor *pubsub.QueueCursor, configuration *mod
|
||||
|
||||
if config.Capture.ForwardWebRTC == "true" {
|
||||
// We get a request with an offer, but we'll forward it.
|
||||
for m := range communication.HandleLiveHDHandshake {
|
||||
/*for m := range communication.HandleLiveHDHandshake {
|
||||
// Forward SDP
|
||||
m.CloudKey = config.Key
|
||||
request, err := json.Marshal(m)
|
||||
if err == nil {
|
||||
mqttClient.Publish("kerberos/webrtc/request", 2, false, request)
|
||||
}
|
||||
}
|
||||
}*/
|
||||
} else {
|
||||
log.Log.Info("HandleLiveStreamHD: Waiting for peer connections.")
|
||||
for handshake := range communication.HandleLiveHDHandshake {
|
||||
log.Log.Info("HandleLiveStreamHD: setting up a peer connection.")
|
||||
key := config.Key + "/" + handshake.Cuuid
|
||||
webrtc.CandidatesMutex.Lock()
|
||||
key := config.Key + "/" + handshake.SessionID
|
||||
_, ok := webrtc.CandidateArrays[key]
|
||||
if !ok {
|
||||
webrtc.CandidateArrays[key] = make(chan string, 30)
|
||||
webrtc.CandidateArrays[key] = make(chan string)
|
||||
}
|
||||
webrtc.CandidatesMutex.Unlock()
|
||||
webrtc.InitializeWebRTCConnection(configuration, communication, mqttClient, videoTrack, audioTrack, handshake, webrtc.CandidateArrays[key])
|
||||
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ func UploadKerberosHub(configuration *models.Configuration, fileName string) (bo
|
||||
if err != nil {
|
||||
err := "UploadKerberosHub: Upload Failed, file doesn't exists anymore."
|
||||
log.Log.Info(err)
|
||||
return false, true, errors.New(err)
|
||||
return false, false, errors.New(err)
|
||||
}
|
||||
|
||||
// Check if we are allowed to upload to the hub with these credentials.
|
||||
|
||||
@@ -44,7 +44,7 @@ func UploadKerberosVault(configuration *models.Configuration, fileName string) (
|
||||
if err != nil {
|
||||
err := "UploadKerberosVault: Upload Failed, file doesn't exists anymore."
|
||||
log.Log.Info(err)
|
||||
return false, true, errors.New(err)
|
||||
return false, false, errors.New(err)
|
||||
}
|
||||
|
||||
publicKey := config.KStorage.CloudKey
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/kerberos-io/agent/machinery/src/capture"
|
||||
"github.com/kerberos-io/agent/machinery/src/cloud"
|
||||
"github.com/kerberos-io/agent/machinery/src/computervision"
|
||||
configService "github.com/kerberos-io/agent/machinery/src/config"
|
||||
"github.com/kerberos-io/agent/machinery/src/log"
|
||||
"github.com/kerberos-io/agent/machinery/src/models"
|
||||
"github.com/kerberos-io/agent/machinery/src/onvif"
|
||||
@@ -72,7 +73,7 @@ func Bootstrap(configDirectory string, configuration *models.Configuration, comm
|
||||
|
||||
// We'll create a MQTT handler, which will be used to communicate with Kerberos Hub.
|
||||
// Configure a MQTT client which helps for a bi-directional communication
|
||||
mqttClient := routers.ConfigureMQTT(configuration, communication)
|
||||
mqttClient := routers.ConfigureMQTT(configDirectory, configuration, communication)
|
||||
|
||||
// Run the agent and fire up all the other
|
||||
// goroutines which do image capture, motion detection, onvif, etc.
|
||||
@@ -87,15 +88,15 @@ func Bootstrap(configDirectory string, configuration *models.Configuration, comm
|
||||
|
||||
if status == "not started" {
|
||||
// We will re open the configuration, might have changed :O!
|
||||
OpenConfig(configDirectory, configuration)
|
||||
configService.OpenConfig(configDirectory, configuration)
|
||||
// We will override the configuration with the environment variables
|
||||
OverrideWithEnvironmentVariables(configuration)
|
||||
configService.OverrideWithEnvironmentVariables(configuration)
|
||||
}
|
||||
|
||||
// Reset the MQTT client, might have provided new information, so we need to reconnect.
|
||||
if routers.HasMQTTClientModified(configuration) {
|
||||
routers.DisconnectMQTT(mqttClient, &configuration.Config)
|
||||
mqttClient = routers.ConfigureMQTT(configuration, communication)
|
||||
mqttClient = routers.ConfigureMQTT(configDirectory, configuration, communication)
|
||||
}
|
||||
|
||||
// We will create a new cancelable context, which will be used to cancel and restart.
|
||||
@@ -134,6 +135,10 @@ func RunAgent(configDirectory string, configuration *models.Configuration, commu
|
||||
width := videoStream.(av.VideoCodecData).Width()
|
||||
height := videoStream.(av.VideoCodecData).Height()
|
||||
|
||||
// Set config values as well
|
||||
configuration.Config.Capture.IPCamera.Width = width
|
||||
configuration.Config.Capture.IPCamera.Height = height
|
||||
|
||||
var queue *pubsub.Queue
|
||||
var subQueue *pubsub.Queue
|
||||
|
||||
@@ -162,6 +167,13 @@ func RunAgent(configDirectory string, configuration *models.Configuration, commu
|
||||
time.Sleep(time.Second * 3)
|
||||
return status
|
||||
}
|
||||
|
||||
width := videoStream.(av.VideoCodecData).Width()
|
||||
height := videoStream.(av.VideoCodecData).Height()
|
||||
|
||||
// Set config values as well
|
||||
configuration.Config.Capture.IPCamera.Width = width
|
||||
configuration.Config.Capture.IPCamera.Height = height
|
||||
}
|
||||
|
||||
if cameraSettings.RTSP != rtspUrl || cameraSettings.SubRTSP != subRtspUrl || cameraSettings.Width != width || cameraSettings.Height != height || cameraSettings.Num != num || cameraSettings.Denum != denum || cameraSettings.Codec != videoStream.(av.VideoCodecData).Type() {
|
||||
@@ -190,6 +202,7 @@ func RunAgent(configDirectory string, configuration *models.Configuration, commu
|
||||
cameraSettings.Denum = denum
|
||||
cameraSettings.Codec = videoStream.(av.VideoCodecData).Type()
|
||||
cameraSettings.Initialized = true
|
||||
|
||||
} else {
|
||||
log.Log.Info("RunAgent: camera settings did not change, keeping decoder")
|
||||
}
|
||||
@@ -251,7 +264,7 @@ func RunAgent(configDirectory string, configuration *models.Configuration, commu
|
||||
}
|
||||
|
||||
// Handle livestream HD (high resolution over WEBRTC)
|
||||
communication.HandleLiveHDHandshake = make(chan models.SDPPayload, 1)
|
||||
communication.HandleLiveHDHandshake = make(chan models.RequestHDStreamPayload, 1)
|
||||
if subStreamEnabled {
|
||||
livestreamHDCursor := subQueue.Latest()
|
||||
go cloud.HandleLiveStreamHD(livestreamHDCursor, configuration, communication, mqttClient, subStreams, subDecoder, &decoderMutex)
|
||||
@@ -285,10 +298,10 @@ func RunAgent(configDirectory string, configuration *models.Configuration, commu
|
||||
(*communication.CancelContext)()
|
||||
|
||||
// We will re open the configuration, might have changed :O!
|
||||
OpenConfig(configDirectory, configuration)
|
||||
configService.OpenConfig(configDirectory, configuration)
|
||||
|
||||
// We will override the configuration with the environment variables
|
||||
OverrideWithEnvironmentVariables(configuration)
|
||||
configService.OverrideWithEnvironmentVariables(configuration)
|
||||
|
||||
// Here we are cleaning up everything!
|
||||
if configuration.Config.Offline != "true" {
|
||||
|
||||
@@ -41,7 +41,8 @@ func ProcessMotion(motionCursor *pubsub.QueueCursor, configuration *models.Confi
|
||||
|
||||
log.Log.Info("ProcessMotion: Motion detection enabled.")
|
||||
|
||||
key := config.HubKey
|
||||
hubKey := config.HubKey
|
||||
deviceKey := config.Key
|
||||
|
||||
// Allocate a VideoFrame
|
||||
frame := ffmpeg.AllocVideoFrame()
|
||||
@@ -167,10 +168,24 @@ func ProcessMotion(motionCursor *pubsub.QueueCursor, configuration *models.Confi
|
||||
// If offline mode is disabled, send a message to the hub
|
||||
if config.Offline != "true" {
|
||||
if mqttClient != nil {
|
||||
if key != "" {
|
||||
mqttClient.Publish("kerberos/"+key+"/device/"+config.Key+"/motion", 2, false, "motion")
|
||||
if hubKey != "" {
|
||||
message := models.Message{
|
||||
Payload: models.Payload{
|
||||
Action: "motion",
|
||||
DeviceId: configuration.Config.Key,
|
||||
Value: map[string]interface{}{
|
||||
"timestamp": time.Now().Unix(),
|
||||
},
|
||||
},
|
||||
}
|
||||
payload, err := models.PackageMQTTMessage(configuration, message)
|
||||
if err == nil {
|
||||
mqttClient.Publish("kerberos/hub/"+hubKey, 0, false, payload)
|
||||
} else {
|
||||
log.Log.Info("ProcessMotion: failed to package MQTT message: " + err.Error())
|
||||
}
|
||||
} else {
|
||||
mqttClient.Publish("kerberos/device/"+config.Key+"/motion", 2, false, "motion")
|
||||
mqttClient.Publish("kerberos/agent/"+deviceKey, 2, false, "motion")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package components
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -90,11 +90,18 @@ func OpenConfig(configDirectory string, configuration *models.Configuration) {
|
||||
|
||||
if res.Err() != nil {
|
||||
log.Log.Error("Could not find global configuration, using default configuration.")
|
||||
panic("Could not find global configuration, using default configuration.")
|
||||
}
|
||||
err := res.Decode(&globalConfig)
|
||||
if err != nil {
|
||||
log.Log.Error("Could not find global configuration, using default configuration.")
|
||||
panic("Could not find global configuration, using default configuration.")
|
||||
}
|
||||
if globalConfig.Type != "global" {
|
||||
log.Log.Error("Could not find global configuration, might missed the mongodb connection.")
|
||||
panic("Could not find global configuration, might missed the mongodb connection.")
|
||||
}
|
||||
|
||||
configuration.GlobalConfig = globalConfig
|
||||
|
||||
var customConfig models.Config
|
||||
@@ -110,6 +117,11 @@ func OpenConfig(configDirectory string, configuration *models.Configuration) {
|
||||
if err != nil {
|
||||
log.Log.Error("Could not find configuration for " + deploymentName + ", using global configuration.")
|
||||
}
|
||||
|
||||
if customConfig.Type != "config" {
|
||||
log.Log.Error("Could not find custom configuration, might missed the mongodb connection.")
|
||||
panic("Could not find custom configuration, might missed the mongodb connection.")
|
||||
}
|
||||
configuration.CustomConfig = customConfig
|
||||
|
||||
// We will merge both configs in a single config file.
|
||||
@@ -129,8 +141,13 @@ func OpenConfig(configDirectory string, configuration *models.Configuration) {
|
||||
},
|
||||
)
|
||||
|
||||
// Merge Config toplevel
|
||||
// Reset main configuration Config.
|
||||
configuration.Config = models.Config{}
|
||||
|
||||
// Merge the global settings in the main config
|
||||
conjungo.Merge(&configuration.Config, configuration.GlobalConfig, opts)
|
||||
|
||||
// Now we might override some settings with the custom config
|
||||
conjungo.Merge(&configuration.Config, configuration.CustomConfig, opts)
|
||||
|
||||
// Merge Kerberos Vault settings
|
||||
@@ -145,6 +162,9 @@ func OpenConfig(configDirectory string, configuration *models.Configuration) {
|
||||
conjungo.Merge(&s3, configuration.CustomConfig.S3, opts)
|
||||
configuration.Config.S3 = &s3
|
||||
|
||||
// Merge timetable manually because it's an array
|
||||
configuration.Config.Timetable = configuration.CustomConfig.Timetable
|
||||
|
||||
// Cleanup
|
||||
opts = nil
|
||||
|
||||
@@ -198,7 +218,7 @@ func OverrideWithEnvironmentVariables(configuration *models.Configuration) {
|
||||
configuration.Config.Key = value
|
||||
break
|
||||
case "AGENT_NAME":
|
||||
configuration.Config.Name = value
|
||||
configuration.Config.FriendlyName = value
|
||||
break
|
||||
case "AGENT_TIMEZONE":
|
||||
configuration.Config.Timezone = value
|
||||
@@ -441,6 +461,24 @@ func OverrideWithEnvironmentVariables(configuration *models.Configuration) {
|
||||
case "AGENT_DROPBOX_DIRECTORY":
|
||||
configuration.Config.Dropbox.Directory = value
|
||||
break
|
||||
|
||||
/* When encryption is enabled */
|
||||
case "AGENT_ENCRYPTION":
|
||||
if value == "true" {
|
||||
configuration.Config.Encryption.Enabled = true
|
||||
} else {
|
||||
configuration.Config.Encryption.Enabled = false
|
||||
}
|
||||
break
|
||||
case "AGENT_ENCRYPTION_FINGERPRINT":
|
||||
configuration.Config.Encryption.Fingerprint = value
|
||||
break
|
||||
case "AGENT_ENCRYPTION_PRIVATE_KEY":
|
||||
configuration.Config.Encryption.PrivateKey = value
|
||||
break
|
||||
case "AGENT_ENCRYPTION_SYMMETRIC_KEY":
|
||||
configuration.Config.Encryption.SymmetricKey = value
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
148
machinery/src/encryption/main.go
Normal file
148
machinery/src/encryption/main.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package encryption
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"hash"
|
||||
)
|
||||
|
||||
// DecryptWithPrivateKey decrypts data with private key
|
||||
func DecryptWithPrivateKey(ciphertext string, privateKey *rsa.PrivateKey) ([]byte, error) {
|
||||
|
||||
// decode our encrypted string into cipher bytes
|
||||
cipheredValue, _ := base64.StdEncoding.DecodeString(ciphertext)
|
||||
|
||||
// decrypt the data
|
||||
out, err := rsa.DecryptPKCS1v15(nil, privateKey, cipheredValue)
|
||||
|
||||
return out, err
|
||||
}
|
||||
|
||||
// SignWithPrivateKey signs data with private key
|
||||
func SignWithPrivateKey(data []byte, privateKey *rsa.PrivateKey) ([]byte, error) {
|
||||
|
||||
// hash the data with sha256
|
||||
hashed := sha256.Sum256(data)
|
||||
|
||||
// sign the data
|
||||
signature, err := rsa.SignPKCS1v15(nil, privateKey, crypto.SHA256, hashed[:])
|
||||
|
||||
return signature, err
|
||||
}
|
||||
|
||||
func AesEncrypt(content string, password string) (string, error) {
|
||||
salt := make([]byte, 8)
|
||||
_, err := rand.Read(salt)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
key, iv, err := DefaultEvpKDF([]byte(password), salt)
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
mode := cipher.NewCBCEncrypter(block, iv)
|
||||
cipherBytes := PKCS5Padding([]byte(content), aes.BlockSize)
|
||||
mode.CryptBlocks(cipherBytes, cipherBytes)
|
||||
|
||||
data := make([]byte, 16+len(cipherBytes))
|
||||
copy(data[:8], []byte("Salted__"))
|
||||
copy(data[8:16], salt)
|
||||
copy(data[16:], cipherBytes)
|
||||
|
||||
cipherText := base64.StdEncoding.EncodeToString(data)
|
||||
return cipherText, nil
|
||||
}
|
||||
|
||||
func AesDecrypt(cipherText string, password string) (string, error) {
|
||||
data, err := base64.StdEncoding.DecodeString(cipherText)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if string(data[:8]) != "Salted__" {
|
||||
return "", errors.New("invalid crypto js aes encryption")
|
||||
}
|
||||
|
||||
salt := data[8:16]
|
||||
cipherBytes := data[16:]
|
||||
key, iv, err := DefaultEvpKDF([]byte(password), salt)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
mode := cipher.NewCBCDecrypter(block, iv)
|
||||
mode.CryptBlocks(cipherBytes, cipherBytes)
|
||||
|
||||
result := PKCS5UnPadding(cipherBytes)
|
||||
return string(result), nil
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/questions/27677236/encryption-in-javascript-and-decryption-with-php/27678978#27678978
|
||||
// https://github.com/brix/crypto-js/blob/8e6d15bf2e26d6ff0af5277df2604ca12b60a718/src/evpkdf.js#L55
|
||||
func EvpKDF(password []byte, salt []byte, keySize int, iterations int, hashAlgorithm string) ([]byte, error) {
|
||||
var block []byte
|
||||
var hasher hash.Hash
|
||||
derivedKeyBytes := make([]byte, 0)
|
||||
switch hashAlgorithm {
|
||||
case "md5":
|
||||
hasher = md5.New()
|
||||
default:
|
||||
return []byte{}, errors.New("not implement hasher algorithm")
|
||||
}
|
||||
for len(derivedKeyBytes) < keySize*4 {
|
||||
if len(block) > 0 {
|
||||
hasher.Write(block)
|
||||
}
|
||||
hasher.Write(password)
|
||||
hasher.Write(salt)
|
||||
block = hasher.Sum([]byte{})
|
||||
hasher.Reset()
|
||||
|
||||
for i := 1; i < iterations; i++ {
|
||||
hasher.Write(block)
|
||||
block = hasher.Sum([]byte{})
|
||||
hasher.Reset()
|
||||
}
|
||||
derivedKeyBytes = append(derivedKeyBytes, block...)
|
||||
}
|
||||
return derivedKeyBytes[:keySize*4], nil
|
||||
}
|
||||
|
||||
func DefaultEvpKDF(password []byte, salt []byte) (key []byte, iv []byte, err error) {
|
||||
// https://github.com/brix/crypto-js/blob/8e6d15bf2e26d6ff0af5277df2604ca12b60a718/src/cipher-core.js#L775
|
||||
keySize := 256 / 32
|
||||
ivSize := 128 / 32
|
||||
derivedKeyBytes, err := EvpKDF(password, salt, keySize+ivSize, 1, "md5")
|
||||
if err != nil {
|
||||
return []byte{}, []byte{}, err
|
||||
}
|
||||
return derivedKeyBytes[:keySize*4], derivedKeyBytes[keySize*4:], nil
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/questions/41579325/golang-how-do-i-decrypt-with-des-cbc-and-pkcs7
|
||||
func PKCS5UnPadding(src []byte) []byte {
|
||||
length := len(src)
|
||||
unpadding := int(src[length-1])
|
||||
return src[:(length - unpadding)]
|
||||
}
|
||||
|
||||
func PKCS5Padding(src []byte, blockSize int) []byte {
|
||||
padding := blockSize - len(src)%blockSize
|
||||
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||
return append(src, padtext...)
|
||||
}
|
||||
@@ -29,3 +29,8 @@ type OnvifZoom struct {
|
||||
OnvifCredentials OnvifCredentials `json:"onvif_credentials,omitempty" bson:"onvif_credentials"`
|
||||
Zoom float64 `json:"zoom,omitempty" bson:"zoom"`
|
||||
}
|
||||
|
||||
type OnvifPreset struct {
|
||||
OnvifCredentials OnvifCredentials `json:"onvif_credentials,omitempty" bson:"onvif_credentials"`
|
||||
Preset string `json:"preset,omitempty" bson:"preset"`
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ type Communication struct {
|
||||
HandleHeartBeat chan string
|
||||
HandleLiveSD chan int64
|
||||
HandleLiveHDKeepalive chan string
|
||||
HandleLiveHDHandshake chan SDPPayload
|
||||
HandleLiveHDHandshake chan RequestHDStreamPayload
|
||||
HandleLiveHDPeers chan string
|
||||
HandleONVIF chan OnvifAction
|
||||
IsConfiguring *abool.AtomicBool
|
||||
|
||||
@@ -21,7 +21,7 @@ type Config struct {
|
||||
AutoClean string `json:"auto_clean"`
|
||||
RemoveAfterUpload string `json:"remove_after_upload"`
|
||||
MaxDirectorySize int64 `json:"max_directory_size"`
|
||||
Timezone string `json:"timezone,omitempty" bson:"timezone,omitempty"`
|
||||
Timezone string `json:"timezone"`
|
||||
Capture Capture `json:"capture"`
|
||||
Timetable []*Timetable `json:"timetable"`
|
||||
Region *Region `json:"region"`
|
||||
@@ -42,6 +42,7 @@ type Config struct {
|
||||
HubPrivateKey string `json:"hub_private_key" bson:"hub_private_key"`
|
||||
HubSite string `json:"hub_site" bson:"hub_site"`
|
||||
ConditionURI string `json:"condition_uri" bson:"condition_uri"`
|
||||
Encryption *Encryption `json:"encryption" bson:"encryption"`
|
||||
}
|
||||
|
||||
// Capture defines which camera type (Id) you are using (IP, USB or Raspberry Pi camera),
|
||||
@@ -70,13 +71,15 @@ type Capture struct {
|
||||
// IPCamera configuration, such as the RTSP url of the IPCamera and the FPS.
|
||||
// Also includes ONVIF integration
|
||||
type IPCamera struct {
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
FPS string `json:"fps"`
|
||||
RTSP string `json:"rtsp"`
|
||||
SubRTSP string `json:"sub_rtsp"`
|
||||
FPS string `json:"fps"`
|
||||
ONVIF string `json:"onvif,omitempty" bson:"onvif"`
|
||||
ONVIFXAddr string `json:"onvif_xaddr,omitempty" bson:"onvif_xaddr"`
|
||||
ONVIFUsername string `json:"onvif_username,omitempty" bson:"onvif_username"`
|
||||
ONVIFPassword string `json:"onvif_password,omitempty" bson:"onvif_password"`
|
||||
ONVIFXAddr string `json:"onvif_xaddr" bson:"onvif_xaddr"`
|
||||
ONVIFUsername string `json:"onvif_username" bson:"onvif_username"`
|
||||
ONVIFPassword string `json:"onvif_password" bson:"onvif_password"`
|
||||
}
|
||||
|
||||
// USBCamera configuration, such as the device path (/dev/video*)
|
||||
@@ -155,3 +158,11 @@ type Dropbox struct {
|
||||
AccessToken string `json:"access_token,omitempty" bson:"access_token,omitempty"`
|
||||
Directory string `json:"directory,omitempty" bson:"directory,omitempty"`
|
||||
}
|
||||
|
||||
// Encryption
|
||||
type Encryption struct {
|
||||
Enabled bool `json:"enabled" bson:"enabled"`
|
||||
Fingerprint string `json:"fingerprint" bson:"fingerprint"`
|
||||
PrivateKey string `json:"private_key" bson:"private_key"`
|
||||
SymmetricKey string `json:"symmetric_key" bson:"symmetric_key"`
|
||||
}
|
||||
|
||||
151
machinery/src/models/MQTT.go
Normal file
151
machinery/src/models/MQTT.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/kerberos-io/agent/machinery/src/encryption"
|
||||
"github.com/kerberos-io/agent/machinery/src/log"
|
||||
)
|
||||
|
||||
func PackageMQTTMessage(configuration *Configuration, msg Message) ([]byte, error) {
|
||||
// Create a Version 4 UUID.
|
||||
u2, err := uuid.NewV4()
|
||||
if err != nil {
|
||||
log.Log.Error("failed to generate UUID: " + err.Error())
|
||||
}
|
||||
|
||||
// We'll generate an unique id, and encrypt / decrypt it using the private key if available.
|
||||
msg.Mid = u2.String()
|
||||
msg.DeviceId = msg.Payload.DeviceId
|
||||
msg.Timestamp = time.Now().Unix()
|
||||
|
||||
// At the moment we don't do the encryption part, but we'll implement it
|
||||
// once the legacy methods (subscriptions are moved).
|
||||
msg.Encrypted = false
|
||||
if configuration.Config.Encryption != nil && configuration.Config.Encryption.Enabled {
|
||||
msg.Encrypted = true
|
||||
}
|
||||
msg.PublicKey = ""
|
||||
msg.Fingerprint = ""
|
||||
|
||||
if msg.Encrypted {
|
||||
pload := msg.Payload
|
||||
|
||||
// Pload to base64
|
||||
data, err := json.Marshal(pload)
|
||||
if err != nil {
|
||||
log.Log.Error("failed to marshal payload: " + err.Error())
|
||||
}
|
||||
|
||||
// Encrypt the value
|
||||
privateKey := configuration.Config.Encryption.PrivateKey
|
||||
r := strings.NewReader(privateKey)
|
||||
pemBytes, _ := ioutil.ReadAll(r)
|
||||
block, _ := pem.Decode(pemBytes)
|
||||
if block == nil {
|
||||
log.Log.Error("MQTTListenerHandler: error decoding PEM block containing private key")
|
||||
} else {
|
||||
// Parse private key
|
||||
b := block.Bytes
|
||||
key, err := x509.ParsePKCS8PrivateKey(b)
|
||||
if err != nil {
|
||||
log.Log.Error("MQTTListenerHandler: error parsing private key: " + err.Error())
|
||||
}
|
||||
|
||||
// Conver key to *rsa.PrivateKey
|
||||
rsaKey, _ := key.(*rsa.PrivateKey)
|
||||
|
||||
// Create a 16bit key random
|
||||
k := configuration.Config.Encryption.SymmetricKey
|
||||
encryptedValue, err := encryption.AesEncrypt(string(data), k)
|
||||
|
||||
// Sign the encrypted value
|
||||
signature, err := encryption.SignWithPrivateKey([]byte(encryptedValue), rsaKey)
|
||||
base64Signature := base64.StdEncoding.EncodeToString(signature)
|
||||
|
||||
msg.Payload.EncryptedValue = encryptedValue
|
||||
msg.Payload.Signature = base64Signature
|
||||
msg.Payload.Value = make(map[string]interface{})
|
||||
}
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(msg)
|
||||
return payload, err
|
||||
}
|
||||
|
||||
// The message structure which is used to send over
|
||||
// and receive messages from the MQTT broker
|
||||
type Message struct {
|
||||
Mid string `json:"mid"`
|
||||
DeviceId string `json:"device_id"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Encrypted bool `json:"encrypted"`
|
||||
PublicKey string `json:"public_key"`
|
||||
Fingerprint string `json:"fingerprint"`
|
||||
Payload Payload `json:"payload"`
|
||||
}
|
||||
|
||||
// The payload structure which is used to send over
|
||||
// and receive messages from the MQTT broker
|
||||
type Payload struct {
|
||||
Action string `json:"action"`
|
||||
DeviceId string `json:"device_id"`
|
||||
Signature string `json:"signature"`
|
||||
EncryptedValue string `json:"encrypted_value"`
|
||||
Value map[string]interface{} `json:"value"`
|
||||
}
|
||||
|
||||
// We received a recording request, we'll send it to the motion handler.
|
||||
type RecordPayload struct {
|
||||
Timestamp int64 `json:"timestamp"` // timestamp of the recording request.
|
||||
}
|
||||
|
||||
// We received a preset position request, we'll request it through onvif and send it back.
|
||||
type PTZPositionPayload struct {
|
||||
Timestamp int64 `json:"timestamp"` // timestamp of the preset request.
|
||||
}
|
||||
|
||||
// We received a request config request, we'll fetch the current config and send it back.
|
||||
type RequestConfigPayload struct {
|
||||
Timestamp int64 `json:"timestamp"` // timestamp of the preset request.
|
||||
}
|
||||
|
||||
// We received a update config request, we'll update the current config and send a confirmation back.
|
||||
type UpdateConfigPayload struct {
|
||||
Timestamp int64 `json:"timestamp"` // timestamp of the preset request.
|
||||
Config Config `json:"config"`
|
||||
}
|
||||
|
||||
// We received a request SD stream request
|
||||
type RequestSDStreamPayload struct {
|
||||
Timestamp int64 `json:"timestamp"` // timestamp
|
||||
}
|
||||
|
||||
// We received a request HD stream request
|
||||
type RequestHDStreamPayload struct {
|
||||
Timestamp int64 `json:"timestamp"` // timestamp
|
||||
HubKey string `json:"hub_key"` // hub key
|
||||
SessionID string `json:"session_id"` // session id
|
||||
SessionDescription string `json:"session_description"` // session description
|
||||
}
|
||||
|
||||
// We received a receive HD candidates request
|
||||
type ReceiveHDCandidatesPayload struct {
|
||||
Timestamp int64 `json:"timestamp"` // timestamp
|
||||
SessionID string `json:"session_id"` // session id
|
||||
Candidate string `json:"candidate"` // candidate
|
||||
}
|
||||
|
||||
type NavigatePTZPayload struct {
|
||||
Timestamp int64 `json:"timestamp"` // timestamp
|
||||
DeviceId string `json:"device_id"` // device id
|
||||
Action string `json:"action"` // action
|
||||
}
|
||||
@@ -12,4 +12,13 @@ type OnvifActionPTZ struct {
|
||||
Down int `json:"down" bson:"down"`
|
||||
Center int `json:"center" bson:"center"`
|
||||
Zoom float64 `json:"zoom" bson:"zoom"`
|
||||
X float64 `json:"x" bson:"x"`
|
||||
Y float64 `json:"y" bson:"y"`
|
||||
Z float64 `json:"z" bson:"z"`
|
||||
Preset string `json:"preset" bson:"preset"`
|
||||
}
|
||||
|
||||
type OnvifActionPreset struct {
|
||||
Name string `json:"name" bson:"name"`
|
||||
Token string `json:"token" bson:"token"`
|
||||
}
|
||||
|
||||
@@ -45,14 +45,74 @@ func HandleONVIFActions(configuration *models.Configuration, communication *mode
|
||||
|
||||
if err == nil {
|
||||
|
||||
if onvifAction.Action == "ptz" {
|
||||
if onvifAction.Action == "absolute-move" {
|
||||
|
||||
// We will move the camera to zero position.
|
||||
x := ptzAction.X
|
||||
y := ptzAction.Y
|
||||
z := ptzAction.Z
|
||||
|
||||
// Check which PTZ Space we need to use
|
||||
functions, _, _ := GetPTZFunctionsFromDevice(configurations)
|
||||
|
||||
// Log functions
|
||||
log.Log.Info("HandleONVIFActions: functions: " + strings.Join(functions, ", "))
|
||||
|
||||
// Check if we need to use absolute or continuous move
|
||||
/*canAbsoluteMove := false
|
||||
canContinuousMove := false
|
||||
|
||||
if len(functions) > 0 {
|
||||
for _, function := range functions {
|
||||
if function == "AbsolutePanTiltMove" || function == "AbsoluteZoomMove" {
|
||||
canAbsoluteMove = true
|
||||
} else if function == "ContinuousPanTiltMove" || function == "ContinuousZoomMove" {
|
||||
canContinuousMove = true
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
// Ideally we should be able to use the AbsolutePanTiltMove function, but it looks like
|
||||
// the current detection through GetPTZFuntionsFromDevice is not working properly. Therefore we will fallback
|
||||
// on the ContinuousPanTiltMove function which is more compatible with more cameras.
|
||||
err = AbsolutePanTiltMoveFake(device, configurations, token, x, y, z)
|
||||
if err != nil {
|
||||
log.Log.Error("HandleONVIFActions (AbsolutePanTitleMoveFake): " + err.Error())
|
||||
} else {
|
||||
log.Log.Info("HandleONVIFActions (AbsolutePanTitleMoveFake): successfully moved camera")
|
||||
}
|
||||
|
||||
/*if canAbsoluteMove {
|
||||
err = AbsolutePanTiltMove(device, configurations, token, x, y, z)
|
||||
if err != nil {
|
||||
log.Log.Error("HandleONVIFActions (AbsolutePanTitleMove): " + err.Error())
|
||||
}
|
||||
} else if canContinuousMove {
|
||||
err = AbsolutePanTiltMoveFake(device, configurations, token, x, y, z)
|
||||
if err != nil {
|
||||
log.Log.Error("HandleONVIFActions (AbsolutePanTitleMoveFake): " + err.Error())
|
||||
}
|
||||
}*/
|
||||
|
||||
} else if onvifAction.Action == "preset" {
|
||||
|
||||
// Execute the preset
|
||||
preset := ptzAction.Preset
|
||||
err := GoToPresetFromDevice(device, preset)
|
||||
if err != nil {
|
||||
log.Log.Error("HandleONVIFActions (GotoPreset): " + err.Error())
|
||||
} else {
|
||||
log.Log.Info("HandleONVIFActions (GotoPreset): successfully moved camera")
|
||||
}
|
||||
|
||||
} else if onvifAction.Action == "ptz" {
|
||||
|
||||
if err == nil {
|
||||
|
||||
if ptzAction.Center == 1 {
|
||||
|
||||
// We will move the camera to zero position.
|
||||
err := AbsolutePanTiltMove(device, configurations, token, 0, 0)
|
||||
err := AbsolutePanTiltMove(device, configurations, token, 0, 0, 0)
|
||||
if err != nil {
|
||||
log.Log.Error("HandleONVIFActions (AbsolutePanTitleMove): " + err.Error())
|
||||
}
|
||||
@@ -179,18 +239,83 @@ func GetPTZConfigurationsFromDevice(device *onvif.Device) (ptz.GetConfigurations
|
||||
return configurations, err
|
||||
}
|
||||
|
||||
func AbsolutePanTiltMove(device *onvif.Device, configuration ptz.GetConfigurationsResponse, token xsd.ReferenceToken, pan float32, tilt float32) error {
|
||||
func GetPositionFromDevice(configuration models.Configuration) (xsd.PTZVector, error) {
|
||||
var position xsd.PTZVector
|
||||
// Connect to Onvif device
|
||||
cameraConfiguration := configuration.Config.Capture.IPCamera
|
||||
device, err := ConnectToOnvifDevice(&cameraConfiguration)
|
||||
if err == nil {
|
||||
|
||||
absoluteVector := xsd.Vector2D{
|
||||
X: float64(pan),
|
||||
Y: float64(tilt),
|
||||
// Get token from the first profile
|
||||
token, err := GetTokenFromProfile(device, 0)
|
||||
if err == nil {
|
||||
// Get the PTZ configurations from the device
|
||||
position, err := GetPosition(device, token)
|
||||
if err == nil {
|
||||
return position, err
|
||||
} else {
|
||||
log.Log.Error("GetPositionFromDevice: " + err.Error())
|
||||
return position, err
|
||||
}
|
||||
} else {
|
||||
log.Log.Error("GetPositionFromDevice: " + err.Error())
|
||||
return position, err
|
||||
}
|
||||
} else {
|
||||
log.Log.Error("GetPositionFromDevice: " + err.Error())
|
||||
return position, err
|
||||
}
|
||||
}
|
||||
|
||||
func GetPosition(device *onvif.Device, token xsd.ReferenceToken) (xsd.PTZVector, error) {
|
||||
// We'll try to receive the PTZ configurations from the server
|
||||
var status ptz.GetStatusResponse
|
||||
var position xsd.PTZVector
|
||||
|
||||
// Get the PTZ configurations from the device
|
||||
resp, err := device.CallMethod(ptz.GetStatus{
|
||||
ProfileToken: token,
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
defer resp.Body.Close()
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
if err == nil {
|
||||
stringBody := string(b)
|
||||
decodedXML, et, err := getXMLNode(stringBody, "GetStatusResponse")
|
||||
if err != nil {
|
||||
log.Log.Error("GetPositionFromDevice: " + err.Error())
|
||||
return position, err
|
||||
} else {
|
||||
if err := decodedXML.DecodeElement(&status, et); err != nil {
|
||||
log.Log.Error("GetPositionFromDevice: " + err.Error())
|
||||
return position, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
position = status.PTZStatus.Position
|
||||
return position, err
|
||||
}
|
||||
|
||||
func AbsolutePanTiltMove(device *onvif.Device, configuration ptz.GetConfigurationsResponse, token xsd.ReferenceToken, pan float64, tilt float64, zoom float64) error {
|
||||
|
||||
absolutePantiltVector := xsd.Vector2D{
|
||||
X: pan,
|
||||
Y: tilt,
|
||||
Space: configuration.PTZConfiguration.DefaultAbsolutePantTiltPositionSpace,
|
||||
}
|
||||
|
||||
absoluteZoomVector := xsd.Vector1D{
|
||||
X: zoom,
|
||||
Space: configuration.PTZConfiguration.DefaultAbsoluteZoomPositionSpace,
|
||||
}
|
||||
|
||||
res, err := device.CallMethod(ptz.AbsoluteMove{
|
||||
ProfileToken: token,
|
||||
Position: xsd.PTZVector{
|
||||
PanTilt: absoluteVector,
|
||||
PanTilt: absolutePantiltVector,
|
||||
Zoom: absoluteZoomVector,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -199,11 +324,255 @@ func AbsolutePanTiltMove(device *onvif.Device, configuration ptz.GetConfiguratio
|
||||
}
|
||||
|
||||
bs, _ := ioutil.ReadAll(res.Body)
|
||||
log.Log.Debug("AbsoluteMove: " + string(bs))
|
||||
log.Log.Info("AbsoluteMove: " + string(bs))
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// This function will simulate the AbsolutePanTiltMove function.
|
||||
// However the AboslutePanTiltMove function is not working on all cameras.
|
||||
// So we'll use the ContinuousMove function to simulate the AbsolutePanTiltMove function using the position polling.
|
||||
func AbsolutePanTiltMoveFake(device *onvif.Device, configuration ptz.GetConfigurationsResponse, token xsd.ReferenceToken, pan float64, tilt float64, zoom float64) error {
|
||||
position, err := GetPosition(device, token)
|
||||
if position.PanTilt.X >= pan-0.01 && position.PanTilt.X <= pan+0.01 && position.PanTilt.Y >= tilt-0.01 && position.PanTilt.Y <= tilt+0.01 && position.Zoom.X >= zoom-0.01 && position.Zoom.X <= zoom+0.01 {
|
||||
log.Log.Debug("AbsolutePanTiltMoveFake: already at position")
|
||||
} else {
|
||||
|
||||
// The speed of panning, the higher the faster we'll pan the camera
|
||||
// value is a range between 0 and 1.
|
||||
speed := 0.6
|
||||
wait := 100 * time.Millisecond
|
||||
|
||||
// We'll move quickly to the position (might be inaccurate)
|
||||
err = ZoomOutCompletely(device, configuration, token)
|
||||
err = PanUntilPosition(device, configuration, token, pan, zoom, speed, wait)
|
||||
err = TiltUntilPosition(device, configuration, token, tilt, zoom, speed, wait)
|
||||
|
||||
// Now we'll move a bit slower to make sure we are ok (will be more accurate)
|
||||
speed = 0.1
|
||||
wait = 200 * time.Millisecond
|
||||
|
||||
err = PanUntilPosition(device, configuration, token, pan, zoom, speed, wait)
|
||||
err = TiltUntilPosition(device, configuration, token, tilt, zoom, speed, wait)
|
||||
err = ZoomUntilPosition(device, configuration, token, zoom, speed, wait)
|
||||
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func ZoomOutCompletely(device *onvif.Device, configuration ptz.GetConfigurationsResponse, token xsd.ReferenceToken) error {
|
||||
// Zoom out completely!!!
|
||||
zoomOut := xsd.Vector1D{
|
||||
X: -1,
|
||||
Space: configuration.PTZConfiguration.DefaultContinuousZoomVelocitySpace,
|
||||
}
|
||||
_, err := device.CallMethod(ptz.ContinuousMove{
|
||||
ProfileToken: token,
|
||||
Velocity: xsd.PTZSpeedZoom{
|
||||
Zoom: zoomOut,
|
||||
},
|
||||
})
|
||||
for {
|
||||
position, _ := GetPosition(device, token)
|
||||
if position.Zoom.X == 0 {
|
||||
break
|
||||
}
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
}
|
||||
|
||||
device.CallMethod(ptz.Stop{
|
||||
ProfileToken: token,
|
||||
Zoom: true,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func PanUntilPosition(device *onvif.Device, configuration ptz.GetConfigurationsResponse, token xsd.ReferenceToken, pan float64, zoom float64, speed float64, wait time.Duration) error {
|
||||
position, err := GetPosition(device, token)
|
||||
|
||||
if position.PanTilt.X >= pan-0.01 && position.PanTilt.X <= pan+0.01 {
|
||||
|
||||
} else {
|
||||
|
||||
// We'll need to determine if we need to move CW or CCW.
|
||||
// Check the current position and compare it with the desired position.
|
||||
directionX := speed
|
||||
if position.PanTilt.X > pan {
|
||||
directionX = speed * -1
|
||||
}
|
||||
|
||||
panTiltVector := xsd.Vector2D{
|
||||
X: directionX,
|
||||
Y: 0,
|
||||
Space: configuration.PTZConfiguration.DefaultContinuousPanTiltVelocitySpace,
|
||||
}
|
||||
res, err := device.CallMethod(ptz.ContinuousMove{
|
||||
ProfileToken: token,
|
||||
Velocity: xsd.PTZSpeedPanTilt{
|
||||
PanTilt: panTiltVector,
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Log.Error("ContinuousPanTiltMove (Pan): " + err.Error())
|
||||
}
|
||||
|
||||
bs, _ := ioutil.ReadAll(res.Body)
|
||||
log.Log.Debug("ContinuousPanTiltMove (Pan): " + string(bs))
|
||||
|
||||
// While moving we'll check if we reached the desired position.
|
||||
// or if we overshot the desired position.
|
||||
|
||||
// Break after 3seconds
|
||||
now := time.Now()
|
||||
for {
|
||||
position, _ := GetPosition(device, token)
|
||||
if position.PanTilt.X == -1 || position.PanTilt.X == 1 || (directionX > 0 && position.PanTilt.X >= pan) || (directionX < 0 && position.PanTilt.X <= pan) || (position.PanTilt.X >= pan-0.01 && position.PanTilt.X <= pan+0.01) {
|
||||
break
|
||||
}
|
||||
if time.Since(now) > 3*time.Second {
|
||||
break
|
||||
}
|
||||
time.Sleep(wait)
|
||||
}
|
||||
|
||||
_, errStop := device.CallMethod(ptz.Stop{
|
||||
ProfileToken: token,
|
||||
PanTilt: true,
|
||||
Zoom: true,
|
||||
})
|
||||
|
||||
if errStop != nil {
|
||||
log.Log.Error("ContinuousPanTiltMove (Pan): " + errStop.Error())
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func TiltUntilPosition(device *onvif.Device, configuration ptz.GetConfigurationsResponse, token xsd.ReferenceToken, tilt float64, zoom float64, speed float64, wait time.Duration) error {
|
||||
position, err := GetPosition(device, token)
|
||||
|
||||
if position.PanTilt.Y >= tilt-0.005 && position.PanTilt.Y <= tilt+0.005 {
|
||||
|
||||
} else {
|
||||
|
||||
// We'll need to determine if we need to move CW or CCW.
|
||||
// Check the current position and compare it with the desired position.
|
||||
directionY := speed
|
||||
if position.PanTilt.Y > tilt {
|
||||
directionY = speed * -1
|
||||
}
|
||||
|
||||
panTiltVector := xsd.Vector2D{
|
||||
X: 0,
|
||||
Y: directionY,
|
||||
Space: configuration.PTZConfiguration.DefaultContinuousPanTiltVelocitySpace,
|
||||
}
|
||||
res, err := device.CallMethod(ptz.ContinuousMove{
|
||||
ProfileToken: token,
|
||||
Velocity: xsd.PTZSpeedPanTilt{
|
||||
PanTilt: panTiltVector,
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Log.Error("ContinuousPanTiltMove (Tilt): " + err.Error())
|
||||
}
|
||||
|
||||
bs, _ := ioutil.ReadAll(res.Body)
|
||||
log.Log.Debug("ContinuousPanTiltMove (Tilt) " + string(bs))
|
||||
|
||||
// While moving we'll check if we reached the desired position.
|
||||
// or if we overshot the desired position.
|
||||
|
||||
// Break after 3seconds
|
||||
now := time.Now()
|
||||
for {
|
||||
position, _ := GetPosition(device, token)
|
||||
if position.PanTilt.Y == -1 || position.PanTilt.Y == 1 || (directionY > 0 && position.PanTilt.Y >= tilt) || (directionY < 0 && position.PanTilt.Y <= tilt) || (position.PanTilt.Y >= tilt-0.005 && position.PanTilt.Y <= tilt+0.005) {
|
||||
break
|
||||
}
|
||||
if time.Since(now) > 3*time.Second {
|
||||
break
|
||||
}
|
||||
time.Sleep(wait)
|
||||
}
|
||||
|
||||
_, errStop := device.CallMethod(ptz.Stop{
|
||||
ProfileToken: token,
|
||||
PanTilt: true,
|
||||
Zoom: true,
|
||||
})
|
||||
|
||||
if errStop != nil {
|
||||
log.Log.Error("ContinuousPanTiltMove (Tilt): " + errStop.Error())
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func ZoomUntilPosition(device *onvif.Device, configuration ptz.GetConfigurationsResponse, token xsd.ReferenceToken, zoom float64, speed float64, wait time.Duration) error {
|
||||
position, err := GetPosition(device, token)
|
||||
|
||||
if position.Zoom.X >= zoom-0.005 && position.Zoom.X <= zoom+0.005 {
|
||||
|
||||
} else {
|
||||
|
||||
// We'll need to determine if we need to move CW or CCW.
|
||||
// Check the current position and compare it with the desired position.
|
||||
directionZ := speed
|
||||
if position.Zoom.X > zoom {
|
||||
directionZ = speed * -1
|
||||
}
|
||||
|
||||
zoomVector := xsd.Vector1D{
|
||||
X: directionZ,
|
||||
Space: configuration.PTZConfiguration.DefaultContinuousZoomVelocitySpace,
|
||||
}
|
||||
res, err := device.CallMethod(ptz.ContinuousMove{
|
||||
ProfileToken: token,
|
||||
Velocity: xsd.PTZSpeedZoom{
|
||||
Zoom: zoomVector,
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Log.Error("ContinuousPanTiltMove (Zoom): " + err.Error())
|
||||
}
|
||||
|
||||
bs, _ := ioutil.ReadAll(res.Body)
|
||||
log.Log.Debug("ContinuousPanTiltMove (Zoom) " + string(bs))
|
||||
|
||||
// While moving we'll check if we reached the desired position.
|
||||
// or if we overshot the desired position.
|
||||
|
||||
// Break after 3seconds
|
||||
now := time.Now()
|
||||
for {
|
||||
position, _ := GetPosition(device, token)
|
||||
if position.Zoom.X == -1 || position.Zoom.X == 1 || (directionZ > 0 && position.Zoom.X >= zoom) || (directionZ < 0 && position.Zoom.X <= zoom) || (position.Zoom.X >= zoom-0.005 && position.Zoom.X <= zoom+0.005) {
|
||||
break
|
||||
}
|
||||
if time.Since(now) > 3*time.Second {
|
||||
break
|
||||
}
|
||||
time.Sleep(wait)
|
||||
}
|
||||
|
||||
_, errStop := device.CallMethod(ptz.Stop{
|
||||
ProfileToken: token,
|
||||
PanTilt: true,
|
||||
Zoom: true,
|
||||
})
|
||||
|
||||
if errStop != nil {
|
||||
log.Log.Error("ContinuousPanTiltMove (Zoom): " + errStop.Error())
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func ContinuousPanTilt(device *onvif.Device, configuration ptz.GetConfigurationsResponse, token xsd.ReferenceToken, pan float64, tilt float64) error {
|
||||
|
||||
panTiltVector := xsd.Vector2D{
|
||||
@@ -226,7 +595,7 @@ func ContinuousPanTilt(device *onvif.Device, configuration ptz.GetConfigurations
|
||||
bs, _ := ioutil.ReadAll(res.Body)
|
||||
log.Log.Debug("ContinuousPanTiltMove: " + string(bs))
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
res, errStop := device.CallMethod(ptz.Stop{
|
||||
ProfileToken: token,
|
||||
@@ -299,6 +668,89 @@ func GetCapabilitiesFromDevice(device *onvif.Device) []string {
|
||||
return capabilities
|
||||
}
|
||||
|
||||
func GetPresetsFromDevice(device *onvif.Device) ([]models.OnvifActionPreset, error) {
|
||||
var presets []models.OnvifActionPreset
|
||||
var presetsResponse ptz.GetPresetsResponse
|
||||
|
||||
// Get token from the first profile
|
||||
token, err := GetTokenFromProfile(device, 0)
|
||||
if err == nil {
|
||||
resp, err := device.CallMethod(ptz.GetPresets{
|
||||
ProfileToken: token,
|
||||
})
|
||||
|
||||
defer resp.Body.Close()
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
if err == nil {
|
||||
stringBody := string(b)
|
||||
decodedXML, et, err := getXMLNode(stringBody, "GetPresetsResponse")
|
||||
if err != nil {
|
||||
log.Log.Error("GetPresetsFromDevice: " + err.Error())
|
||||
return presets, err
|
||||
} else {
|
||||
if err := decodedXML.DecodeElement(&presetsResponse, et); err != nil {
|
||||
log.Log.Error("GetPresetsFromDevice: " + err.Error())
|
||||
return presets, err
|
||||
}
|
||||
|
||||
for _, preset := range presetsResponse.Preset {
|
||||
p := models.OnvifActionPreset{
|
||||
Name: string(preset.Name),
|
||||
Token: string(preset.Token),
|
||||
}
|
||||
|
||||
presets = append(presets, p)
|
||||
}
|
||||
|
||||
return presets, err
|
||||
}
|
||||
} else {
|
||||
log.Log.Error("GetPresetsFromDevice: " + err.Error())
|
||||
}
|
||||
} else {
|
||||
log.Log.Error("GetPresetsFromDevice: " + err.Error())
|
||||
}
|
||||
|
||||
return presets, err
|
||||
}
|
||||
|
||||
func GoToPresetFromDevice(device *onvif.Device, presetName string) error {
|
||||
var goToPresetResponse ptz.GotoPresetResponse
|
||||
|
||||
// Get token from the first profile
|
||||
token, err := GetTokenFromProfile(device, 0)
|
||||
if err == nil {
|
||||
|
||||
resp, err := device.CallMethod(ptz.GotoPreset{
|
||||
ProfileToken: token,
|
||||
PresetToken: xsd.ReferenceToken(presetName),
|
||||
})
|
||||
|
||||
defer resp.Body.Close()
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
if err == nil {
|
||||
stringBody := string(b)
|
||||
decodedXML, et, err := getXMLNode(stringBody, "GotoPresetResponses")
|
||||
if err != nil {
|
||||
log.Log.Error("GoToPresetFromDevice: " + err.Error())
|
||||
return err
|
||||
} else {
|
||||
if err := decodedXML.DecodeElement(&goToPresetResponse, et); err != nil {
|
||||
log.Log.Error("GoToPresetFromDevice: " + err.Error())
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
log.Log.Error("GoToPresetFromDevice: " + err.Error())
|
||||
}
|
||||
} else {
|
||||
log.Log.Error("GoToPresetFromDevice: " + err.Error())
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func getXMLNode(xmlBody string, nodeName string) (*xml.Decoder, *xml.StartElement, error) {
|
||||
xmlBytes := bytes.NewBufferString(xmlBody)
|
||||
decodedXML := xml.NewDecoder(xmlBytes)
|
||||
|
||||
@@ -250,3 +250,105 @@ func DoOnvifZoom(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// GetOnvifPresets godoc
|
||||
// @Router /api/camera/onvif/presets [post]
|
||||
// @ID camera-onvif-presets
|
||||
// @Tags camera
|
||||
// @Param config body models.OnvifCredentials true "OnvifCredentials"
|
||||
// @Summary Will return the ONVIF presets for the specific camera.
|
||||
// @Description Will return the ONVIF presets for the specific camera.
|
||||
// @Success 200 {object} models.APIResponse
|
||||
func GetOnvifPresets(c *gin.Context) {
|
||||
var onvifCredentials models.OnvifCredentials
|
||||
err := c.BindJSON(&onvifCredentials)
|
||||
|
||||
if err == nil && onvifCredentials.ONVIFXAddr != "" {
|
||||
|
||||
configuration := &models.Configuration{
|
||||
Config: models.Config{
|
||||
Capture: models.Capture{
|
||||
IPCamera: models.IPCamera{
|
||||
ONVIFXAddr: onvifCredentials.ONVIFXAddr,
|
||||
ONVIFUsername: onvifCredentials.ONVIFUsername,
|
||||
ONVIFPassword: onvifCredentials.ONVIFPassword,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cameraConfiguration := configuration.Config.Capture.IPCamera
|
||||
device, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
|
||||
if err == nil {
|
||||
presets, err := onvif.GetPresetsFromDevice(device)
|
||||
if err == nil {
|
||||
c.JSON(200, gin.H{
|
||||
"presets": presets,
|
||||
})
|
||||
} else {
|
||||
c.JSON(400, gin.H{
|
||||
"data": "Something went wrong: " + err.Error(),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
c.JSON(400, gin.H{
|
||||
"data": "Something went wrong: " + err.Error(),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
c.JSON(400, gin.H{
|
||||
"data": "Something went wrong: " + err.Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// GoToOnvifPReset godoc
|
||||
// @Router /api/camera/onvif/gotopreset [post]
|
||||
// @ID camera-onvif-gotopreset
|
||||
// @Tags camera
|
||||
// @Param config body models.OnvifPreset true "OnvifPreset"
|
||||
// @Summary Will activate the desired ONVIF preset.
|
||||
// @Description Will activate the desired ONVIF preset.
|
||||
// @Success 200 {object} models.APIResponse
|
||||
func GoToOnvifPreset(c *gin.Context) {
|
||||
var onvifPreset models.OnvifPreset
|
||||
err := c.BindJSON(&onvifPreset)
|
||||
|
||||
if err == nil && onvifPreset.OnvifCredentials.ONVIFXAddr != "" {
|
||||
|
||||
configuration := &models.Configuration{
|
||||
Config: models.Config{
|
||||
Capture: models.Capture{
|
||||
IPCamera: models.IPCamera{
|
||||
ONVIFXAddr: onvifPreset.OnvifCredentials.ONVIFXAddr,
|
||||
ONVIFUsername: onvifPreset.OnvifCredentials.ONVIFUsername,
|
||||
ONVIFPassword: onvifPreset.OnvifCredentials.ONVIFPassword,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cameraConfiguration := configuration.Config.Capture.IPCamera
|
||||
device, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
|
||||
if err == nil {
|
||||
err := onvif.GoToPresetFromDevice(device, onvifPreset.Preset)
|
||||
if err == nil {
|
||||
c.JSON(200, gin.H{
|
||||
"data": "Camera preset activated: " + onvifPreset.Preset,
|
||||
})
|
||||
} else {
|
||||
c.JSON(400, gin.H{
|
||||
"data": "Something went wrong: " + err.Error(),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
c.JSON(400, gin.H{
|
||||
"data": "Something went wrong: " + err.Error(),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
c.JSON(400, gin.H{
|
||||
"data": "Something went wrong: " + err.Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"github.com/kerberos-io/agent/machinery/src/cloud"
|
||||
"github.com/kerberos-io/agent/machinery/src/components"
|
||||
configService "github.com/kerberos-io/agent/machinery/src/config"
|
||||
"github.com/kerberos-io/agent/machinery/src/log"
|
||||
"github.com/kerberos-io/agent/machinery/src/models"
|
||||
"github.com/kerberos-io/agent/machinery/src/utils"
|
||||
@@ -40,7 +41,7 @@ func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware, configDirect
|
||||
var config models.Config
|
||||
err := c.BindJSON(&config)
|
||||
if err == nil {
|
||||
err := components.SaveConfig(configDirectory, config, configuration, communication)
|
||||
err := configService.SaveConfig(configDirectory, config, configuration, communication)
|
||||
if err == nil {
|
||||
c.JSON(200, gin.H{
|
||||
"data": "☄ Reconfiguring",
|
||||
@@ -165,7 +166,7 @@ func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware, configDirect
|
||||
var config models.Config
|
||||
err := c.BindJSON(&config)
|
||||
if err == nil {
|
||||
err := components.SaveConfig(configDirectory, config, configuration, communication)
|
||||
err := configService.SaveConfig(configDirectory, config, configuration, communication)
|
||||
if err == nil {
|
||||
c.JSON(200, gin.H{
|
||||
"data": "☄ Reconfiguring",
|
||||
@@ -215,7 +216,7 @@ func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware, configDirect
|
||||
// We will only send an image once per second.
|
||||
time.Sleep(time.Second * 1)
|
||||
log.Log.Info("AddRoutes (/stream): reading from MJPEG stream")
|
||||
img, err := components.GetImageFromFilePath(configDirectory)
|
||||
img, err := configService.GetImageFromFilePath(configDirectory)
|
||||
return img, err
|
||||
}
|
||||
h := components.StartMotionJPEG(imageFunction, 80)
|
||||
@@ -227,6 +228,8 @@ func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware, configDirect
|
||||
// the camera.
|
||||
api.POST("/camera/onvif/login", LoginToOnvif)
|
||||
api.POST("/camera/onvif/capabilities", GetOnvifCapabilities)
|
||||
api.POST("/camera/onvif/presets", GetOnvifPresets)
|
||||
api.POST("/camera/onvif/gotopreset", GoToOnvifPreset)
|
||||
api.POST("/camera/onvif/pantilt", DoOnvifPanTilt)
|
||||
api.POST("/camera/onvif/zoom", DoOnvifZoom)
|
||||
api.POST("/camera/verify/:streamType", capture.VerifyCamera)
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
package mqtt
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
mqtt "github.com/eclipse/paho.mqtt.golang"
|
||||
configService "github.com/kerberos-io/agent/machinery/src/config"
|
||||
"github.com/kerberos-io/agent/machinery/src/encryption"
|
||||
"github.com/kerberos-io/agent/machinery/src/log"
|
||||
"github.com/kerberos-io/agent/machinery/src/models"
|
||||
"github.com/kerberos-io/agent/machinery/src/onvif"
|
||||
"github.com/kerberos-io/agent/machinery/src/webrtc"
|
||||
)
|
||||
|
||||
@@ -34,7 +42,18 @@ func HasMQTTClientModified(configuration *models.Configuration) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func ConfigureMQTT(configuration *models.Configuration, communication *models.Communication) mqtt.Client {
|
||||
// Configuring MQTT to subscribe for various bi-directional messaging
|
||||
// Listen and reply (a generic method to share and retrieve information)
|
||||
//
|
||||
// - [SUBSCRIPTION] kerberos/agent/{hubkey} (hub -> agent)
|
||||
// - [PUBLISH] kerberos/hub/{hubkey} (agent -> hub)
|
||||
//
|
||||
// !!! LEGACY METHODS BELOW, WE SHOULD LEVERAGE THE ABOVE METHOD!
|
||||
// [PUBlISH]
|
||||
// Next to subscribing to various topics, we'll also publish messages to various topics, find a list of available Publish methods.
|
||||
// - kerberos/{hubkey}/device/{devicekey}/motion: a motion signal
|
||||
|
||||
func ConfigureMQTT(configDirectory string, configuration *models.Configuration, communication *models.Communication) mqtt.Client {
|
||||
|
||||
config := configuration.Config
|
||||
|
||||
@@ -109,23 +128,8 @@ func ConfigureMQTT(configuration *models.Configuration, communication *models.Co
|
||||
// We managed to connect to the MQTT broker, hurray!
|
||||
log.Log.Info("ConfigureMQTT: " + mqttClientID + " connected to " + mqttURL)
|
||||
|
||||
// Create a subscription to know if send out a livestream or not.
|
||||
MQTTListenerHandleLiveSD(c, hubKey, configuration, communication)
|
||||
|
||||
// Create a subscription for the WEBRTC livestream.
|
||||
MQTTListenerHandleLiveHDHandshake(c, hubKey, configuration, communication)
|
||||
|
||||
// Create a subscription for keeping alive the WEBRTC livestream.
|
||||
MQTTListenerHandleLiveHDKeepalive(c, hubKey, configuration, communication)
|
||||
|
||||
// Create a subscription to listen to the number of WEBRTC peers.
|
||||
MQTTListenerHandleLiveHDPeers(c, hubKey, configuration, communication)
|
||||
|
||||
// Create a subscription to listen for WEBRTC candidates.
|
||||
MQTTListenerHandleLiveHDCandidates(c, hubKey, configuration, communication)
|
||||
|
||||
// Create a susbcription to listen for ONVIF actions: e.g. PTZ, Zoom, etc.
|
||||
MQTTListenerHandleONVIF(c, hubKey, configuration, communication)
|
||||
// Create a susbcription for listen and reply
|
||||
MQTTListenerHandler(c, hubKey, configDirectory, configuration, communication)
|
||||
}
|
||||
}
|
||||
mqc := mqtt.NewClient(opts)
|
||||
@@ -140,119 +144,338 @@ func ConfigureMQTT(configuration *models.Configuration, communication *models.Co
|
||||
return nil
|
||||
}
|
||||
|
||||
func MQTTListenerHandleLiveSD(mqttClient mqtt.Client, hubKey string, configuration *models.Configuration, communication *models.Communication) {
|
||||
config := configuration.Config
|
||||
topicRequest := "kerberos/" + hubKey + "/device/" + config.Key + "/request-live"
|
||||
mqttClient.Subscribe(topicRequest, 0, func(c mqtt.Client, msg mqtt.Message) {
|
||||
if communication.CameraConnected {
|
||||
select {
|
||||
case communication.HandleLiveSD <- time.Now().Unix():
|
||||
default:
|
||||
}
|
||||
log.Log.Info("MQTTListenerHandleLiveSD: received request to livestream.")
|
||||
} else {
|
||||
log.Log.Info("MQTTListenerHandleLiveSD: received request to livestream, but camera is not connected.")
|
||||
}
|
||||
msg.Ack()
|
||||
})
|
||||
}
|
||||
func MQTTListenerHandler(mqttClient mqtt.Client, hubKey string, configDirectory string, configuration *models.Configuration, communication *models.Communication) {
|
||||
if hubKey == "" {
|
||||
log.Log.Info("MQTTListenerHandler: no hub key provided, not subscribing to kerberos/hub/{hubkey}")
|
||||
} else {
|
||||
topicOnvif := fmt.Sprintf("kerberos/agent/%s", hubKey)
|
||||
mqttClient.Subscribe(topicOnvif, 1, func(c mqtt.Client, msg mqtt.Message) {
|
||||
|
||||
func MQTTListenerHandleLiveHDHandshake(mqttClient mqtt.Client, hubKey string, configuration *models.Configuration, communication *models.Communication) {
|
||||
config := configuration.Config
|
||||
topicRequestWebRtc := config.Key + "/register"
|
||||
mqttClient.Subscribe(topicRequestWebRtc, 0, func(c mqtt.Client, msg mqtt.Message) {
|
||||
if communication.CameraConnected {
|
||||
var sdp models.SDPPayload
|
||||
json.Unmarshal(msg.Payload(), &sdp)
|
||||
select {
|
||||
case communication.HandleLiveHDHandshake <- sdp:
|
||||
default:
|
||||
}
|
||||
log.Log.Info("MQTTListenerHandleLiveHDHandshake: received request to setup webrtc.")
|
||||
} else {
|
||||
log.Log.Info("MQTTListenerHandleLiveHDHandshake: received request to setup webrtc, but camera is not connected.")
|
||||
}
|
||||
msg.Ack()
|
||||
})
|
||||
}
|
||||
// Decode the message, we are expecting following format.
|
||||
// {
|
||||
// mid: string, "unique id for the message"
|
||||
// timestamp: int64, "unix timestamp when the message was generated"
|
||||
// encrypted: boolean,
|
||||
// fingerprint: string, "fingerprint of the message to validate authenticity"
|
||||
// payload: Payload, "a json object which might be encrypted"
|
||||
// }
|
||||
|
||||
func MQTTListenerHandleLiveHDKeepalive(mqttClient mqtt.Client, hubKey string, configuration *models.Configuration, communication *models.Communication) {
|
||||
config := configuration.Config
|
||||
topicKeepAlive := fmt.Sprintf("kerberos/webrtc/keepalivehub/%s", config.Key)
|
||||
mqttClient.Subscribe(topicKeepAlive, 0, func(c mqtt.Client, msg mqtt.Message) {
|
||||
if communication.CameraConnected {
|
||||
alive := string(msg.Payload())
|
||||
communication.HandleLiveHDKeepalive <- alive
|
||||
log.Log.Info("MQTTListenerHandleLiveHDKeepalive: Received keepalive: " + alive)
|
||||
} else {
|
||||
log.Log.Info("MQTTListenerHandleLiveHDKeepalive: received keepalive, but camera is not connected.")
|
||||
}
|
||||
})
|
||||
}
|
||||
var message models.Message
|
||||
json.Unmarshal(msg.Payload(), &message)
|
||||
|
||||
func MQTTListenerHandleLiveHDPeers(mqttClient mqtt.Client, hubKey string, configuration *models.Configuration, communication *models.Communication) {
|
||||
config := configuration.Config
|
||||
topicPeers := fmt.Sprintf("kerberos/webrtc/peers/%s", config.Key)
|
||||
mqttClient.Subscribe(topicPeers, 0, func(c mqtt.Client, msg mqtt.Message) {
|
||||
if communication.CameraConnected {
|
||||
peerCount := string(msg.Payload())
|
||||
communication.HandleLiveHDPeers <- peerCount
|
||||
log.Log.Info("MQTTListenerHandleLiveHDPeers: Number of peers listening: " + peerCount)
|
||||
} else {
|
||||
log.Log.Info("MQTTListenerHandleLiveHDPeers: received peer count, but camera is not connected.")
|
||||
}
|
||||
})
|
||||
}
|
||||
// We will receive all messages from our hub, so we'll need to filter to the relevant device.
|
||||
if message.Mid != "" && message.Timestamp != 0 && message.DeviceId == configuration.Config.Key {
|
||||
// Messages might be encrypted, if so we'll
|
||||
// need to decrypt them.
|
||||
var payload models.Payload
|
||||
if message.Encrypted && configuration.Config.Encryption != nil && configuration.Config.Encryption.Enabled {
|
||||
encryptedValue := message.Payload.EncryptedValue
|
||||
if len(encryptedValue) > 0 {
|
||||
symmetricKey := configuration.Config.Encryption.SymmetricKey
|
||||
privateKey := configuration.Config.Encryption.PrivateKey
|
||||
r := strings.NewReader(privateKey)
|
||||
pemBytes, _ := ioutil.ReadAll(r)
|
||||
block, _ := pem.Decode(pemBytes)
|
||||
if block == nil {
|
||||
log.Log.Error("MQTTListenerHandler: error decoding PEM block containing private key")
|
||||
return
|
||||
} else {
|
||||
// Parse private key
|
||||
b := block.Bytes
|
||||
key, err := x509.ParsePKCS8PrivateKey(b)
|
||||
if err != nil {
|
||||
log.Log.Error("MQTTListenerHandler: error parsing private key: " + err.Error())
|
||||
return
|
||||
} else {
|
||||
// Conver key to *rsa.PrivateKey
|
||||
rsaKey, _ := key.(*rsa.PrivateKey)
|
||||
|
||||
func MQTTListenerHandleLiveHDCandidates(mqttClient mqtt.Client, hubKey string, configuration *models.Configuration, communication *models.Communication) {
|
||||
config := configuration.Config
|
||||
topicCandidates := "candidate/cloud"
|
||||
mqttClient.Subscribe(topicCandidates, 0, func(c mqtt.Client, msg mqtt.Message) {
|
||||
if communication.CameraConnected {
|
||||
var candidate models.Candidate
|
||||
json.Unmarshal(msg.Payload(), &candidate)
|
||||
if candidate.CloudKey == config.Key {
|
||||
key := candidate.CloudKey + "/" + candidate.Cuuid
|
||||
candidatesExists := false
|
||||
var channel chan string
|
||||
for !candidatesExists {
|
||||
webrtc.CandidatesMutex.Lock()
|
||||
channel, candidatesExists = webrtc.CandidateArrays[key]
|
||||
webrtc.CandidatesMutex.Unlock()
|
||||
// Get encrypted key from message, delimited by :::
|
||||
encryptedKey := strings.Split(encryptedValue, ":::")[0] // encrypted with RSA
|
||||
encryptedValue := strings.Split(encryptedValue, ":::")[1] // encrypted with AES
|
||||
// Convert encrypted value to []byte
|
||||
decryptedKey, err := encryption.DecryptWithPrivateKey(encryptedKey, rsaKey)
|
||||
if decryptedKey != nil {
|
||||
if string(decryptedKey) == symmetricKey {
|
||||
// Decrypt value with decryptedKey
|
||||
decryptedValue, err := encryption.AesDecrypt(encryptedValue, string(decryptedKey))
|
||||
if err != nil {
|
||||
log.Log.Error("MQTTListenerHandler: error decrypting message: " + err.Error())
|
||||
return
|
||||
}
|
||||
json.Unmarshal([]byte(decryptedValue), &payload)
|
||||
} else {
|
||||
log.Log.Error("MQTTListenerHandler: error decrypting message, assymetric keys do not match.")
|
||||
return
|
||||
}
|
||||
} else if err != nil {
|
||||
log.Log.Error("MQTTListenerHandler: error decrypting message: " + err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
payload = message.Payload
|
||||
}
|
||||
log.Log.Info("MQTTListenerHandleLiveHDCandidates: " + string(msg.Payload()))
|
||||
channel <- string(msg.Payload())
|
||||
|
||||
// We'll find out which message we received, and act accordingly.
|
||||
log.Log.Info("MQTTListenerHandler: received message with action: " + payload.Action)
|
||||
switch payload.Action {
|
||||
case "record":
|
||||
go HandleRecording(mqttClient, hubKey, payload, configuration, communication)
|
||||
case "get-ptz-position":
|
||||
go HandleGetPTZPosition(mqttClient, hubKey, payload, configuration, communication)
|
||||
case "update-ptz-position":
|
||||
go HandleUpdatePTZPosition(mqttClient, hubKey, payload, configuration, communication)
|
||||
case "navigate-ptz":
|
||||
go HandleNavigatePTZ(mqttClient, hubKey, payload, configuration, communication)
|
||||
case "request-config":
|
||||
go HandleRequestConfig(mqttClient, hubKey, payload, configuration, communication)
|
||||
case "update-config":
|
||||
go HandleUpdateConfig(mqttClient, hubKey, payload, configDirectory, configuration, communication)
|
||||
case "request-sd-stream":
|
||||
go HandleRequestSDStream(mqttClient, hubKey, payload, configuration, communication)
|
||||
case "request-hd-stream":
|
||||
go HandleRequestHDStream(mqttClient, hubKey, payload, configuration, communication)
|
||||
case "receive-hd-candidates":
|
||||
go HandleReceiveHDCandidates(mqttClient, hubKey, payload, configuration, communication)
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
log.Log.Info("MQTTListenerHandleLiveHDCandidates: received candidate, but camera is not connected.")
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func MQTTListenerHandleONVIF(mqttClient mqtt.Client, hubKey string, configuration *models.Configuration, communication *models.Communication) {
|
||||
config := configuration.Config
|
||||
topicOnvif := fmt.Sprintf("kerberos/onvif/%s", config.Key)
|
||||
mqttClient.Subscribe(topicOnvif, 0, func(c mqtt.Client, msg mqtt.Message) {
|
||||
func HandleRecording(mqttClient mqtt.Client, hubKey string, payload models.Payload, configuration *models.Configuration, communication *models.Communication) {
|
||||
value := payload.Value
|
||||
|
||||
// Convert map[string]interface{} to RecordPayload
|
||||
jsonData, _ := json.Marshal(value)
|
||||
var recordPayload models.RecordPayload
|
||||
json.Unmarshal(jsonData, &recordPayload)
|
||||
|
||||
if recordPayload.Timestamp != 0 {
|
||||
motionDataPartial := models.MotionDataPartial{
|
||||
Timestamp: recordPayload.Timestamp,
|
||||
}
|
||||
communication.HandleMotion <- motionDataPartial
|
||||
}
|
||||
}
|
||||
|
||||
func HandleGetPTZPosition(mqttClient mqtt.Client, hubKey string, payload models.Payload, configuration *models.Configuration, communication *models.Communication) {
|
||||
value := payload.Value
|
||||
|
||||
// Convert map[string]interface{} to PTZPositionPayload
|
||||
jsonData, _ := json.Marshal(value)
|
||||
var positionPayload models.PTZPositionPayload
|
||||
json.Unmarshal(jsonData, &positionPayload)
|
||||
|
||||
if positionPayload.Timestamp != 0 {
|
||||
// Get Position from device
|
||||
pos, err := onvif.GetPositionFromDevice(*configuration)
|
||||
if err != nil {
|
||||
log.Log.Error("HandlePTZPosition: error getting position from device: " + err.Error())
|
||||
} else {
|
||||
// Needs to wrapped!
|
||||
posString := fmt.Sprintf("%f,%f,%f", pos.PanTilt.X, pos.PanTilt.Y, pos.Zoom.X)
|
||||
message := models.Message{
|
||||
Payload: models.Payload{
|
||||
Action: "ptz-position",
|
||||
DeviceId: configuration.Config.Key,
|
||||
Value: map[string]interface{}{
|
||||
"timestamp": positionPayload.Timestamp,
|
||||
"position": posString,
|
||||
},
|
||||
},
|
||||
}
|
||||
payload, err := models.PackageMQTTMessage(configuration, message)
|
||||
if err == nil {
|
||||
mqttClient.Publish("kerberos/hub/"+hubKey, 0, false, payload)
|
||||
} else {
|
||||
log.Log.Info("HandlePTZPosition: something went wrong while sending position to hub: " + string(payload))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func HandleUpdatePTZPosition(mqttClient mqtt.Client, hubKey string, payload models.Payload, configuration *models.Configuration, communication *models.Communication) {
|
||||
value := payload.Value
|
||||
|
||||
// Convert map[string]interface{} to PTZPositionPayload
|
||||
jsonData, _ := json.Marshal(value)
|
||||
var onvifAction models.OnvifAction
|
||||
json.Unmarshal(jsonData, &onvifAction)
|
||||
|
||||
if onvifAction.Action != "" {
|
||||
if communication.CameraConnected {
|
||||
var onvifAction models.OnvifAction
|
||||
json.Unmarshal(msg.Payload(), &onvifAction)
|
||||
communication.HandleONVIF <- onvifAction
|
||||
log.Log.Info("MQTTListenerHandleONVIF: Received an action - " + onvifAction.Action)
|
||||
} else {
|
||||
log.Log.Info("MQTTListenerHandleONVIF: received action, but camera is not connected.")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func HandleRequestConfig(mqttClient mqtt.Client, hubKey string, payload models.Payload, configuration *models.Configuration, communication *models.Communication) {
|
||||
value := payload.Value
|
||||
|
||||
// Convert map[string]interface{} to RequestConfigPayload
|
||||
jsonData, _ := json.Marshal(value)
|
||||
var configPayload models.RequestConfigPayload
|
||||
json.Unmarshal(jsonData, &configPayload)
|
||||
|
||||
if configPayload.Timestamp != 0 {
|
||||
// Get Config from the device
|
||||
|
||||
key := configuration.Config.Key
|
||||
name := configuration.Config.Name
|
||||
|
||||
if key != "" && name != "" {
|
||||
|
||||
var configMap map[string]interface{}
|
||||
inrec, _ := json.Marshal(configuration.Config)
|
||||
json.Unmarshal(inrec, &configMap)
|
||||
|
||||
message := models.Message{
|
||||
Payload: models.Payload{
|
||||
Action: "receive-config",
|
||||
DeviceId: configuration.Config.Key,
|
||||
Value: configMap,
|
||||
},
|
||||
}
|
||||
payload, err := models.PackageMQTTMessage(configuration, message)
|
||||
if err == nil {
|
||||
mqttClient.Publish("kerberos/hub/"+hubKey, 0, false, payload)
|
||||
} else {
|
||||
log.Log.Info("HandleRequestConfig: something went wrong while sending config to hub: " + string(payload))
|
||||
}
|
||||
|
||||
} else {
|
||||
log.Log.Info("HandleRequestConfig: no config available")
|
||||
}
|
||||
|
||||
log.Log.Info("HandleRequestConfig: Received a request for the config")
|
||||
}
|
||||
}
|
||||
|
||||
func HandleUpdateConfig(mqttClient mqtt.Client, hubKey string, payload models.Payload, configDirectory string, configuration *models.Configuration, communication *models.Communication) {
|
||||
value := payload.Value
|
||||
|
||||
// Convert map[string]interface{} to UpdateConfigPayload
|
||||
jsonData, _ := json.Marshal(value)
|
||||
var configPayload models.UpdateConfigPayload
|
||||
json.Unmarshal(jsonData, &configPayload)
|
||||
|
||||
if configPayload.Timestamp != 0 {
|
||||
|
||||
config := configPayload.Config
|
||||
err := configService.SaveConfig(configDirectory, config, configuration, communication)
|
||||
if err == nil {
|
||||
log.Log.Info("HandleUpdateConfig: Config updated")
|
||||
message := models.Message{
|
||||
Payload: models.Payload{
|
||||
Action: "acknowledge-update-config",
|
||||
DeviceId: configuration.Config.Key,
|
||||
},
|
||||
}
|
||||
payload, err := models.PackageMQTTMessage(configuration, message)
|
||||
if err == nil {
|
||||
mqttClient.Publish("kerberos/hub/"+hubKey, 0, false, payload)
|
||||
} else {
|
||||
log.Log.Info("HandleRequestConfig: something went wrong while sending acknowledge config to hub: " + string(payload))
|
||||
}
|
||||
} else {
|
||||
log.Log.Info("HandleUpdateConfig: Config update failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func HandleRequestSDStream(mqttClient mqtt.Client, hubKey string, payload models.Payload, configuration *models.Configuration, communication *models.Communication) {
|
||||
value := payload.Value
|
||||
// Convert map[string]interface{} to RequestSDStreamPayload
|
||||
jsonData, _ := json.Marshal(value)
|
||||
var requestSDStreamPayload models.RequestSDStreamPayload
|
||||
json.Unmarshal(jsonData, &requestSDStreamPayload)
|
||||
|
||||
if requestSDStreamPayload.Timestamp != 0 {
|
||||
if communication.CameraConnected {
|
||||
select {
|
||||
case communication.HandleLiveSD <- time.Now().Unix():
|
||||
default:
|
||||
}
|
||||
log.Log.Info("HandleRequestSDStream: received request to livestream.")
|
||||
} else {
|
||||
log.Log.Info("HandleRequestSDStream: received request to livestream, but camera is not connected.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func HandleRequestHDStream(mqttClient mqtt.Client, hubKey string, payload models.Payload, configuration *models.Configuration, communication *models.Communication) {
|
||||
value := payload.Value
|
||||
// Convert map[string]interface{} to RequestHDStreamPayload
|
||||
jsonData, _ := json.Marshal(value)
|
||||
var requestHDStreamPayload models.RequestHDStreamPayload
|
||||
json.Unmarshal(jsonData, &requestHDStreamPayload)
|
||||
|
||||
if requestHDStreamPayload.Timestamp != 0 {
|
||||
if communication.CameraConnected {
|
||||
// Set the Hub key, so we can send back the answer.
|
||||
requestHDStreamPayload.HubKey = hubKey
|
||||
select {
|
||||
case communication.HandleLiveHDHandshake <- requestHDStreamPayload:
|
||||
default:
|
||||
}
|
||||
log.Log.Info("HandleRequestHDStream: received request to setup webrtc.")
|
||||
} else {
|
||||
log.Log.Info("HandleRequestHDStream: received request to setup webrtc, but camera is not connected.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func HandleReceiveHDCandidates(mqttClient mqtt.Client, hubKey string, payload models.Payload, configuration *models.Configuration, communication *models.Communication) {
|
||||
value := payload.Value
|
||||
// Convert map[string]interface{} to ReceiveHDCandidatesPayload
|
||||
jsonData, _ := json.Marshal(value)
|
||||
var receiveHDCandidatesPayload models.ReceiveHDCandidatesPayload
|
||||
json.Unmarshal(jsonData, &receiveHDCandidatesPayload)
|
||||
|
||||
if receiveHDCandidatesPayload.Timestamp != 0 {
|
||||
if communication.CameraConnected {
|
||||
channel := webrtc.CandidateArrays[receiveHDCandidatesPayload.SessionID]
|
||||
log.Log.Info("HandleReceiveHDCandidates: " + receiveHDCandidatesPayload.Candidate)
|
||||
channel <- receiveHDCandidatesPayload.Candidate
|
||||
} else {
|
||||
log.Log.Info("HandleReceiveHDCandidates: received candidate, but camera is not connected.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func HandleNavigatePTZ(mqttClient mqtt.Client, hubKey string, payload models.Payload, configuration *models.Configuration, communication *models.Communication) {
|
||||
value := payload.Value
|
||||
jsonData, _ := json.Marshal(value)
|
||||
var navigatePTZPayload models.NavigatePTZPayload
|
||||
json.Unmarshal(jsonData, &navigatePTZPayload)
|
||||
|
||||
if navigatePTZPayload.Timestamp != 0 {
|
||||
if communication.CameraConnected {
|
||||
action := navigatePTZPayload.Action
|
||||
var onvifAction models.OnvifAction
|
||||
json.Unmarshal([]byte(action), &onvifAction)
|
||||
communication.HandleONVIF <- onvifAction
|
||||
log.Log.Info("HandleNavigatePTZ: Received an action - " + onvifAction.Action)
|
||||
|
||||
} else {
|
||||
log.Log.Info("HandleNavigatePTZ: received action, but camera is not connected.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func DisconnectMQTT(mqttClient mqtt.Client, config *models.Config) {
|
||||
if mqttClient != nil {
|
||||
// Cleanup all subscriptions
|
||||
mqttClient.Unsubscribe("kerberos/" + PREV_HubKey + "/device/" + PREV_AgentKey + "/request-live")
|
||||
mqttClient.Unsubscribe(PREV_AgentKey + "/register")
|
||||
mqttClient.Unsubscribe("kerberos/webrtc/keepalivehub/" + PREV_AgentKey)
|
||||
mqttClient.Unsubscribe("kerberos/webrtc/peers/" + PREV_AgentKey)
|
||||
mqttClient.Unsubscribe("candidate/cloud")
|
||||
mqttClient.Unsubscribe("kerberos/onvif/" + PREV_AgentKey)
|
||||
// New methods
|
||||
mqttClient.Unsubscribe("kerberos/agent/" + PREV_HubKey)
|
||||
mqttClient.Disconnect(1000)
|
||||
mqttClient = nil
|
||||
log.Log.Info("DisconnectMQTT: MQTT client disconnected.")
|
||||
|
||||
@@ -87,19 +87,22 @@ func (w WebRTC) CreateOffer(sd []byte) pionWebRTC.SessionDescription {
|
||||
return offer
|
||||
}
|
||||
|
||||
func InitializeWebRTCConnection(configuration *models.Configuration, communication *models.Communication, mqttClient mqtt.Client, videoTrack *pionWebRTC.TrackLocalStaticSample, audioTrack *pionWebRTC.TrackLocalStaticSample, handshake models.SDPPayload, candidates chan string) {
|
||||
func InitializeWebRTCConnection(configuration *models.Configuration, communication *models.Communication, mqttClient mqtt.Client, videoTrack *pionWebRTC.TrackLocalStaticSample, audioTrack *pionWebRTC.TrackLocalStaticSample, handshake models.RequestHDStreamPayload, candidates chan string) {
|
||||
|
||||
config := configuration.Config
|
||||
|
||||
name := config.Key
|
||||
deviceKey := config.Key
|
||||
stunServers := []string{config.STUNURI}
|
||||
turnServers := []string{config.TURNURI}
|
||||
turnServersUsername := config.TURNUsername
|
||||
turnServersCredential := config.TURNPassword
|
||||
|
||||
// Set variables
|
||||
hubKey := handshake.HubKey
|
||||
sessionDescription := handshake.SessionDescription
|
||||
|
||||
// Create WebRTC object
|
||||
w := CreateWebRTC(name, stunServers, turnServers, turnServersUsername, turnServersCredential)
|
||||
sd, err := w.DecodeSessionDescription(handshake.Sdp)
|
||||
w := CreateWebRTC(deviceKey, stunServers, turnServers, turnServersUsername, turnServersCredential)
|
||||
sd, err := w.DecodeSessionDescription(sessionDescription)
|
||||
|
||||
if err == nil {
|
||||
|
||||
@@ -122,7 +125,6 @@ func InitializeWebRTCConnection(configuration *models.Configuration, communicati
|
||||
Credential: w.TurnServersCredential,
|
||||
},
|
||||
},
|
||||
//ICETransportPolicy: pionWebRTC.ICETransportPolicyRelay,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -143,7 +145,7 @@ func InitializeWebRTCConnection(configuration *models.Configuration, communicati
|
||||
peerConnection.OnICEConnectionStateChange(func(connectionState pionWebRTC.ICEConnectionState) {
|
||||
if connectionState == pionWebRTC.ICEConnectionStateDisconnected {
|
||||
atomic.AddInt64(&peerConnectionCount, -1)
|
||||
peerConnections[handshake.Cuuid] = nil
|
||||
peerConnections[handshake.SessionID] = nil
|
||||
close(candidates)
|
||||
close(w.PacketsCount)
|
||||
if err := peerConnection.Close(); err != nil {
|
||||
@@ -152,9 +154,12 @@ func InitializeWebRTCConnection(configuration *models.Configuration, communicati
|
||||
} else if connectionState == pionWebRTC.ICEConnectionStateConnected {
|
||||
atomic.AddInt64(&peerConnectionCount, 1)
|
||||
} else if connectionState == pionWebRTC.ICEConnectionStateChecking {
|
||||
// Iterate over the candidates and send them to the remote client
|
||||
// Non blocking channel
|
||||
for candidate := range candidates {
|
||||
log.Log.Info("InitializeWebRTCConnection: Received candidate.")
|
||||
if candidateErr := peerConnection.AddICECandidate(pionWebRTC.ICECandidateInit{Candidate: string(candidate)}); candidateErr != nil {
|
||||
log.Log.Error("InitializeWebRTCConnection: something went wrong while adding candidate: " + candidateErr.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -167,7 +172,6 @@ func InitializeWebRTCConnection(configuration *models.Configuration, communicati
|
||||
panic(err)
|
||||
}
|
||||
|
||||
//gatherCompletePromise := pionWebRTC.GatheringCompletePromise(peerConnection)
|
||||
answer, err := peerConnection.CreateAnswer(nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -175,37 +179,64 @@ func InitializeWebRTCConnection(configuration *models.Configuration, communicati
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// When an ICE candidate is available send to the other Pion instance
|
||||
// the other Pion instance will add this candidate by calling AddICECandidate
|
||||
var candidatesMux sync.Mutex
|
||||
// When an ICE candidate is available send to the other peer using the signaling server (MQTT).
|
||||
// The other peer will add this candidate by calling AddICECandidate
|
||||
peerConnection.OnICECandidate(func(candidate *pionWebRTC.ICECandidate) {
|
||||
|
||||
if candidate == nil {
|
||||
return
|
||||
}
|
||||
|
||||
candidatesMux.Lock()
|
||||
defer candidatesMux.Unlock()
|
||||
|
||||
topic := fmt.Sprintf("%s/%s/candidate/edge", name, handshake.Cuuid)
|
||||
log.Log.Info("InitializeWebRTCConnection: Send candidate to " + topic)
|
||||
candiInit := candidate.ToJSON()
|
||||
// Create a config map
|
||||
valueMap := make(map[string]interface{})
|
||||
candateJSON := candidate.ToJSON()
|
||||
sdpmid := "0"
|
||||
candiInit.SDPMid = &sdpmid
|
||||
candi, err := json.Marshal(candiInit)
|
||||
candateJSON.SDPMid = &sdpmid
|
||||
candateBinary, err := json.Marshal(candateJSON)
|
||||
if err == nil {
|
||||
log.Log.Info("InitializeWebRTCConnection:" + string(candi))
|
||||
token := mqttClient.Publish(topic, 2, false, candi)
|
||||
token.Wait()
|
||||
valueMap["candidate"] = string(candateBinary)
|
||||
} else {
|
||||
log.Log.Info("HandleRequestConfig: something went wrong while marshalling candidate: " + err.Error())
|
||||
}
|
||||
|
||||
// We'll send the candidate to the hub
|
||||
message := models.Message{
|
||||
Payload: models.Payload{
|
||||
Action: "receive-hd-candidates",
|
||||
DeviceId: configuration.Config.Key,
|
||||
Value: valueMap,
|
||||
},
|
||||
}
|
||||
payload, err := models.PackageMQTTMessage(configuration, message)
|
||||
if err == nil {
|
||||
mqttClient.Publish("kerberos/hub/"+hubKey, 0, false, payload)
|
||||
} else {
|
||||
log.Log.Info("HandleRequestConfig: something went wrong while sending acknowledge config to hub: " + string(payload))
|
||||
}
|
||||
})
|
||||
|
||||
peerConnections[handshake.Cuuid] = peerConnection
|
||||
// Create a channel which will be used to send candidates to the other peer
|
||||
peerConnections[handshake.SessionID] = peerConnection
|
||||
|
||||
if err == nil {
|
||||
topic := fmt.Sprintf("%s/%s/answer", name, handshake.Cuuid)
|
||||
log.Log.Info("InitializeWebRTCConnection: Send SDP answer to " + topic)
|
||||
mqttClient.Publish(topic, 2, false, []byte(base64.StdEncoding.EncodeToString([]byte(answer.SDP))))
|
||||
// Create a config map
|
||||
valueMap := make(map[string]interface{})
|
||||
valueMap["sdp"] = []byte(base64.StdEncoding.EncodeToString([]byte(answer.SDP)))
|
||||
log.Log.Info("InitializeWebRTCConnection: Send SDP answer")
|
||||
|
||||
// We'll send the candidate to the hub
|
||||
message := models.Message{
|
||||
Payload: models.Payload{
|
||||
Action: "receive-hd-answer",
|
||||
DeviceId: configuration.Config.Key,
|
||||
Value: valueMap,
|
||||
},
|
||||
}
|
||||
payload, err := models.PackageMQTTMessage(configuration, message)
|
||||
if err == nil {
|
||||
mqttClient.Publish("kerberos/hub/"+hubKey, 0, false, payload)
|
||||
} else {
|
||||
log.Log.Info("HandleRequestConfig: something went wrong while sending acknowledge config to hub: " + string(payload))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -358,16 +389,9 @@ func WriteToTrack(livestreamCursor *pubsub.QueueCursor, configuration *models.Co
|
||||
pkt.Data = append(codecData.(h264parser.CodecData).SPS(), pkt.Data...)
|
||||
pkt.Data = append(annexbNALUStartCode(), pkt.Data...)
|
||||
log.Log.Info("WriteToTrack: Sending keyframe")
|
||||
|
||||
if config.Capture.ForwardWebRTC == "true" {
|
||||
log.Log.Info("WriteToTrack: Sending keep a live to remote broker.")
|
||||
topic := fmt.Sprintf("kerberos/webrtc/keepalive/%s", config.Key)
|
||||
mqttClient.Publish(topic, 2, false, "1")
|
||||
}
|
||||
}
|
||||
|
||||
if start {
|
||||
|
||||
sample := pionMedia.Sample{Data: pkt.Data, Duration: bufferDuration}
|
||||
if config.Capture.ForwardWebRTC == "true" {
|
||||
samplePacket, err := json.Marshal(sample)
|
||||
|
||||
215
ui/public/locales/hi/translation.json
Normal file
215
ui/public/locales/hi/translation.json
Normal file
@@ -0,0 +1,215 @@
|
||||
{
|
||||
"breadcrumb": {
|
||||
"watch_recordings": "रिकॉर्डिंग देखें",
|
||||
"configure": "कॉन्फ़िगर"
|
||||
},
|
||||
"buttons": {
|
||||
"save": "सेव्ह",
|
||||
"verify_connection": "कनेक्शन चेक करें"
|
||||
},
|
||||
"navigation": {
|
||||
"profile": "प्रोफ़ाइल",
|
||||
"admin": "व्यवस्थापक",
|
||||
"management": "प्रबंध",
|
||||
"dashboard": "डैशबोर्ड",
|
||||
"recordings": "रिकॉर्डिंग",
|
||||
"settings": "सेटिंग",
|
||||
"help_support": "मदद",
|
||||
"swagger": "स्वैगर एपीआई",
|
||||
"documentation": "प्रलेखन",
|
||||
"ui_library": "यूआई लाइब्रेरी",
|
||||
"layout": "भाषा और लेआऊट",
|
||||
"choose_language": "भाषा चुनें"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "डैशबोर्ड",
|
||||
"heading": "आपके वीडियो निगरानी का अवलोकन",
|
||||
"number_of_days": "दिनों की संख्या",
|
||||
"total_recordings": "कुल रिकॉर्डिंग",
|
||||
"connected": "जुड़े है",
|
||||
"not_connected": "जुड़े नहीं हैं",
|
||||
"offline_mode": "ऑफ़लाइन मोड",
|
||||
"latest_events": "नवीनतम घटनाए",
|
||||
"configure_connection": "कनेक्शन कॉन्फ़िगर करें",
|
||||
"no_events": "कोई घटनाए नहीं",
|
||||
"no_events_description": "कोई रिकॉर्डिंग नहीं मिली, सुनिश्चित करें कि आपका Kerberos एजेंट ठीक से कॉन्फ़िगर किया गया है।",
|
||||
"motion_detected": "मोशन का पता चला",
|
||||
"live_view": "लाइव देखें",
|
||||
"loading_live_view": "लाइव दृश्य लोड हो रहा है",
|
||||
"loading_live_view_description": "रुकिए हम आपका लाइव व्यू यहां लोड कर रहे हैं। ",
|
||||
"time": "समय",
|
||||
"description": "विवरण",
|
||||
"name": "नाम"
|
||||
},
|
||||
"recordings": {
|
||||
"title": "रिकॉर्डिंग",
|
||||
"heading": "आपकी सभी रिकॉर्डिंग एक ही स्थान पर",
|
||||
"search_media": "मीडिया खोजें"
|
||||
},
|
||||
"settings": {
|
||||
"title": "सेटिंग",
|
||||
"heading": "अपना कैमरा ऑनबोर्ड करें",
|
||||
"submenu": {
|
||||
"all": "सभी",
|
||||
"overview": "अवलोकन",
|
||||
"camera": "कैमरा",
|
||||
"recording": "रिकॉर्डिंग",
|
||||
"streaming": "स्ट्रीमिंग",
|
||||
"conditions": "कंडीशन",
|
||||
"persistence": "परसीस्टेन्स"
|
||||
},
|
||||
"info": {
|
||||
"kerberos_hub_demo": "Kerberos हब को क्रियाशील देखने के लिए हमारे Kerberos हब डेमो पर एक नज़र डालें!",
|
||||
"configuration_updated_success": "आपका कॉन्फ़िगरेशन सफलतापूर्वक अपडेट कर दिया गया है.",
|
||||
"configuration_updated_error": "सहेजते समय कुछ ग़लत हो गया.",
|
||||
"verify_hub": "अपनी Kerberos हब सेटिंग सत्यापित की जा रही है।",
|
||||
"verify_hub_success": "कर्बेरोस हब सेटिंग्स सफलतापूर्वक सत्यापित हो गईं।",
|
||||
"verify_hub_error": "कर्बरोस हब का सत्यापन करते समय कुछ गलत हो गया",
|
||||
"verify_persistence": "आपकी दृढ़ता सेटिंग सत्यापित की जा रही है.",
|
||||
"verify_persistence_success": "दृढ़ता सेटिंग्स सफलतापूर्वक सत्यापित की गई हैं।",
|
||||
"verify_persistence_error": "दृढ़ता की पुष्टि करते समय कुछ गलत हो गया",
|
||||
"verify_camera": "अपनी कैमरा सेटिंग सत्यापित कर रहा है।",
|
||||
"verify_camera_success": "कैमरा सेटिंग्स सफलतापूर्वक सत्यापित हो गईं।",
|
||||
"verify_camera_error": "कैमरा सेटिंग्स सत्यापित करते समय कुछ गलत हो गया",
|
||||
"verify_onvif": "अपनी ONVIF सेटिंग्स सत्यापित कर रहा हूँ।",
|
||||
"verify_onvif_success": "ONVIF सेटिंग्स सफलतापूर्वक सत्यापित हो गईं।",
|
||||
"verify_onvif_error": "ONVIF सेटिंग्स सत्यापित करते समय कुछ गलत हो गया"
|
||||
},
|
||||
"overview": {
|
||||
"general": "सामान्य",
|
||||
"description_general": "आपके Kerberos एजेंट के लिए सामान्य सेटिंग्स",
|
||||
"key": "की",
|
||||
"camera_name": "कैमरे का नाम",
|
||||
"timezone": "समय क्षेत्र",
|
||||
"select_timezone": "समयक्षेत्र चुनें",
|
||||
"advanced_configuration": "एडवांस कॉन्फ़िगरेशन",
|
||||
"description_advanced_configuration": "Kerberos एजेंट के विशिष्ट भागों को सक्षम या अक्षम करने के लिए विस्तृत कॉन्फ़िगरेशन विकल्प",
|
||||
"offline_mode": "ऑफ़लाइन मोड",
|
||||
"description_offline_mode": "सभी आउटगोइंग ट्रैफ़िक अक्षम करें"
|
||||
},
|
||||
"camera": {
|
||||
"camera": "कैमरा",
|
||||
"description_camera": "आपकी पसंद के कैमरे से कनेक्शन बनाने के लिए कैमरा सेटिंग्स की आवश्यकता होती है।",
|
||||
"only_h264": "वर्तमान में केवल H264 RTSP स्ट्रीम समर्थित हैं।",
|
||||
"rtsp_url": "RTSP URL",
|
||||
"rtsp_h264": "आपके कैमरे से H264 RTSP कनेक्शन।",
|
||||
"sub_rtsp_url": "दुसरी RTSP URL (लाइवस्ट्रीमिंग के लिए प्रयुक्त)",
|
||||
"sub_rtsp_h264": "आपके कैमरे के कम रिज़ॉल्यूशन के लिए एक दुसरी RTSP कनेक्शन।",
|
||||
"onvif": "ONVIF",
|
||||
"description_onvif": "ONVIF क्षमताओं के साथ संचार करने के लिए क्रेडेन्शियल। ",
|
||||
"onvif_xaddr": "ONVIF xaddr",
|
||||
"onvif_username": "ONVIF उपयोक्तानाम",
|
||||
"onvif_password": "ओएनवीआईएफ पासवर्ड",
|
||||
"verify_connection": "कनेक्शन सत्यापित करें",
|
||||
"verify_sub_connection": "उप कनेक्शन सत्यापित करें"
|
||||
},
|
||||
"recording": {
|
||||
"recording": "रिकॉर्डिंग",
|
||||
"description_recording": "निर्दिष्ट करें कि आप रिकॉर्डिंग कैसे करना चाहेंगे. ",
|
||||
"continuous_recording": "लगातार रिकॉर्डिंग",
|
||||
"description_continuous_recording": "24/7 या गति आधारित रिकॉर्डिंग करें।",
|
||||
"max_duration": "अधिकतम वीडियो अवधि (सेकंड)",
|
||||
"description_max_duration": "रिकॉर्डिंग की अधिकतम अवधि.",
|
||||
"pre_recording": "पूर्व रिकॉर्डिंग (key frames buffered)",
|
||||
"description_pre_recording": "किसी घटना के घटित होने से सेकंड पहले.",
|
||||
"post_recording": "पोस्ट रिकॉर्डिंग (सेकंड)",
|
||||
"description_post_recording": "किसी घटना के घटित होने के सेकंड बाद.",
|
||||
"threshold": "रिकॉर्डिंग सीमा (पिक्सेल)",
|
||||
"description_threshold": "रिकॉर्ड करने के लिए पिक्सेल की संख्या बदल दी गई",
|
||||
"autoclean": "अपने आप क्लीन करे",
|
||||
"description_autoclean": "निर्दिष्ट करें कि क्या Kerberos एजेंट एक विशिष्ट क्षमता (एमबी) तक पहुंचने पर रिकॉर्डिंग को क्लीन कर सकता है। ",
|
||||
"autoclean_enable": "स्वतः क्लीन सक्षम करें",
|
||||
"autoclean_description_enable": "क्षमता पूरी होने पर सबसे पुरानी रिकॉर्डिंग हटा दें।",
|
||||
"autoclean_max_directory_size": "अधिकतम डिरेक्टरी आकार (एमबी)",
|
||||
"autoclean_description_max_directory_size": "संग्रहीत रिकॉर्डिंग की अधिकतम एमबी।",
|
||||
"fragmentedrecordings": "खंडित रिकॉर्डिंग",
|
||||
"description_fragmentedrecordings": "जब रिकॉर्डिंग खंडित हो जाती हैं तो वे HLS स्ट्रीम के लिए उपयुक्त होती हैं। ",
|
||||
"fragmentedrecordings_enable": "विखंडन सक्षम करें",
|
||||
"fragmentedrecordings_description_enable": "HLS के लिए खंडित रिकॉर्डिंग आवश्यक हैं।",
|
||||
"fragmentedrecordings_duration": "खंड अवधि",
|
||||
"fragmentedrecordings_description_duration": "एक टुकड़े की अवधि."
|
||||
},
|
||||
"streaming": {
|
||||
"stun_turn": "WebRTC के लिए STUN/TURN",
|
||||
"description_stun_turn": "पूर्ण-रिज़ॉल्यूशन लाइवस्ट्रीमिंग के लिए हम WebRTC की अवधारणा का उपयोग करते हैं। ",
|
||||
"stun_server": "STUN server",
|
||||
"turn_server": "TURN server",
|
||||
"turn_username": "उपयोगकर्ता नाम",
|
||||
"turn_password": "पासवर्ड",
|
||||
"stun_turn_forward": "फोरवर्डींग और ट्रांसकोडिंग",
|
||||
"stun_turn_description_forward": "TURN/STUN संचार के लिए अनुकूलन और संवर्द्धन।",
|
||||
"stun_turn_webrtc": "WebRTC ब्रोकर को फोरवर्डींग किया जा रहा है",
|
||||
"stun_turn_description_webrtc": "MQTT के माध्यम से h264 स्ट्रीम को फोरवर्डींग करें",
|
||||
"stun_turn_transcode": "ट्रांसकोड स्ट्रीम",
|
||||
"stun_turn_description_transcode": "स्ट्रीम को कम रिज़ॉल्यूशन में बदलें",
|
||||
"stun_turn_downscale": "डाउनस्केल रिज़ॉल्यूशन (% या मूल रिज़ॉल्यूशन में)",
|
||||
"mqtt": "MQTT",
|
||||
"description_mqtt": "एक MQTT ब्रोकर का उपयोग काम्युनिकेट करने के लिए किया जाता है",
|
||||
"description2_mqtt": "उदाहरण के लिए लाइवस्ट्रीमिंग या ONVIF (PTZ) क्षमताओं को प्राप्त करने के लिए Kerberos एजेंट को।",
|
||||
"mqtt_brokeruri": "Broker Uri",
|
||||
"mqtt_username": "उपयोगकर्ता नाम",
|
||||
"mqtt_password": "पासवर्ड"
|
||||
},
|
||||
"conditions": {
|
||||
"timeofinterest": "रुचि का समय",
|
||||
"description_timeofinterest": "रिकॉर्डिंग केवल विशिष्ट समय अंतराल (समय क्षेत्र के आधार पर) के बीच करें।",
|
||||
"timeofinterest_enabled": "सक्रिय",
|
||||
"timeofinterest_description_enabled": "सक्षम होने पर आप समय विंडो निर्दिष्ट कर सकते हैं",
|
||||
"sunday": "रविवार",
|
||||
"monday": "सोमवार",
|
||||
"tuesday": "मंगलवार",
|
||||
"wednesday": "बुधवार",
|
||||
"thursday": "गुरुवार",
|
||||
"friday": "शुक्रवार",
|
||||
"saturday": "शनिवार",
|
||||
"externalcondition": "बाह्य स्थिति",
|
||||
"description_externalcondition": "बाहरी वेबसेवा के आधार पर रिकॉर्डिंग को सक्षम या अक्षम किया जा सकता है।",
|
||||
"regionofinterest": "दिलचस्पी के क्षेत्र",
|
||||
"description_regionofinterest": "एक या अधिक क्षेत्रों को परिभाषित करने से, गति को केवल आपके द्वारा परिभाषित क्षेत्रों में ही ट्रैक किया जाएगा।"
|
||||
},
|
||||
"persistence": {
|
||||
"kerberoshub": "Kerberos हब",
|
||||
"description_kerberoshub": "Kerberos एजेंट दिल की धड़कनों को सेंट्रल में भेज सकते हैं",
|
||||
"description2_kerberoshub": "आपके वीडियो परिदृश्य के बारे में वास्तविक समय की जानकारी दिखाने के लिए दिल की धड़कन और अन्य प्रासंगिक जानकारी को केर्बरोस हब से समन्वयित किया जाता है।",
|
||||
"persistence": "अटलता",
|
||||
"saasoffering": "Kerberos हब (SAAS offering)",
|
||||
"description_persistence": "अपनी रिकॉर्डिंग संग्रहीत करने की क्षमता होना हर चीज़ की शुरुआत है। ",
|
||||
"description2_persistence": ", या कोई तृतीय पक्ष प्रदाता",
|
||||
"select_persistence": "एक दृढ़ता का चयन करें",
|
||||
"kerberoshub_proxyurl": "Kerberos हब प्रॉक्सी URL",
|
||||
"kerberoshub_description_proxyurl": "आपकी रिकॉर्डिंग अपलोड करने के लिए प्रॉक्सी एंडपॉइंट।",
|
||||
"kerberoshub_apiurl": "Kerberos हब API URL",
|
||||
"kerberoshub_description_apiurl": "आपकी रिकॉर्डिंग अपलोड करने के लिए API एंडपॉइंट।",
|
||||
"kerberoshub_publickey": "सार्वजनिक की",
|
||||
"kerberoshub_description_publickey": "आपके Kerberos हब खाते को दी गई सार्वजनिक की।",
|
||||
"kerberoshub_privatekey": "निजी चाबी",
|
||||
"kerberoshub_description_privatekey": "आपके Kerberos हब खाते को दी गई निजी की।",
|
||||
"kerberoshub_site": "साइट",
|
||||
"kerberoshub_description_site": "साइट आईडी Kerberos एजेंट Kerberos हब से संबंधित हैं।",
|
||||
"kerberoshub_region": "क्षेत्र",
|
||||
"kerberoshub_description_region": "जिस क्षेत्र में हम अपनी रिकॉर्डिंग संग्रहीत कर रहे हैं।",
|
||||
"kerberoshub_bucket": "बकेट",
|
||||
"kerberoshub_description_bucket": "जिस बकेट में हम अपनी रिकॉर्डिंग संग्रहीत कर रहे हैं।",
|
||||
"kerberoshub_username": "उपयोगकर्ता नाम/निर्देशिका (Kerberos हब उपयोगकर्ता नाम से मेल खाना चाहिए)",
|
||||
"kerberoshub_description_username": "आपके Kerberos हब खाते का उपयोगकर्ता नाम।",
|
||||
"kerberosvault_apiurl": "Kerberos वॉल्ट API URL",
|
||||
"kerberosvault_description_apiurl": "कर्बरोस वॉल्ट एपीआई",
|
||||
"kerberosvault_provider": "प्रदाता",
|
||||
"kerberosvault_description_provider": "वह प्रदाता जिसे आपकी रिकॉर्डिंग भेजी जाएगी।",
|
||||
"kerberosvault_directory": "निर्देशिका (Kerberos हब उपयोगकर्ता नाम से मेल खाना चाहिए)",
|
||||
"kerberosvault_description_directory": "उप निर्देशिका रिकॉर्डिंग आपके प्रदाता में संग्रहीत की जाएगी।",
|
||||
"kerberosvault_accesskey": "प्रवेश की चाबी",
|
||||
"kerberosvault_description_accesskey": "आपके Kerberos वॉल्ट खाते की एक्सेस की।",
|
||||
"kerberosvault_secretkey": "गुप्त की",
|
||||
"kerberosvault_description_secretkey": "आपके कर्बेरोस वॉल्ट खाते की गुप्त की।",
|
||||
"dropbox_directory": "निर्देशिका",
|
||||
"dropbox_description_directory": "वह उप निर्देशिका जहां रिकॉर्डिंग आपके ड्रॉपबॉक्स खाते में संग्रहीत की जाएगी।",
|
||||
"dropbox_accesstoken": "एक्सेस टोकन",
|
||||
"dropbox_description_accesstoken": "आपके ड्रॉपबॉक्स खाते/ऐप का एक्सेस टोकन।",
|
||||
"verify_connection": "कनेक्शन सत्यापित करें",
|
||||
"remove_after_upload": "एक बार जब रिकॉर्डिंग कुछ दृढ़ता पर अपलोड हो जाती है, तो हो सकता है कि आप उन्हें स्थानीय Kerberos एजेंट से हटाना चाहें।",
|
||||
"remove_after_upload_description": "सफलतापूर्वक अपलोड होने के बाद रिकॉर्डिंग हटा दें।",
|
||||
"remove_after_upload_enabled": "अपलोड पर डिलीट सक्षम"
|
||||
}
|
||||
}
|
||||
}
|
||||
215
ui/public/locales/it/translation.json
Normal file
215
ui/public/locales/it/translation.json
Normal file
@@ -0,0 +1,215 @@
|
||||
{
|
||||
"breadcrumb": {
|
||||
"watch_recordings": "Guarda registrazioni",
|
||||
"configure": "Configura"
|
||||
},
|
||||
"buttons": {
|
||||
"save": "Salva",
|
||||
"verify_connection": "Verifica connessione"
|
||||
},
|
||||
"navigation": {
|
||||
"profile": "Profilo",
|
||||
"admin": "admin",
|
||||
"management": "Gestione",
|
||||
"dashboard": "Dashboard",
|
||||
"recordings": "Registrazioni",
|
||||
"settings": "Impostazioni",
|
||||
"help_support": "Aiuto e supporto",
|
||||
"swagger": "Swagger API",
|
||||
"documentation": "Documentazione",
|
||||
"ui_library": "Biblioteca UI",
|
||||
"layout": "Lingua e layout",
|
||||
"choose_language": "Scegli lingua"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
"heading": "Panoramica della videosorveglianza",
|
||||
"number_of_days": "Numero di giorni",
|
||||
"total_recordings": "Registrazioni totali",
|
||||
"connected": "Connesso",
|
||||
"not_connected": "Non connesso",
|
||||
"offline_mode": "Modalità offline",
|
||||
"latest_events": "Ultimi eventi",
|
||||
"configure_connection": "Configura connessione",
|
||||
"no_events": "Nessun evento",
|
||||
"no_events_description": "Non sono state trovate registrazioni, assicurati che il Kerberos Agent sia configurato correttamente.",
|
||||
"motion_detected": "Movimento rilevato",
|
||||
"live_view": "Vista in diretta",
|
||||
"loading_live_view": "Caricamento vista in diretta",
|
||||
"loading_live_view_description": "Attendi mentre viene caricata la vista in diretta. Se non l'hai ancora fatto, configura la connessione con la videocamera nelle pagine di impostazione.",
|
||||
"time": "Ora",
|
||||
"description": "Descrizione",
|
||||
"name": "Nome"
|
||||
},
|
||||
"recordings": {
|
||||
"title": "Registrazioni",
|
||||
"heading": "Tutte le tue registrazioni in un posto solo",
|
||||
"search_media": "Cerca video"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Impostazioni",
|
||||
"heading": "Panoramica impostazioni videocamera e Agent",
|
||||
"submenu": {
|
||||
"all": "All",
|
||||
"overview": "Panoramica",
|
||||
"camera": "Videocamera",
|
||||
"recording": "Registrazione",
|
||||
"streaming": "Streaming",
|
||||
"conditions": "Criteri",
|
||||
"persistence": "Persistenza"
|
||||
},
|
||||
"info": {
|
||||
"kerberos_hub_demo": "Dai un'occhiata al nostro ambiente demo di Kerberos Hub per vederlo in azione!",
|
||||
"configuration_updated_success": "La configurazione è stata aggiornata con successo.",
|
||||
"configuration_updated_error": "Si è verificato un problema durante il salvataggio.",
|
||||
"verify_hub": "Controllo delle impostazioni di Kerberos Hub.",
|
||||
"verify_hub_success": "Impostazioni di Kerberos Hub verificate correttamente.",
|
||||
"verify_hub_error": "Si è verificato un problema durante la verifica delle impostazioni di Kerberos Hub",
|
||||
"verify_persistence": "Controlla le impostazioni della persistenza.",
|
||||
"verify_persistence_success": "Impostazioni della persistenza verificate correttamente.",
|
||||
"verify_persistence_error": "Si è verificato un problema durante la verifica delle impostazioni della persistenza",
|
||||
"verify_camera": "Controlla le impostazioni della videocamera.",
|
||||
"verify_camera_success": "Impostazioni videocamera verificate correttamente.",
|
||||
"verify_camera_error": "Si è verificato un problema durante la verifica delle impostazioni della videocamera",
|
||||
"verify_onvif": "Controlla le impostazioni ONVIF.",
|
||||
"verify_onvif_success": "Impostazioni ONVIF verificate correttamente.",
|
||||
"verify_onvif_error": "Si è verificato un problema durante la verifica delle impostazioni ONVIF"
|
||||
},
|
||||
"overview": {
|
||||
"general": "Generale",
|
||||
"description_general": "Impostazioni generali del Kerberos Agent",
|
||||
"key": "Chiave",
|
||||
"camera_name": "Nome videocamera",
|
||||
"timezone": "Fuso orario",
|
||||
"select_timezone": "Seleziona un fuso orario",
|
||||
"advanced_configuration": "Configurazione avanzata",
|
||||
"description_advanced_configuration": "Opzioni di configurazione dettagliate per abilitare o disabilitare parti specifiche del Kerberos Agent",
|
||||
"offline_mode": "Modalità offline",
|
||||
"description_offline_mode": "Disabilita traffico in uscita"
|
||||
},
|
||||
"camera": {
|
||||
"camera": "Videocamera",
|
||||
"description_camera": "Le impostazioni della fotocamera sono necessarie per stabilire una connessione con la videocamera scelta.",
|
||||
"only_h264": "Al momento sono supportati solo streams RTSP H264.",
|
||||
"rtsp_url": "Url RTSP",
|
||||
"rtsp_h264": "Connessione RTSP H264 alla videocamera.",
|
||||
"sub_rtsp_url": "Sub-url RTSP (per lo streaming in diretta)",
|
||||
"sub_rtsp_h264": "URL RTSP supplementare della videocamera con risoluzione inferiore per lo streaming in diretta.",
|
||||
"onvif": "ONVIF",
|
||||
"description_onvif": "Credenziali per interagire con le funzionalità ONVIF come PTZ o altre funzioni fornite dalla videocamera.",
|
||||
"onvif_xaddr": "ONVIF xaddr",
|
||||
"onvif_username": "ONVIF username",
|
||||
"onvif_password": "ONVIF password",
|
||||
"verify_connection": "Verifica connessione",
|
||||
"verify_sub_connection": "Verifica sub-connessione"
|
||||
},
|
||||
"recording": {
|
||||
"recording": "Registrazione",
|
||||
"description_recording": "Specificare se effettuare le registrazioni con un'impostazione continua 24/7 oppure basata sulla rilevazione di movimento.",
|
||||
"continuous_recording": "Registrazione continua",
|
||||
"description_continuous_recording": "Effettuare registrazioni 24/7 o basate sul movimento.",
|
||||
"max_duration": "massima durata video (in secondi)",
|
||||
"description_max_duration": "Durata massima della registrazione.",
|
||||
"pre_recording": "pre registrazione (buffering dei key frames)",
|
||||
"description_pre_recording": "Secondi prima del verificarsi di un evento.",
|
||||
"post_recording": "post registrazione (in)",
|
||||
"description_post_recording": "Secondi dopo il verificarsi di un evento.",
|
||||
"threshold": "Soglia di registrazione (in pixel)",
|
||||
"description_threshold": "Numero di pixel modificati per avviare la registrazione",
|
||||
"autoclean": "Cancellazione automatica",
|
||||
"description_autoclean": "Specificare se l'Agente Kerberos può cancellare le registrazioni quando viene raggiunta una specifica capacità di archiviazione (in MB). Questo rimuoverà le registrazioni più vecchie quando la capacità viene raggiunta.",
|
||||
"autoclean_enable": "Abilita cancellazione automatica",
|
||||
"autoclean_description_enable": "Rimuovere la registrazione più vecchia al raggiungimento della capacità.",
|
||||
"autoclean_max_directory_size": "Dimensione massima della cartella (in MB)",
|
||||
"autoclean_description_max_directory_size": "Dimensione massima in MB delle registrazioni salvate.",
|
||||
"fragmentedrecordings": "Registrazioni frammentate",
|
||||
"description_fragmentedrecordings": "Quando le registrazioni sono frammentate, sono adatte ad uno stream HLS. Se attivato, il contenitore MP4 avrà un aspetto leggermente diverso.",
|
||||
"fragmentedrecordings_enable": "Abilita frammentazione",
|
||||
"fragmentedrecordings_description_enable": "Per utilizzare gli stream HLS sono necessarie registrazioni frammentate.",
|
||||
"fragmentedrecordings_duration": "durata frammento",
|
||||
"fragmentedrecordings_description_duration": "Durata del singolo frammento."
|
||||
},
|
||||
"streaming": {
|
||||
"stun_turn": "STUN/TURN per WebRTC",
|
||||
"description_stun_turn": "Per lo streaming in diretta a massima risoluzione viene impiegato WebRTC. Una delle sue funzionalità chiave è la ICE-candidate, che consente di attraversare il NAT utilizzando i concetti di STUN/TURN.",
|
||||
"stun_server": "STUN server",
|
||||
"turn_server": "TURN server",
|
||||
"turn_username": "Username",
|
||||
"turn_password": "Password",
|
||||
"stun_turn_forward": "Inoltro e transcodifica",
|
||||
"stun_turn_description_forward": "Ottimizzazioni e miglioramenti per la comunicazione TURN/STUN.",
|
||||
"stun_turn_webrtc": "Inoltro al broker WebRTC",
|
||||
"stun_turn_description_webrtc": "Inoltro dello stream h264 via MQTT",
|
||||
"stun_turn_transcode": "Transcodifica stream",
|
||||
"stun_turn_description_transcode": "Conversione dello stream in una risoluzione inferiore",
|
||||
"stun_turn_downscale": "Riduzione della risoluzione (in % o risoluzione originale)",
|
||||
"mqtt": "MQTT",
|
||||
"description_mqtt": "Un broker MQTT è usato per comunicare da",
|
||||
"description2_mqtt": "al Kerberos Agent, per ottenere, ad esempio, funzionalità di livestreaming o ONVIF (PTZ).",
|
||||
"mqtt_brokeruri": "Uri Broker",
|
||||
"mqtt_username": "Username",
|
||||
"mqtt_password": "Password"
|
||||
},
|
||||
"conditions": {
|
||||
"timeofinterest": "Periodo di interesse",
|
||||
"description_timeofinterest": "Effettua registrazioni solamente all'interno di specifici intervalli orari (basato sul fuso orario).",
|
||||
"timeofinterest_enabled": "Abilitato",
|
||||
"timeofinterest_description_enabled": "Se abilitato, è possibile specificare una finestra temporale",
|
||||
"sunday": "Domenica",
|
||||
"monday": "Lunedì",
|
||||
"tuesday": "Martedì",
|
||||
"wednesday": "Mercoledì",
|
||||
"thursday": "Giovedì",
|
||||
"friday": "Venerdì",
|
||||
"saturday": "Sabato",
|
||||
"externalcondition": "Condizione esterna",
|
||||
"description_externalcondition": "È possibile attivare o disattivare la dipendenza da un servizio esterno di registrazione.",
|
||||
"regionofinterest": "Regione di interesse",
|
||||
"description_regionofinterest": "Definendo una o più regioni, il movimento verrà tracciato solo al loro interno."
|
||||
},
|
||||
"persistence": {
|
||||
"kerberoshub": "Kerberos Hub",
|
||||
"description_kerberoshub": "Kerberos Agents can send heartbeats to a central",
|
||||
"description2_kerberoshub": "installation. Heartbeats and other relevant information are synced to Kerberos Hub to show realtime information about your video landscape.",
|
||||
"persistence": "Persistenza",
|
||||
"saasoffering": "Kerberos Hub (soluzione SAAS)",
|
||||
"description_persistence": "La possibilità di poter salvare le tue registrazioni rappresenta l'inizio di tutto. Puoi scegliere tra il nostro",
|
||||
"description2_persistence": ", oppure un provider di terze parti",
|
||||
"select_persistence": "Seleziona una persistenza",
|
||||
"kerberoshub_proxyurl": "URL Proxy Kerberos Hub",
|
||||
"kerberoshub_description_proxyurl": "Endpoint del Proxy per l'upload delle registrazioni.",
|
||||
"kerberoshub_apiurl": "API URL Kerberos Hub",
|
||||
"kerberoshub_description_apiurl": "Endpoint API per l'upload delle registrazioni.",
|
||||
"kerberoshub_publickey": "Chiave pubblica",
|
||||
"kerberoshub_description_publickey": "Chiave pubblica dell'account Kerberos Hub.",
|
||||
"kerberoshub_privatekey": "Chiave privata",
|
||||
"kerberoshub_description_privatekey": "Chiave privata dell'account Kerberos Hub.",
|
||||
"kerberoshub_site": "Sito",
|
||||
"kerberoshub_description_site": "ID del sito a cui appartengono i Kerberos Agents in Kerberos Hub.",
|
||||
"kerberoshub_region": "Regione",
|
||||
"kerberoshub_description_region": "La regione in cui memorizziamo le registrazioni.",
|
||||
"kerberoshub_bucket": "Bucket",
|
||||
"kerberoshub_description_bucket": "Bucket in cui memorizziamo le registrazioni.",
|
||||
"kerberoshub_username": "Username/Cartella (dovrebbe essere uguale allo username di Kerberos Hub)",
|
||||
"kerberoshub_description_username": "Username del tuo account Kerberos Hub.",
|
||||
"kerberosvault_apiurl": "API URL Kerberos Vault",
|
||||
"kerberosvault_description_apiurl": "API di Kerberos Vault",
|
||||
"kerberosvault_provider": "Provider",
|
||||
"kerberosvault_description_provider": "Provider al quale saranno inviate le registrazioni.",
|
||||
"kerberosvault_directory": "Cartella (dovrebbe essere uguale allo username di Kerberos Hub)",
|
||||
"kerberosvault_description_directory": "Sotto cartella in cui saranno memorizzate le tue registrazioni nel provider.",
|
||||
"kerberosvault_accesskey": "Access key",
|
||||
"kerberosvault_description_accesskey": "Access key del tuo account Kerberos Vault.",
|
||||
"kerberosvault_secretkey": "Secret key",
|
||||
"kerberosvault_description_secretkey": "Secret key del tuo account Kerberos Vault.",
|
||||
"dropbox_directory": "Cartella",
|
||||
"dropbox_description_directory": "Sottcartella dell'account Dropbox in cui saranno salvate le registrazioni.",
|
||||
"dropbox_accesstoken": "Access token",
|
||||
"dropbox_description_accesstoken": "Access token del tuo account/app Dropbox.",
|
||||
"verify_connection": "Verifica connessione",
|
||||
"remove_after_upload": "Una volta che le registrazioni sono state caricate su una certa persistenza, si potrebbe volerle rimuovere dal Kerberos Agent locale.",
|
||||
"remove_after_upload_description": "Cancella le registrazioni dopo che sono state caricate correttamente.",
|
||||
"remove_after_upload_enabled": "Abilita cancellazione al caricamento"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ const LanguageSelect = () => {
|
||||
fr: { label: 'Francais', dir: 'ltr', active: false },
|
||||
pl: { label: 'Polski', dir: 'ltr', active: false },
|
||||
de: { label: 'Deutsch', dir: 'ltr', active: false },
|
||||
it: { label: 'Italiano', dir: 'ltr', active: false },
|
||||
pt: { label: 'Português', dir: 'ltr', active: false },
|
||||
es: { label: 'Español', dir: 'ltr', active: false },
|
||||
ja: { label: '日本', dir: 'rlt', active: false },
|
||||
|
||||
@@ -729,7 +729,7 @@ class Settings extends React.Component {
|
||||
/>
|
||||
)}
|
||||
{verifyOnvifError && (
|
||||
<InfoBar type="alert" message={`${verifyOnvifErrorMessage}`} />
|
||||
<InfoBar type="alert" message={verifyOnvifErrorMessage} />
|
||||
)}
|
||||
|
||||
{loadingHub && (
|
||||
|
||||
Reference in New Issue
Block a user