mirror of
https://github.com/kerberos-io/agent.git
synced 2026-03-03 12:50:12 +00:00
Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54bc1989f9 | ||
|
|
94b71a0868 | ||
|
|
c071057eec | ||
|
|
e8a355d992 | ||
|
|
ca84664071 | ||
|
|
dd7fcb31b1 | ||
|
|
324fffde6b | ||
|
|
cd8347d20f | ||
|
|
efcbf52b06 | ||
|
|
c33469a7b3 | ||
|
|
3717535f0b | ||
|
|
8eb2de5e28 | ||
|
|
96f6bcb1dd | ||
|
|
860077a3eb | ||
|
|
8be9343314 | ||
|
|
dac04fbb57 | ||
|
|
b9acf4c150 | ||
|
|
6608018f86 | ||
|
|
552f5dbea6 | ||
|
|
2844a5a419 | ||
|
|
c4b9610f58 | ||
|
|
6a44498730 | ||
|
|
a2cebaf90b | ||
|
|
3f58f26dfd | ||
|
|
a8d5f56f1e | ||
|
|
1eb62d80c7 | ||
|
|
e474a62dbc | ||
|
|
f29b952001 | ||
|
|
38247ac9f6 | ||
|
|
580f17028a | ||
|
|
48d933a561 | ||
|
|
0c70ab6158 | ||
|
|
839185dac8 | ||
|
|
ba6cdef9d5 | ||
|
|
bedb3c0d7f | ||
|
|
2539255940 | ||
|
|
24136f8b15 | ||
|
|
910bb3c079 | ||
|
|
47f4c19617 | ||
|
|
280a81809a | ||
|
|
59358acb30 | ||
|
|
ebd655ac73 | ||
|
|
6325e37aae | ||
|
|
ecabc47847 | ||
|
|
31cc3d8939 | ||
|
|
c71cb71d08 | ||
|
|
65a739ea75 | ||
|
|
410a62e9ef | ||
|
|
aa76dd1ec8 | ||
|
|
384448d123 | ||
|
|
414f74758c | ||
|
|
25403ccdab | ||
|
|
4c03132b83 | ||
|
|
470f8f1cb6 | ||
|
|
5308376a67 | ||
|
|
2b112d29cf | ||
|
|
20d2517e74 | ||
|
|
12902e2482 | ||
|
|
baca44beef | ||
|
|
d7580744e2 | ||
|
|
04f4bc9bf2 |
18
.github/workflows/docker.yml
vendored
18
.github/workflows/docker.yml
vendored
@@ -55,13 +55,17 @@ jobs:
|
||||
name: ${{ steps.short-sha.outputs.sha }}
|
||||
tag: ${{ steps.short-sha.outputs.sha }}
|
||||
artifacts: "agent-${{matrix.architecture}}.tar"
|
||||
# You can now run snapcraft shell commands
|
||||
- name: Install Snapcraft
|
||||
uses: samuelmeuli/action-snapcraft@v2
|
||||
- name: add user to lxd group
|
||||
run: sudo usermod -a -G lxd $USER
|
||||
- name: Use Snapcraft
|
||||
run: tar -xf agent-${{matrix.architecture}}.tar && snapcraft
|
||||
# Taken from GoReleaser's own release workflow.
|
||||
# The available Snapcraft Action has some bugs described in the issue below.
|
||||
# The mkdirs are a hack for https://github.com/goreleaser/goreleaser/issues/1715.
|
||||
#- name: Setup Snapcraft
|
||||
# run: |
|
||||
# sudo apt-get update
|
||||
# sudo apt-get -yq --no-install-suggests --no-install-recommends install snapcraft
|
||||
# mkdir -p $HOME/.cache/snapcraft/download
|
||||
# mkdir -p $HOME/.cache/snapcraft/stage-packages
|
||||
#- name: Use Snapcraft
|
||||
# run: tar -xf agent-${{matrix.architecture}}.tar && snapcraft
|
||||
build-other:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
|
||||
@@ -10,7 +10,7 @@ ENV GOSUMDB=off
|
||||
##########################################
|
||||
# Installing some additional dependencies.
|
||||
|
||||
RUN apt-get upgrade -y && apt-get update && apt-get install -y --no-install-recommends \
|
||||
RUN apt-get upgrade -y && apt-get update && apt-get install -y --fix-missing --no-install-recommends \
|
||||
git build-essential cmake pkg-config unzip libgtk2.0-dev \
|
||||
curl ca-certificates libcurl4-openssl-dev libssl-dev libjpeg62-turbo-dev && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
36
README.md
36
README.md
@@ -18,6 +18,7 @@
|
||||
[](https://brianmacdonald.github.io/Ethonate/address#0xf4a759C9436E2280Ea9cdd23d3144D95538fF4bE)
|
||||
<a target="_blank" href="https://twitter.com/kerberosio?ref_src=twsrc%5Etfw"><img src="https://img.shields.io/twitter/url.svg?label=Follow%20%40kerberosio&style=social&url=https%3A%2F%2Ftwitter.com%2Fkerberosio" alt="Twitter Widget"></a>
|
||||
[](https://discord.gg/Bj77Vqfp2G)
|
||||
[](https://snapcraft.io/kerberosio)
|
||||
|
||||
[**Docker Hub**](https://hub.docker.com/r/kerberos/agent) | [**Documentation**](https://doc.kerberos.io) | [**Website**](https://kerberos.io) | [**View Demo**](https://demo.kerberos.io)
|
||||
|
||||
@@ -28,7 +29,7 @@ Kerberos Agent is an isolated and scalable video (surveillance) management agent
|
||||
## :thinking: Prerequisites
|
||||
|
||||
- An IP camera which supports a RTSP H264 encoded stream,
|
||||
- (or) a USB camera, Raspberry Pi camera or other camera, that [you can tranform to a valid RTSP H264 stream](https://github.com/kerberos-io/camera-to-rtsp).
|
||||
- (or) a USB camera, Raspberry Pi camera or other camera, that [you can transform to a valid RTSP H264 stream](https://github.com/kerberos-io/camera-to-rtsp).
|
||||
- Any hardware (ARMv6, ARMv7, ARM64, AMD) that can run a binary or container, for example: a Raspberry Pi, NVidia Jetson, Intel NUC, a VM, Bare metal machine or a full blown Kubernetes cluster.
|
||||
|
||||
## :video_camera: Is my camera working?
|
||||
@@ -41,6 +42,7 @@ There are a myriad of cameras out there (USB, IP and other cameras), and it migh
|
||||
|
||||
1. [Quickstart - Docker](#quickstart---docker)
|
||||
2. [Quickstart - Balena](#quickstart---balena)
|
||||
3. [Quickstart - Snap](#quickstart---snap)
|
||||
|
||||
### Introduction
|
||||
|
||||
@@ -82,6 +84,16 @@ Run Kerberos Agent with [Balena Cloud](https://www.balena.io/) super powers. Mon
|
||||
|
||||
[](https://dashboard.balena-cloud.com/deploy?repoUrl=https://github.com/kerberos-io/balena-agent)
|
||||
|
||||
## Quickstart - Snap
|
||||
|
||||
Run Kerberos Agent with our [Snapcraft package](https://snapcraft.io/kerberosio).
|
||||
|
||||
snap install kerberosio
|
||||
|
||||
Once installed you can find your Kerberos Agent configration at `/var/snap/kerberosio/common`. Run the Kerberos Agent as following
|
||||
|
||||
sudo kerberosio.agent -action=run -port=80
|
||||
|
||||
## A world of Kerberos Agents
|
||||
|
||||
The Kerberos Agent is an isolated and scalable video (surveillance) management agent with a strong focus on user experience, scalability, resilience, extension and integration. Next to the Kerberos Agent, Kerberos.io provides many other tools such as [Kerberos Factory](https://github.com/kerberos-io/factory), [Kerberos Vault](https://github.com/kerberos-io/vault) and [Kerberos Hub](https://github.com/kerberos-io/hub) to provide additional capabilities: bring your own cloud, bring your own storage, central overview, live streaming, machine learning etc.
|
||||
@@ -97,8 +109,10 @@ This repository contains everything you'll need to know about our core product,
|
||||
- Single camera per instance (e.g. one container per camera).
|
||||
- Primary and secondary stream setup (record full-res, stream low-res).
|
||||
- Low resolution streaming through MQTT and full resolution streaming through WebRTC.
|
||||
- End-to-end encryption through MQTT using RSA and AES.
|
||||
- Ability to specifiy conditions: offline mode, motion region, time table, continuous recording, etc.
|
||||
- Post- and pre-recording on motion detection.
|
||||
- Encryption at rest using AES-256-CBC.
|
||||
- Ability to create fragmented recordings, and streaming though HLS fMP4.
|
||||
- [Deploy where you want](#how-to-run-and-deploy-a-kerberos-agent) with the tools you use: `docker`, `docker compose`, `ansible`, `terraform`, `kubernetes`, etc.
|
||||
- Cloud storage/persistance: Kerberos Hub, Kerberos Vault and Dropbox. [(WIP: Minio, Storj, Google Drive, FTP etc.)](https://github.com/kerberos-io/agent/issues/95)
|
||||
@@ -120,6 +134,7 @@ We have documented the different deployment models [in the `deployments` directo
|
||||
- [Terraform](https://github.com/kerberos-io/agent/tree/master/deployments#5-terraform)
|
||||
- [Salt](https://github.com/kerberos-io/agent/tree/master/deployments#6-salt)
|
||||
- [Balena](https://github.com/kerberos-io/agent/tree/master/deployments#8-balena)
|
||||
- [Snap](https://github.com/kerberos-io/agent/tree/master/deployments#9-snap)
|
||||
|
||||
By default your Kerberos Agents will store all its configuration and recordings inside the container. To help you automate and have a more consistent data governance, you can attach volumes to configure and persist data of your Kerberos Agents, and/or configure each Kerberos Agent through environment variables.
|
||||
|
||||
@@ -134,6 +149,20 @@ The default username and password for the Kerberos Agent is:
|
||||
|
||||
**_Please note that you change the username and password for a final installation, see [Configure with environment variables](#configure-with-environment-variables) below._**
|
||||
|
||||
## Encryption
|
||||
|
||||
You can encrypt your recordings and outgoing MQTT messages with your own AES and RSA keys by enabling the encryption settings. Once enabled all your recordings will be encrypted using AES-256-CBC and your symmetric key. You can either use the default `openssl` toolchain to decrypt the recordings with your AES key, as following:
|
||||
|
||||
openssl aes-256-cbc -d -md md5 -in encrypted.mp4 -out decrypted.mp4 -k your-key-96ab185xxxxxxxcxxxxxxxx6a59c62e8
|
||||
|
||||
, and additionally you can decrypt a folder of recordings, using the Kerberos Agent binary as following:
|
||||
|
||||
go run main.go -action decrypt ./data/recordings your-key-96ab185xxxxxxxcxxxxxxxx6a59c62e8
|
||||
|
||||
or for a single file:
|
||||
|
||||
go run main.go -action decrypt ./data/recordings/video.mp4 your-key-96ab185xxxxxxxcxxxxxxxx6a59c62e8
|
||||
|
||||
## Configure and persist with volume mounts
|
||||
|
||||
An example of how to mount a host directory is shown below using `docker`, but is applicable for [all the deployment models and tools described above](#running-and-automating-a-kerberos-agent).
|
||||
@@ -214,6 +243,11 @@ 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 for MQTT messages. | "false" |
|
||||
| `AGENT_ENCRYPTION_RECORDINGS` | Enable 'true' or disable 'false' end-to-end encryption for recordings. | "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
|
||||
|
||||
|
||||
@@ -54,7 +54,9 @@ All of the previously deployments, `docker`, `kubernetes` and `openshift` are gr
|
||||
|
||||
## 6. Terraform
|
||||
|
||||
To be written
|
||||
Terraform is a tool for infrastructure provisioning to build infrastructure through code, often called Infrastructure as Code. So, Terraform allows you to automate and manage your infrastructure, your platform, and the services that run on that platform. By using Terraform you can deploy your Kerberos Agents remotely at scale.
|
||||
|
||||
> Learn more [about Kerberos Agent with Terraform](https://github.com/kerberos-io/agent/tree/master/deployments/terraform).
|
||||
|
||||
## 7. Salt
|
||||
|
||||
@@ -67,3 +69,11 @@ Balena Cloud provide a seamless way of building and deploying applications at sc
|
||||
Together with the Balena.io team we've build a Balena App, called [`video-surveillance`](https://hub.balena.io/apps/2064752/video-surveillance), which any can use to deploy a video surveillance system in a matter of minutes with all the expected management features you can think of.
|
||||
|
||||
> Learn more [about Kerberos Agent with Balena](https://github.com/kerberos-io/agent/tree/master/deployments/balena).
|
||||
|
||||
## 9. Snap
|
||||
|
||||
The Snap Store, also known as the Ubuntu Store , is a commercial centralized software store operated by Canonical. Similar to AppImage or Flatpak the Snap Store is able to provide up to date software no matter what version of Linux you are running and how old your libraries are.
|
||||
|
||||
We have published our own snap `Kerberos Agent` on the Snap Store, allowing you to seamless install a Kerberos Agent on your Linux devive.
|
||||
|
||||
> Learn more [about Kerberos Agent with Snap](https://github.com/kerberos-io/agent/tree/master/deployments/snap).
|
||||
|
||||
15
deployments/snap/README.md
Normal file
15
deployments/snap/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Deployment with Snap Store
|
||||
|
||||
By browsing to the Snap Store, you'll be able [to find our own snap `Kerberos Agent`](https://snapcraft.io/kerberosio). You can either install the `Kerberos Agent` through the command line.
|
||||
|
||||
snap install kerberosio
|
||||
|
||||
Or use the Desktop client to have a visual interface.
|
||||
|
||||

|
||||
|
||||
Once installed you can find your Kerberos Agent configration at `/var/snap/kerberosio/common`. Run the Kerberos Agent as following.
|
||||
|
||||
sudo kerberosio.agent -action=run -port=80
|
||||
|
||||
If successfull you'll be able to browse to port `80` or if you defined a different port. This will open the Kerberos Agent interface.
|
||||
BIN
deployments/snap/snapstore.png
Normal file
BIN
deployments/snap/snapstore.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 616 KiB |
41
deployments/terraform/README.md
Normal file
41
deployments/terraform/README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Deployment with Terraform
|
||||
|
||||
If you are using Terraform as part of your DevOps stack, you might utilise it to deploy your Kerberos Agents. Within this deployment folder we have added an example Terraform file `docker.tf`, which installs the Kerberos Agent `docker` container on a remote system over `SSH`. We might create our own provider in the future, or add additional examples for example `snap`, `kubernetes`, etc.
|
||||
|
||||
For this example we will install Kerberos Agent using `docker` on a remote `linux` machine. Therefore we'll make sure we have the `TelkomIndonesia/linux` provider initialised.
|
||||
|
||||
terraform init
|
||||
|
||||
Once initialised you should see similar output:
|
||||
|
||||
Initializing the backend...
|
||||
|
||||
Initializing provider plugins...
|
||||
- Reusing previous version of telkomindonesia/linux from the dependency lock file
|
||||
- Using previously-installed telkomindonesia/linux v0.7.0
|
||||
|
||||
Go and open the `docker.tf` file and locate the `linux` provider, modify following credentials accordingly. Make sure they match for creating an `SSH` connection.
|
||||
|
||||
provider "linux" {
|
||||
host = "x.y.z.u"
|
||||
port = 22
|
||||
user = "root"
|
||||
password = "password"
|
||||
}
|
||||
|
||||
Apply the `docker.tf` file, to install `docker` and the `kerberos/agent` docker container.
|
||||
|
||||
terraform apply
|
||||
|
||||
Once done you should see following output, and you should be able to reach the remote machine on port `80` or if configured differently the specified port you've defined.
|
||||
|
||||
Do you want to perform these actions?
|
||||
Terraform will perform the actions described above.
|
||||
Only 'yes' will be accepted to approve.
|
||||
|
||||
Enter a value: yes
|
||||
|
||||
linux_script.install_docker_kerberos_agent: Modifying... [id=a56cf7b0-db66-4f9b-beec-8a4dcef2a0c7]
|
||||
linux_script.install_docker_kerberos_agent: Modifications complete after 3s [id=a56cf7b0-db66-4f9b-beec-8a4dcef2a0c7]
|
||||
|
||||
Apply complete! Resources: 0 added, 1 changed, 0 destroyed.
|
||||
47
deployments/terraform/docker.tf
Normal file
47
deployments/terraform/docker.tf
Normal file
@@ -0,0 +1,47 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
linux = {
|
||||
source = "TelkomIndonesia/linux"
|
||||
version = "0.7.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "linux" {
|
||||
host = "x.y.z.u"
|
||||
port = 22
|
||||
user = "root"
|
||||
password = "password"
|
||||
}
|
||||
|
||||
locals {
|
||||
image = "kerberos/agent"
|
||||
version = "latest"
|
||||
port = 80
|
||||
}
|
||||
|
||||
resource "linux_script" "install_docker" {
|
||||
lifecycle_commands {
|
||||
create = "apt update && apt install -y $PACKAGE_NAME"
|
||||
read = "apt-cache policy $PACKAGE_NAME | grep 'Installed:' | grep -v '(none)' | awk '{ print $2 }' | xargs | tr -d '\n'"
|
||||
update = "apt update && apt install -y $PACKAGE_NAME"
|
||||
delete = "apt remove -y $PACKAGE_NAME"
|
||||
}
|
||||
environment = {
|
||||
PACKAGE_NAME = "docker"
|
||||
}
|
||||
}
|
||||
|
||||
resource "linux_script" "install_docker_kerberos_agent" {
|
||||
lifecycle_commands {
|
||||
create = "docker pull $IMAGE:$VERSION && docker run -d -p $PORT:80 --name agent $IMAGE:$VERSION"
|
||||
read = "docker inspect agent"
|
||||
update = "docker pull $IMAGE:$VERSION && docker rm agent --force && docker run -d -p $PORT:80 --name agent $IMAGE:$VERSION"
|
||||
delete = "docker rm agent --force"
|
||||
}
|
||||
environment = {
|
||||
IMAGE = local.image
|
||||
VERSION = local.version
|
||||
PORT = local.port
|
||||
}
|
||||
}
|
||||
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.
|
||||
|
||||
@@ -2,8 +2,9 @@ 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/joy4 v1.0.63 => ../../../../github.com/kerberos-io/joy4
|
||||
|
||||
// replace github.com/kerberos-io/onvif v0.0.6 => ../../../../github.com/kerberos-io/onvif
|
||||
|
||||
require (
|
||||
github.com/InVisionApp/conjungo v1.1.0
|
||||
@@ -20,12 +21,13 @@ require (
|
||||
github.com/gin-contrib/pprof v1.4.0
|
||||
github.com/gin-gonic/contrib v0.0.0-20221130124618-7e01895a63f2
|
||||
github.com/gin-gonic/gin v1.8.2
|
||||
github.com/gofrs/uuid v3.2.0+incompatible
|
||||
github.com/golang-jwt/jwt/v4 v4.4.3
|
||||
github.com/golang-module/carbon/v2 v2.2.3
|
||||
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/joy4 v1.0.64
|
||||
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
|
||||
@@ -36,6 +38,7 @@ require (
|
||||
github.com/swaggo/gin-swagger v1.5.3
|
||||
github.com/swaggo/swag v1.8.9
|
||||
github.com/tevino/abool v1.2.0
|
||||
github.com/zaf/g711 v0.0.0-20220109202201-cf0017bf0359
|
||||
go.mongodb.org/mongo-driver v1.7.5
|
||||
gopkg.in/DataDog/dd-trace-go.v1 v1.46.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||
@@ -72,7 +75,6 @@ require (
|
||||
github.com/go-playground/validator/v10 v10.11.1 // indirect
|
||||
github.com/go-stack/stack v1.8.0 // indirect
|
||||
github.com/goccy/go-json v0.10.0 // indirect
|
||||
github.com/gofrs/uuid v3.2.0+incompatible // indirect
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
|
||||
@@ -264,10 +264,10 @@ github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/kellydunn/golang-geo v0.7.0 h1:A5j0/BvNgGwY6Yb6inXQxzYwlPHc6WVZR+MrarZYNNg=
|
||||
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/joy4 v1.0.64 h1:gTUSotHSOhp9mNqEecgq88tQHvpj7TjmrvPUsPm0idg=
|
||||
github.com/kerberos-io/joy4 v1.0.64/go.mod h1:nZp4AjvKvTOXRrmDyAIOw+Da+JA5OcSo/JundGfOlFU=
|
||||
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=
|
||||
@@ -471,6 +471,8 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/zaf/g711 v0.0.0-20220109202201-cf0017bf0359 h1:P9yeMx2iNJxJqXEwLtMjSwWcD2a0AlFmFByeosMZhLM=
|
||||
github.com/zaf/g711 v0.0.0-20220109202201-cf0017bf0359/go.mod h1:ySLGJD8AQluMQuu5JDvfJrwsBra+8iX1jFsKS8KfB2I=
|
||||
github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs=
|
||||
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
|
||||
go.mongodb.org/mongo-driver v1.7.5 h1:ny3p0reEpgsR2cfA5cjgwFZg3Cv/ofFh/8jbhGtz9VI=
|
||||
|
||||
@@ -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"
|
||||
@@ -76,6 +78,21 @@ func main() {
|
||||
case "discover":
|
||||
log.Log.Info(timeout)
|
||||
|
||||
case "decrypt":
|
||||
log.Log.Info("Decrypting: " + flag.Arg(0) + " with key: " + flag.Arg(1))
|
||||
symmetricKey := []byte(flag.Arg(1))
|
||||
|
||||
if symmetricKey == nil || len(symmetricKey) == 0 {
|
||||
log.Log.Fatal("Main: symmetric key should not be empty")
|
||||
return
|
||||
}
|
||||
if len(symmetricKey) != 32 {
|
||||
log.Log.Fatal("Main: symmetric key should be 32 bytes")
|
||||
return
|
||||
}
|
||||
|
||||
utils.Decrypt(flag.Arg(0), symmetricKey)
|
||||
|
||||
case "run":
|
||||
{
|
||||
// Print Kerberos.io ASCII art
|
||||
@@ -92,10 +109,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 +130,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
|
||||
@@ -16,12 +16,27 @@ import (
|
||||
"github.com/kerberos-io/joy4/format"
|
||||
)
|
||||
|
||||
func OpenRTSP(ctx context.Context, url string) (av.DemuxCloser, []av.CodecData, error) {
|
||||
func OpenRTSP(ctx context.Context, url string, withBackChannel bool) (av.DemuxCloser, []av.CodecData, error) {
|
||||
format.RegisterAll()
|
||||
infile, err := avutil.Open(ctx, url)
|
||||
|
||||
// Try with backchannel first (if variable is set to true)
|
||||
// If set to true, it will try to open the stream with a backchannel
|
||||
// If fails we will try again (see below).
|
||||
infile, err := avutil.Open(ctx, url, withBackChannel)
|
||||
if err == nil {
|
||||
streams, errstreams := infile.Streams()
|
||||
return infile, streams, errstreams
|
||||
if len(streams) > 0 {
|
||||
return infile, streams, errstreams
|
||||
} else {
|
||||
// Try again without backchannel
|
||||
log.Log.Info("OpenRTSP: trying without backchannel")
|
||||
withBackChannel = false
|
||||
infile, err := avutil.Open(ctx, url, withBackChannel)
|
||||
if err == nil {
|
||||
streams, errstreams := infile.Streams()
|
||||
return infile, streams, errstreams
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, []av.CodecData{}, err
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"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/utils"
|
||||
@@ -405,6 +406,27 @@ func HandleRecordStream(queue *pubsub.Queue, configDirectory string, configurati
|
||||
utils.CreateFragmentedMP4(fullName, config.Capture.FragmentedDuration)
|
||||
}
|
||||
|
||||
// Check if we need to encrypt the recording.
|
||||
if config.Encryption != nil && config.Encryption.Enabled == "true" && config.Encryption.Recordings == "true" && config.Encryption.SymmetricKey != "" {
|
||||
// reopen file into memory 'fullName'
|
||||
contents, err := os.ReadFile(fullName)
|
||||
if err == nil {
|
||||
// encrypt
|
||||
encryptedContents, err := encryption.AesEncrypt(contents, config.Encryption.SymmetricKey)
|
||||
if err == nil {
|
||||
// write back to file
|
||||
err := os.WriteFile(fullName, []byte(encryptedContents), 0644)
|
||||
if err != nil {
|
||||
log.Log.Error("HandleRecordStream: error writing file: " + err.Error())
|
||||
}
|
||||
} else {
|
||||
log.Log.Error("HandleRecordStream: error encrypting file: " + err.Error())
|
||||
}
|
||||
} else {
|
||||
log.Log.Error("HandleRecordStream: error reading file: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Create a symbol linc.
|
||||
fc, _ := os.Create(configDirectory + "/data/cloud/" + name)
|
||||
fc.Close()
|
||||
@@ -447,7 +469,7 @@ func VerifyCamera(c *gin.Context) {
|
||||
if streamType == "secondary" {
|
||||
rtspUrl = cameraStreams.SubRTSP
|
||||
}
|
||||
_, codecs, err := OpenRTSP(ctx, rtspUrl)
|
||||
_, codecs, err := OpenRTSP(ctx, rtspUrl, true)
|
||||
if err == nil {
|
||||
|
||||
videoIdx := -1
|
||||
|
||||
@@ -121,7 +121,7 @@ func HandleUpload(configDirectory string, configuration *models.Configuration, c
|
||||
|
||||
// Check if we need to remove the original recording
|
||||
// removeAfterUpload is set to false by default
|
||||
if config.RemoveAfterUpload == "true" {
|
||||
if config.RemoveAfterUpload != "false" {
|
||||
err := os.Remove(configDirectory + "/data/recordings/" + fileName)
|
||||
if err != nil {
|
||||
log.Log.Error("HandleUpload: " + err.Error())
|
||||
@@ -231,7 +231,7 @@ loop:
|
||||
log.Log.Debug("HandleHeartBeat: stopping as Offline is enabled.")
|
||||
} else {
|
||||
|
||||
url := config.HeartbeatURI
|
||||
hubURI := config.HeartbeatURI
|
||||
key := ""
|
||||
username := ""
|
||||
vaultURI := ""
|
||||
@@ -247,70 +247,115 @@ loop:
|
||||
|
||||
// This is the new way ;)
|
||||
if config.HubURI != "" {
|
||||
url = config.HubURI + "/devices/heartbeat"
|
||||
hubURI = config.HubURI + "/devices/heartbeat"
|
||||
}
|
||||
if config.HubKey != "" {
|
||||
key = config.HubKey
|
||||
}
|
||||
|
||||
if key != "" {
|
||||
// Check if we have a friendly name or not.
|
||||
name := config.Name
|
||||
if config.FriendlyName != "" {
|
||||
name = config.FriendlyName
|
||||
}
|
||||
// Check if we have a friendly name or not.
|
||||
name := config.Name
|
||||
if config.FriendlyName != "" {
|
||||
name = config.FriendlyName
|
||||
}
|
||||
|
||||
// Get some system information
|
||||
// like the uptime, hostname, memory usage, etc.
|
||||
system, _ := GetSystemInfo()
|
||||
// Get some system information
|
||||
// like the uptime, hostname, memory usage, etc.
|
||||
system, _ := GetSystemInfo()
|
||||
|
||||
// We will formated the uptime to a human readable format
|
||||
// this will be used on Kerberos Hub: Uptime -> 1 day and 2 hours.
|
||||
uptimeFormatted := uptimeStart.Format("2006-01-02 15:04:05")
|
||||
uptimeString := carbon.Parse(uptimeFormatted).DiffForHumans()
|
||||
uptimeString = strings.ReplaceAll(uptimeString, "ago", "")
|
||||
// Check if the agent is running inside a cluster (Kerberos Factory) or as
|
||||
// an open source agent
|
||||
isEnterprise := false
|
||||
if os.Getenv("DEPLOYMENT") == "factory" || os.Getenv("MACHINERY_ENVIRONMENT") == "kubernetes" {
|
||||
isEnterprise = true
|
||||
}
|
||||
|
||||
// Do the same for boottime
|
||||
bootTimeFormatted := time.Unix(int64(system.BootTime), 0).Format("2006-01-02 15:04:05")
|
||||
boottimeString := carbon.Parse(bootTimeFormatted).DiffForHumans()
|
||||
boottimeString = strings.ReplaceAll(boottimeString, "ago", "")
|
||||
// Congert to string
|
||||
macs, _ := json.Marshal(system.MACs)
|
||||
ips, _ := json.Marshal(system.IPs)
|
||||
cameraConnected := "true"
|
||||
if !communication.CameraConnected {
|
||||
cameraConnected = "false"
|
||||
}
|
||||
|
||||
// We'll check which mode is enabled for the camera.
|
||||
onvifEnabled := "false"
|
||||
onvifZoom := "false"
|
||||
onvifPanTilt := "false"
|
||||
if config.Capture.IPCamera.ONVIFXAddr != "" {
|
||||
cameraConfiguration := configuration.Config.Capture.IPCamera
|
||||
device, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
|
||||
hasBackChannel := "false"
|
||||
if communication.HasBackChannel {
|
||||
hasBackChannel = "true"
|
||||
}
|
||||
|
||||
// We will formated the uptime to a human readable format
|
||||
// this will be used on Kerberos Hub: Uptime -> 1 day and 2 hours.
|
||||
uptimeFormatted := uptimeStart.Format("2006-01-02 15:04:05")
|
||||
uptimeString := carbon.Parse(uptimeFormatted).DiffForHumans()
|
||||
uptimeString = strings.ReplaceAll(uptimeString, "ago", "")
|
||||
|
||||
// Do the same for boottime
|
||||
bootTimeFormatted := time.Unix(int64(system.BootTime), 0).Format("2006-01-02 15:04:05")
|
||||
boottimeString := carbon.Parse(bootTimeFormatted).DiffForHumans()
|
||||
boottimeString = strings.ReplaceAll(boottimeString, "ago", "")
|
||||
|
||||
// We'll check which mode is enabled for the camera.
|
||||
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)
|
||||
if err == nil {
|
||||
configurations, err := onvif.GetPTZConfigurationsFromDevice(device)
|
||||
if err == nil {
|
||||
configurations, err := onvif.GetPTZConfigurationsFromDevice(device)
|
||||
if err == nil {
|
||||
onvifEnabled = "true"
|
||||
_, canZoom, canPanTilt := onvif.GetPTZFunctionsFromDevice(configurations)
|
||||
if canZoom {
|
||||
onvifZoom = "true"
|
||||
}
|
||||
if canPanTilt {
|
||||
onvifPanTilt = "true"
|
||||
}
|
||||
onvifEnabled = "true"
|
||||
_, canZoom, canPanTilt := onvif.GetPTZFunctionsFromDevice(configurations)
|
||||
if canZoom {
|
||||
onvifZoom = "true"
|
||||
}
|
||||
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
|
||||
// an open source agent
|
||||
isEnterprise := false
|
||||
if os.Getenv("DEPLOYMENT") == "factory" || os.Getenv("MACHINERY_ENVIRONMENT") == "kubernetes" {
|
||||
isEnterprise = true
|
||||
var client *http.Client
|
||||
if os.Getenv("AGENT_TLS_INSECURE") == "true" {
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
client = &http.Client{Transport: tr}
|
||||
} else {
|
||||
client = &http.Client{}
|
||||
}
|
||||
|
||||
// Congert to string
|
||||
macs, _ := json.Marshal(system.MACs)
|
||||
ips, _ := json.Marshal(system.IPs)
|
||||
cameraConnected := "true"
|
||||
if communication.CameraConnected == false {
|
||||
cameraConnected = "false"
|
||||
}
|
||||
// We need a hub URI and hub public key before we will send a heartbeat
|
||||
if hubURI != "" && key != "" {
|
||||
|
||||
var object = fmt.Sprintf(`{
|
||||
"key" : "%s",
|
||||
@@ -339,30 +384,22 @@ loop:
|
||||
"onvif" : "%s",
|
||||
"onvif_zoom" : "%s",
|
||||
"onvif_pantilt" : "%s",
|
||||
"onvif_presets": "%s",
|
||||
"onvif_presets_list": %s,
|
||||
"cameraConnected": "%s",
|
||||
"hasBackChannel": "%s",
|
||||
"numberoffiles" : "33",
|
||||
"timestamp" : 1564747908,
|
||||
"cameratype" : "IPCamera",
|
||||
"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, hasBackChannel)
|
||||
|
||||
var jsonStr = []byte(object)
|
||||
buffy := bytes.NewBuffer(jsonStr)
|
||||
req, _ := http.NewRequest("POST", url, buffy)
|
||||
req, _ := http.NewRequest("POST", hubURI, buffy)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
var client *http.Client
|
||||
if os.Getenv("AGENT_TLS_INSECURE") == "true" {
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
client = &http.Client{Transport: tr}
|
||||
} else {
|
||||
client = &http.Client{}
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if resp != nil {
|
||||
resp.Body.Close()
|
||||
@@ -376,27 +413,68 @@ loop:
|
||||
}
|
||||
log.Log.Error("HandleHeartBeat: (400) Something went wrong while sending to Kerberos Hub.")
|
||||
}
|
||||
|
||||
// If we have a Kerberos Vault connected, we will also send some analytics
|
||||
// to that service.
|
||||
vaultURI = config.KStorage.URI
|
||||
if vaultURI != "" {
|
||||
buffy = bytes.NewBuffer(jsonStr)
|
||||
req, _ = http.NewRequest("POST", vaultURI+"/devices/heartbeat", buffy)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err = client.Do(req)
|
||||
if resp != nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
if err == nil && resp.StatusCode == 200 {
|
||||
log.Log.Info("HandleHeartBeat: (200) Heartbeat received by Kerberos Vault.")
|
||||
} else {
|
||||
log.Log.Error("HandleHeartBeat: (400) Something went wrong while sending to Kerberos Vault.")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Log.Error("HandleHeartBeat: Disabled as we do not have a public key defined.")
|
||||
}
|
||||
|
||||
// If we have a Kerberos Vault connected, we will also send some analytics
|
||||
// to that service.
|
||||
vaultURI = config.KStorage.URI
|
||||
if vaultURI != "" {
|
||||
|
||||
var object = fmt.Sprintf(`{
|
||||
"key" : "%s",
|
||||
"version" : "3.0.0",
|
||||
"release" : "%s",
|
||||
"cpuid" : "%s",
|
||||
"clouduser" : "%s",
|
||||
"cloudpublickey" : "%s",
|
||||
"cameraname" : "%s",
|
||||
"enterprise" : %t,
|
||||
"hostname" : "%s",
|
||||
"architecture" : "%s",
|
||||
"totalMemory" : "%d",
|
||||
"usedMemory" : "%d",
|
||||
"freeMemory" : "%d",
|
||||
"processMemory" : "%d",
|
||||
"mac_list" : %s,
|
||||
"ip_list" : %s,
|
||||
"board" : "",
|
||||
"disk1size" : "%s",
|
||||
"disk3size" : "%s",
|
||||
"diskvdasize" : "%s",
|
||||
"uptime" : "%s",
|
||||
"boot_time" : "%s",
|
||||
"siteID" : "%s",
|
||||
"onvif" : "%s",
|
||||
"onvif_zoom" : "%s",
|
||||
"onvif_pantilt" : "%s",
|
||||
"onvif_presets": "%s",
|
||||
"onvif_presets_list": %s,
|
||||
"cameraConnected": "%s",
|
||||
"numberoffiles" : "33",
|
||||
"timestamp" : 1564747908,
|
||||
"cameratype" : "IPCamera",
|
||||
"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, onvifPresets, onvifPresetsList, cameraConnected)
|
||||
|
||||
var jsonStr = []byte(object)
|
||||
buffy := bytes.NewBuffer(jsonStr)
|
||||
req, _ := http.NewRequest("POST", vaultURI+"/devices/heartbeat", buffy)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if resp != nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
if err == nil && resp.StatusCode == 200 {
|
||||
log.Log.Info("HandleHeartBeat: (200) Heartbeat received by Kerberos Vault.")
|
||||
} else {
|
||||
log.Log.Error("HandleHeartBeat: (400) Something went wrong while sending to Kerberos Vault.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This will check if we need to stop the thread,
|
||||
@@ -428,19 +506,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 +537,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 +571,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,26 +589,19 @@ 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()
|
||||
_, ok := webrtc.CandidateArrays[key]
|
||||
if !ok {
|
||||
webrtc.CandidateArrays[key] = make(chan string, 30)
|
||||
}
|
||||
webrtc.CandidatesMutex.Unlock()
|
||||
webrtc.InitializeWebRTCConnection(configuration, communication, mqttClient, videoTrack, audioTrack, handshake, webrtc.CandidateArrays[key])
|
||||
go webrtc.InitializeWebRTCConnection(configuration, communication, mqttClient, videoTrack, audioTrack, handshake)
|
||||
|
||||
}
|
||||
}
|
||||
@@ -741,7 +821,7 @@ func VerifyPersistence(c *gin.Context, configDirectory string) {
|
||||
"_6-967003_" + config.Name + "_200-200-400-400_24_769.mp4"
|
||||
|
||||
// Open test-480p.mp4
|
||||
file, err := os.Open(configDirectory + "/test-480p.mp4")
|
||||
file, err := os.Open(configDirectory + "/data/test-480p.mp4")
|
||||
if err != nil {
|
||||
msg := "VerifyPersistence: error reading test-480p.mp4: " + err.Error()
|
||||
log.Log.Error(msg)
|
||||
|
||||
@@ -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
|
||||
|
||||
80
machinery/src/components/Audio.go
Normal file
80
machinery/src/components/Audio.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package components
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/kerberos-io/agent/machinery/src/log"
|
||||
"github.com/kerberos-io/agent/machinery/src/models"
|
||||
"github.com/kerberos-io/joy4/av"
|
||||
"github.com/zaf/g711"
|
||||
)
|
||||
|
||||
func GetBackChannelAudioCodec(streams []av.CodecData, communication *models.Communication) av.AudioCodecData {
|
||||
for _, stream := range streams {
|
||||
if stream.Type().IsAudio() {
|
||||
if stream.Type().String() == "PCM_MULAW" {
|
||||
pcmuCodec := stream.(av.AudioCodecData)
|
||||
if pcmuCodec.IsBackChannel() {
|
||||
communication.HasBackChannel = true
|
||||
return pcmuCodec
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func WriteAudioToBackchannel(infile av.DemuxCloser, streams []av.CodecData, communication *models.Communication) {
|
||||
log.Log.Info("WriteAudioToBackchannel: looking for backchannel audio codec")
|
||||
|
||||
pcmuCodec := GetBackChannelAudioCodec(streams, communication)
|
||||
if pcmuCodec != nil {
|
||||
log.Log.Info("WriteAudioToBackchannel: found backchannel audio codec")
|
||||
|
||||
length := 0
|
||||
channel := pcmuCodec.GetIndex() * 2 // This is the same calculation as Interleaved property in the SDP file.
|
||||
for audio := range communication.HandleAudio {
|
||||
// Encode PCM to MULAW
|
||||
var bufferUlaw []byte
|
||||
for _, v := range audio.Data {
|
||||
b := g711.EncodeUlawFrame(v)
|
||||
bufferUlaw = append(bufferUlaw, b)
|
||||
}
|
||||
infile.Write(bufferUlaw, channel, uint32(length))
|
||||
length = (length + len(bufferUlaw)) % 65536
|
||||
time.Sleep(128 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
log.Log.Info("WriteAudioToBackchannel: finished")
|
||||
|
||||
}
|
||||
|
||||
func WriteFileToBackChannel(infile av.DemuxCloser) {
|
||||
// Do the warmup!
|
||||
file, err := os.Open("./audiofile.bye")
|
||||
if err != nil {
|
||||
fmt.Println("WriteFileToBackChannel: error opening audiofile.bye file")
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Read file into buffer
|
||||
reader := bufio.NewReader(file)
|
||||
buffer := make([]byte, 1024)
|
||||
|
||||
count := 0
|
||||
for {
|
||||
_, err := reader.Read(buffer)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
// Send to backchannel
|
||||
fmt.Println(buffer)
|
||||
infile.Write(buffer, 2, uint32(count))
|
||||
|
||||
count = count + 1024
|
||||
time.Sleep(128 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
@@ -11,9 +11,12 @@ import (
|
||||
mqtt "github.com/eclipse/paho.mqtt.golang"
|
||||
"github.com/kerberos-io/joy4/cgo/ffmpeg"
|
||||
|
||||
//"github.com/youpy/go-wav"
|
||||
|
||||
"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 +75,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 +90,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.
|
||||
@@ -115,9 +118,10 @@ func RunAgent(configDirectory string, configuration *models.Configuration, commu
|
||||
status := "not started"
|
||||
|
||||
// Currently only support H264 encoded cameras, this will change.
|
||||
// Establishing the camera connection
|
||||
// Establishing the camera connection without backchannel if no substream
|
||||
rtspUrl := config.Capture.IPCamera.RTSP
|
||||
infile, streams, err := capture.OpenRTSP(context.Background(), rtspUrl)
|
||||
withBackChannel := true
|
||||
infile, streams, err := capture.OpenRTSP(context.Background(), rtspUrl, withBackChannel)
|
||||
|
||||
// We will initialise the camera settings object
|
||||
// so we can check if the camera settings have changed, and we need
|
||||
@@ -134,6 +138,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
|
||||
|
||||
@@ -150,7 +158,8 @@ func RunAgent(configDirectory string, configuration *models.Configuration, commu
|
||||
subStreamEnabled := false
|
||||
subRtspUrl := config.Capture.IPCamera.SubRTSP
|
||||
if subRtspUrl != "" && subRtspUrl != rtspUrl {
|
||||
subInfile, subStreams, err = capture.OpenRTSP(context.Background(), subRtspUrl)
|
||||
withBackChannel := false
|
||||
subInfile, subStreams, err = capture.OpenRTSP(context.Background(), subRtspUrl, withBackChannel) // We'll try to enable backchannel for the substream.
|
||||
if err == nil {
|
||||
log.Log.Info("RunAgent: opened RTSP sub stream " + subRtspUrl)
|
||||
subStreamEnabled = true
|
||||
@@ -162,10 +171,18 @@ 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() {
|
||||
if cameraSettings.Initialized {
|
||||
|
||||
if cameraSettings.RTSP != "" && cameraSettings.SubRTSP != "" && cameraSettings.Initialized {
|
||||
decoder.Close()
|
||||
if subStreamEnabled {
|
||||
subDecoder.Close()
|
||||
@@ -189,6 +206,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")
|
||||
}
|
||||
@@ -230,6 +248,9 @@ func RunAgent(configDirectory string, configuration *models.Configuration, commu
|
||||
go capture.HandleSubStream(subInfile, subQueue, communication)
|
||||
}
|
||||
|
||||
// Handle processing of audio
|
||||
communication.HandleAudio = make(chan models.AudioDataPartial)
|
||||
|
||||
// Handle processing of motion
|
||||
communication.HandleMotion = make(chan models.MotionDataPartial, 1)
|
||||
if subStreamEnabled {
|
||||
@@ -250,7 +271,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)
|
||||
@@ -271,6 +292,10 @@ func RunAgent(configDirectory string, configuration *models.Configuration, commu
|
||||
// If we reach this point, we have a working RTSP connection.
|
||||
communication.CameraConnected = true
|
||||
|
||||
// We might have a camera with audio backchannel enabled.
|
||||
// Check if we have a stream with a backchannel and is PCMU encoded.
|
||||
go WriteAudioToBackchannel(infile, streams, communication)
|
||||
|
||||
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
// This will go into a blocking state, once this channel is triggered
|
||||
// the agent will cleanup and restart.
|
||||
@@ -284,10 +309,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" {
|
||||
@@ -314,6 +339,8 @@ func RunAgent(configDirectory string, configuration *models.Configuration, commu
|
||||
}
|
||||
close(communication.HandleMotion)
|
||||
communication.HandleMotion = nil
|
||||
close(communication.HandleAudio)
|
||||
communication.HandleAudio = nil
|
||||
|
||||
// Waiting for some seconds to make sure everything is properly closed.
|
||||
log.Log.Info("RunAgent: waiting 3 seconds to make sure everything is properly closed.")
|
||||
|
||||
@@ -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"
|
||||
@@ -84,23 +84,44 @@ func OpenConfig(configDirectory string, configuration *models.Configuration) {
|
||||
collection := db.Collection("configuration")
|
||||
|
||||
var globalConfig models.Config
|
||||
err := collection.FindOne(context.Background(), bson.M{
|
||||
res := collection.FindOne(context.Background(), bson.M{
|
||||
"type": "global",
|
||||
}).Decode(&globalConfig)
|
||||
})
|
||||
|
||||
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
|
||||
deploymentName := os.Getenv("DEPLOYMENT_NAME")
|
||||
err = collection.FindOne(context.Background(), bson.M{
|
||||
res = collection.FindOne(context.Background(), bson.M{
|
||||
"type": "config",
|
||||
"name": deploymentName,
|
||||
}).Decode(&customConfig)
|
||||
})
|
||||
if res.Err() != nil {
|
||||
log.Log.Error("Could not find configuration for " + deploymentName + ", using global configuration.")
|
||||
}
|
||||
err = res.Decode(&customConfig)
|
||||
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.
|
||||
@@ -120,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
|
||||
@@ -136,6 +162,15 @@ func OpenConfig(configDirectory string, configuration *models.Configuration) {
|
||||
conjungo.Merge(&s3, configuration.CustomConfig.S3, opts)
|
||||
configuration.Config.S3 = &s3
|
||||
|
||||
// Merge Encryption settings
|
||||
var encryption models.Encryption
|
||||
conjungo.Merge(&encryption, configuration.GlobalConfig.Encryption, opts)
|
||||
conjungo.Merge(&encryption, configuration.CustomConfig.Encryption, opts)
|
||||
configuration.Config.Encryption = &encryption
|
||||
|
||||
// Merge timetable manually because it's an array
|
||||
configuration.Config.Timetable = configuration.CustomConfig.Timetable
|
||||
|
||||
// Cleanup
|
||||
opts = nil
|
||||
|
||||
@@ -189,7 +224,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
|
||||
@@ -432,6 +467,23 @@ func OverrideWithEnvironmentVariables(configuration *models.Configuration) {
|
||||
case "AGENT_DROPBOX_DIRECTORY":
|
||||
configuration.Config.Dropbox.Directory = value
|
||||
break
|
||||
|
||||
/* When encryption is enabled */
|
||||
case "AGENT_ENCRYPTION":
|
||||
configuration.Config.Encryption.Enabled = value
|
||||
break
|
||||
case "AGENT_ENCRYPTION_RECORDINGS":
|
||||
configuration.Config.Encryption.Recordings = value
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -463,6 +515,15 @@ func SaveConfig(configDirectory string, config models.Config, configuration *mod
|
||||
}
|
||||
|
||||
func StoreConfig(configDirectory string, config models.Config) error {
|
||||
|
||||
// Encryption key can be set wrong.
|
||||
if config.Encryption != nil {
|
||||
encryptionPrivateKey := config.Encryption.PrivateKey
|
||||
// Replace \\n by \n
|
||||
encryptionPrivateKey = strings.ReplaceAll(encryptionPrivateKey, "\\n", "\n")
|
||||
config.Encryption.PrivateKey = encryptionPrivateKey
|
||||
}
|
||||
|
||||
// Save into database
|
||||
if os.Getenv("DEPLOYMENT") == "factory" || os.Getenv("MACHINERY_ENVIRONMENT") == "kubernetes" {
|
||||
// Write to mongodb
|
||||
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 []byte, password string) ([]byte, error) {
|
||||
salt := make([]byte, 8)
|
||||
_, err := rand.Read(salt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key, iv, err := DefaultEvpKDF([]byte(password), salt)
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mode := cipher.NewCBCEncrypter(block, iv)
|
||||
cipherBytes := PKCS5Padding(content, aes.BlockSize)
|
||||
mode.CryptBlocks(cipherBytes, cipherBytes)
|
||||
|
||||
cipherText := make([]byte, 16+len(cipherBytes))
|
||||
copy(cipherText[:8], []byte("Salted__"))
|
||||
copy(cipherText[8:16], salt)
|
||||
copy(cipherText[16:], cipherBytes)
|
||||
|
||||
//cipherText := base64.StdEncoding.EncodeToString(data)
|
||||
return cipherText, nil
|
||||
}
|
||||
|
||||
func AesDecrypt(cipherText []byte, password string) ([]byte, error) {
|
||||
//data, err := base64.StdEncoding.DecodeString(cipherText)
|
||||
//if err != nil {
|
||||
// return nil, err
|
||||
//}
|
||||
if string(cipherText[:8]) != "Salted__" {
|
||||
return nil, errors.New("invalid crypto js aes encryption")
|
||||
}
|
||||
|
||||
salt := cipherText[8:16]
|
||||
cipherBytes := cipherText[16:]
|
||||
key, iv, err := DefaultEvpKDF([]byte(password), salt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mode := cipher.NewCBCDecrypter(block, iv)
|
||||
mode.CryptBlocks(cipherBytes, cipherBytes)
|
||||
|
||||
result := PKCS5UnPadding(cipherBytes)
|
||||
return 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"`
|
||||
}
|
||||
|
||||
6
machinery/src/models/AudioData.go
Normal file
6
machinery/src/models/AudioData.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package models
|
||||
|
||||
type AudioDataPartial struct {
|
||||
Timestamp int64 `json:"timestamp" bson:"timestamp"`
|
||||
Data []int16 `json:"data" bson:"data"`
|
||||
}
|
||||
@@ -22,11 +22,12 @@ type Communication struct {
|
||||
HandleStream chan string
|
||||
HandleSubStream chan string
|
||||
HandleMotion chan MotionDataPartial
|
||||
HandleAudio chan AudioDataPartial
|
||||
HandleUpload chan string
|
||||
HandleHeartBeat chan string
|
||||
HandleLiveSD chan int64
|
||||
HandleLiveHDKeepalive chan string
|
||||
HandleLiveHDHandshake chan SDPPayload
|
||||
HandleLiveHDHandshake chan RequestHDStreamPayload
|
||||
HandleLiveHDPeers chan string
|
||||
HandleONVIF chan OnvifAction
|
||||
IsConfiguring *abool.AtomicBool
|
||||
@@ -38,4 +39,5 @@ type Communication struct {
|
||||
SubDecoder *ffmpeg.VideoDecoder
|
||||
Image string
|
||||
CameraConnected bool
|
||||
HasBackChannel bool
|
||||
}
|
||||
|
||||
@@ -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,omitempty" bson:"encryption,omitempty"`
|
||||
}
|
||||
|
||||
// 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,12 @@ 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 string `json:"enabled" bson:"enabled"`
|
||||
Recordings string `json:"recordings" bson:"recordings"`
|
||||
Fingerprint string `json:"fingerprint" bson:"fingerprint"`
|
||||
PrivateKey string `json:"private_key" bson:"private_key"`
|
||||
SymmetricKey string `json:"symmetric_key" bson:"symmetric_key"`
|
||||
}
|
||||
|
||||
161
machinery/src/models/MQTT.go
Normal file
161
machinery/src/models/MQTT.go
Normal file
@@ -0,0 +1,161 @@
|
||||
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 == "true" {
|
||||
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(data, k)
|
||||
if err == nil {
|
||||
|
||||
data := base64.StdEncoding.EncodeToString(encryptedValue)
|
||||
// Sign the encrypted value
|
||||
signature, err := encryption.SignWithPrivateKey([]byte(data), rsaKey)
|
||||
if err == nil {
|
||||
base64Signature := base64.StdEncoding.EncodeToString(signature)
|
||||
msg.Payload.EncryptedValue = data
|
||||
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 audio input
|
||||
type AudioPayload struct {
|
||||
Timestamp int64 `json:"timestamp"` // timestamp of the recording request.
|
||||
Data []int16 `json:"data"`
|
||||
}
|
||||
|
||||
// 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,22 +668,87 @@ func GetCapabilitiesFromDevice(device *onvif.Device) []string {
|
||||
return capabilities
|
||||
}
|
||||
|
||||
func getXMLNode(xmlBody string, nodeName string) (*xml.Decoder, *xml.StartElement, error) {
|
||||
xmlBytes := bytes.NewBufferString(xmlBody)
|
||||
decodedXML := xml.NewDecoder(xmlBytes)
|
||||
for {
|
||||
token, err := decodedXML.Token()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
switch et := token.(type) {
|
||||
case xml.StartElement:
|
||||
if et.Name.Local == nodeName {
|
||||
return decodedXML, &et, nil
|
||||
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 nil, nil, errors.New("error in NodeName - username and password might be wrong")
|
||||
|
||||
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 GetPTZFunctionsFromDevice(configurations ptz.GetConfigurationsResponse) ([]string, bool, bool) {
|
||||
@@ -402,3 +836,21 @@ func VerifyOnvifConnection(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func getXMLNode(xmlBody string, nodeName string) (*xml.Decoder, *xml.StartElement, error) {
|
||||
xmlBytes := bytes.NewBufferString(xmlBody)
|
||||
decodedXML := xml.NewDecoder(xmlBytes)
|
||||
for {
|
||||
token, err := decodedXML.Token()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
switch et := token.(type) {
|
||||
case xml.StartElement:
|
||||
if et.Name.Local == nodeName {
|
||||
return decodedXML, &et, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, nil, errors.New("error in NodeName - username and password might be wrong")
|
||||
}
|
||||
|
||||
@@ -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,7 +1,9 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
jwt "github.com/appleboy/gin-jwt/v2"
|
||||
"github.com/gin-contrib/pprof"
|
||||
@@ -12,6 +14,7 @@ import (
|
||||
"log"
|
||||
|
||||
_ "github.com/kerberos-io/agent/machinery/docs"
|
||||
"github.com/kerberos-io/agent/machinery/src/encryption"
|
||||
"github.com/kerberos-io/agent/machinery/src/models"
|
||||
swaggerFiles "github.com/swaggo/files"
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
@@ -77,7 +80,7 @@ func StartServer(configDirectory string, configuration *models.Configuration, co
|
||||
r.Use(static.Serve("/settings", static.LocalFile(configDirectory+"/www", true)))
|
||||
r.Use(static.Serve("/login", static.LocalFile(configDirectory+"/www", true)))
|
||||
r.Handle("GET", "/file/*filepath", func(c *gin.Context) {
|
||||
Files(c, configDirectory)
|
||||
Files(c, configDirectory, configuration)
|
||||
})
|
||||
|
||||
// Run the api on port
|
||||
@@ -87,8 +90,50 @@ func StartServer(configDirectory string, configuration *models.Configuration, co
|
||||
}
|
||||
}
|
||||
|
||||
func Files(c *gin.Context, configDirectory string) {
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
c.Header("Content-Type", "video/mp4")
|
||||
c.File(configDirectory + "/data/recordings" + c.Param("filepath"))
|
||||
func Files(c *gin.Context, configDirectory string, configuration *models.Configuration) {
|
||||
|
||||
// Get File
|
||||
filePath := configDirectory + "/data/recordings" + c.Param("filepath")
|
||||
_, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
c.JSON(404, gin.H{"error": "File not found"})
|
||||
return
|
||||
}
|
||||
|
||||
contents, err := os.ReadFile(filePath)
|
||||
if err == nil {
|
||||
|
||||
// Get symmetric key
|
||||
symmetricKey := configuration.Config.Encryption.SymmetricKey
|
||||
// Decrypt file
|
||||
if symmetricKey != "" {
|
||||
|
||||
// Read file
|
||||
if err != nil {
|
||||
c.JSON(404, gin.H{"error": "File not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Decrypt file
|
||||
contents, err = encryption.AesDecrypt(contents, symmetricKey)
|
||||
if err != nil {
|
||||
c.JSON(404, gin.H{"error": "File not found"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Get fileSize from contents
|
||||
fileSize := len(contents)
|
||||
|
||||
// Send file to gin
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
c.Header("Content-Disposition", "attachment; filename="+filePath)
|
||||
c.Header("Content-Type", "video/mp4")
|
||||
c.Header("Content-Length", strconv.Itoa(fileSize))
|
||||
// Send contents to gin
|
||||
io.WriteString(c.Writer, string(contents))
|
||||
} else {
|
||||
c.JSON(404, gin.H{"error": "File not found"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
package mqtt
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"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 +43,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 +129,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 +145,371 @@ 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 == "true" {
|
||||
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
|
||||
data, err := base64.StdEncoding.DecodeString(encryptedValue)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
decryptedValue, err := encryption.AesDecrypt(data, string(decryptedKey))
|
||||
if err != nil {
|
||||
log.Log.Error("MQTTListenerHandler: error decrypting message: " + err.Error())
|
||||
return
|
||||
}
|
||||
json.Unmarshal(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-audio-backchannel":
|
||||
go HandleAudio(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 HandleAudio(mqttClient mqtt.Client, hubKey string, payload models.Payload, configuration *models.Configuration, communication *models.Communication) {
|
||||
value := payload.Value
|
||||
|
||||
// Convert map[string]interface{} to AudioPayload
|
||||
jsonData, _ := json.Marshal(value)
|
||||
var audioPayload models.AudioPayload
|
||||
json.Unmarshal(jsonData, &audioPayload)
|
||||
|
||||
if audioPayload.Timestamp != 0 {
|
||||
audioDataPartial := models.AudioDataPartial{
|
||||
Timestamp: audioPayload.Timestamp,
|
||||
Data: audioPayload.Data,
|
||||
}
|
||||
communication.HandleAudio <- audioDataPartial
|
||||
}
|
||||
}
|
||||
|
||||
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 != "" {
|
||||
|
||||
// Copy the config, as we don't want to share the encryption part.
|
||||
deepCopy := configuration.Config
|
||||
|
||||
var configMap map[string]interface{}
|
||||
inrec, _ := json.Marshal(deepCopy)
|
||||
json.Unmarshal(inrec, &configMap)
|
||||
|
||||
// Unset encryption part.
|
||||
delete(configMap, "encryption")
|
||||
|
||||
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
|
||||
|
||||
// Make sure to remove Encryption part, as we don't want to save it.
|
||||
config.Encryption = configuration.Config.Encryption
|
||||
|
||||
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 {
|
||||
// Register candidate channel
|
||||
key := configuration.Config.Key + "/" + receiveHDCandidatesPayload.SessionID
|
||||
go webrtc.RegisterCandidates(key, receiveHDCandidatesPayload)
|
||||
} 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.")
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/kerberos-io/agent/machinery/src/encryption"
|
||||
"github.com/kerberos-io/agent/machinery/src/log"
|
||||
"github.com/kerberos-io/agent/machinery/src/models"
|
||||
)
|
||||
@@ -330,3 +331,67 @@ func PrintConfiguration(configuration *models.Configuration) {
|
||||
}
|
||||
log.Log.Info("Printing our configuration (config.json): " + configurationVariables)
|
||||
}
|
||||
|
||||
func Decrypt(directoryOrFile string, symmetricKey []byte) {
|
||||
// Check if file or directory
|
||||
fileInfo, err := os.Stat(directoryOrFile)
|
||||
if err != nil {
|
||||
log.Log.Fatal(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var files []string
|
||||
if fileInfo.IsDir() {
|
||||
// Create decrypted directory
|
||||
err = os.MkdirAll(directoryOrFile+"/decrypted", 0755)
|
||||
if err != nil {
|
||||
log.Log.Fatal(err.Error())
|
||||
return
|
||||
}
|
||||
dir, err := os.ReadDir(directoryOrFile)
|
||||
if err != nil {
|
||||
log.Log.Fatal(err.Error())
|
||||
return
|
||||
}
|
||||
for _, file := range dir {
|
||||
// Check if file is not a directory
|
||||
if !file.IsDir() {
|
||||
// Check if an mp4 file
|
||||
if strings.HasSuffix(file.Name(), ".mp4") {
|
||||
files = append(files, directoryOrFile+"/"+file.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
files = append(files, directoryOrFile)
|
||||
}
|
||||
|
||||
// We'll loop over all files and decrypt them one by one.
|
||||
for _, file := range files {
|
||||
|
||||
// Read file
|
||||
content, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
log.Log.Fatal(err.Error())
|
||||
return
|
||||
}
|
||||
// Decrypt using AES key
|
||||
decrypted, err := encryption.AesDecrypt(content, string(symmetricKey))
|
||||
if err != nil {
|
||||
log.Log.Fatal("Something went wrong while decrypting: " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Write decrypted content to file with appended .decrypted
|
||||
// Get filename split by / and get last element.
|
||||
fileParts := strings.Split(file, "/")
|
||||
fileName := fileParts[len(fileParts)-1]
|
||||
pathToFile := strings.Join(fileParts[:len(fileParts)-1], "/")
|
||||
|
||||
err = os.WriteFile(pathToFile+"/decrypted/"+fileName, []byte(decrypted), 0644)
|
||||
if err != nil {
|
||||
log.Log.Fatal(err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,19 +87,44 @@ 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 RegisterCandidates(key string, candidate models.ReceiveHDCandidatesPayload) {
|
||||
// Set lock
|
||||
CandidatesMutex.Lock()
|
||||
channel := CandidateArrays[key]
|
||||
if channel == nil {
|
||||
channel = make(chan string)
|
||||
CandidateArrays[key] = channel
|
||||
}
|
||||
log.Log.Info("HandleReceiveHDCandidates: " + candidate.Candidate)
|
||||
channel <- candidate.Candidate
|
||||
CandidatesMutex.Unlock()
|
||||
}
|
||||
|
||||
func InitializeWebRTCConnection(configuration *models.Configuration, communication *models.Communication, mqttClient mqtt.Client, videoTrack *pionWebRTC.TrackLocalStaticSample, audioTrack *pionWebRTC.TrackLocalStaticSample, handshake models.RequestHDStreamPayload) {
|
||||
|
||||
config := configuration.Config
|
||||
|
||||
name := config.Key
|
||||
deviceKey := config.Key
|
||||
stunServers := []string{config.STUNURI}
|
||||
turnServers := []string{config.TURNURI}
|
||||
turnServersUsername := config.TURNUsername
|
||||
turnServersCredential := config.TURNPassword
|
||||
|
||||
// We create a channel which will hold the candidates for this session.
|
||||
sessionKey := config.Key + "/" + handshake.SessionID
|
||||
CandidatesMutex.Lock()
|
||||
_, ok := CandidateArrays[sessionKey]
|
||||
if !ok {
|
||||
CandidateArrays[sessionKey] = make(chan string)
|
||||
}
|
||||
CandidatesMutex.Unlock()
|
||||
|
||||
// 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 {
|
||||
|
||||
@@ -129,32 +154,43 @@ func InitializeWebRTCConnection(configuration *models.Configuration, communicati
|
||||
if err == nil && peerConnection != nil {
|
||||
|
||||
if _, err = peerConnection.AddTrack(videoTrack); err != nil {
|
||||
panic(err)
|
||||
//panic(err)
|
||||
}
|
||||
|
||||
if _, err = peerConnection.AddTrack(audioTrack); err != nil {
|
||||
panic(err)
|
||||
//panic(err)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
//panic(err)
|
||||
}
|
||||
|
||||
peerConnection.OnICEConnectionStateChange(func(connectionState pionWebRTC.ICEConnectionState) {
|
||||
if connectionState == pionWebRTC.ICEConnectionStateDisconnected {
|
||||
atomic.AddInt64(&peerConnectionCount, -1)
|
||||
peerConnections[handshake.Cuuid] = nil
|
||||
close(candidates)
|
||||
|
||||
// Set lock
|
||||
CandidatesMutex.Lock()
|
||||
peerConnections[handshake.SessionID] = nil
|
||||
_, ok := CandidateArrays[sessionKey]
|
||||
if ok {
|
||||
close(CandidateArrays[sessionKey])
|
||||
}
|
||||
CandidatesMutex.Unlock()
|
||||
|
||||
close(w.PacketsCount)
|
||||
if err := peerConnection.Close(); err != nil {
|
||||
panic(err)
|
||||
//panic(err)
|
||||
}
|
||||
} else if connectionState == pionWebRTC.ICEConnectionStateConnected {
|
||||
atomic.AddInt64(&peerConnectionCount, 1)
|
||||
} else if connectionState == pionWebRTC.ICEConnectionStateChecking {
|
||||
for candidate := range candidates {
|
||||
// Iterate over the candidates and send them to the remote client
|
||||
// Non blocking channel
|
||||
for candidate := range CandidateArrays[sessionKey] {
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -164,48 +200,76 @@ func InitializeWebRTCConnection(configuration *models.Configuration, communicati
|
||||
|
||||
offer := w.CreateOffer(sd)
|
||||
if err = peerConnection.SetRemoteDescription(offer); err != nil {
|
||||
panic(err)
|
||||
//panic(err)
|
||||
}
|
||||
|
||||
//gatherCompletePromise := pionWebRTC.GatheringCompletePromise(peerConnection)
|
||||
answer, err := peerConnection.CreateAnswer(nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
//panic(err)
|
||||
} else if err = peerConnection.SetLocalDescription(answer); err != nil {
|
||||
panic(err)
|
||||
//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)
|
||||
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 {
|
||||
log.Log.Info("InitializeWebRTCConnection:" + string(candateBinary))
|
||||
token := mqttClient.Publish("kerberos/hub/"+hubKey, 2, false, payload)
|
||||
token.Wait()
|
||||
} 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 {
|
||||
token := mqttClient.Publish("kerberos/hub/"+hubKey, 2, false, payload)
|
||||
token.Wait()
|
||||
} else {
|
||||
log.Log.Info("HandleRequestConfig: something went wrong while sending acknowledge config to hub: " + string(payload))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -358,16 +422,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)
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"jsx-a11y/media-has-caption": "off",
|
||||
"jsx-a11y/anchor-is-valid": "off",
|
||||
"jsx-a11y/click-events-have-key-events": "off",
|
||||
"jsx-a11y/control-has-associated-label": "off",
|
||||
"jsx-a11y/no-noninteractive-element-interactions": "off",
|
||||
"jsx-a11y/no-static-element-interactions": "off",
|
||||
"jsx-a11y/label-has-associated-control": [
|
||||
|
||||
@@ -85,7 +85,16 @@
|
||||
"advanced_configuration": "Erweiterte Konfiguration",
|
||||
"description_advanced_configuration": "Erweiterte Einstellungen um Funktionen des Kerberos Agent zu aktivieren oder deaktivieren",
|
||||
"offline_mode": "Offline Modus",
|
||||
"description_offline_mode": "Ausgehende Verbindungen deaktivieren"
|
||||
"description_offline_mode": "Ausgehende Verbindungen deaktivieren",
|
||||
"encryption": "Encryption",
|
||||
"description_encryption": "Enable encryption for all outgoing traffic. MQTT messages and/or recordings will be encrypted using AES-256. A private key is used for signing.",
|
||||
"encryption_enabled": "Enable MQTT encryption",
|
||||
"description_encryption_enabled": "Enable encryption for all MQTT messages.",
|
||||
"encryption_recordings_enabled": "Enable recording encryption",
|
||||
"description_encryption_recordings_enabled": "Enable encryption for all recordings.",
|
||||
"encryption_fingerprint": "Fingerprint",
|
||||
"encryption_privatekey": "Private key",
|
||||
"encryption_symmetrickey": "Symmetric key"
|
||||
},
|
||||
"camera": {
|
||||
"camera": "Kamera",
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
"heading": "Overview of your video surveilance",
|
||||
"heading": "Overview of your video surveillance",
|
||||
"number_of_days": "Number of days",
|
||||
"total_recordings": "Total recordings",
|
||||
"connected": "Connected",
|
||||
@@ -85,7 +85,16 @@
|
||||
"advanced_configuration": "Advanced configuration",
|
||||
"description_advanced_configuration": "Detailed configuration options to enable or disable specific parts of the Kerberos Agent",
|
||||
"offline_mode": "Offline mode",
|
||||
"description_offline_mode": "Disable all outgoing traffic"
|
||||
"description_offline_mode": "Disable all outgoing traffic",
|
||||
"encryption": "Encryption",
|
||||
"description_encryption": "Enable encryption for all outgoing traffic. MQTT messages and/or recordings will be encrypted using AES-256. A private key is used for signing.",
|
||||
"encryption_enabled": "Enable MQTT encryption",
|
||||
"description_encryption_enabled": "Enable encryption for all MQTT messages.",
|
||||
"encryption_recordings_enabled": "Enable recording encryption",
|
||||
"description_encryption_recordings_enabled": "Enable encryption for all recordings.",
|
||||
"encryption_fingerprint": "Fingerprint",
|
||||
"encryption_privatekey": "Private key",
|
||||
"encryption_symmetrickey": "Symmetric key"
|
||||
},
|
||||
"camera": {
|
||||
"camera": "Camera",
|
||||
@@ -142,7 +151,7 @@
|
||||
"stun_turn_description_webrtc": "Forward h264 stream through MQTT",
|
||||
"stun_turn_transcode": "Transcode stream",
|
||||
"stun_turn_description_transcode": "Convert stream to a lower resolution",
|
||||
"stun_turn_downscale": "Downscale resolution (in % or original resolution)",
|
||||
"stun_turn_downscale": "Downscale resolution (in % of original resolution)",
|
||||
"mqtt": "MQTT",
|
||||
"description_mqtt": "A MQTT broker is used to communicate from",
|
||||
"description2_mqtt": "to the Kerberos Agent, to achieve for example livestreaming or ONVIF (PTZ) capabilities.",
|
||||
|
||||
@@ -85,7 +85,16 @@
|
||||
"advanced_configuration": "Advanced configuration",
|
||||
"description_advanced_configuration": "Detailed configuration options to enable or disable specific parts of the Kerberos Agent",
|
||||
"offline_mode": "Offline mode",
|
||||
"description_offline_mode": "Disable all outgoing traffic"
|
||||
"description_offline_mode": "Disable all outgoing traffic",
|
||||
"encryption": "Encryption",
|
||||
"description_encryption": "Enable encryption for all outgoing traffic. MQTT messages and/or recordings will be encrypted using AES-256. A private key is used for signing.",
|
||||
"encryption_enabled": "Enable MQTT encryption",
|
||||
"description_encryption_enabled": "Enable encryption for all MQTT messages.",
|
||||
"encryption_recordings_enabled": "Enable recording encryption",
|
||||
"description_encryption_recordings_enabled": "Enable encryption for all recordings.",
|
||||
"encryption_fingerprint": "Fingerprint",
|
||||
"encryption_privatekey": "Private key",
|
||||
"encryption_symmetrickey": "Symmetric key"
|
||||
},
|
||||
"camera": {
|
||||
"camera": "Camera",
|
||||
|
||||
@@ -84,7 +84,16 @@
|
||||
"advanced_configuration": "Configuration avancée",
|
||||
"description_advanced_configuration": "Les options de configuration détaillées pour activer ou désactiver des composants spécifiques de l'Agent Kerberos",
|
||||
"offline_mode": "Mode hors-ligne",
|
||||
"description_offline_mode": "Désactiver tout le trafic sortant"
|
||||
"description_offline_mode": "Désactiver tout le trafic sortant",
|
||||
"encryption": "Encryption",
|
||||
"description_encryption": "Enable encryption for all outgoing traffic. MQTT messages and/or recordings will be encrypted using AES-256. A private key is used for signing.",
|
||||
"encryption_enabled": "Enable MQTT encryption",
|
||||
"description_encryption_enabled": "Enable encryption for all MQTT messages.",
|
||||
"encryption_recordings_enabled": "Enable recording encryption",
|
||||
"description_encryption_recordings_enabled": "Enable encryption for all recordings.",
|
||||
"encryption_fingerprint": "Fingerprint",
|
||||
"encryption_privatekey": "Private key",
|
||||
"encryption_symmetrickey": "Symmetric key"
|
||||
},
|
||||
"camera": {
|
||||
"camera": "Caméra",
|
||||
|
||||
224
ui/public/locales/hi/translation.json
Normal file
224
ui/public/locales/hi/translation.json
Normal file
@@ -0,0 +1,224 @@
|
||||
{
|
||||
"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": "सभी आउटगोइंग ट्रैफ़िक अक्षम करें",
|
||||
"encryption": "Encryption",
|
||||
"description_encryption": "Enable encryption for all outgoing traffic. MQTT messages and/or recordings will be encrypted using AES-256. A private key is used for signing.",
|
||||
"encryption_enabled": "Enable MQTT encryption",
|
||||
"description_encryption_enabled": "Enable encryption for all MQTT messages.",
|
||||
"encryption_recordings_enabled": "Enable recording encryption",
|
||||
"description_encryption_recordings_enabled": "Enable encryption for all recordings.",
|
||||
"encryption_fingerprint": "Fingerprint",
|
||||
"encryption_privatekey": "Private key",
|
||||
"encryption_symmetrickey": "Symmetric key"
|
||||
},
|
||||
"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": "अपलोड पर डिलीट सक्षम"
|
||||
}
|
||||
}
|
||||
}
|
||||
224
ui/public/locales/it/translation.json
Normal file
224
ui/public/locales/it/translation.json
Normal file
@@ -0,0 +1,224 @@
|
||||
{
|
||||
"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",
|
||||
"encryption": "Encryption",
|
||||
"description_encryption": "Enable encryption for all outgoing traffic. MQTT messages and/or recordings will be encrypted using AES-256. A private key is used for signing.",
|
||||
"encryption_enabled": "Enable MQTT encryption",
|
||||
"description_encryption_enabled": "Enable encryption for all MQTT messages.",
|
||||
"encryption_recordings_enabled": "Enable recording encryption",
|
||||
"description_encryption_recordings_enabled": "Enable encryption for all recordings.",
|
||||
"encryption_fingerprint": "Fingerprint",
|
||||
"encryption_privatekey": "Private key",
|
||||
"encryption_symmetrickey": "Symmetric key"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,7 +85,16 @@
|
||||
"advanced_configuration": "詳細設定",
|
||||
"description_advanced_configuration": "Kerberos エージェントの特定の部分を有効または無効にするための詳細な構成オプション",
|
||||
"offline_mode": "オフラインモード",
|
||||
"description_offline_mode": "すべての送信トラフィックを無効にする"
|
||||
"description_offline_mode": "すべての送信トラフィックを無効にする",
|
||||
"encryption": "Encryption",
|
||||
"description_encryption": "Enable encryption for all outgoing traffic. MQTT messages and/or recordings will be encrypted using AES-256. A private key is used for signing.",
|
||||
"encryption_enabled": "Enable MQTT encryption",
|
||||
"description_encryption_enabled": "Enable encryption for all MQTT messages.",
|
||||
"encryption_recordings_enabled": "Enable recording encryption",
|
||||
"description_encryption_recordings_enabled": "Enable encryption for all recordings.",
|
||||
"encryption_fingerprint": "Fingerprint",
|
||||
"encryption_privatekey": "Private key",
|
||||
"encryption_symmetrickey": "Symmetric key"
|
||||
},
|
||||
"camera": {
|
||||
"camera": "カメラ",
|
||||
|
||||
@@ -85,7 +85,16 @@
|
||||
"advanced_configuration": "Geavanceerde instellingen",
|
||||
"description_advanced_configuration": "Detail instellingen om bepaalde functionaliteiten van je Kerberos Agent aan en uit te zetten",
|
||||
"offline_mode": "Offline modus",
|
||||
"description_offline_mode": "Uitzetten van uitgaande connectiviteit"
|
||||
"description_offline_mode": "Uitzetten van uitgaande connectiviteit",
|
||||
"encryption": "Encrypteer",
|
||||
"description_encryption": "Activeer encryptie voor alle uitgaande verkeer. MQTT berichten en/of opnames worden geencrypteerd met AES-256. Een private sleutel wordt gebruikt voor het ondertekenen.",
|
||||
"encryption_enabled": "Activeer MQTT encryptie",
|
||||
"description_encryption_enabled": "Activeer encryptie voor alle MQTT berichten.",
|
||||
"encryption_recordings_enabled": "Activeer opname encryptie",
|
||||
"description_encryption_recordings_enabled": "Activeer encryptie voor alle opnames.",
|
||||
"encryption_fingerprint": "Vingerafdruk",
|
||||
"encryption_privatekey": "Private sleutel",
|
||||
"encryption_symmetrickey": "Symmetrische sleutel"
|
||||
},
|
||||
"camera": {
|
||||
"camera": "Camera",
|
||||
|
||||
@@ -85,7 +85,16 @@
|
||||
"advanced_configuration": "Advanced configuration",
|
||||
"description_advanced_configuration": "Detailed configuration options to enable or disable specific parts of the Kerberos Agent",
|
||||
"offline_mode": "Offline mode",
|
||||
"description_offline_mode": "Disable all outgoing traffic"
|
||||
"description_offline_mode": "Disable all outgoing traffic",
|
||||
"encryption": "Encryption",
|
||||
"description_encryption": "Enable encryption for all outgoing traffic. MQTT messages and/or recordings will be encrypted using AES-256. A private key is used for signing.",
|
||||
"encryption_enabled": "Enable MQTT encryption",
|
||||
"description_encryption_enabled": "Enable encryption for all MQTT messages.",
|
||||
"encryption_recordings_enabled": "Enable recording encryption",
|
||||
"description_encryption_recordings_enabled": "Enable encryption for all recordings.",
|
||||
"encryption_fingerprint": "Fingerprint",
|
||||
"encryption_privatekey": "Private key",
|
||||
"encryption_symmetrickey": "Symmetric key"
|
||||
},
|
||||
"camera": {
|
||||
"camera": "Camera",
|
||||
|
||||
@@ -85,7 +85,16 @@
|
||||
"advanced_configuration": "Configurações avançadas",
|
||||
"description_advanced_configuration": "Opções de configuração detalhadas para habilitar ou desabilitar partes específicas do Kerberos Agent",
|
||||
"offline_mode": "Modo Offline",
|
||||
"description_offline_mode": "Desative todo o tráfego de saída"
|
||||
"description_offline_mode": "Desative todo o tráfego de saída",
|
||||
"encryption": "Encryption",
|
||||
"description_encryption": "Enable encryption for all outgoing traffic. MQTT messages and/or recordings will be encrypted using AES-256. A private key is used for signing.",
|
||||
"encryption_enabled": "Enable MQTT encryption",
|
||||
"description_encryption_enabled": "Enable encryption for all MQTT messages.",
|
||||
"encryption_recordings_enabled": "Enable recording encryption",
|
||||
"description_encryption_recordings_enabled": "Enable encryption for all recordings.",
|
||||
"encryption_fingerprint": "Fingerprint",
|
||||
"encryption_privatekey": "Private key",
|
||||
"encryption_symmetrickey": "Symmetric key"
|
||||
},
|
||||
"camera": {
|
||||
"camera": "Câmera",
|
||||
|
||||
224
ui/public/locales/ru/translation.json
Normal file
224
ui/public/locales/ru/translation.json
Normal file
@@ -0,0 +1,224 @@
|
||||
{
|
||||
"breadcrumb": {
|
||||
"watch_recordings": "Смотреть записи",
|
||||
"configure": "Настроить"
|
||||
},
|
||||
"buttons": {
|
||||
"save": "Сохранить",
|
||||
"verify_connection": "Проверить подключение"
|
||||
},
|
||||
"navigation": {
|
||||
"profile": "Профиль",
|
||||
"admin": "admin",
|
||||
"management": "Управление",
|
||||
"dashboard": "Панель",
|
||||
"recordings": "Записи",
|
||||
"settings": "Настройки",
|
||||
"help_support": "Помощь & Поддержка",
|
||||
"swagger": "Swagger API",
|
||||
"documentation": "Документация",
|
||||
"ui_library": "UI Библиотека",
|
||||
"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 Agent правильно настроен.",
|
||||
"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 Hub в действии!",
|
||||
"configuration_updated_success": "Настройки успешно обновлены.",
|
||||
"configuration_updated_error": "При сохранении что-то пошло не так.",
|
||||
"verify_hub": "Проверка настроек Kerberos Hub.",
|
||||
"verify_hub_success": "Настройки Kerberos Hub успешно проверены.",
|
||||
"verify_hub_error": "Что-то пошло не так при проверке концентратора Kerberos Hub",
|
||||
"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 Agent",
|
||||
"key": "Ключ",
|
||||
"camera_name": "Название камеры",
|
||||
"timezone": "Часовой пояс",
|
||||
"select_timezone": "Выберите часовой пояс",
|
||||
"advanced_configuration": "Расширенные настройки",
|
||||
"description_advanced_configuration": "Расширенные настройки для включения или отключения определенных частей Kerberos Agent",
|
||||
"offline_mode": "Автономный режим",
|
||||
"description_offline_mode": "Отключить весь исходящий трафик",
|
||||
"encryption": "Шифрование",
|
||||
"description_encryption": "Включите шифрование для всего исходящего трафика. MQTT-сообщения и/или записи будут зашифрованы с использованием AES-256. Для подписи используется закрытый ключ.",
|
||||
"encryption_enabled": "Включить шифрование MQTT",
|
||||
"description_encryption_enabled": "Включает шифрование для всех сообщений MQTT.",
|
||||
"encryption_recordings_enabled": "Включить шифрование записей",
|
||||
"description_encryption_recordings_enabled": "Включает шифрование для всех записей.",
|
||||
"encryption_fingerprint": "Отпечаток",
|
||||
"encryption_privatekey": "Закрытый ключ",
|
||||
"encryption_symmetrickey": "Симметричный ключ"
|
||||
},
|
||||
"camera": {
|
||||
"camera": "Камера",
|
||||
"description_camera": "Настройки камеры необходимы для установки соединения с выбранной камерой.",
|
||||
"only_h264": "В настоящее время поддерживаются только потоки H264 RTSP.",
|
||||
"rtsp_url": "Адрес основного потока RTSP",
|
||||
"rtsp_h264": "Подключение к камере по протоколу H264 RTSP.",
|
||||
"sub_rtsp_url": "Адрес дополнительного потока RTSP (используется для прямой трансляции)",
|
||||
"sub_rtsp_h264": "Дополнительное RTSP-соединение с низким разрешением камеры.",
|
||||
"onvif": "ONVIF",
|
||||
"description_onvif": "Учетные данные для связи по протоколу ONVIF. Они используются для PTZ или других возможностей, предоставляемых камерой.",
|
||||
"onvif_xaddr": "ONVIF xaddr",
|
||||
"onvif_username": "ONVIF пользователь",
|
||||
"onvif_password": "ONVIF пароль",
|
||||
"verify_connection": "Проверка основного соединения",
|
||||
"verify_sub_connection": "Проверка дополнительного подключения"
|
||||
},
|
||||
"recording": {
|
||||
"recording": "Запись",
|
||||
"description_recording": "Укажите, как вы хотите вести запись. Непрерывная круглосуточная запись или запись по движению.",
|
||||
"continuous_recording": "Непрерывная запись",
|
||||
"description_continuous_recording": "Осуществлять 24/7 запись или запись по движению.",
|
||||
"max_duration": "максимальная продолжительность видео (секунд)",
|
||||
"description_max_duration": "Максимальная продолжительность записи.",
|
||||
"pre_recording": "Предзапись (секунд)",
|
||||
"description_pre_recording": "Секунд до наступления события.",
|
||||
"post_recording": "Записывать после (секунд)",
|
||||
"description_post_recording": "Секунд после наступления события.",
|
||||
"threshold": "Уровень срабатывания записи (пикселей)",
|
||||
"description_threshold": "Количество пикселей, измененных для записи",
|
||||
"autoclean": "Автоочистка",
|
||||
"description_autoclean": "Укажите, может ли Kerberos Agent очищать записи при достижении определенного объема памяти (МБ). При этом по достижении указанной емкости будут удаляться самые старые записи.",
|
||||
"autoclean_enable": "Включить автоматическую очистку",
|
||||
"autoclean_description_enable": "При достижении емкости удаляется самая старая запись.",
|
||||
"autoclean_max_directory_size": "Максимальный размер каталога (МБ)",
|
||||
"autoclean_description_max_directory_size": "Максимальное количество хранимых мегабайт записей.",
|
||||
"fragmentedrecordings": "Фрагментированные записи",
|
||||
"description_fragmentedrecordings": "Когда записи фрагментированы, они подходят для HLS-потока. При включении контейнер MP4 будет выглядеть несколько иначе.",
|
||||
"fragmentedrecordings_enable": "Включить фрагментацию",
|
||||
"fragmentedrecordings_description_enable": "Фрагментированные записи необходимы для HLS.",
|
||||
"fragmentedrecordings_duration": "продолжительность фрагмента",
|
||||
"fragmentedrecordings_description_duration": "Продолжительность одного фрагмента."
|
||||
},
|
||||
"streaming": {
|
||||
"stun_turn": "STUN/TURN для WebRTC",
|
||||
"description_stun_turn": "Для организации трансляций в полном разрешении мы используем технологию WebRTC. Одной из ключевых возможностей является функция ICE-candidate, которая позволяет обходить NAT, используя концепции STUN/TURN.",
|
||||
"stun_server": "STUN сервер",
|
||||
"turn_server": "TURN сервер",
|
||||
"turn_username": "Имя пользователя",
|
||||
"turn_password": "Пароль",
|
||||
"stun_turn_forward": "Переадресация и транскодирование",
|
||||
"stun_turn_description_forward": "Оптимизация и усовершенствование связи TURN/STUN.",
|
||||
"stun_turn_webrtc": "Переадресация на WebRTC-брокера",
|
||||
"stun_turn_description_webrtc": "Передача потока h264 через MQTT",
|
||||
"stun_turn_transcode": "Транскодирование потока",
|
||||
"stun_turn_description_transcode": "Преобразование потока в меньшее разрешение",
|
||||
"stun_turn_downscale": "Уменьшение разрешения (в % от исходного разрешения)",
|
||||
"mqtt": "MQTT",
|
||||
"description_mqtt": "Брокер MQTT используется для обмена данными с",
|
||||
"description2_mqtt": "к Kerberos Agent, чтобы, например, получить возможность трансляции видео или ONVIF (PTZ).",
|
||||
"mqtt_brokeruri": "Адрес брокера",
|
||||
"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 Hub",
|
||||
"description_kerberoshub": "Kerberos Agent'ы могут отправлять heartbeat сообщения в центральный",
|
||||
"description2_kerberoshub": "узел. Heartbeat и другая необходимая информация синхронизируются с Kerberos Hub для отображения информации о видеоландшафте в реальном времени.",
|
||||
"persistence": "Хранилище",
|
||||
"saasoffering": "Kerberos Hub (SAAS предложение)",
|
||||
"description_persistence": "Возможность хранения записей - это начало всего. Вы можете выбрать один из наших вариантов",
|
||||
"description2_persistence": ", или стороннего провайдера",
|
||||
"select_persistence": "Выберите хранилище",
|
||||
"kerberoshub_proxyurl": "Kerberos Hub Proxy URL",
|
||||
"kerberoshub_description_proxyurl": "Конечная точка Proxy для загрузки записей.",
|
||||
"kerberoshub_apiurl": "Kerberos Hub API URL",
|
||||
"kerberoshub_description_apiurl": "Конечная точка API для загрузки записей.",
|
||||
"kerberoshub_publickey": "Открытый ключ",
|
||||
"kerberoshub_description_publickey": "Открытый ключ, присвоенный вашей учетной записи Kerberos Hub.",
|
||||
"kerberoshub_privatekey": "Закрытый ключ",
|
||||
"kerberoshub_description_privatekey": "Закрытый ключ, присвоенный вашей учетной записи Kerberos Hub.",
|
||||
"kerberoshub_site": "Сайт",
|
||||
"kerberoshub_description_site": "Идентификатор сайта, к которому принадлежат агенты Kerberos (Agent) в Kerberos Hub.",
|
||||
"kerberoshub_region": "Регион",
|
||||
"kerberoshub_description_region": "Регион, в котором хранятся наши записи.",
|
||||
"kerberoshub_bucket": "Bucket",
|
||||
"kerberoshub_description_bucket": "Bucket, в котором мы храним наши записи.",
|
||||
"kerberoshub_username": "Имя пользователя/каталог (должно соответствовать имени пользователя в Kerberos Hub)",
|
||||
"kerberoshub_description_username": "Имя пользователя вашей учетной записи Kerberos Hub.",
|
||||
"kerberosvault_apiurl": "Kerberos Vault API URL",
|
||||
"kerberosvault_description_apiurl": "The Kerberos Vault API",
|
||||
"kerberosvault_provider": "Провайдер",
|
||||
"kerberosvault_description_provider": "Провайдер, которому будут отправляться ваши записи.",
|
||||
"kerberosvault_directory": "Каталог (должен совпадать с именем пользователя в Kerberos Hub)",
|
||||
"kerberosvault_description_directory": "Подкаталог, в котором будут храниться записи у вашего провайдера.",
|
||||
"kerberosvault_accesskey": "Ключ доступа",
|
||||
"kerberosvault_description_accesskey": "Ключ доступа вашей учетной записи Kerberos Vault.",
|
||||
"kerberosvault_secretkey": "Секретный ключ",
|
||||
"kerberosvault_description_secretkey": "Секретный ключ учетной записи Kerberos Vault.",
|
||||
"dropbox_directory": "Каталог",
|
||||
"dropbox_description_directory": "Подкаталог, в котором будут храниться записи в вашем аккаунте Dropbox.",
|
||||
"dropbox_accesstoken": "Токен доступа",
|
||||
"dropbox_description_accesstoken": "Токен доступа вашего аккаунта/приложения Dropbox.",
|
||||
"verify_connection": "Проверка соединения",
|
||||
"remove_after_upload": "Как только записи будут загружены на какой-либо сервер, вы, возможно, захотите удалить их из локального агента Kerberos.",
|
||||
"remove_after_upload_description": "Удаление записей после их успешной загрузки.",
|
||||
"remove_after_upload_enabled": "Включено удаление при выгрузке"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,7 +85,16 @@
|
||||
"advanced_configuration": "高级配置",
|
||||
"description_advanced_configuration": "启用或禁用 Kerberos Agent 特定部分详细配置选项",
|
||||
"offline_mode": "离线模式",
|
||||
"description_offline_mode": "禁用所有传出流量"
|
||||
"description_offline_mode": "禁用所有传出流量",
|
||||
"encryption": "Encryption",
|
||||
"description_encryption": "Enable encryption for all outgoing traffic. MQTT messages and/or recordings will be encrypted using AES-256. A private key is used for signing.",
|
||||
"encryption_enabled": "Enable MQTT encryption",
|
||||
"description_encryption_enabled": "Enable encryption for all MQTT messages.",
|
||||
"encryption_recordings_enabled": "Enable recording encryption",
|
||||
"description_encryption_recordings_enabled": "Enable encryption for all recordings.",
|
||||
"encryption_fingerprint": "Fingerprint",
|
||||
"encryption_privatekey": "Private key",
|
||||
"encryption_symmetrickey": "Symmetric key"
|
||||
},
|
||||
"camera": {
|
||||
"camera": "相机",
|
||||
|
||||
@@ -20,9 +20,12 @@ 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 },
|
||||
hi: { label: 'हिंदी', dir: 'ltr', active: false },
|
||||
ru: { label: 'Русский', dir: 'ltr', active: false },
|
||||
};
|
||||
|
||||
if (!languageMap[selected]) {
|
||||
|
||||
@@ -14,7 +14,7 @@ i18n
|
||||
escapeValue: false,
|
||||
},
|
||||
load: 'languageOnly',
|
||||
whitelist: ['de', 'en', 'nl', 'fr', 'pl', 'es', 'pt', 'ja'],
|
||||
whitelist: ['de', 'en', 'nl', 'fr', 'pl', 'es', 'pt', 'ja', 'ru'],
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
|
||||
@@ -729,7 +729,7 @@ class Settings extends React.Component {
|
||||
/>
|
||||
)}
|
||||
{verifyOnvifError && (
|
||||
<InfoBar type="alert" message={`${verifyOnvifErrorMessage}`} />
|
||||
<InfoBar type="alert" message={verifyOnvifErrorMessage} />
|
||||
)}
|
||||
|
||||
{loadingHub && (
|
||||
@@ -810,6 +810,24 @@ class Settings extends React.Component {
|
||||
this.onUpdateDropdown('', 'timezone', value[0], config)
|
||||
}
|
||||
/>
|
||||
<br />
|
||||
<hr />
|
||||
<p>
|
||||
{t('settings.overview.description_advanced_configuration')}
|
||||
</p>
|
||||
<div className="toggle-wrapper">
|
||||
<Toggle
|
||||
on={config.offline === 'true'}
|
||||
disabled={false}
|
||||
onClick={(event) =>
|
||||
this.onUpdateToggle('', 'offline', event, config)
|
||||
}
|
||||
/>
|
||||
<div>
|
||||
<span>{t('settings.overview.offline_mode')}</span>
|
||||
<p>{t('settings.overview.description_offline_mode')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</BlockBody>
|
||||
<BlockFooter>
|
||||
<Button
|
||||
@@ -1239,25 +1257,95 @@ class Settings extends React.Component {
|
||||
{showOverviewSection && (
|
||||
<Block>
|
||||
<BlockHeader>
|
||||
<h4>{t('settings.overview.advanced_configuration')}</h4>
|
||||
<h4>{t('settings.overview.encryption')}</h4>
|
||||
</BlockHeader>
|
||||
<BlockBody>
|
||||
<p>
|
||||
{t('settings.overview.description_advanced_configuration')}
|
||||
</p>
|
||||
<p>{t('settings.overview.description_encryption')}</p>
|
||||
<div className="toggle-wrapper">
|
||||
<Toggle
|
||||
on={config.offline === 'true'}
|
||||
on={config.encryption.enabled === 'true'}
|
||||
disabled={false}
|
||||
onClick={(event) =>
|
||||
this.onUpdateToggle('', 'offline', event, config)
|
||||
this.onUpdateToggle(
|
||||
'encryption',
|
||||
'enabled',
|
||||
event,
|
||||
config.encryption
|
||||
)
|
||||
}
|
||||
/>
|
||||
<div>
|
||||
<span>{t('settings.overview.offline_mode')}</span>
|
||||
<p>{t('settings.overview.description_offline_mode')}</p>
|
||||
<span>{t('settings.overview.encryption_enabled')}</span>
|
||||
<p>
|
||||
{t('settings.overview.description_encryption_enabled')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="toggle-wrapper">
|
||||
<Toggle
|
||||
on={config.encryption.recordings === 'true'}
|
||||
disabled={false}
|
||||
onClick={(event) =>
|
||||
this.onUpdateToggle(
|
||||
'encryption',
|
||||
'recordings',
|
||||
event,
|
||||
config.encryption
|
||||
)
|
||||
}
|
||||
/>
|
||||
<div>
|
||||
<span>
|
||||
{t('settings.overview.encryption_recordings_enabled')}
|
||||
</span>
|
||||
<p>
|
||||
{t(
|
||||
'settings.overview.description_encryption_recordings_enabled'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
noPadding
|
||||
label={t('settings.overview.encryption_fingerprint')}
|
||||
value={config.encryption.fingerprint}
|
||||
onChange={(value) =>
|
||||
this.onUpdateField(
|
||||
'encryption',
|
||||
'fingerprint',
|
||||
value,
|
||||
config.encryption
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
noPadding
|
||||
label={t('settings.overview.encryption_privatekey')}
|
||||
value={config.encryption.private_key}
|
||||
onChange={(value) =>
|
||||
this.onUpdateField(
|
||||
'encryption',
|
||||
'private_key',
|
||||
value,
|
||||
config.encryption
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
noPadding
|
||||
label={t('settings.overview.encryption_symmetrickey')}
|
||||
value={config.encryption.symmetric_key}
|
||||
onChange={(value) =>
|
||||
this.onUpdateField(
|
||||
'encryption',
|
||||
'symmetric_key',
|
||||
value,
|
||||
config.encryption
|
||||
)
|
||||
}
|
||||
/>
|
||||
</BlockBody>
|
||||
<BlockFooter>
|
||||
<Button
|
||||
|
||||
Reference in New Issue
Block a user