mirror of
https://github.com/kerberos-io/agent.git
synced 2026-03-03 10:50:14 +00:00
Compare commits
144 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
d879174f4c | ||
|
|
5a1a62a723 | ||
|
|
c519b01092 | ||
|
|
c2ff7ff785 | ||
|
|
44ec8c0534 | ||
|
|
21c0e01137 | ||
|
|
f7ced6056d | ||
|
|
00917e3f88 | ||
|
|
bcfed04a07 | ||
|
|
bf97bd72f1 | ||
|
|
4b8b6bf66a | ||
|
|
4b6c25bb85 | ||
|
|
729b38999e | ||
|
|
4cbf0323f1 | ||
|
|
1f5cb8ca88 | ||
|
|
8be0a04502 | ||
|
|
bdc0039a24 | ||
|
|
756b893ecd | ||
|
|
36323b076f | ||
|
|
95f43b6444 | ||
|
|
5c23a62ac3 | ||
|
|
2b425a2ddd | ||
|
|
abeeb95204 | ||
|
|
6aed20c466 | ||
|
|
6672535544 | ||
|
|
ed397b6ecc | ||
|
|
530e4c654e | ||
|
|
913bd1ba12 | ||
|
|
84e532be47 | ||
|
|
3341e99af1 | ||
|
|
ced6e678ec | ||
|
|
340a5d7ef6 | ||
|
|
60e8edc876 | ||
|
|
9cf9babd73 | ||
|
|
229c246e1c | ||
|
|
15d9bcda4f | ||
|
|
068063695e | ||
|
|
b1722844f3 | ||
|
|
eb5ab48d6c | ||
|
|
b64f1039d7 | ||
|
|
6fcd6e53a1 | ||
|
|
25537b5f02 | ||
|
|
2fad541e06 | ||
|
|
afefd32a1f | ||
|
|
89e01e065c | ||
|
|
02f3e6a1e2 | ||
|
|
ec5a00f3df | ||
|
|
2860775954 | ||
|
|
d2e8e04833 | ||
|
|
fad90390a6 | ||
|
|
d6ba875473 | ||
|
|
e9ea34c20f | ||
|
|
99cc7d419f | ||
|
|
1a6dc27535 | ||
|
|
93f40a8d34 | ||
|
|
d7f7de97b4 | ||
|
|
50babedcbf | ||
|
|
4352d993ed | ||
|
|
1e144e1c60 | ||
|
|
d4e37e0bae | ||
|
|
026bf93980 | ||
|
|
43c166666c | ||
|
|
f3bda88f3e | ||
|
|
b0af6b2e2b | ||
|
|
2925e19b90 | ||
|
|
e67b6a1800 | ||
|
|
bbbed49887 | ||
|
|
65fd400d4d | ||
|
|
87f681cfe1 | ||
|
|
f935360fda | ||
|
|
71cd315142 | ||
|
|
d9694ac1a3 | ||
|
|
08f589586d | ||
|
|
192f78ae78 | ||
|
|
ef20d4c0b1 | ||
|
|
af95c0f798 | ||
|
|
0e32a10ff5 | ||
|
|
e59c2b179d | ||
|
|
e5d03f19de | ||
|
|
58c3e73f6f | ||
|
|
8ca2c44422 | ||
|
|
a2e584a225 | ||
|
|
c4cda0afb0 | ||
|
|
adbb923e92 | ||
|
|
b16d028293 | ||
|
|
07646e483d |
@@ -14,7 +14,7 @@
|
||||
// Uncomment the next line to run commands after the container is created - for example installing curl.
|
||||
"postCreateCommand": "cd ui && yarn install && yarn build && cd ../machinery && go mod download",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers-contrib/features/ansible:1": {},
|
||||
"ghcr.io/devcontainers-contrib/features/ansible:1": {}
|
||||
},
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
|
||||
3
.github/workflows/docker-dev.yml
vendored
3
.github/workflows/docker-dev.yml
vendored
@@ -34,7 +34,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
architecture: [arm64, arm/v7, arm/v6]
|
||||
#architecture: [arm64, arm/v7, arm/v6]
|
||||
architecture: [arm64, arm/v7]
|
||||
steps:
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
|
||||
13
.github/workflows/docker.yml
vendored
13
.github/workflows/docker.yml
vendored
@@ -43,6 +43,7 @@ jobs:
|
||||
run: docker buildx build --platform linux/$(echo ${{matrix.architecture}} | tr - /) -t $REPO-arch:arch-$(echo ${{matrix.architecture}} | tr / -)-${{steps.short-sha.outputs.sha}} --output type=tar,dest=output-${{matrix.architecture}}.tar .
|
||||
- name: Strip binary
|
||||
run: mkdir -p output/ && tar -xf output-${{matrix.architecture}}.tar -C output && rm output-${{matrix.architecture}}.tar && cd output/ && tar -cf ../agent-${{matrix.architecture}}.tar -C home/agent . && rm -rf output
|
||||
# We'll make a GitHub release and push the build (tar) as an artifact
|
||||
- uses: rickstaa/action-create-tag@v1
|
||||
with:
|
||||
tag: ${{ steps.short-sha.outputs.sha }}
|
||||
@@ -54,6 +55,17 @@ jobs:
|
||||
name: ${{ steps.short-sha.outputs.sha }}
|
||||
tag: ${{ steps.short-sha.outputs.sha }}
|
||||
artifacts: "agent-${{matrix.architecture}}.tar"
|
||||
# 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:
|
||||
@@ -62,6 +74,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
architecture: [arm64, arm-v7, arm-v6]
|
||||
#architecture: [arm64, arm-v7]
|
||||
steps:
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,4 +10,5 @@ machinery/data/recordings
|
||||
machinery/data/snapshots
|
||||
machinery/test*
|
||||
machinery/init-dev.sh
|
||||
machinery/.env
|
||||
deployments/docker/private-docker-compose.yaml
|
||||
@@ -10,7 +10,7 @@ ENV GOSUMDB=off
|
||||
##########################################
|
||||
# Installing some additional dependencies.
|
||||
|
||||
RUN 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/*
|
||||
@@ -32,7 +32,7 @@ RUN cat /go/src/github.com/kerberos-io/agent/machinery/version
|
||||
|
||||
RUN cd /go/src/github.com/kerberos-io/agent/machinery && \
|
||||
go mod download && \
|
||||
go build -tags timetzdata --ldflags '-s -w -extldflags "-static -latomic"' main.go && \
|
||||
go build -tags timetzdata,netgo,osusergo --ldflags '-s -w -extldflags "-static -latomic"' main.go && \
|
||||
mkdir -p /agent && \
|
||||
mv main /agent && \
|
||||
mv version /agent && \
|
||||
@@ -122,6 +122,7 @@ RUN cp /home/agent/data/config/config.json /home/agent/data/config.template.json
|
||||
# Set permissions correctly
|
||||
|
||||
RUN chown -R agent:kerberosio /home/agent/data
|
||||
RUN chown -R agent:kerberosio /home/agent/www
|
||||
|
||||
###########################
|
||||
# Grant the necessary root capabilities to the process trying to bind to the privileged port
|
||||
@@ -146,4 +147,4 @@ HEALTHCHECK CMD curl --fail http://localhost:80 || exit 1
|
||||
# Leeeeettttt'ssss goooooo!!!
|
||||
# Run the shizzle from the right working directory.
|
||||
WORKDIR /home/agent
|
||||
CMD ["./main", "run", "opensource", "80"]
|
||||
CMD ["./main", "-action", "run", "-port", "80"]
|
||||
|
||||
173
README.md
173
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,8 +29,8 @@ 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 stream](https://github.com/kerberos-io/camera-to-rtsp).
|
||||
- Any hardware that can run a container, for example: a Raspberry Pi, NVidia Jetson, Intel NUC, a VM, Bare metal machine or a full blown Kubernetes cluster.
|
||||
- (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
|
||||
|
||||
@@ -78,12 +80,19 @@ If you want to connect to an USB or Raspberry Pi camera, [you'll need to run our
|
||||
|
||||
## Quickstart - Balena
|
||||
|
||||
Run Kerberos Agent with Balena super powers. Monitor your agent with seamless remote access, and an encrypted https endpoint.
|
||||
Checkout our fleet on [Balena Hub](https://hub.balena.io/fleets?0%5B0%5D%5Bn%5D=any&0%5B0%5D%5Bo%5D=full_text_search&0%5B0%5D%5Bv%5D=agent), and add your agent.
|
||||
Run Kerberos Agent with [Balena Cloud](https://www.balena.io/) super powers. Monitor your Kerberos Agent with seamless remote access, over the air updates, an encrypted public `https` endpoint and many more. Checkout our application `video-surveillance` on [Balena Hub](https://hub.balena.io/apps/2064752/video-surveillance), and create your first or fleet of Kerberos Agent(s).
|
||||
|
||||
[](https://dashboard.balena-cloud.com/deploy?repoUrl=https://github.com/kerberos-io/agent)
|
||||
[](https://dashboard.balena-cloud.com/deploy?repoUrl=https://github.com/kerberos-io/balena-agent)
|
||||
|
||||
**_Work In Progress_** - Currently we only support IP and USB Cameras, we have [an approach for leveraging the Raspberry Pi camera](https://github.com/kerberos-io/camera-to-rtsp), but this isn't working as expected with Balena. If you require this, you'll need to use the traditional Docker deployment with sidecar as mentioned above.
|
||||
## 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
|
||||
|
||||
@@ -97,11 +106,18 @@ This repository contains everything you'll need to know about our core product,
|
||||
- Simplified and modern user interface.
|
||||
- Multi architecture (ARMv7, ARMv8, amd64, etc).
|
||||
- Multi camera support: IP Cameras (H264), USB cameras and Raspberry Pi Cameras [through a RTSP proxy](https://github.com/kerberos-io/camera-to-rtsp).
|
||||
- Single camera per instance (e.g. oneå container per camera).
|
||||
- Ability to specifiy conditions: motion region, time table, continuous recording, etc.
|
||||
- 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 (Kerberos Hub, Kerberos Vault). WIP: Minio, Storj, 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)
|
||||
- WIP: Integrations (Webhooks, MQTT, Script, etc).
|
||||
- REST API access and documentation through Swagger (trigger recording, update configuration, etc).
|
||||
- MIT License
|
||||
|
||||
## How to run and deploy a Kerberos Agent
|
||||
@@ -117,6 +133,8 @@ We have documented the different deployment models [in the `deployments` directo
|
||||
- [Red Hat OpenShift with Ansible](https://github.com/kerberos-io/agent/tree/master/deployments#4-red-hat-ansible-and-openshift)
|
||||
- [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.
|
||||
|
||||
@@ -131,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).
|
||||
@@ -142,7 +174,10 @@ You attach a volume to your container by leveraging the `-v` option. To mount yo
|
||||
-v $(pwd)/agent/recordings:/home/agent/data/recordings \
|
||||
-d --restart=always kerberos/agent:latest
|
||||
|
||||
More example [can be found in the deployment section](https://github.com/kerberos-io/agent/tree/master/deployments) for each deployment and automation tool.
|
||||
More example [can be found in the deployment section](https://github.com/kerberos-io/agent/tree/master/deployments) for each deployment and automation tool. Please note to verify the permissions of the directory/volume you are attaching. More information in [this issue](https://github.com/kerberos-io/agent/issues/80).
|
||||
|
||||
chmod -R 755 kerberos-agent/
|
||||
chown 100:101 kerberos-agent/ -R
|
||||
|
||||
## Configure with environment variables
|
||||
|
||||
@@ -155,48 +190,64 @@ Next to attaching the configuration file, it is also possible to override the co
|
||||
-e AGENT_CAPTURE_CONTINUOUS=true \
|
||||
-d --restart=always kerberos/agent:latest
|
||||
|
||||
| Name | Description | Default Value |
|
||||
| --------------------------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------- |
|
||||
| `AGENT_USERNAME` | The username used to authenticate against the Kerberos Agent login page. | "root" |
|
||||
| `AGENT_PASSWORD` | The password used to authenticate against the Kerberos Agent login page. | "root" |
|
||||
| `AGENT_KEY` | A unique identifier for your Kerberos Agent, this is auto-generated but can be overriden. | "" |
|
||||
| `AGENT_NAME` | The agent friendly-name. | "agent" |
|
||||
| `AGENT_TIMEZONE` | Timezone which is used for converting time. | "Africa/Ceuta" |
|
||||
| `AGENT_OFFLINE` | Makes sure no external connection is made. | "false" |
|
||||
| `AGENT_AUTO_CLEAN` | Cleans up the recordings directory. | "true" |
|
||||
| `AGENT_AUTO_CLEAN_MAX_SIZE` | If `AUTO_CLEAN` enabled, set the max size of the recordings directory in (MB). | "100" |
|
||||
| `AGENT_CAPTURE_IPCAMERA_RTSP` | Full-HD RTSP endpoint to the camera you're targetting. | "" |
|
||||
| `AGENT_CAPTURE_IPCAMERA_SUB_RTSP` | Sub-stream RTSP endpoint used for livestreaming (WebRTC). | "" |
|
||||
| `AGENT_CAPTURE_IPCAMERA_ONVIF` | Mark as a compliant ONVIF device. | "" |
|
||||
| `AGENT_CAPTURE_IPCAMERA_ONVIF_XADDR` | ONVIF endpoint/address running on the camera. | "" |
|
||||
| `AGENT_CAPTURE_IPCAMERA_ONVIF_USERNAME` | ONVIF username to authenticate against. | "" |
|
||||
| `AGENT_CAPTURE_IPCAMERA_ONVIF_PASSWORD` | ONVIF password to authenticate against. | "" |
|
||||
| `AGENT_CAPTURE_RECORDING` | Toggle for enabling making recordings. | "true" |
|
||||
| `AGENT_CAPTURE_CONTINUOUS` | Toggle for enabling continuous or motion based recording. | "false" |
|
||||
| `AGENT_CAPTURE_PRERECORDING` | If `CONTINUOUS` set to `false`, specify the recording time (seconds) before after motion event. | "10" |
|
||||
| `AGENT_CAPTURE_POSTRECORDING` | If `CONTINUOUS` set to `false`, specify the recording time (seconds) after motion event. | "20" |
|
||||
| `AGENT_CAPTURE_MAXLENGTH` | The maximum length of a single recording (seconds). | "30" |
|
||||
| `AGENT_CAPTURE_PIXEL_CHANGE` | If `CONTINUOUS` set to `false`, the number of pixel require to change before motion triggers. | "150" |
|
||||
| `AGENT_CAPTURE_FRAGMENTED` | Set the format of the recorded MP4 to fragmented (suitable for HLS). | "false" |
|
||||
| `AGENT_CAPTURE_FRAGMENTED_DURATION` | If `AGENT_CAPTURE_FRAGMENTED` set to `true`, define the duration (seconds) of a fragment. | "8" |
|
||||
| `AGENT_CLOUD` | Store recordings in a Kerberos Hub (s3) or your Kerberos Vault (kstorage). | "s3" |
|
||||
| `AGENT_HUB_URI` | The Kerberos Hub API, defaults to our Kerberos Hub SAAS. | "https://api.cloud.kerberos.io" |
|
||||
| `AGENT_HUB_KEY` | The access key linked to your account in Kerberos Hub. | "" |
|
||||
| `AGENT_HUB_PRIVATE_KEY` | The secret access key linked to your account in Kerberos Hub. | "" |
|
||||
| `AGENT_HUB_USERNAME` | Your Kerberos Hub username, which owns the above access and secret keys. | "" |
|
||||
| `AGENT_HUB_SITE` | The site ID of a site you've created in your Kerberos Hub account. | "" |
|
||||
| `AGENT_MQTT_URI` | A MQTT broker endpoint that is used for bi-directional communication (live view, onvif, etc) | "tcp://mqtt.kerberos.io:1883" |
|
||||
| `AGENT_MQTT_USERNAME` | Username of the MQTT broker. | "" |
|
||||
| `AGENT_MQTT_PASSWORD` | Password of the MQTT broker. | "" |
|
||||
| `AGENT_STUN_URI` | When using WebRTC, you'll need to provide a STUN server. | "stun:turn.kerberos.io:8443" |
|
||||
| `AGENT_TURN_URI` | When using WebRTC, you'll need to provide a TURN server. | "turn:turn.kerberos.io:8443" |
|
||||
| `AGENT_TURN_USERNAME` | TURN username used for WebRTC. | "username1" |
|
||||
| `AGENT_TURN_PASSWORD` | TURN password used for WebRTC. | "password1" |
|
||||
| `AGENT_KERBEROSVAULT_URI` | The Kerberos Vault API url. | "" . |
|
||||
| `AGENT_KERBEROSVAULT_ACCESS_KEY` | The access key of a Kerberos Vault account. | "" |
|
||||
| `AGENT_KERBEROSVAULT_SECRET_KEY` | The secret key of a Kerberos Vault account. | "" |
|
||||
| `AGENT_KERBEROSVAULT_PROVIDER` | A Kerberos Vault provider you have created (optional). | "" |
|
||||
| `AGENT_KERBEROSVAULT_DIRECTORY` | The directory, in the provider, where the recordings will be stored in. | "" |
|
||||
| Name | Description | Default Value |
|
||||
| --------------------------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------ |
|
||||
| `AGENT_MODE` | You can choose to run this in 'release' for production, and or 'demo' for showcasing. | "release" |
|
||||
| `AGENT_TLS_INSECURE` | Specify if you want to use `InsecureSkipVerify` for the internal HTTP client. | "false" |
|
||||
| `AGENT_USERNAME` | The username used to authenticate against the Kerberos Agent login page. | "root" |
|
||||
| `AGENT_PASSWORD` | The password used to authenticate against the Kerberos Agent login page. | "root" |
|
||||
| `AGENT_KEY` | A unique identifier for your Kerberos Agent, this is auto-generated but can be overriden. | "" |
|
||||
| `AGENT_NAME` | The agent friendly-name. | "agent" |
|
||||
| `AGENT_TIMEZONE` | Timezone which is used for converting time. | "Africa/Ceuta" |
|
||||
| `AGENT_REMOVE_AFTER_UPLOAD` | When enabled, recordings uploaded successfully to a storage will be removed from disk. | "true" |
|
||||
| `AGENT_OFFLINE` | Makes sure no external connection is made. | "false" |
|
||||
| `AGENT_AUTO_CLEAN` | Cleans up the recordings directory. | "true" |
|
||||
| `AGENT_AUTO_CLEAN_MAX_SIZE` | If `AUTO_CLEAN` enabled, set the max size of the recordings directory in (MB). | "100" |
|
||||
| `AGENT_TIME` | Enable the timetable for Kerberos Agent | "false" |
|
||||
| `AGENT_TIMETABLE` | A (weekly) time table to specify when to make recordings "start1,end1,start2,end2;start1.. | "" |
|
||||
| `AGENT_REGION_POLYGON` | A single polygon set for motion detection: "x1,y1;x2,y2;x3,y3;... | "" |
|
||||
| `AGENT_CAPTURE_IPCAMERA_RTSP` | Full-HD RTSP endpoint to the camera you're targetting. | "" |
|
||||
| `AGENT_CAPTURE_IPCAMERA_SUB_RTSP` | Sub-stream RTSP endpoint used for livestreaming (WebRTC). | "" |
|
||||
| `AGENT_CAPTURE_IPCAMERA_ONVIF` | Mark as a compliant ONVIF device. | "" |
|
||||
| `AGENT_CAPTURE_IPCAMERA_ONVIF_XADDR` | ONVIF endpoint/address running on the camera. | "" |
|
||||
| `AGENT_CAPTURE_IPCAMERA_ONVIF_USERNAME` | ONVIF username to authenticate against. | "" |
|
||||
| `AGENT_CAPTURE_IPCAMERA_ONVIF_PASSWORD` | ONVIF password to authenticate against. | "" |
|
||||
| `AGENT_CAPTURE_MOTION` | Toggle for enabling or disabling motion. | "true" |
|
||||
| `AGENT_CAPTURE_LIVEVIEW` | Toggle for enabling or disabling liveview. | "true" |
|
||||
| `AGENT_CAPTURE_SNAPSHOTS` | Toggle for enabling or disabling snapshot generation. | "true" |
|
||||
| `AGENT_CAPTURE_RECORDING` | Toggle for enabling making recordings. | "true" |
|
||||
| `AGENT_CAPTURE_CONTINUOUS` | Toggle for enabling continuous "true" or motion "false". | "false" |
|
||||
| `AGENT_CAPTURE_PRERECORDING` | If `CONTINUOUS` set to `false`, specify the recording time (seconds) before after motion event. | "10" |
|
||||
| `AGENT_CAPTURE_POSTRECORDING` | If `CONTINUOUS` set to `false`, specify the recording time (seconds) after motion event. | "20" |
|
||||
| `AGENT_CAPTURE_MAXLENGTH` | The maximum length of a single recording (seconds). | "30" |
|
||||
| `AGENT_CAPTURE_PIXEL_CHANGE` | If `CONTINUOUS` set to `false`, the number of pixel require to change before motion triggers. | "150" |
|
||||
| `AGENT_CAPTURE_FRAGMENTED` | Set the format of the recorded MP4 to fragmented (suitable for HLS). | "false" |
|
||||
| `AGENT_CAPTURE_FRAGMENTED_DURATION` | If `AGENT_CAPTURE_FRAGMENTED` set to `true`, define the duration (seconds) of a fragment. | "8" |
|
||||
| `AGENT_MQTT_URI` | A MQTT broker endpoint that is used for bi-directional communication (live view, onvif, etc) | "tcp://mqtt.kerberos.io:1883" |
|
||||
| `AGENT_MQTT_USERNAME` | Username of the MQTT broker. | "" |
|
||||
| `AGENT_MQTT_PASSWORD` | Password of the MQTT broker. | "" |
|
||||
| `AGENT_STUN_URI` | When using WebRTC, you'll need to provide a STUN server. | "stun:turn.kerberos.io:8443" |
|
||||
| `AGENT_TURN_URI` | When using WebRTC, you'll need to provide a TURN server. | "turn:turn.kerberos.io:8443" |
|
||||
| `AGENT_TURN_USERNAME` | TURN username used for WebRTC. | "username1" |
|
||||
| `AGENT_TURN_PASSWORD` | TURN password used for WebRTC. | "password1" |
|
||||
| `AGENT_CLOUD` | Store recordings in Kerberos Hub (s3), Kerberos Vault (kstorage) or Dropbox (dropbox). | "s3" |
|
||||
| `AGENT_HUB_URI` | The Kerberos Hub API, defaults to our Kerberos Hub SAAS. | "https://api.hub.domain.com" |
|
||||
| `AGENT_HUB_KEY` | The access key linked to your account in Kerberos Hub. | "" |
|
||||
| `AGENT_HUB_PRIVATE_KEY` | The secret access key linked to your account in Kerberos Hub. | "" |
|
||||
| `AGENT_HUB_REGION` | The Kerberos Hub region, to which you want to upload. | "" |
|
||||
| `AGENT_HUB_SITE` | The site ID of a site you've created in your Kerberos Hub account. | "" |
|
||||
| `AGENT_KERBEROSVAULT_URI` | The Kerberos Vault API url. | "https://vault.domain.com/api" |
|
||||
| `AGENT_KERBEROSVAULT_ACCESS_KEY` | The access key of a Kerberos Vault account. | "" |
|
||||
| `AGENT_KERBEROSVAULT_SECRET_KEY` | The secret key of a Kerberos Vault account. | "" |
|
||||
| `AGENT_KERBEROSVAULT_PROVIDER` | A Kerberos Vault provider you have created (optional). | "" |
|
||||
| `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
|
||||
|
||||
@@ -219,9 +270,9 @@ On opening of the GitHub Codespace, some dependencies will be installed. Once th
|
||||
const dev = {
|
||||
ENV: 'dev',
|
||||
HOSTNAME: externalHost,
|
||||
//API_URL: `${protocol}//${hostname}:8080/api`,
|
||||
//URL: `${protocol}//${hostname}:8080`,
|
||||
//WS_URL: `${websocketprotocol}//${hostname}:8080/ws`,
|
||||
//API_URL: `${protocol}//${hostname}:80/api`,
|
||||
//URL: `${protocol}//${hostname}:80`,
|
||||
//WS_URL: `${websocketprotocol}//${hostname}:80/ws`,
|
||||
|
||||
// Uncomment, and comment the above lines, when using codespaces or other special DNS names (which you can't control)
|
||||
API_URL: `${protocol}//${externalHost}/api`,
|
||||
@@ -234,7 +285,7 @@ Go and open two terminals one for the `ui` project and one for the `machinery` p
|
||||
1. Terminal A:
|
||||
|
||||
cd machinery/
|
||||
go run main.go run camera 80
|
||||
go run main.go -action run -port 80
|
||||
|
||||
2. Terminal B:
|
||||
|
||||
@@ -275,7 +326,7 @@ You can simply run the `machinery` using following commands.
|
||||
|
||||
git clone https://github.com/kerberos-io/agent
|
||||
cd machinery
|
||||
go run main.go run mycameraname 80
|
||||
go run main.go -action run -port 80
|
||||
|
||||
This will launch the Kerberos Agent and run a webserver on port `80`. You can change the port by your own preference. We strongly support the usage of [Goland](https://www.jetbrains.com/go/) or [Visual Studio Code](https://code.visualstudio.com/), as it comes with all the debugging and linting features builtin.
|
||||
|
||||
@@ -310,14 +361,6 @@ By running the `docker build` command, you will create the Kerberos Agent Docker
|
||||
|
||||
docker build -t kerberos/agent .
|
||||
|
||||
## Support our project
|
||||
|
||||
If you like our product please feel free to execute an Ethereum donation. All donations will flow back and split to our Open Source contributors, as they are the heart of this community.
|
||||
|
||||
<img width="272" alt="Ethereum donation linke" src="https://user-images.githubusercontent.com/1546779/173443671-3d773068-ae10-4862-a990-dc7c89f3d9c2.png">
|
||||
|
||||
Ethereum Address: `0xf4a759C9436E2280Ea9cdd23d3144D95538fF4bE`
|
||||
|
||||
## What is new?
|
||||
|
||||
This repository contains the next generation of Kerberos.io, **Kerberos Agent (v3)**, and is the successor of the machinery and web repositories. A switch in technologies and architecture has been made. This version is still under active development and can be followed on the [develop branch](https://github.com/kerberos-io/agent/tree/develop) and [project overview](https://github.com/kerberos-io/agent/projects/1).
|
||||
|
||||
@@ -14,6 +14,7 @@ We will discuss following deployment models.
|
||||
- [5. Kerberos Factory](#5-kerberos-factory)
|
||||
- [6. Terraform](#6-terraform)
|
||||
- [7. Salt](#7-salt)
|
||||
- [8. Balena](#8-balena)
|
||||
|
||||
## 0. Static binary
|
||||
|
||||
@@ -53,8 +54,26 @@ 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
|
||||
|
||||
To be written
|
||||
|
||||
## 8. Balena
|
||||
|
||||
Balena Cloud provide a seamless way of building and deploying applications at scale through the conceps of `blocks`, `apps` and `fleets`. Once you have your `app` deployed, for example our Kerberos Agent, you can benefit from features such as: remote access, over the air updates, an encrypted public `https` endpoint and many more.
|
||||
|
||||
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).
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
|
||||
initContainers:
|
||||
- name: download-config
|
||||
image: kerberos/agent:1b96d01
|
||||
image: kerberos/agent:latest
|
||||
volumeMounts:
|
||||
- name: kerberos-data
|
||||
mountPath: /home/agent/data/config
|
||||
@@ -96,7 +96,7 @@
|
||||
|
||||
containers:
|
||||
- name: agent
|
||||
image: kerberos/agent:1b96d01
|
||||
image: kerberos/agent:latest
|
||||
volumeMounts:
|
||||
- name: kerberos-data
|
||||
mountPath: /home/agent/data/config
|
||||
|
||||
31
deployments/balena/README.md
Normal file
31
deployments/balena/README.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Deployment with Balena
|
||||
|
||||
Balena Cloud provide a seamless way of building and deploying applications at scale through the conceps of `blocks`, `apps` and `fleets`. Once you have your `app` deployed, for example our Kerberos Agent, you can benefit from features such as: remote access, over the air updates, an encrypted public `https` endpoint and many more.
|
||||
|
||||
We provide two mechanisms to deploy Kerberos Agent to a Balena Cloud fleet:
|
||||
|
||||
1. Use Kerberos Agent as [a block part of your application](https://github.com/kerberos-io/balena-agent-block).
|
||||
2. Use Kerberos Agent as [a stand-alone application](https://github.com/kerberos-io/balena-agent).
|
||||
|
||||
## Block
|
||||
|
||||
Within Balena you can build the concept of a block, which is the equivalent of container image or a function in a typical programming language. The idea of blocks, you can find a more thorough explanation [here](https://docs.balena.io/learn/develop/blocks/), is that you can compose and combine multiple `blocks` to level up to the concept an `app`.
|
||||
|
||||
You as a developer can choose which `blocks` you would like to use, to build the desired `application` state you prefer. For example you can use the [Kerberos Agent block](https://hub.balena.io/blocks/2064662/agent) to compose a video surveillance system as part of your existing set of blocks.
|
||||
|
||||
You can the `Kerberos Agent` block by defining following elements in your `compose` file.
|
||||
|
||||
agent:
|
||||
image: bh.cr/kerberos_io/agent
|
||||
|
||||
## App
|
||||
|
||||
Next to building individual `blocks` you as a developer can also decide to build up an application, composed of one or more `blocks` or third-party containers, and publish it as an `app` to the Balena Hub. This is exactly [what we've done..](https://hub.balena.io/apps/2064752/video-surveillance)
|
||||
|
||||
On Balena Hub we have created the []`video-surveillance` application](https://hub.balena.io/apps/2064752/video-surveillance) that utilises the [Kerberos Agent `block`](https://hub.balena.io/blocks/2064662/agent). The idea of this application is that utilises the foundation of our Kerberos Agent, but that it might include more `blocks` over time to increase and improve functionalities from other community projects.
|
||||
|
||||
To deploy the application you can simply press below `Deploy button` or you can navigate to the [Balena Hub apps page](https://hub.balena.io/apps/2064752/video-surveillance).
|
||||
|
||||
[](https://dashboard.balena-cloud.com/deploy?repoUrl=https://github.com/kerberos-io/agent)
|
||||
|
||||
You can find the source code, `balena.yaml` and `docker-compose.yaml` files in the [`balena-agent` repository](https://github.com/kerberos-io/balena-agent).
|
||||
@@ -21,7 +21,7 @@ spec:
|
||||
|
||||
initContainers:
|
||||
- name: download-config
|
||||
image: kerberos/agent:1b96d01
|
||||
image: kerberos/agent:latest
|
||||
volumeMounts:
|
||||
- name: kerberos-data
|
||||
mountPath: /home/agent/data/config
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,10 @@
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "${fileDirname}"
|
||||
}
|
||||
"program": "main.go",
|
||||
"args": ["-action", "run"],
|
||||
"envFile": "${workspaceFolder}/.env",
|
||||
"buildFlags": "--tags dynamic",
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
"time": "false",
|
||||
"offline": "false",
|
||||
"auto_clean": "true",
|
||||
"remove_after_upload": "true",
|
||||
"max_directory_size": 100,
|
||||
"timezone": "Africa/Ceuta",
|
||||
"capture": {
|
||||
@@ -22,6 +23,10 @@
|
||||
"device": ""
|
||||
},
|
||||
"continuous": "false",
|
||||
"recording": "true",
|
||||
"snapshots": "true",
|
||||
"liveview": "true",
|
||||
"motion": "true",
|
||||
"postrecording": 20,
|
||||
"prerecording": 10,
|
||||
"maxlengthrecording": 30,
|
||||
@@ -90,9 +95,10 @@
|
||||
"s3": {
|
||||
"proxyuri": "http://proxy.kerberos.io",
|
||||
"bucket": "kerberosaccept",
|
||||
"region": "eu-west1"
|
||||
"region": "eu-west-1"
|
||||
},
|
||||
"kstorage": {},
|
||||
"dropbox": {},
|
||||
"mqtturi": "tcp://mqtt.kerberos.io:1883",
|
||||
"mqtt_username": "",
|
||||
"mqtt_password": "",
|
||||
@@ -105,5 +111,6 @@
|
||||
"hub_key": "",
|
||||
"hub_private_key": "",
|
||||
"hub_site": "",
|
||||
"condition_uri": ""
|
||||
}
|
||||
"condition_uri": "",
|
||||
"encryption": {}
|
||||
}
|
||||
|
||||
BIN
machinery/data/test-480p.mp4
Normal file
BIN
machinery/data/test-480p.mp4
Normal file
Binary file not shown.
@@ -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.",
|
||||
@@ -244,6 +302,40 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/onvif/verify": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"description": "Will verify the ONVIF connectivity.",
|
||||
"tags": [
|
||||
"config"
|
||||
],
|
||||
"summary": "Will verify the ONVIF connectivity.",
|
||||
"operationId": "verify-onvif",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Camera Config",
|
||||
"name": "cameraConfig",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.IPCamera"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.APIResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/persistence/verify": {
|
||||
"post": {
|
||||
"security": [
|
||||
@@ -283,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": {
|
||||
@@ -347,9 +446,15 @@ const docTemplate = `{
|
||||
"ipcamera": {
|
||||
"$ref": "#/definitions/models.IPCamera"
|
||||
},
|
||||
"liveview": {
|
||||
"type": "string"
|
||||
},
|
||||
"maxlengthrecording": {
|
||||
"type": "integer"
|
||||
},
|
||||
"motion": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -365,6 +470,12 @@ const docTemplate = `{
|
||||
"raspicamera": {
|
||||
"$ref": "#/definitions/models.RaspiCamera"
|
||||
},
|
||||
"recording": {
|
||||
"type": "string"
|
||||
},
|
||||
"snapshots": {
|
||||
"type": "string"
|
||||
},
|
||||
"transcodingresolution": {
|
||||
"type": "integer"
|
||||
},
|
||||
@@ -391,6 +502,12 @@ const docTemplate = `{
|
||||
"condition_uri": {
|
||||
"type": "string"
|
||||
},
|
||||
"dropbox": {
|
||||
"$ref": "#/definitions/models.Dropbox"
|
||||
},
|
||||
"friendly_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"heartbeaturi": {
|
||||
"description": "obsolete",
|
||||
"type": "string"
|
||||
@@ -434,6 +551,9 @@ const docTemplate = `{
|
||||
"region": {
|
||||
"$ref": "#/definitions/models.Region"
|
||||
},
|
||||
"remove_after_upload": {
|
||||
"type": "string"
|
||||
},
|
||||
"s3": {
|
||||
"$ref": "#/definitions/models.S3"
|
||||
},
|
||||
@@ -477,6 +597,17 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.Dropbox": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"access_token": {
|
||||
"type": "string"
|
||||
},
|
||||
"directory": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.IPCamera": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -484,7 +615,7 @@ const docTemplate = `{
|
||||
"type": "string"
|
||||
},
|
||||
"onvif": {
|
||||
"type": "boolean"
|
||||
"type": "string"
|
||||
},
|
||||
"onvif_password": {
|
||||
"type": "string"
|
||||
@@ -555,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.",
|
||||
@@ -236,6 +294,40 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/onvif/verify": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"description": "Will verify the ONVIF connectivity.",
|
||||
"tags": [
|
||||
"config"
|
||||
],
|
||||
"summary": "Will verify the ONVIF connectivity.",
|
||||
"operationId": "verify-onvif",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Camera Config",
|
||||
"name": "cameraConfig",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.IPCamera"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/models.APIResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/persistence/verify": {
|
||||
"post": {
|
||||
"security": [
|
||||
@@ -275,8 +367,15 @@
|
||||
"models.APIResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"can_pan_tilt": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"can_zoom": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"data": {},
|
||||
"message": {}
|
||||
"message": {},
|
||||
"ptz_functions": {}
|
||||
}
|
||||
},
|
||||
"models.Authentication": {
|
||||
@@ -339,9 +438,15 @@
|
||||
"ipcamera": {
|
||||
"$ref": "#/definitions/models.IPCamera"
|
||||
},
|
||||
"liveview": {
|
||||
"type": "string"
|
||||
},
|
||||
"maxlengthrecording": {
|
||||
"type": "integer"
|
||||
},
|
||||
"motion": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -357,6 +462,12 @@
|
||||
"raspicamera": {
|
||||
"$ref": "#/definitions/models.RaspiCamera"
|
||||
},
|
||||
"recording": {
|
||||
"type": "string"
|
||||
},
|
||||
"snapshots": {
|
||||
"type": "string"
|
||||
},
|
||||
"transcodingresolution": {
|
||||
"type": "integer"
|
||||
},
|
||||
@@ -383,6 +494,12 @@
|
||||
"condition_uri": {
|
||||
"type": "string"
|
||||
},
|
||||
"dropbox": {
|
||||
"$ref": "#/definitions/models.Dropbox"
|
||||
},
|
||||
"friendly_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"heartbeaturi": {
|
||||
"description": "obsolete",
|
||||
"type": "string"
|
||||
@@ -426,6 +543,9 @@
|
||||
"region": {
|
||||
"$ref": "#/definitions/models.Region"
|
||||
},
|
||||
"remove_after_upload": {
|
||||
"type": "string"
|
||||
},
|
||||
"s3": {
|
||||
"$ref": "#/definitions/models.S3"
|
||||
},
|
||||
@@ -469,6 +589,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.Dropbox": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"access_token": {
|
||||
"type": "string"
|
||||
},
|
||||
"directory": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"models.IPCamera": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -476,7 +607,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"onvif": {
|
||||
"type": "boolean"
|
||||
"type": "string"
|
||||
},
|
||||
"onvif_password": {
|
||||
"type": "string"
|
||||
@@ -547,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:
|
||||
@@ -44,8 +49,12 @@ definitions:
|
||||
type: integer
|
||||
ipcamera:
|
||||
$ref: '#/definitions/models.IPCamera'
|
||||
liveview:
|
||||
type: string
|
||||
maxlengthrecording:
|
||||
type: integer
|
||||
motion:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
pixelChangeThreshold:
|
||||
@@ -56,6 +65,10 @@ definitions:
|
||||
type: integer
|
||||
raspicamera:
|
||||
$ref: '#/definitions/models.RaspiCamera'
|
||||
recording:
|
||||
type: string
|
||||
snapshots:
|
||||
type: string
|
||||
transcodingresolution:
|
||||
type: integer
|
||||
transcodingwebrtc:
|
||||
@@ -73,6 +86,10 @@ definitions:
|
||||
type: string
|
||||
condition_uri:
|
||||
type: string
|
||||
dropbox:
|
||||
$ref: '#/definitions/models.Dropbox'
|
||||
friendly_name:
|
||||
type: string
|
||||
heartbeaturi:
|
||||
description: obsolete
|
||||
type: string
|
||||
@@ -102,6 +119,8 @@ definitions:
|
||||
type: string
|
||||
region:
|
||||
$ref: '#/definitions/models.Region'
|
||||
remove_after_upload:
|
||||
type: string
|
||||
s3:
|
||||
$ref: '#/definitions/models.S3'
|
||||
stunuri:
|
||||
@@ -130,12 +149,19 @@ definitions:
|
||||
"y":
|
||||
type: number
|
||||
type: object
|
||||
models.Dropbox:
|
||||
properties:
|
||||
access_token:
|
||||
type: string
|
||||
directory:
|
||||
type: string
|
||||
type: object
|
||||
models.IPCamera:
|
||||
properties:
|
||||
fps:
|
||||
type: string
|
||||
onvif:
|
||||
type: boolean
|
||||
type: string
|
||||
onvif_password:
|
||||
type: string
|
||||
onvif_username:
|
||||
@@ -181,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:
|
||||
@@ -289,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.
|
||||
@@ -327,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.
|
||||
@@ -414,6 +485,27 @@ paths:
|
||||
summary: Get Authorization token.
|
||||
tags:
|
||||
- authentication
|
||||
/api/onvif/verify:
|
||||
post:
|
||||
description: Will verify the ONVIF connectivity.
|
||||
operationId: verify-onvif
|
||||
parameters:
|
||||
- description: Camera Config
|
||||
in: body
|
||||
name: cameraConfig
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/models.IPCamera'
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/models.APIResponse'
|
||||
security:
|
||||
- Bearer: []
|
||||
summary: Will verify the ONVIF connectivity.
|
||||
tags:
|
||||
- config
|
||||
/api/persistence/verify:
|
||||
post:
|
||||
description: Will verify the persistence.
|
||||
|
||||
@@ -2,38 +2,45 @@ module github.com/kerberos-io/agent/machinery
|
||||
|
||||
go 1.19
|
||||
|
||||
//replace github.com/kerberos-io/joy4 v1.0.54 => ../../../../github.com/kerberos-io/joy4
|
||||
//replace github.com/kerberos-io/joy4 v1.0.60 => ../../../../github.com/kerberos-io/joy4
|
||||
|
||||
//replace github.com/kerberos-io/onvif v0.0.5 => ../../../../github.com/kerberos-io/onvif
|
||||
// replace github.com/kerberos-io/onvif v0.0.6 => ../../../../github.com/kerberos-io/onvif
|
||||
|
||||
require (
|
||||
github.com/InVisionApp/conjungo v1.1.0
|
||||
github.com/appleboy/gin-jwt/v2 v2.9.1
|
||||
github.com/asticode/go-astits v1.11.0
|
||||
github.com/bluenviron/gortsplib/v3 v3.6.1
|
||||
github.com/bluenviron/mediacommon v0.5.0
|
||||
github.com/cedricve/go-onvif v0.0.0-20200222191200-567e8ce298f6
|
||||
github.com/deepch/vdk v0.0.19
|
||||
github.com/dropbox/dropbox-sdk-go-unofficial/v6 v6.0.5
|
||||
github.com/eclipse/paho.mqtt.golang v1.4.2
|
||||
github.com/elastic/go-sysinfo v1.9.0
|
||||
github.com/gin-contrib/cors v1.4.0
|
||||
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.55
|
||||
github.com/kerberos-io/onvif v0.0.5
|
||||
github.com/kerberos-io/joy4 v1.0.63
|
||||
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
|
||||
github.com/pion/rtp v1.7.13
|
||||
github.com/pion/webrtc/v3 v3.1.50
|
||||
github.com/sirupsen/logrus v1.9.0
|
||||
github.com/swaggo/files v1.0.0
|
||||
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/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||
)
|
||||
|
||||
@@ -49,6 +56,7 @@ require (
|
||||
github.com/Microsoft/go-winio v0.5.1 // indirect
|
||||
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||
github.com/asticode/go-astikit v0.30.0 // indirect
|
||||
github.com/beevik/etree v1.1.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/clbanning/mxj v1.8.4 // indirect
|
||||
@@ -65,14 +73,18 @@ require (
|
||||
github.com/go-playground/locales v0.14.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||
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
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/google/pprof v0.0.0-20210423192551-a2663126120b // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.15.0 // indirect
|
||||
github.com/klauspost/cpuid v1.2.3 // indirect
|
||||
github.com/kylelemons/go-gypsy v1.0.0 // indirect
|
||||
github.com/leodido/go-urn v1.2.1 // indirect
|
||||
@@ -84,6 +96,7 @@ require (
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/onsi/gomega v1.27.4 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
|
||||
github.com/philhofer/fwd v1.1.1 // indirect
|
||||
github.com/pion/datachannel v1.5.5 // indirect
|
||||
@@ -94,7 +107,6 @@ require (
|
||||
github.com/pion/mdns v0.0.5 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/rtcp v1.2.10 // indirect
|
||||
github.com/pion/rtp v1.7.13 // indirect
|
||||
github.com/pion/sctp v1.8.5 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.6 // indirect
|
||||
github.com/pion/srtp/v2 v2.0.10 // indirect
|
||||
@@ -109,17 +121,23 @@ require (
|
||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||
github.com/tinylib/msgp v1.1.6 // indirect
|
||||
github.com/ugorji/go/codec v1.2.7 // indirect
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||
github.com/xdg-go/scram v1.0.2 // indirect
|
||||
github.com/xdg-go/stringprep v1.0.2 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
|
||||
github.com/ziutek/mymysql v1.5.4 // indirect
|
||||
go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 // indirect
|
||||
golang.org/x/crypto v0.4.0 // indirect
|
||||
golang.org/x/net v0.4.0 // indirect
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect
|
||||
golang.org/x/sys v0.3.0 // indirect
|
||||
golang.org/x/text v0.5.0 // indirect
|
||||
golang.org/x/net v0.9.0 // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/sys v0.7.0 // indirect
|
||||
golang.org/x/text v0.9.0 // indirect
|
||||
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect
|
||||
golang.org/x/tools v0.1.12 // indirect
|
||||
golang.org/x/tools v0.7.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
google.golang.org/appengine v1.6.6 // indirect
|
||||
google.golang.org/grpc v1.32.0 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
gopkg.in/ini.v1 v1.42.0 // indirect
|
||||
|
||||
344
machinery/go.sum
344
machinery/go.sum
@@ -1,6 +1,39 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
|
||||
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
|
||||
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
|
||||
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
|
||||
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
|
||||
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
||||
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
||||
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
|
||||
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
||||
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
||||
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/DataDog/datadog-agent/pkg/obfuscate v0.0.0-20211129110424-6491aa3bf583 h1:3nVO1nQyh64IUY6BPZUpMYMZ738Pu+LsMt3E0eqqIYw=
|
||||
github.com/DataDog/datadog-agent/pkg/obfuscate v0.0.0-20211129110424-6491aa3bf583/go.mod h1:EP9f4GqaDJyP1F5jTNMtzdIpw3JpNs3rMSJOnYywCiw=
|
||||
github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.42.0-rc.1 h1:Rmz52Xlc5k3WzAHzD0SCH4USCzyti7EbK4HtrHys3ME=
|
||||
@@ -31,8 +64,16 @@ github.com/appleboy/gin-jwt/v2 v2.9.1 h1:l29et8iLW6omcHltsOP6LLk4s3v4g2FbFs0koxG
|
||||
github.com/appleboy/gin-jwt/v2 v2.9.1/go.mod h1:jwcPZJ92uoC9nOUTOKWoN/f6JZOgMSKlFSHw5/FrRUk=
|
||||
github.com/appleboy/gofight/v2 v2.1.2 h1:VOy3jow4vIK8BRQJoC/I9muxyYlJ2yb9ht2hZoS3rf4=
|
||||
github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw=
|
||||
github.com/asticode/go-astikit v0.30.0 h1:DkBkRQRIxYcknlaU7W7ksNfn4gMFsB0tqMJflxkRsZA=
|
||||
github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
|
||||
github.com/asticode/go-astits v1.11.0 h1:GTHUXht0ZXAJXsVbsLIcyfHr1Bchi4QQwMARw2ZWAng=
|
||||
github.com/asticode/go-astits v1.11.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI=
|
||||
github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
|
||||
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
|
||||
github.com/bluenviron/gortsplib/v3 v3.6.1 h1:+/kPiwmdRwUasU5thOBATJQ4/yD+vrIEutJyRTB/f+0=
|
||||
github.com/bluenviron/gortsplib/v3 v3.6.1/go.mod h1:gc6Z8pBUMC9QBqYxcOY9eVxjDPOrmFcwVH61Xs3Gu2A=
|
||||
github.com/bluenviron/mediacommon v0.5.0 h1:YsVFlEknaXWhZGfz+Y1QbuzXLMVSmHODc7OnRqZoITY=
|
||||
github.com/bluenviron/mediacommon v0.5.0/go.mod h1:t0dqPsWUTchyvib0MhixIwXEgvDX4V9G+I0GzWLQRb8=
|
||||
github.com/cedricve/go-onvif v0.0.0-20200222191200-567e8ce298f6 h1:bzFZYgZD5vf4PWaa2GjOh90HG88uKi2a+B6VnQcDlCA=
|
||||
github.com/cedricve/go-onvif v0.0.0-20200222191200-567e8ce298f6/go.mod h1:nBrjN2nMHendp0Cvb/6GaJ1v92Qv/kzqxWtNBnKJEK0=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
@@ -58,6 +99,8 @@ github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/Lu
|
||||
github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug=
|
||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
|
||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dropbox/dropbox-sdk-go-unofficial/v6 v6.0.5 h1:FT+t0UEDykcor4y3dMVKXIiWJETBpRgERYTGlmMd7HU=
|
||||
github.com/dropbox/dropbox-sdk-go-unofficial/v6 v6.0.5/go.mod h1:rSS3kM9XMzSQ6pw91Qgd6yB5jdt70N4OdtrAf74As5M=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
|
||||
@@ -70,6 +113,7 @@ github.com/elastic/go-windows v1.0.0/go.mod h1:TsU0Nrp7/y3+VwE82FoZF8gC/XFg/Elz6
|
||||
github.com/elgs/gostrgen v0.0.0-20161222160715-9d61ae07eeae h1:3KvK2DmA7TxQ6PZ2f0rWbdqjgJhRcqgbY70bBeE4clI=
|
||||
github.com/elgs/gostrgen v0.0.0-20161222160715-9d61ae07eeae/go.mod h1:wruC5r2gHdr/JIUs5Rr1V45YtsAzKXZxAnn/5rPC97g=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y=
|
||||
@@ -93,6 +137,9 @@ github.com/gin-gonic/gin v1.6.2/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwv
|
||||
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
|
||||
github.com/gin-gonic/gin v1.8.2 h1:UzKToD9/PoFj/V4rvlKqTRKnQYyz8Sc1MJlv4JHPtvY=
|
||||
github.com/gin-gonic/gin v1.8.2/go.mod h1:qw5AYuDrzRTnhvusDsrov+fDIxp9Dleuu12h8nfB398=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
|
||||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
@@ -115,6 +162,8 @@ github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GO
|
||||
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
|
||||
github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ=
|
||||
github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
|
||||
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=
|
||||
@@ -127,41 +176,79 @@ github.com/golang-module/carbon/v2 v2.2.3 h1:WvGIc5+qzq9drNzH+Gnjh1TZ0JgDY/IA+m2
|
||||
github.com/golang-module/carbon/v2 v2.2.3/go.mod h1:LdzRApgmDT/wt0eNT8MEJbHfJdSqCtT46uZhfF30dqI=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20210423192551-a2663126120b h1:l2YRhr+YLzmSp7KJMswRVk/lO5SwoFIcCLzJsVj+YPc=
|
||||
github.com/google/pprof v0.0.0-20210423192551-a2663126120b/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 h1:rp+c0RAYOWj8l6qbCUTSiRLG/iKnW3K3/QfPPuSsBt4=
|
||||
@@ -171,14 +258,20 @@ github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFF
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
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.55 h1:P5RISBp8kUowgb/bvqLPVKPJL9n9jI/wXBCLs+XFMWg=
|
||||
github.com/kerberos-io/joy4 v1.0.55/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.63 h1:1VucVzE+WojpWWVKxpVpjmnArnZ2XIQSqn9Sldm6PrE=
|
||||
github.com/kerberos-io/joy4 v1.0.63/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=
|
||||
github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/klauspost/cpuid v1.2.3 h1:CCtW0xUnWGVINKvE/WWOYKdsPV6mawAtvQuSl8guwQs=
|
||||
github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
@@ -220,6 +313,7 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/nsmith5/mjpeg v0.0.0-20200913181537-54b8ada0e53e h1:bQo/jQ9qvcw7zqnovm8IbLsaOq3F+ELUQcxtxvalQvA=
|
||||
github.com/nsmith5/mjpeg v0.0.0-20200913181537-54b8ada0e53e/go.mod h1:PW9xCZScEClMBP22n37i0SnN/8B9YzNXTNvOaIkLjv0=
|
||||
@@ -236,8 +330,9 @@ github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
|
||||
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
|
||||
github.com/onsi/gomega v1.27.4 h1:Z2AnStgsdSayCMDiCU42qIz+HLqEPcgiOCXjAU/w+8E=
|
||||
github.com/onsi/gomega v1.27.4/go.mod h1:riYq/GJKh8hhoM01HN6Vmuy93AarCXCBGpvFDK3q3fQ=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
|
||||
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
|
||||
@@ -293,6 +388,7 @@ github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsK
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
@@ -300,6 +396,7 @@ github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5
|
||||
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
|
||||
github.com/richardartoul/molecule v1.0.1-0.20221107223329-32cfee06a052 h1:Qp27Idfgi6ACvFQat5+VJvlYToylpM/hcyLBI3WaKPA=
|
||||
github.com/richardartoul/molecule v1.0.1-0.20221107223329-32cfee06a052/go.mod h1:uvX/8buq8uVeiZiFht+0lqSLBHF+uGV8BrTv8W/SIwk=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
@@ -332,8 +429,8 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w=
|
||||
github.com/swaggo/files v1.0.0 h1:1gGXVIeUFCS/dta17rnP0iOpr6CXFwKD7EO5ID233e4=
|
||||
github.com/swaggo/files v1.0.0/go.mod h1:N59U6URJLyU1PQgFqPM7wXLMhJx7QAolnvfQkqO13kc=
|
||||
@@ -349,6 +446,7 @@ github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw=
|
||||
github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tinylib/msgp v1.1.6 h1:i+SbKraHhnrf9M5MYmvQhFnbLhAXSDWF8WWsuyRdocw=
|
||||
@@ -359,19 +457,42 @@ github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLY
|
||||
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
|
||||
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
||||
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
github.com/xdg-go/scram v1.0.2 h1:akYIkZ28e6A96dkWNJQu3nmCzH3YfwMPQExUYDaRv7w=
|
||||
github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
|
||||
github.com/xdg-go/stringprep v1.0.2 h1:6iq84/ryjjeRmMJwxutI51F2GIPlP5BfTvXHeYjyhBc=
|
||||
github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
|
||||
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA=
|
||||
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
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=
|
||||
go.mongodb.org/mongo-driver v1.7.5/go.mod h1:VXEWRZ6URJIkUq2SCAyapmhH0ZLRBP+FT4xhp5Zvxng=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go4.org/intern v0.0.0-20211027215823-ae77deb06f29 h1:UXLjNohABv4S58tHmeuIZDO6e3mHpW2Dx33gaNt03LE=
|
||||
go4.org/intern v0.0.0-20211027215823-ae77deb06f29/go.mod h1:cS2ma+47FKrLPdXFpr7CuxiTW3eyJbWew4qx0qtQWDA=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 h1:FyBZqvoA/jbNzuAWLQE2kG820zMAkcilx6BMjGbL/E4=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
@@ -382,24 +503,69 @@ golang.org/x/crypto v0.0.0-20221010152910-d6f0a8c073c2/go.mod h1:IxCIyHEi3zRg3s0
|
||||
golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8=
|
||||
golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
@@ -416,29 +582,62 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
|
||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||
golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
|
||||
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5 h1:Lm4OryKCca1vehdsWogr9N4t7NfZxLbJoc/H0w4K4S4=
|
||||
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -460,49 +659,150 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
|
||||
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 h1:GZokNIeuVkl3aZHJchRrr13WCsols02MLUcz1U9is6M=
|
||||
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
|
||||
golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
|
||||
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200726014623-da3ae01ef02d h1:HJaAqDnKreMkv+AQyf1Mcw0jEmL9kKBNL07RDJu1N/k=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
|
||||
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
||||
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987 h1:PDIOdWxZ8eRizhKa1AAvY53xsvLB1cWorMjslvY3VA8=
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.32.0 h1:zWTV+LMdc3kaiJMSTOFz2UgSBgx8RNQoTGiZu3fR9S0=
|
||||
google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
@@ -510,7 +810,11 @@ google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
@@ -529,8 +833,6 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/ini.v1 v1.42.0 h1:7N3gPTt50s8GuLortA00n8AqRTk75qOP98+mTPpgzRk=
|
||||
gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 h1:VpOs+IwYnYBaFnrNAeB8UUWtL3vEUnzSCL1nVjPhqrw=
|
||||
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
@@ -550,8 +852,16 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
howett.net/plist v0.0.0-20181124034731-591f970eefbb h1:jhnBjNi9UFpfpl8YZhA9CrOqpnJdvzuiHsl/dnxl11M=
|
||||
howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=
|
||||
inet.af/netaddr v0.0.0-20220617031823-097006376321 h1:B4dC8ySKTQXasnjDTMsoCMf1sQG4WsMej0WXaHxunmU=
|
||||
inet.af/netaddr v0.0.0-20220617031823-097006376321/go.mod h1:OIezDfdzOgFhuw4HuWapWq2e9l0H9tK4F1j+ETRtF3k=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"context"
|
||||
"flag"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
@@ -17,7 +20,6 @@ import (
|
||||
var VERSION = "3.0.0"
|
||||
|
||||
func main() {
|
||||
|
||||
// You might be interested in debugging the agent.
|
||||
if os.Getenv("DATADOG_AGENT_ENABLED") == "true" {
|
||||
if os.Getenv("DATADOG_AGENT_K8S_ENABLED") == "true" {
|
||||
@@ -26,7 +28,7 @@ func main() {
|
||||
} else {
|
||||
service := os.Getenv("DATADOG_AGENT_SERVICE")
|
||||
environment := os.Getenv("DATADOG_AGENT_ENVIRONMENT")
|
||||
fmt.Println("Starting Datadog Agent with service: " + service + " and environment: " + environment)
|
||||
log.Log.Info("Starting Datadog Agent with service: " + service + " and environment: " + environment)
|
||||
rules := []tracer.SamplingRule{tracer.RateRule(1)}
|
||||
tracer.Start(
|
||||
tracer.WithSamplingRules(rules),
|
||||
@@ -50,35 +52,54 @@ func main() {
|
||||
}
|
||||
|
||||
// Start the show ;)
|
||||
action := os.Args[1]
|
||||
// We'll parse the flags (named variables), and start the agent.
|
||||
|
||||
var action string
|
||||
var configDirectory string
|
||||
var name string
|
||||
var port string
|
||||
var timeout string
|
||||
|
||||
flag.StringVar(&action, "action", "version", "Tell us what you want do 'run' or 'version'")
|
||||
flag.StringVar(&configDirectory, "config", ".", "Where is the configuration stored")
|
||||
flag.StringVar(&name, "name", "agent", "Provide a name for the agent")
|
||||
flag.StringVar(&port, "port", "80", "On which port should the agent run")
|
||||
flag.StringVar(&timeout, "timeout", "2000", "Number of milliseconds to wait for the ONVIF discovery to complete")
|
||||
flag.Parse()
|
||||
|
||||
timezone, _ := time.LoadLocation("CET")
|
||||
log.Log.Init(timezone)
|
||||
log.Log.Init(configDirectory, timezone)
|
||||
|
||||
switch action {
|
||||
|
||||
case "version":
|
||||
log.Log.Info("You are currrently running Kerberos Agent " + VERSION)
|
||||
|
||||
case "pending-upload":
|
||||
name := os.Args[2]
|
||||
fmt.Println(name)
|
||||
|
||||
case "discover":
|
||||
timeout := os.Args[2]
|
||||
fmt.Println(timeout)
|
||||
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":
|
||||
{
|
||||
name := os.Args[2]
|
||||
port := os.Args[3]
|
||||
// Print Kerberos.io ASCII art
|
||||
utils.PrintASCIIArt()
|
||||
|
||||
// Check the folder permissions, it might be that we do not have permissions to write
|
||||
// recordings, update the configuration or save snapshots.
|
||||
err := utils.CheckDataDirectoryPermissions()
|
||||
if err != nil {
|
||||
log.Log.Fatal(err.Error())
|
||||
}
|
||||
// Print the environment variables which include "AGENT_" as prefix.
|
||||
utils.PrintEnvironmentVariables()
|
||||
|
||||
// Read the config on start, and pass it to the other
|
||||
// function and features. Please note that this might be changed
|
||||
@@ -88,21 +109,28 @@ func main() {
|
||||
configuration.Port = port
|
||||
|
||||
// Open this configuration either from Kerberos Agent or Kerberos Factory.
|
||||
components.OpenConfig(&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)
|
||||
|
||||
// Check the folder permissions, it might be that we do not have permissions to write
|
||||
// recordings, update the configuration or save snapshots.
|
||||
utils.CheckDataDirectoryPermissions(configDirectory)
|
||||
|
||||
// Set timezone
|
||||
timezone, _ := time.LoadLocation(configuration.Config.Timezone)
|
||||
log.Log.Init(timezone)
|
||||
log.Log.Init(configDirectory, timezone)
|
||||
|
||||
// Check if we have a device Key or not, if not
|
||||
// we will generate one.
|
||||
if configuration.Config.Key == "" {
|
||||
key := utils.RandStringBytesMaskImpr(30)
|
||||
configuration.Config.Key = key
|
||||
err := components.StoreConfig(configuration.Config)
|
||||
err := configService.StoreConfig(configDirectory, configuration.Config)
|
||||
if err == nil {
|
||||
log.Log.Info("Main: updated unique key for agent to: " + key)
|
||||
} else {
|
||||
@@ -110,16 +138,22 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// Create a cancelable context, which will be used to cancel and restart.
|
||||
// This is used to restart the agent when the configuration is updated.
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// Bootstrapping the agent
|
||||
communication := models.Communication{
|
||||
Context: &ctx,
|
||||
CancelContext: &cancel,
|
||||
HandleBootstrap: make(chan string, 1),
|
||||
}
|
||||
go components.Bootstrap(&configuration, &communication)
|
||||
go components.Bootstrap(configDirectory, &configuration, &communication)
|
||||
|
||||
// Start the REST API.
|
||||
routers.StartWebserver(&configuration, &communication)
|
||||
routers.StartWebserver(configDirectory, &configuration, &communication)
|
||||
}
|
||||
default:
|
||||
fmt.Println("Sorry I don't understand :(")
|
||||
log.Log.Error("Main: Sorry I don't understand :(")
|
||||
}
|
||||
}
|
||||
|
||||
1
machinery/src/api/onvif.go
Normal file
1
machinery/src/api/onvif.go
Normal file
@@ -0,0 +1 @@
|
||||
package api
|
||||
@@ -1,6 +1,7 @@
|
||||
package capture
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -15,9 +16,9 @@ import (
|
||||
"github.com/kerberos-io/joy4/format"
|
||||
)
|
||||
|
||||
func OpenRTSP(url string) (av.DemuxCloser, []av.CodecData, error) {
|
||||
func OpenRTSP(ctx context.Context, url string) (av.DemuxCloser, []av.CodecData, error) {
|
||||
format.RegisterAll()
|
||||
infile, err := avutil.Open(url)
|
||||
infile, err := avutil.Open(ctx, url)
|
||||
if err == nil {
|
||||
streams, errstreams := infile.Streams()
|
||||
return infile, streams, errstreams
|
||||
@@ -60,60 +61,6 @@ func DecodeImage(frame *ffmpeg.VideoFrame, pkt av.Packet, decoder *ffmpeg.VideoD
|
||||
return img, err
|
||||
}
|
||||
|
||||
func GetStreamInsights(infile av.DemuxCloser, streams []av.CodecData) (int, int, int, int) {
|
||||
var width, height, fps, gopsize int
|
||||
for _, stream := range streams {
|
||||
if stream.Type().IsAudio() {
|
||||
//astream := stream.(av.AudioCodecData)
|
||||
} else if stream.Type().IsVideo() {
|
||||
vstream := stream.(av.VideoCodecData)
|
||||
width = vstream.Width()
|
||||
height = vstream.Height()
|
||||
}
|
||||
}
|
||||
|
||||
loop:
|
||||
for timeout := time.After(1 * time.Second); ; {
|
||||
var err error
|
||||
if _, err = infile.ReadPacket(); err != nil { // sometimes this throws an end of file..
|
||||
log.Log.Error("HandleStream: " + err.Error())
|
||||
}
|
||||
fps++
|
||||
select {
|
||||
case <-timeout:
|
||||
break loop
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
gopCounter := 0
|
||||
start := false
|
||||
for {
|
||||
var pkt av.Packet
|
||||
var err error
|
||||
if pkt, err = infile.ReadPacket(); err != nil { // sometimes this throws an end of file..
|
||||
log.Log.Error("HandleStream: " + err.Error())
|
||||
}
|
||||
// Could be that a decode is throwing errors.
|
||||
if len(pkt.Data) > 0 {
|
||||
if start {
|
||||
gopCounter = gopCounter + 1
|
||||
}
|
||||
|
||||
if pkt.IsKeyFrame {
|
||||
if start == false {
|
||||
start = true
|
||||
} else {
|
||||
gopsize = gopCounter
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return width, height, fps, gopsize
|
||||
}
|
||||
|
||||
func HandleStream(infile av.DemuxCloser, queue *pubsub.Queue, communication *models.Communication) { //, wg *sync.WaitGroup) {
|
||||
|
||||
log.Log.Debug("HandleStream: started")
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
package capture
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strconv"
|
||||
"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"
|
||||
@@ -16,7 +18,7 @@ import (
|
||||
"github.com/kerberos-io/joy4/av"
|
||||
)
|
||||
|
||||
func CleanupRecordingDirectory(configuration *models.Configuration) {
|
||||
func CleanupRecordingDirectory(configDirectory string, configuration *models.Configuration) {
|
||||
autoClean := configuration.Config.AutoClean
|
||||
if autoClean == "true" {
|
||||
maxSize := configuration.Config.MaxDirectorySize
|
||||
@@ -24,7 +26,7 @@ func CleanupRecordingDirectory(configuration *models.Configuration) {
|
||||
maxSize = 300
|
||||
}
|
||||
// Total size of the recording directory.
|
||||
recordingsDirectory := "./data/recordings"
|
||||
recordingsDirectory := configDirectory + "/data/recordings"
|
||||
size, err := utils.DirSize(recordingsDirectory)
|
||||
if err == nil {
|
||||
sizeInMB := size / 1000 / 1000
|
||||
@@ -50,7 +52,7 @@ func CleanupRecordingDirectory(configuration *models.Configuration) {
|
||||
}
|
||||
}
|
||||
|
||||
func HandleRecordStream(queue *pubsub.Queue, configuration *models.Configuration, communication *models.Communication, streams []av.CodecData) {
|
||||
func HandleRecordStream(queue *pubsub.Queue, configDirectory string, configuration *models.Configuration, communication *models.Communication, streams []av.CodecData) {
|
||||
|
||||
config := configuration.Config
|
||||
|
||||
@@ -133,13 +135,13 @@ func HandleRecordStream(queue *pubsub.Queue, configuration *models.Configuration
|
||||
}
|
||||
|
||||
// Create a symbol link.
|
||||
fc, _ := os.Create("./data/cloud/" + name)
|
||||
fc, _ := os.Create(configDirectory + "/data/cloud/" + name)
|
||||
fc.Close()
|
||||
|
||||
recordingStatus = "idle"
|
||||
|
||||
// Clean up the recording directory if necessary.
|
||||
CleanupRecordingDirectory(configuration)
|
||||
CleanupRecordingDirectory(configDirectory, configuration)
|
||||
}
|
||||
|
||||
// If not yet started and a keyframe, let's make a recording
|
||||
@@ -191,7 +193,7 @@ func HandleRecordStream(queue *pubsub.Queue, configuration *models.Configuration
|
||||
"769"
|
||||
|
||||
name = s + ".mp4"
|
||||
fullName = "./data/recordings/" + name
|
||||
fullName = configDirectory + "/data/recordings/" + name
|
||||
|
||||
// Running...
|
||||
log.Log.Info("Recording started")
|
||||
@@ -258,7 +260,7 @@ func HandleRecordStream(queue *pubsub.Queue, configuration *models.Configuration
|
||||
}
|
||||
|
||||
// Create a symbol link.
|
||||
fc, _ := os.Create("./data/cloud/" + name)
|
||||
fc, _ := os.Create(configDirectory + "/data/cloud/" + name)
|
||||
fc.Close()
|
||||
|
||||
recordingStatus = "idle"
|
||||
@@ -314,7 +316,7 @@ func HandleRecordStream(queue *pubsub.Queue, configuration *models.Configuration
|
||||
"769"
|
||||
|
||||
name := s + ".mp4"
|
||||
fullName := "./data/recordings/" + name
|
||||
fullName := configDirectory + "/data/recordings/" + name
|
||||
|
||||
// Running...
|
||||
log.Log.Info("HandleRecordStream: Recording started")
|
||||
@@ -404,12 +406,33 @@ func HandleRecordStream(queue *pubsub.Queue, configuration *models.Configuration
|
||||
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("./data/cloud/" + name)
|
||||
fc, _ := os.Create(configDirectory + "/data/cloud/" + name)
|
||||
fc.Close()
|
||||
|
||||
// Clean up the recording directory if necessary.
|
||||
CleanupRecordingDirectory(configuration)
|
||||
CleanupRecordingDirectory(configDirectory, configuration)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -431,6 +454,10 @@ func VerifyCamera(c *gin.Context) {
|
||||
var cameraStreams models.CameraStreams
|
||||
err := c.BindJSON(&cameraStreams)
|
||||
|
||||
// Should return in 5 seconds.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err == nil {
|
||||
|
||||
streamType := c.Param("streamType")
|
||||
@@ -442,7 +469,7 @@ func VerifyCamera(c *gin.Context) {
|
||||
if streamType == "secondary" {
|
||||
rtspUrl = cameraStreams.SubRTSP
|
||||
}
|
||||
_, codecs, err := OpenRTSP(rtspUrl)
|
||||
_, codecs, err := OpenRTSP(ctx, rtspUrl)
|
||||
if err == nil {
|
||||
|
||||
videoIdx := -1
|
||||
|
||||
@@ -15,14 +15,12 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-module/carbon/v2"
|
||||
"github.com/kerberos-io/joy4/av/pubsub"
|
||||
"github.com/minio/minio-go/v6"
|
||||
|
||||
mqtt "github.com/eclipse/paho.mqtt.golang"
|
||||
av "github.com/kerberos-io/joy4/av"
|
||||
"github.com/kerberos-io/joy4/cgo/ffmpeg"
|
||||
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
@@ -34,8 +32,8 @@ import (
|
||||
"github.com/kerberos-io/agent/machinery/src/webrtc"
|
||||
)
|
||||
|
||||
func PendingUpload() {
|
||||
ff, err := utils.ReadDirectory("./data/cloud/")
|
||||
func PendingUpload(configDirectory string) {
|
||||
ff, err := utils.ReadDirectory(configDirectory + "/data/cloud/")
|
||||
if err == nil {
|
||||
for _, f := range ff {
|
||||
log.Log.Info(f.Name())
|
||||
@@ -43,21 +41,22 @@ func PendingUpload() {
|
||||
}
|
||||
}
|
||||
|
||||
func HandleUpload(configuration *models.Configuration, communication *models.Communication) {
|
||||
func HandleUpload(configDirectory string, configuration *models.Configuration, communication *models.Communication) {
|
||||
|
||||
log.Log.Debug("HandleUpload: started")
|
||||
|
||||
config := configuration.Config
|
||||
watchDirectory := "./data/cloud/"
|
||||
watchDirectory := configDirectory + "/data/cloud/"
|
||||
|
||||
if config.Offline == "true" {
|
||||
log.Log.Debug("HandleUpload: stopping as Offline is enabled.")
|
||||
} else {
|
||||
|
||||
// Half a second delay between two uploads
|
||||
delay := 500 * time.Millisecond
|
||||
|
||||
loop:
|
||||
for {
|
||||
ff, err := utils.ReadDirectory(watchDirectory)
|
||||
|
||||
// This will check if we need to stop the thread,
|
||||
// because of a reconfiguration.
|
||||
select {
|
||||
@@ -66,7 +65,10 @@ func HandleUpload(configuration *models.Configuration, communication *models.Com
|
||||
case <-time.After(2 * time.Second):
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
ff, err := utils.ReadDirectory(watchDirectory)
|
||||
if err != nil {
|
||||
log.Log.Error("HandleUpload: " + err.Error())
|
||||
} else {
|
||||
for _, f := range ff {
|
||||
|
||||
// This will check if we need to stop the thread,
|
||||
@@ -78,11 +80,66 @@ func HandleUpload(configuration *models.Configuration, communication *models.Com
|
||||
}
|
||||
|
||||
fileName := f.Name()
|
||||
if config.Cloud == "s3" {
|
||||
UploadS3(configuration, fileName, watchDirectory)
|
||||
} else if config.Cloud == "kstorage" {
|
||||
UploadKerberosVault(configuration, fileName, watchDirectory)
|
||||
uploaded := false
|
||||
configured := false
|
||||
err = nil
|
||||
if config.Cloud == "s3" || config.Cloud == "kerberoshub" {
|
||||
uploaded, configured, err = UploadKerberosHub(configuration, fileName)
|
||||
} else if config.Cloud == "kstorage" || config.Cloud == "kerberosvault" {
|
||||
uploaded, configured, err = UploadKerberosVault(configuration, fileName)
|
||||
} else if config.Cloud == "dropbox" {
|
||||
uploaded, configured, err = UploadDropbox(configuration, fileName)
|
||||
} else if config.Cloud == "gdrive" {
|
||||
// Todo: implement gdrive upload
|
||||
} else if config.Cloud == "onedrive" {
|
||||
// Todo: implement onedrive upload
|
||||
} else if config.Cloud == "minio" {
|
||||
// Todo: implement minio upload
|
||||
} else if config.Cloud == "webdav" {
|
||||
// Todo: implement webdav upload
|
||||
} else if config.Cloud == "ftp" {
|
||||
// Todo: implement ftp upload
|
||||
} else if config.Cloud == "sftp" {
|
||||
// Todo: implement sftp upload
|
||||
} else if config.Cloud == "aws" {
|
||||
// Todo: need to be updated, was previously used for hub.
|
||||
uploaded, configured, err = UploadS3(configuration, fileName)
|
||||
} else if config.Cloud == "azure" {
|
||||
// Todo: implement azure upload
|
||||
} else if config.Cloud == "google" {
|
||||
// Todo: implement google upload
|
||||
}
|
||||
// And so on... (have a look here -> https://github.com/kerberos-io/agent/issues/95)
|
||||
|
||||
// Check if the file is uploaded, if so, remove it.
|
||||
if uploaded {
|
||||
delay = 500 * time.Millisecond // reset
|
||||
err := os.Remove(watchDirectory + fileName)
|
||||
if err != nil {
|
||||
log.Log.Error("HandleUpload: " + err.Error())
|
||||
}
|
||||
|
||||
// Check if we need to remove the original recording
|
||||
// removeAfterUpload is set to false by default
|
||||
if config.RemoveAfterUpload != "false" {
|
||||
err := os.Remove(configDirectory + "/data/recordings/" + fileName)
|
||||
if err != nil {
|
||||
log.Log.Error("HandleUpload: " + err.Error())
|
||||
}
|
||||
}
|
||||
} else if !configured {
|
||||
err := os.Remove(watchDirectory + fileName)
|
||||
if err != nil {
|
||||
log.Log.Error("HandleUpload: " + err.Error())
|
||||
}
|
||||
} else {
|
||||
delay = 20 * time.Second // slow down
|
||||
if err != nil {
|
||||
log.Log.Error("HandleUpload: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(delay)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -165,36 +222,36 @@ func GetSystemInfo() (models.System, error) {
|
||||
func HandleHeartBeat(configuration *models.Configuration, communication *models.Communication, uptimeStart time.Time) {
|
||||
log.Log.Debug("HandleHeartBeat: started")
|
||||
|
||||
config := configuration.Config
|
||||
loop:
|
||||
for {
|
||||
|
||||
if config.Offline == "true" {
|
||||
log.Log.Debug("HandleHeartBeat: stopping as Offline is enabled.")
|
||||
} else {
|
||||
config := configuration.Config
|
||||
|
||||
url := config.HeartbeatURI
|
||||
key := ""
|
||||
username := ""
|
||||
vaultURI := ""
|
||||
if config.Offline == "true" {
|
||||
log.Log.Debug("HandleHeartBeat: stopping as Offline is enabled.")
|
||||
} else {
|
||||
|
||||
hubURI := config.HeartbeatURI
|
||||
key := ""
|
||||
username := ""
|
||||
vaultURI := ""
|
||||
|
||||
username = config.S3.Username
|
||||
if config.Cloud == "s3" && config.S3 != nil && config.S3.Publickey != "" {
|
||||
username = config.S3.Username
|
||||
key = config.S3.Publickey
|
||||
} else if config.Cloud == "kstorage" && config.KStorage != nil && config.KStorage.CloudKey != "" {
|
||||
key = config.KStorage.CloudKey
|
||||
username = config.KStorage.Directory
|
||||
}
|
||||
if config.Cloud == "s3" && config.S3 != nil && config.S3.Publickey != "" {
|
||||
username = config.S3.Username
|
||||
key = config.S3.Publickey
|
||||
} else if config.Cloud == "kstorage" && config.KStorage != nil && config.KStorage.CloudKey != "" {
|
||||
key = config.KStorage.CloudKey
|
||||
username = config.KStorage.Directory
|
||||
}
|
||||
|
||||
// This is the new way ;)
|
||||
if config.HubURI != "" {
|
||||
url = config.HubURI + "/devices/heartbeat"
|
||||
}
|
||||
if config.HubKey != "" {
|
||||
key = config.HubKey
|
||||
}
|
||||
|
||||
loop:
|
||||
for {
|
||||
// This is the new way ;)
|
||||
if config.HubURI != "" {
|
||||
hubURI = config.HubURI + "/devices/heartbeat"
|
||||
}
|
||||
if config.HubKey != "" {
|
||||
key = config.HubKey
|
||||
}
|
||||
|
||||
// Check if we have a friendly name or not.
|
||||
name := config.Name
|
||||
@@ -206,6 +263,26 @@ func HandleHeartBeat(configuration *models.Configuration, communication *models.
|
||||
// like the uptime, hostname, memory usage, etc.
|
||||
system, _ := GetSystemInfo()
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Congert to string
|
||||
macs, _ := json.Marshal(system.MACs)
|
||||
ips, _ := json.Marshal(system.IPs)
|
||||
cameraConnected := "true"
|
||||
if !communication.CameraConnected {
|
||||
cameraConnected = "false"
|
||||
}
|
||||
|
||||
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")
|
||||
@@ -219,89 +296,176 @@ func HandleHeartBeat(configuration *models.Configuration, communication *models.
|
||||
|
||||
// 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 != "" {
|
||||
device, err := onvif.ConnectToOnvifDevice(configuration)
|
||||
cameraConfiguration := configuration.Config.Capture.IPCamera
|
||||
device, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
|
||||
if err == nil {
|
||||
capabilities := onvif.GetCapabilitiesFromDevice(device)
|
||||
for _, v := range capabilities {
|
||||
if v == "PTZ" || v == "ptz" {
|
||||
onvifEnabled = "true"
|
||||
configurations, err := onvif.GetPTZConfigurationsFromDevice(device)
|
||||
if err == nil {
|
||||
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("[]")
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Congert to string
|
||||
macs, _ := json.Marshal(system.MACs)
|
||||
ips, _ := json.Marshal(system.IPs)
|
||||
|
||||
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",
|
||||
"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)
|
||||
|
||||
var jsonStr = []byte(object)
|
||||
buffy := bytes.NewBuffer(jsonStr)
|
||||
req, _ := http.NewRequest("POST", url, buffy)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if resp != nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
if err == nil && resp.StatusCode == 200 {
|
||||
communication.CloudTimestamp.Store(time.Now().Unix())
|
||||
log.Log.Info("HandleHeartBeat: (200) Heartbeat received by Kerberos Hub.")
|
||||
} else {
|
||||
log.Log.Error("HandleHeartBeat: (400) Something went wrong while sending to Kerberos Hub.")
|
||||
log.Log.Debug("HandleHeartBeat: ONVIF is not enabled.")
|
||||
onvifPresetsList = []byte("[]")
|
||||
}
|
||||
|
||||
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{}
|
||||
}
|
||||
|
||||
// We need a hub URI and hub public key before we will send a heartbeat
|
||||
if hubURI != "" && key != "" {
|
||||
|
||||
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",
|
||||
"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, onvifPresets, onvifPresetsList, cameraConnected, hasBackChannel)
|
||||
|
||||
var jsonStr = []byte(object)
|
||||
buffy := bytes.NewBuffer(jsonStr)
|
||||
req, _ := http.NewRequest("POST", hubURI, 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 {
|
||||
communication.CloudTimestamp.Store(time.Now().Unix())
|
||||
log.Log.Info("HandleHeartBeat: (200) Heartbeat received by Kerberos Hub.")
|
||||
} else {
|
||||
if communication.CloudTimestamp != nil && communication.CloudTimestamp.Load() != nil {
|
||||
communication.CloudTimestamp.Store(int64(0))
|
||||
}
|
||||
log.Log.Error("HandleHeartBeat: (400) Something went wrong while sending to Kerberos Hub.")
|
||||
}
|
||||
} 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 != "" {
|
||||
buffy = bytes.NewBuffer(jsonStr)
|
||||
req, _ = http.NewRequest("POST", vaultURI+"/devices/heartbeat", buffy)
|
||||
|
||||
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")
|
||||
|
||||
client = &http.Client{}
|
||||
resp, err = client.Do(req)
|
||||
resp, err := client.Do(req)
|
||||
if resp != nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
@@ -311,14 +475,14 @@ func HandleHeartBeat(configuration *models.Configuration, communication *models.
|
||||
log.Log.Error("HandleHeartBeat: (400) Something went wrong while sending to Kerberos Vault.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This will check if we need to stop the thread,
|
||||
// because of a reconfiguration.
|
||||
select {
|
||||
case <-communication.HandleHeartBeat:
|
||||
break loop
|
||||
case <-time.After(15 * time.Second):
|
||||
}
|
||||
// This will check if we need to stop the thread,
|
||||
// because of a reconfiguration.
|
||||
select {
|
||||
case <-communication.HandleHeartBeat:
|
||||
break loop
|
||||
case <-time.After(15 * time.Second):
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,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
|
||||
@@ -375,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.
|
||||
@@ -389,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
|
||||
@@ -410,31 +583,32 @@ func HandleLiveStreamHD(livestreamCursor *pubsub.QueueCursor, configuration *mod
|
||||
if config.Capture.Liveview != "false" {
|
||||
|
||||
// Should create a track here.
|
||||
track := webrtc.NewVideoTrack()
|
||||
go webrtc.WriteToTrack(livestreamCursor, configuration, communication, mqttClient, track, codecs, decoder, decoderMutex)
|
||||
videoTrack := webrtc.NewVideoTrack(codecs)
|
||||
audioTrack := webrtc.NewAudioTrack(codecs)
|
||||
go webrtc.WriteToTrack(livestreamCursor, configuration, communication, mqttClient, videoTrack, audioTrack, codecs, decoder, decoderMutex)
|
||||
|
||||
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
|
||||
key := config.Key + "/" + handshake.SessionID
|
||||
webrtc.CandidatesMutex.Lock()
|
||||
_, ok := webrtc.CandidateArrays[key]
|
||||
if !ok {
|
||||
webrtc.CandidateArrays[key] = make(chan string, 30)
|
||||
webrtc.CandidateArrays[key] = make(chan string)
|
||||
}
|
||||
webrtc.CandidatesMutex.Unlock()
|
||||
webrtc.InitializeWebRTCConnection(configuration, communication, mqttClient, track, handshake, webrtc.CandidateArrays[key])
|
||||
webrtc.InitializeWebRTCConnection(configuration, communication, mqttClient, videoTrack, audioTrack, handshake, webrtc.CandidateArrays[key])
|
||||
|
||||
}
|
||||
}
|
||||
@@ -463,15 +637,23 @@ func VerifyHub(c *gin.Context) {
|
||||
err := c.BindJSON(&config)
|
||||
|
||||
if err == nil {
|
||||
hubKey := config.HubKey
|
||||
hubURI := config.HubURI
|
||||
publicKey := config.HubKey
|
||||
privateKey := config.HubPrivateKey
|
||||
|
||||
content := []byte(`{"message": "fake-message"}`)
|
||||
body := bytes.NewReader(content)
|
||||
req, err := http.NewRequest("POST", hubURI+"/queue/test", body)
|
||||
req, err := http.NewRequest("POST", hubURI+"/subscription/verify", nil)
|
||||
if err == nil {
|
||||
req.Header.Set("X-Kerberos-Cloud-Key", hubKey)
|
||||
client := &http.Client{}
|
||||
req.Header.Set("X-Kerberos-Hub-PublicKey", publicKey)
|
||||
req.Header.Set("X-Kerberos-Hub-PrivateKey", privateKey)
|
||||
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 err == nil {
|
||||
@@ -519,96 +701,96 @@ func VerifyHub(c *gin.Context) {
|
||||
// @Summary Will verify the persistence.
|
||||
// @Description Will verify the persistence.
|
||||
// @Success 200 {object} models.APIResponse
|
||||
func VerifyPersistence(c *gin.Context) {
|
||||
func VerifyPersistence(c *gin.Context, configDirectory string) {
|
||||
|
||||
var config models.Config
|
||||
err := c.BindJSON(&config)
|
||||
if err != nil || config.Cloud != "" {
|
||||
|
||||
if config.Cloud == "s3" {
|
||||
if config.Cloud == "dropbox" {
|
||||
VerifyDropbox(config, c)
|
||||
} else if config.Cloud == "s3" || config.Cloud == "kerberoshub" {
|
||||
|
||||
// timestamp_microseconds_instanceName_regionCoordinates_numberOfChanges_token
|
||||
// 1564859471_6-474162_oprit_577-283-727-375_1153_27.mp4
|
||||
// - Timestamp
|
||||
// - Size + - + microseconds
|
||||
// - device
|
||||
// - Region
|
||||
// - Number of changes
|
||||
// - Token
|
||||
|
||||
aws_access_key_id := config.S3.Publickey
|
||||
aws_secret_access_key := config.S3.Secretkey
|
||||
aws_region := config.S3.Region
|
||||
|
||||
// This is the new way ;)
|
||||
if config.HubKey != "" {
|
||||
aws_access_key_id = config.HubKey
|
||||
}
|
||||
if config.HubPrivateKey != "" {
|
||||
aws_secret_access_key = config.HubPrivateKey
|
||||
}
|
||||
|
||||
s3Client, err := minio.NewWithRegion("s3.amazonaws.com", aws_access_key_id, aws_secret_access_key, true, aws_region)
|
||||
if err != nil {
|
||||
if config.HubURI == "" ||
|
||||
config.HubKey == "" ||
|
||||
config.HubPrivateKey == "" ||
|
||||
config.S3.Region == "" {
|
||||
msg := "VerifyPersistence: Kerberos Hub not properly configured."
|
||||
log.Log.Info(msg)
|
||||
c.JSON(400, models.APIResponse{
|
||||
Data: "Creation of Kerberos Hub connection failed: " + err.Error(),
|
||||
Data: msg,
|
||||
})
|
||||
} else {
|
||||
|
||||
// Check if we need to use the proxy.
|
||||
if config.S3.ProxyURI != "" {
|
||||
var transport http.RoundTripper = &http.Transport{
|
||||
Proxy: func(*http.Request) (*url.URL, error) {
|
||||
return url.Parse(config.S3.ProxyURI)
|
||||
},
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
s3Client.SetCustomTransport(transport)
|
||||
// Open 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)
|
||||
c.JSON(400, models.APIResponse{
|
||||
Data: msg,
|
||||
})
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
req, err := http.NewRequest("POST", config.HubURI+"/storage/upload", file)
|
||||
if err != nil {
|
||||
msg := "VerifyPersistence: error reading Kerberos Hub HEAD request, " + config.HubURI + "/storage: " + err.Error()
|
||||
log.Log.Error(msg)
|
||||
c.JSON(400, models.APIResponse{
|
||||
Data: msg,
|
||||
})
|
||||
}
|
||||
|
||||
deviceKey := "fake-key"
|
||||
devicename := "justatest"
|
||||
coordinates := "200-200-400-400"
|
||||
eventToken := "769"
|
||||
|
||||
timestamp := time.Now().Unix()
|
||||
fileName := strconv.FormatInt(timestamp, 10) + "_6-967003_justatest_200-200-400-400_24_769.mp4"
|
||||
content := []byte("test-file")
|
||||
body := bytes.NewReader(content)
|
||||
fileName := strconv.FormatInt(timestamp, 10) +
|
||||
"_6-967003_" + config.Name + "_200-200-400-400_24_769.mp4"
|
||||
req.Header.Set("X-Kerberos-Storage-FileName", fileName)
|
||||
req.Header.Set("X-Kerberos-Storage-Capture", "IPCamera")
|
||||
req.Header.Set("X-Kerberos-Storage-Device", config.Key)
|
||||
req.Header.Set("X-Kerberos-Hub-PublicKey", config.HubKey)
|
||||
req.Header.Set("X-Kerberos-Hub-PrivateKey", config.HubPrivateKey)
|
||||
req.Header.Set("X-Kerberos-Hub-Region", config.S3.Region)
|
||||
|
||||
n, err := s3Client.PutObject(config.S3.Bucket,
|
||||
config.S3.Username+"/"+fileName,
|
||||
body,
|
||||
body.Size(),
|
||||
minio.PutObjectOptions{
|
||||
ContentType: "video/mp4",
|
||||
StorageClass: "ONEZONE_IA",
|
||||
UserMetadata: map[string]string{
|
||||
"event-timestamp": strconv.FormatInt(timestamp, 10),
|
||||
"event-microseconds": deviceKey,
|
||||
"event-instancename": devicename,
|
||||
"event-regioncoordinates": coordinates,
|
||||
"event-numberofchanges": deviceKey,
|
||||
"event-token": eventToken,
|
||||
"productid": deviceKey,
|
||||
"publickey": aws_access_key_id,
|
||||
"uploadtime": "now",
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
c.JSON(400, models.APIResponse{
|
||||
Data: "Upload of fake recording failed: " + err.Error(),
|
||||
})
|
||||
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 {
|
||||
c.JSON(200, models.APIResponse{
|
||||
Data: "Upload Finished: file has been uploaded to bucket: " + strconv.FormatInt(n, 10),
|
||||
client = &http.Client{}
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if resp != nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
|
||||
if err == nil && resp != nil {
|
||||
if resp.StatusCode == 200 {
|
||||
msg := "VerifyPersistence: Upload allowed using the credentials provided (" + config.HubKey + ", " + config.HubPrivateKey + ")"
|
||||
log.Log.Info(msg)
|
||||
c.JSON(200, models.APIResponse{
|
||||
Data: msg,
|
||||
})
|
||||
} else {
|
||||
msg := "VerifyPersistence: Upload NOT allowed using the credentials provided (" + config.HubKey + ", " + config.HubPrivateKey + ")"
|
||||
log.Log.Info(msg)
|
||||
c.JSON(400, models.APIResponse{
|
||||
Data: msg,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
msg := "VerifyPersistence: Error creating Kerberos Hub request"
|
||||
log.Log.Info(msg)
|
||||
c.JSON(400, models.APIResponse{
|
||||
Data: msg,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if config.Cloud == "kstorage" {
|
||||
} else if config.Cloud == "kstorage" || config.Cloud == "kerberosvault" {
|
||||
|
||||
uri := config.KStorage.URI
|
||||
accessKey := config.KStorage.AccessKey
|
||||
@@ -617,10 +799,18 @@ func VerifyPersistence(c *gin.Context) {
|
||||
provider := config.KStorage.Provider
|
||||
|
||||
if err == nil && uri != "" && accessKey != "" && secretAccessKey != "" {
|
||||
var postData = []byte(`{"title":"Buy cheese and bread for breakfast."}`)
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("POST", uri+"/ping", bytes.NewReader(postData))
|
||||
|
||||
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{}
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", uri+"/ping", nil)
|
||||
req.Header.Add("X-Kerberos-Storage-AccessKey", accessKey)
|
||||
req.Header.Add("X-Kerberos-Storage-SecretAccessKey", secretAccessKey)
|
||||
resp, err := client.Do(req)
|
||||
@@ -632,33 +822,44 @@ func VerifyPersistence(c *gin.Context) {
|
||||
|
||||
if provider != "" || directory != "" {
|
||||
|
||||
hubKey := config.KStorage.CloudKey
|
||||
// This is the new way ;)
|
||||
if config.HubKey != "" {
|
||||
hubKey = config.HubKey
|
||||
}
|
||||
|
||||
// Generate a random name.
|
||||
timestamp := time.Now().Unix()
|
||||
fileName := strconv.FormatInt(timestamp, 10) +
|
||||
"_6-967003_justatest_200-200-400-400_24_769.mp4"
|
||||
content := []byte("test-file")
|
||||
body := bytes.NewReader(content)
|
||||
//fileSize := int64(len(content))
|
||||
"_6-967003_" + config.Name + "_200-200-400-400_24_769.mp4"
|
||||
|
||||
req, err := http.NewRequest("POST", uri+"/storage", body)
|
||||
// Open 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)
|
||||
c.JSON(400, models.APIResponse{
|
||||
Data: msg,
|
||||
})
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
req, err := http.NewRequest("POST", uri+"/storage", file)
|
||||
if err == nil {
|
||||
|
||||
req.Header.Set("Content-Type", "video/mp4")
|
||||
req.Header.Set("X-Kerberos-Storage-CloudKey", hubKey)
|
||||
req.Header.Set("X-Kerberos-Storage-CloudKey", config.HubKey)
|
||||
req.Header.Set("X-Kerberos-Storage-AccessKey", accessKey)
|
||||
req.Header.Set("X-Kerberos-Storage-SecretAccessKey", secretAccessKey)
|
||||
req.Header.Set("X-Kerberos-Storage-Provider", provider)
|
||||
req.Header.Set("X-Kerberos-Storage-FileName", fileName)
|
||||
req.Header.Set("X-Kerberos-Storage-Device", "test")
|
||||
req.Header.Set("X-Kerberos-Storage-Device", config.Key)
|
||||
req.Header.Set("X-Kerberos-Storage-Capture", "IPCamera")
|
||||
req.Header.Set("X-Kerberos-Storage-Directory", directory)
|
||||
client := &http.Client{}
|
||||
|
||||
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)
|
||||
|
||||
@@ -671,41 +872,45 @@ func VerifyPersistence(c *gin.Context) {
|
||||
c.JSON(200, body)
|
||||
} else {
|
||||
c.JSON(400, models.APIResponse{
|
||||
Data: "Something went wrong while verifying your persistence settings. Make sure your provider is the same as the storage provider in your Kerberos Vault, and the relevant storage provider is configured properly.",
|
||||
Data: "VerifyPersistence: Something went wrong while verifying your persistence settings. Make sure your provider is the same as the storage provider in your Kerberos Vault, and the relevant storage provider is configured properly.",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
c.JSON(400, models.APIResponse{
|
||||
Data: "Upload of fake recording failed: " + err.Error(),
|
||||
Data: "VerifyPersistence: Upload of fake recording failed: " + err.Error(),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
c.JSON(400, models.APIResponse{
|
||||
Data: "Something went wrong while creating /storage POST request." + err.Error(),
|
||||
Data: "VerifyPersistence: Something went wrong while creating /storage POST request." + err.Error(),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
c.JSON(400, models.APIResponse{
|
||||
Data: "Provider and/or directory is missing from the request.",
|
||||
Data: "VerifyPersistence: Provider and/or directory is missing from the request.",
|
||||
})
|
||||
}
|
||||
} else {
|
||||
c.JSON(400, models.APIResponse{
|
||||
Data: "Something went wrong while verifying storage credentials: " + string(body),
|
||||
Data: "VerifyPersistence: Something went wrong while verifying storage credentials: " + string(body),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
c.JSON(400, models.APIResponse{
|
||||
Data: "Something went wrong while verifying storage credentials:" + err.Error(),
|
||||
Data: "VerifyPersistence: Something went wrong while verifying storage credentials:" + err.Error(),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
c.JSON(400, models.APIResponse{
|
||||
Data: "VerifyPersistence: please fill-in the required Kerberos Vault credentials.",
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
c.JSON(400, models.APIResponse{
|
||||
Data: "No persistence was specified, so do not know what to verify:" + err.Error(),
|
||||
Data: "VerifyPersistence: No persistence was specified, so do not know what to verify:" + err.Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
135
machinery/src/cloud/Dropbox.go
Normal file
135
machinery/src/cloud/Dropbox.go
Normal file
@@ -0,0 +1,135 @@
|
||||
// Package cloud contains the Dropbox implementation of the Cloud interface.
|
||||
// It uses the Dropbox SDK to upload files to Dropbox.
|
||||
package cloud
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"os"
|
||||
|
||||
"github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox"
|
||||
"github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox/files"
|
||||
"github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox/users"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/kerberos-io/agent/machinery/src/log"
|
||||
"github.com/kerberos-io/agent/machinery/src/models"
|
||||
)
|
||||
|
||||
// UploadDropbox uploads the file to your Dropbox account using the access token and directory.
|
||||
func UploadDropbox(configuration *models.Configuration, fileName string) (bool, bool, error) {
|
||||
|
||||
config := configuration.Config
|
||||
token := config.Dropbox.AccessToken
|
||||
directory := config.Dropbox.Directory
|
||||
if directory != "" {
|
||||
// Check if trailing slash if not we'll add one.
|
||||
if directory[len(directory)-1:] != "/" {
|
||||
directory = directory + "/"
|
||||
}
|
||||
}
|
||||
|
||||
if token == "" {
|
||||
err := "UploadDropbox: Dropbox not properly configured"
|
||||
log.Log.Info(err)
|
||||
return false, true, errors.New(err)
|
||||
}
|
||||
|
||||
// Upload to Dropbox
|
||||
log.Log.Info("UploadDropbox: Uploading to Dropbox")
|
||||
log.Log.Info("UploadDropbox: Upload started for " + fileName)
|
||||
fullname := "data/recordings/" + fileName
|
||||
|
||||
dConfig := dropbox.Config{
|
||||
Token: token,
|
||||
LogLevel: dropbox.LogInfo, // if needed, set the desired logging level. Default is off
|
||||
}
|
||||
|
||||
file, err := os.OpenFile(fullname, os.O_RDWR, 0755)
|
||||
if file != nil {
|
||||
defer file.Close()
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
// Upload the file
|
||||
dbf := files.New(dConfig)
|
||||
res, err := dbf.Upload(&files.UploadArg{
|
||||
CommitInfo: files.CommitInfo{
|
||||
Path: "/" + directory + fileName,
|
||||
Mode: &files.WriteMode{
|
||||
Tagged: dropbox.Tagged{
|
||||
Tag: "overwrite",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, file)
|
||||
|
||||
if err != nil {
|
||||
log.Log.Error("UploadDropbox: Error uploading file: " + err.Error())
|
||||
return false, false, err
|
||||
}
|
||||
|
||||
log.Log.Info("UploadDropbox: File uploaded successfully, " + res.Name)
|
||||
return true, true, nil
|
||||
}
|
||||
|
||||
log.Log.Error("UploadDropbox: Error opening file: " + err.Error())
|
||||
return false, true, err
|
||||
}
|
||||
|
||||
// VerifyDropbox verifies if the Dropbox token is valid and it is able to upload a file.
|
||||
func VerifyDropbox(config models.Config, c *gin.Context) {
|
||||
|
||||
token := config.Dropbox.AccessToken
|
||||
directory := config.Dropbox.Directory
|
||||
if directory != "" {
|
||||
// Check if trailing slash if not we'll add one.
|
||||
if directory[len(directory)-1:] != "/" {
|
||||
directory = directory + "/"
|
||||
}
|
||||
}
|
||||
|
||||
if token != "" {
|
||||
dConfig := dropbox.Config{
|
||||
Token: token,
|
||||
LogLevel: dropbox.LogInfo, // if needed, set the desired logging level. Default is off
|
||||
}
|
||||
dbx := users.New(dConfig)
|
||||
_, err := dbx.GetCurrentAccount()
|
||||
if err != nil {
|
||||
c.JSON(400, models.APIResponse{
|
||||
Data: "Something went wrong while reaching the Dropbox API: " + err.Error(),
|
||||
})
|
||||
} else {
|
||||
|
||||
// Upload the file
|
||||
content := TestFile
|
||||
file := bytes.NewReader(content)
|
||||
|
||||
dbf := files.New(dConfig)
|
||||
_, err := dbf.Upload(&files.UploadArg{
|
||||
CommitInfo: files.CommitInfo{
|
||||
Path: "/" + directory + "kerbers-agent-test.mp4",
|
||||
Mode: &files.WriteMode{
|
||||
Tagged: dropbox.Tagged{
|
||||
Tag: "overwrite",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, file)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(400, models.APIResponse{
|
||||
Data: "Something went wrong while reaching the Dropbox API: " + err.Error(),
|
||||
})
|
||||
} else {
|
||||
c.JSON(200, models.APIResponse{
|
||||
Data: "Dropbox is working fine.",
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
c.JSON(400, models.APIResponse{
|
||||
Data: "Dropbox token is not set.",
|
||||
})
|
||||
}
|
||||
}
|
||||
131
machinery/src/cloud/KerberosHub.go
Normal file
131
machinery/src/cloud/KerberosHub.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package cloud
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/kerberos-io/agent/machinery/src/log"
|
||||
"github.com/kerberos-io/agent/machinery/src/models"
|
||||
)
|
||||
|
||||
func UploadKerberosHub(configuration *models.Configuration, fileName string) (bool, bool, error) {
|
||||
config := configuration.Config
|
||||
|
||||
if config.HubURI == "" ||
|
||||
config.HubKey == "" ||
|
||||
config.HubPrivateKey == "" ||
|
||||
config.S3.Region == "" {
|
||||
err := "UploadKerberosHub: Kerberos Hub not properly configured."
|
||||
log.Log.Info(err)
|
||||
return false, false, errors.New(err)
|
||||
}
|
||||
|
||||
// timestamp_microseconds_instanceName_regionCoordinates_numberOfChanges_token
|
||||
// 1564859471_6-474162_oprit_577-283-727-375_1153_27.mp4
|
||||
// - Timestamp
|
||||
// - Size + - + microseconds
|
||||
// - device
|
||||
// - Region
|
||||
// - Number of changes
|
||||
// - Token
|
||||
|
||||
log.Log.Info("UploadKerberosHub: Uploading to Kerberos Hub (" + config.HubURI + ")")
|
||||
log.Log.Info("UploadKerberosHub: Upload started for " + fileName)
|
||||
fullname := "data/recordings/" + fileName
|
||||
|
||||
// Check if we still have the file otherwise we abort the request.
|
||||
file, err := os.OpenFile(fullname, os.O_RDWR, 0755)
|
||||
if file != nil {
|
||||
defer file.Close()
|
||||
}
|
||||
if err != nil {
|
||||
err := "UploadKerberosHub: Upload Failed, file doesn't exists anymore."
|
||||
log.Log.Info(err)
|
||||
return false, false, errors.New(err)
|
||||
}
|
||||
|
||||
// Check if we are allowed to upload to the hub with these credentials.
|
||||
// There might be different reasons like (muted, read-only..)
|
||||
req, err := http.NewRequest("HEAD", config.HubURI+"/storage/upload", nil)
|
||||
if err != nil {
|
||||
errorMessage := "UploadKerberosHub: error reading HEAD request, " + config.HubURI + "/storage: " + err.Error()
|
||||
log.Log.Error(errorMessage)
|
||||
return false, true, errors.New(errorMessage)
|
||||
}
|
||||
|
||||
req.Header.Set("X-Kerberos-Storage-FileName", fileName)
|
||||
req.Header.Set("X-Kerberos-Storage-Capture", "IPCamera")
|
||||
req.Header.Set("X-Kerberos-Storage-Device", config.Key)
|
||||
req.Header.Set("X-Kerberos-Hub-PublicKey", config.HubKey)
|
||||
req.Header.Set("X-Kerberos-Hub-PrivateKey", config.HubPrivateKey)
|
||||
req.Header.Set("X-Kerberos-Hub-Region", config.S3.Region)
|
||||
|
||||
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 {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
if resp != nil {
|
||||
if err == nil {
|
||||
if resp.StatusCode == 200 {
|
||||
log.Log.Info("UploadKerberosHub: Upload allowed using the credentials provided (" + config.HubKey + ", " + config.HubPrivateKey + ")")
|
||||
} else {
|
||||
log.Log.Info("UploadKerberosHub: Upload NOT allowed using the credentials provided (" + config.HubKey + ", " + config.HubPrivateKey + ")")
|
||||
return false, true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now we know we are allowed to upload to the hub, we can start uploading.
|
||||
req, err = http.NewRequest("POST", config.HubURI+"/storage/upload", file)
|
||||
if err != nil {
|
||||
errorMessage := "UploadKerberosHub: error reading POST request, " + config.KStorage.URI + "/storage/upload: " + err.Error()
|
||||
log.Log.Error(errorMessage)
|
||||
return false, true, errors.New(errorMessage)
|
||||
}
|
||||
req.Header.Set("Content-Type", "video/mp4")
|
||||
req.Header.Set("X-Kerberos-Storage-FileName", fileName)
|
||||
req.Header.Set("X-Kerberos-Storage-Capture", "IPCamera")
|
||||
req.Header.Set("X-Kerberos-Storage-Device", config.Key)
|
||||
req.Header.Set("X-Kerberos-Hub-PublicKey", config.HubKey)
|
||||
req.Header.Set("X-Kerberos-Hub-PrivateKey", config.HubPrivateKey)
|
||||
req.Header.Set("X-Kerberos-Hub-Region", config.S3.Region)
|
||||
resp, err = client.Do(req)
|
||||
if resp != nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
if resp != nil {
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err == nil {
|
||||
if resp.StatusCode == 200 {
|
||||
log.Log.Info("UploadKerberosHub: Upload Finished, " + resp.Status + ".")
|
||||
return true, true, nil
|
||||
} else {
|
||||
log.Log.Info("UploadKerberosHub: Upload Failed, " + resp.Status + ", " + string(body))
|
||||
return false, true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
errorMessage := "UploadKerberosHub: Upload Failed, " + err.Error()
|
||||
log.Log.Info(errorMessage)
|
||||
return false, true, errors.New(errorMessage)
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package cloud
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -9,19 +11,19 @@ import (
|
||||
"github.com/kerberos-io/agent/machinery/src/models"
|
||||
)
|
||||
|
||||
func UploadKerberosVault(configuration *models.Configuration, fileName string, directory string) bool {
|
||||
func UploadKerberosVault(configuration *models.Configuration, fileName string) (bool, bool, error) {
|
||||
|
||||
config := configuration.Config
|
||||
|
||||
if config.KStorage.AccessKey == "" ||
|
||||
config.KStorage.SecretAccessKey == "" ||
|
||||
config.KStorage.Provider == "" ||
|
||||
config.KStorage.Directory == "" ||
|
||||
config.KStorage.URI == "" {
|
||||
log.Log.Info("UploadKerberosVault: Kerberos Vault not properly configured.")
|
||||
err := "UploadKerberosVault: Kerberos Vault not properly configured."
|
||||
log.Log.Info(err)
|
||||
return false, false, errors.New(err)
|
||||
}
|
||||
|
||||
//fmt.Println("Uploading...")
|
||||
// timestamp_microseconds_instanceName_regionCoordinates_numberOfChanges_token
|
||||
// 1564859471_6-474162_oprit_577-283-727-375_1153_27.mp4
|
||||
// - Timestamp
|
||||
@@ -30,21 +32,20 @@ func UploadKerberosVault(configuration *models.Configuration, fileName string, d
|
||||
// - Region
|
||||
// - Number of changes
|
||||
// - Token
|
||||
|
||||
// KerberosCloud, this means storage is disabled and proxy enabled.
|
||||
log.Log.Info("UploadKerberosVault: Uploading to Kerberos Vault (" + config.KStorage.URI + ")")
|
||||
|
||||
log.Log.Info("UploadKerberosVault: Upload started for " + fileName)
|
||||
fullname := "data/recordings/" + fileName
|
||||
|
||||
file, err := os.OpenFile(fullname, os.O_RDWR, 0755)
|
||||
if err != nil {
|
||||
log.Log.Info("UploadKerberosVault: Upload Failed, file doesn't exists anymore.")
|
||||
os.Remove(directory + "/" + fileName)
|
||||
return false
|
||||
if file != nil {
|
||||
defer file.Close()
|
||||
}
|
||||
if err != nil {
|
||||
err := "UploadKerberosVault: Upload Failed, file doesn't exists anymore."
|
||||
log.Log.Info(err)
|
||||
return false, false, errors.New(err)
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
publicKey := config.KStorage.CloudKey
|
||||
// This is the new way ;)
|
||||
@@ -54,7 +55,9 @@ func UploadKerberosVault(configuration *models.Configuration, fileName string, d
|
||||
|
||||
req, err := http.NewRequest("POST", config.KStorage.URI+"/storage", file)
|
||||
if err != nil {
|
||||
log.Log.Error("Error reading request. " + err.Error())
|
||||
errorMessage := "UploadKerberosVault: error reading request, " + config.KStorage.URI + "/storage: " + err.Error()
|
||||
log.Log.Error(errorMessage)
|
||||
return false, true, errors.New(errorMessage)
|
||||
}
|
||||
req.Header.Set("Content-Type", "video/mp4")
|
||||
req.Header.Set("X-Kerberos-Storage-CloudKey", publicKey)
|
||||
@@ -65,11 +68,18 @@ func UploadKerberosVault(configuration *models.Configuration, fileName string, d
|
||||
req.Header.Set("X-Kerberos-Storage-Device", config.Key)
|
||||
req.Header.Set("X-Kerberos-Storage-Capture", "IPCamera")
|
||||
req.Header.Set("X-Kerberos-Storage-Directory", config.KStorage.Directory)
|
||||
//client := &http.Client{Timeout: time.Second * 30}
|
||||
client := &http.Client{}
|
||||
|
||||
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 {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
@@ -80,17 +90,16 @@ func UploadKerberosVault(configuration *models.Configuration, fileName string, d
|
||||
if err == nil {
|
||||
if resp.StatusCode == 200 {
|
||||
log.Log.Info("UploadKerberosVault: Upload Finished, " + resp.Status + ", " + string(body))
|
||||
// We will remove the file from disk as well
|
||||
os.Remove(fullname)
|
||||
os.Remove(directory + "/" + fileName)
|
||||
return true, true, nil
|
||||
} else {
|
||||
log.Log.Info("UploadKerberosVault: Upload Failed, " + resp.Status + ", " + string(body))
|
||||
return false, true, nil
|
||||
}
|
||||
resp.Body.Close()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Log.Info("UploadKerberosVault: Upload Failed, " + err.Error())
|
||||
}
|
||||
return true
|
||||
|
||||
errorMessage := "UploadKerberosVault: Upload Failed, " + err.Error()
|
||||
log.Log.Info(errorMessage)
|
||||
return false, true, errors.New(errorMessage)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package cloud
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -13,11 +14,10 @@ import (
|
||||
"github.com/minio/minio-go/v6"
|
||||
)
|
||||
|
||||
func UploadS3(configuration *models.Configuration, fileName string, directory string) bool {
|
||||
func UploadS3(configuration *models.Configuration, fileName string) (bool, bool, error) {
|
||||
|
||||
config := configuration.Config
|
||||
|
||||
//fmt.Println("Uploading...")
|
||||
// timestamp_microseconds_instanceName_regionCoordinates_numberOfChanges_token
|
||||
// 1564859471_6-474162_oprit_577-283-727-375_1153_27.mp4
|
||||
// - Timestamp
|
||||
@@ -28,10 +28,12 @@ func UploadS3(configuration *models.Configuration, fileName string, directory st
|
||||
// - Token
|
||||
|
||||
if config.S3 == nil {
|
||||
log.Log.Error("UploadS3: Uploading Failed, as no settings found")
|
||||
return false
|
||||
errorMessage := "UploadS3: Uploading Failed, as no settings found"
|
||||
log.Log.Error(errorMessage)
|
||||
return false, false, errors.New(errorMessage)
|
||||
}
|
||||
|
||||
// Legacy support, should get rid of it!
|
||||
aws_access_key_id := config.S3.Publickey
|
||||
aws_secret_access_key := config.S3.Secretkey
|
||||
aws_region := config.S3.Region
|
||||
@@ -44,9 +46,18 @@ func UploadS3(configuration *models.Configuration, fileName string, directory st
|
||||
aws_secret_access_key = config.HubPrivateKey
|
||||
}
|
||||
|
||||
// Check if we have some credentials otherwise we abort the request.
|
||||
if aws_access_key_id == "" || aws_secret_access_key == "" {
|
||||
errorMessage := "UploadS3: Uploading Failed, as no credentials found"
|
||||
log.Log.Error(errorMessage)
|
||||
return false, false, errors.New(errorMessage)
|
||||
}
|
||||
|
||||
s3Client, err := minio.NewWithRegion("s3.amazonaws.com", aws_access_key_id, aws_secret_access_key, true, aws_region)
|
||||
if err != nil {
|
||||
log.Log.Error(err.Error())
|
||||
errorMessage := "UploadS3: " + err.Error()
|
||||
log.Log.Error(errorMessage)
|
||||
return false, true, errors.New(errorMessage)
|
||||
}
|
||||
|
||||
// Check if we need to use the proxy.
|
||||
@@ -62,9 +73,9 @@ func UploadS3(configuration *models.Configuration, fileName string, directory st
|
||||
|
||||
fileParts := strings.Split(fileName, "_")
|
||||
if len(fileParts) == 1 {
|
||||
log.Log.Error("ERROR: " + fileName + " is not a valid name.")
|
||||
os.Remove(directory + "/" + fileName)
|
||||
return false
|
||||
errorMessage := "UploadS3: " + fileName + " is not a valid name."
|
||||
log.Log.Error(errorMessage)
|
||||
return false, true, errors.New(errorMessage)
|
||||
}
|
||||
|
||||
deviceKey := config.Key
|
||||
@@ -78,18 +89,21 @@ func UploadS3(configuration *models.Configuration, fileName string, directory st
|
||||
fullname := "data/recordings/" + fileName
|
||||
|
||||
file, err := os.OpenFile(fullname, os.O_RDWR, 0755)
|
||||
defer file.Close()
|
||||
if file != nil {
|
||||
defer file.Close()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Log.Error("UploadS3: " + err.Error())
|
||||
os.Remove(directory + "/" + fileName)
|
||||
return false
|
||||
errorMessage := "UploadS3: " + err.Error()
|
||||
log.Log.Error(errorMessage)
|
||||
return false, true, errors.New(errorMessage)
|
||||
}
|
||||
|
||||
fileInfo, err := file.Stat()
|
||||
if err != nil {
|
||||
log.Log.Error("UploadS3: " + err.Error())
|
||||
os.Remove(directory + "/" + fileName)
|
||||
return false
|
||||
errorMessage := "UploadS3: " + err.Error()
|
||||
log.Log.Error(errorMessage)
|
||||
return false, true, errors.New(errorMessage)
|
||||
}
|
||||
|
||||
n, err := s3Client.PutObject(config.S3.Bucket,
|
||||
@@ -113,11 +127,11 @@ func UploadS3(configuration *models.Configuration, fileName string, directory st
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Log.Error("UploadS3: Uploading Failed, " + err.Error())
|
||||
return false
|
||||
errorMessage := "UploadS3: Uploading Failed, " + err.Error()
|
||||
log.Log.Error(errorMessage)
|
||||
return false, true, errors.New(errorMessage)
|
||||
} else {
|
||||
log.Log.Info("UploadS3: Upload Finished, file has been uploaded to bucket: " + strconv.FormatInt(n, 10))
|
||||
os.Remove(directory + "/" + fileName)
|
||||
return true
|
||||
return true, true, nil
|
||||
}
|
||||
}
|
||||
|
||||
4
machinery/src/cloud/TestFile.go
Normal file
4
machinery/src/cloud/TestFile.go
Normal file
File diff suppressed because one or more lines are too long
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)
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,22 @@
|
||||
package components
|
||||
|
||||
import (
|
||||
"context"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
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"
|
||||
@@ -21,7 +26,7 @@ import (
|
||||
"github.com/tevino/abool"
|
||||
)
|
||||
|
||||
func Bootstrap(configuration *models.Configuration, communication *models.Communication) {
|
||||
func Bootstrap(configDirectory string, configuration *models.Configuration, communication *models.Communication) {
|
||||
log.Log.Debug("Bootstrap: started")
|
||||
|
||||
// We will keep track of the Kerberos Agent up time
|
||||
@@ -53,6 +58,7 @@ func Bootstrap(configuration *models.Configuration, communication *models.Commun
|
||||
communication.HandleLiveSD = make(chan int64, 1)
|
||||
communication.HandleLiveHDKeepalive = make(chan string, 1)
|
||||
communication.HandleLiveHDPeers = make(chan string, 1)
|
||||
communication.HandleONVIF = make(chan models.OnvifAction, 1)
|
||||
communication.IsConfiguring = abool.New()
|
||||
|
||||
// Before starting the agent, we have a control goroutine, that might
|
||||
@@ -64,31 +70,76 @@ func Bootstrap(configuration *models.Configuration, communication *models.Commun
|
||||
subDecoder := &ffmpeg.VideoDecoder{}
|
||||
cameraSettings := &models.Camera{}
|
||||
|
||||
// Handle heartbeats
|
||||
go cloud.HandleHeartBeat(configuration, communication, uptimeStart)
|
||||
|
||||
// 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(configDirectory, configuration, communication)
|
||||
|
||||
// Run the agent and fire up all the other
|
||||
// goroutines which do image capture, motion detection, onvif, etc.
|
||||
|
||||
for {
|
||||
|
||||
// This will blocking until receiving a signal to be restarted, reconfigured, stopped, etc.
|
||||
status := RunAgent(configuration, communication, uptimeStart, cameraSettings, decoder, subDecoder)
|
||||
status := RunAgent(configDirectory, configuration, communication, mqttClient, uptimeStart, cameraSettings, decoder, subDecoder)
|
||||
|
||||
if status == "stop" {
|
||||
break
|
||||
}
|
||||
// We will re open the configuration, might have changed :O!
|
||||
OpenConfig(configuration)
|
||||
|
||||
if status == "not started" {
|
||||
// We will re open the configuration, might have changed :O!
|
||||
configService.OpenConfig(configDirectory, configuration)
|
||||
// We will override the configuration with the environment variables
|
||||
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(configDirectory, configuration, communication)
|
||||
}
|
||||
|
||||
// We will create a new cancelable context, which will be used to cancel and restart.
|
||||
// This is used to restart the agent when the configuration is updated.
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
communication.Context = &ctx
|
||||
communication.CancelContext = &cancel
|
||||
}
|
||||
log.Log.Debug("Bootstrap: finished")
|
||||
}
|
||||
|
||||
func RunAgent(configuration *models.Configuration, communication *models.Communication, uptimeStart time.Time, cameraSettings *models.Camera, decoder *ffmpeg.VideoDecoder, subDecoder *ffmpeg.VideoDecoder) string {
|
||||
log.Log.Debug("RunAgent: started")
|
||||
func RunAgent(configDirectory string, configuration *models.Configuration, communication *models.Communication, mqttClient mqtt.Client, uptimeStart time.Time, cameraSettings *models.Camera, decoder *ffmpeg.VideoDecoder, subDecoder *ffmpeg.VideoDecoder) string {
|
||||
|
||||
log.Log.Debug("RunAgent: bootstrapping agent")
|
||||
config := configuration.Config
|
||||
|
||||
status := "not started"
|
||||
|
||||
// Currently only support H264 encoded cameras, this will change.
|
||||
// Establishing the camera connection
|
||||
log.Log.Info("RunAgent: opening RTSP stream")
|
||||
rtspUrl := config.Capture.IPCamera.RTSP
|
||||
infile, streams, err := capture.OpenRTSP(rtspUrl)
|
||||
infile, streams, err := capture.OpenRTSP(context.Background(), rtspUrl)
|
||||
|
||||
// We will initialise the camera settings object
|
||||
// so we can check if the camera settings have changed, and we need
|
||||
// to reload the decoders.
|
||||
|
||||
videoStream, _ := capture.GetVideoStream(streams)
|
||||
if videoStream == nil {
|
||||
log.Log.Error("RunAgent: no video stream found, might be the wrong codec (we only support H264 for the moment)")
|
||||
time.Sleep(time.Second * 3)
|
||||
return status
|
||||
}
|
||||
|
||||
num, denum := videoStream.(av.VideoCodecData).Framerate()
|
||||
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
|
||||
@@ -96,11 +147,9 @@ func RunAgent(configuration *models.Configuration, communication *models.Communi
|
||||
var decoderMutex sync.Mutex
|
||||
var subDecoderMutex sync.Mutex
|
||||
|
||||
status := "not started"
|
||||
|
||||
if err == nil {
|
||||
|
||||
log.Log.Info("RunAgent: opened RTSP stream")
|
||||
log.Log.Info("RunAgent: opened RTSP stream: " + rtspUrl)
|
||||
|
||||
// We might have a secondary rtsp url, so we might need to use that.
|
||||
var subInfile av.DemuxCloser
|
||||
@@ -108,24 +157,30 @@ func RunAgent(configuration *models.Configuration, communication *models.Communi
|
||||
subStreamEnabled := false
|
||||
subRtspUrl := config.Capture.IPCamera.SubRTSP
|
||||
if subRtspUrl != "" && subRtspUrl != rtspUrl {
|
||||
subInfile, subStreams, err = capture.OpenRTSP(subRtspUrl)
|
||||
subInfile, subStreams, err = capture.OpenRTSP(context.Background(), subRtspUrl)
|
||||
if err == nil {
|
||||
log.Log.Info("RunAgent: opened RTSP sub stream")
|
||||
log.Log.Info("RunAgent: opened RTSP sub stream " + subRtspUrl)
|
||||
subStreamEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
// We will initialise the camera settings object
|
||||
// so we can check if the camera settings have changed, and we need
|
||||
// to reload the decoders.
|
||||
videoStream, _ := capture.GetVideoStream(streams)
|
||||
num, denum := videoStream.(av.VideoCodecData).Framerate()
|
||||
width := videoStream.(av.VideoCodecData).Width()
|
||||
height := videoStream.(av.VideoCodecData).Height()
|
||||
videoStream, _ := capture.GetVideoStream(subStreams)
|
||||
if videoStream == nil {
|
||||
log.Log.Error("RunAgent: no video substream found, might be the wrong codec (we only support H264 for the moment)")
|
||||
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()
|
||||
@@ -149,6 +204,7 @@ func RunAgent(configuration *models.Configuration, communication *models.Communi
|
||||
cameraSettings.Denum = denum
|
||||
cameraSettings.Codec = videoStream.(av.VideoCodecData).Type()
|
||||
cameraSettings.Initialized = true
|
||||
|
||||
} else {
|
||||
log.Log.Info("RunAgent: camera settings did not change, keeping decoder")
|
||||
}
|
||||
@@ -182,13 +238,6 @@ func RunAgent(configuration *models.Configuration, communication *models.Communi
|
||||
subQueue.WriteHeader(subStreams)
|
||||
}
|
||||
|
||||
// Configure a MQTT client which helps for a bi-directional communication
|
||||
communication.HandleONVIF = make(chan models.OnvifAction, 1)
|
||||
mqttClient := routers.ConfigureMQTT(configuration, communication)
|
||||
|
||||
// Handle heartbeats
|
||||
go cloud.HandleHeartBeat(configuration, communication, uptimeStart)
|
||||
|
||||
// Handle the camera stream
|
||||
go capture.HandleStream(infile, queue, communication)
|
||||
|
||||
@@ -197,6 +246,9 @@ func RunAgent(configuration *models.Configuration, communication *models.Communi
|
||||
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 {
|
||||
@@ -217,7 +269,7 @@ func RunAgent(configuration *models.Configuration, communication *models.Communi
|
||||
}
|
||||
|
||||
// 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)
|
||||
@@ -227,29 +279,49 @@ func RunAgent(configuration *models.Configuration, communication *models.Communi
|
||||
}
|
||||
|
||||
// Handle recording, will write an mp4 to disk.
|
||||
go capture.HandleRecordStream(queue, configuration, communication, streams)
|
||||
go capture.HandleRecordStream(queue, configDirectory, configuration, communication, streams)
|
||||
|
||||
// Handle Upload to cloud provider (Kerberos Hub, Kerberos Vault and others)
|
||||
go cloud.HandleUpload(configuration, communication)
|
||||
go cloud.HandleUpload(configDirectory, configuration, communication)
|
||||
|
||||
// Handle ONVIF actions
|
||||
go onvif.HandleONVIFActions(configuration, communication)
|
||||
|
||||
// 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.
|
||||
|
||||
status = <-communication.HandleBootstrap
|
||||
|
||||
// If we reach this point, we are stopping the stream.
|
||||
communication.CameraConnected = false
|
||||
|
||||
// Cancel the main context, this will stop all the other goroutines.
|
||||
(*communication.CancelContext)()
|
||||
|
||||
// We will re open the configuration, might have changed :O!
|
||||
configService.OpenConfig(configDirectory, configuration)
|
||||
|
||||
// We will override the configuration with the environment variables
|
||||
configService.OverrideWithEnvironmentVariables(configuration)
|
||||
|
||||
// Here we are cleaning up everything!
|
||||
if configuration.Config.Offline != "true" {
|
||||
communication.HandleHeartBeat <- "stop"
|
||||
communication.HandleUpload <- "stop"
|
||||
}
|
||||
communication.HandleStream <- "stop"
|
||||
if subStreamEnabled {
|
||||
communication.HandleSubStream <- "stop"
|
||||
}
|
||||
time.Sleep(time.Second * 1)
|
||||
|
||||
time.Sleep(time.Second * 3)
|
||||
|
||||
infile.Close()
|
||||
infile = nil
|
||||
@@ -263,22 +335,15 @@ func RunAgent(configuration *models.Configuration, communication *models.Communi
|
||||
subQueue = nil
|
||||
communication.SubQueue = nil
|
||||
}
|
||||
close(communication.HandleONVIF)
|
||||
communication.HandleONVIF = nil
|
||||
close(communication.HandleLiveHDHandshake)
|
||||
communication.HandleLiveHDHandshake = nil
|
||||
close(communication.HandleMotion)
|
||||
communication.HandleMotion = nil
|
||||
|
||||
// Disconnect MQTT
|
||||
routers.DisconnectMQTT(mqttClient, &configuration.Config)
|
||||
|
||||
// Wait a few seconds to stop the decoder.
|
||||
time.Sleep(time.Second * 3)
|
||||
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.")
|
||||
time.Sleep(time.Second * 3)
|
||||
|
||||
} else {
|
||||
log.Log.Error("Something went wrong while opening RTSP: " + err.Error())
|
||||
time.Sleep(time.Second * 3)
|
||||
@@ -300,29 +365,32 @@ func ControlAgent(communication *models.Communication) {
|
||||
var previousPacket int64 = 0
|
||||
var occurence = 0
|
||||
for {
|
||||
packetsR := packageCounter.Load().(int64)
|
||||
if packetsR == previousPacket {
|
||||
// If we are already reconfiguring,
|
||||
// we dont need to check if the stream is blocking.
|
||||
if !communication.IsConfiguring.IsSet() {
|
||||
occurence = occurence + 1
|
||||
|
||||
// If camera is connected, we'll check if we are still receiving packets.
|
||||
if communication.CameraConnected {
|
||||
packetsR := packageCounter.Load().(int64)
|
||||
if packetsR == previousPacket {
|
||||
// If we are already reconfiguring,
|
||||
// we dont need to check if the stream is blocking.
|
||||
if !communication.IsConfiguring.IsSet() {
|
||||
occurence = occurence + 1
|
||||
}
|
||||
} else {
|
||||
occurence = 0
|
||||
}
|
||||
} else {
|
||||
|
||||
occurence = 0
|
||||
log.Log.Info("ControlAgent: Number of packets read " + strconv.FormatInt(packetsR, 10))
|
||||
|
||||
// After 15 seconds without activity this is thrown..
|
||||
if occurence == 3 {
|
||||
log.Log.Info("Main: Restarting machinery.")
|
||||
communication.HandleBootstrap <- "restart"
|
||||
time.Sleep(2 * time.Second)
|
||||
occurence = 0
|
||||
}
|
||||
previousPacket = packageCounter.Load().(int64)
|
||||
}
|
||||
|
||||
log.Log.Info("ControlAgent: Number of packets read " + strconv.FormatInt(packetsR, 10))
|
||||
|
||||
// After 15 seconds without activity this is thrown..
|
||||
if occurence == 3 {
|
||||
log.Log.Info("Main: Restarting machinery.")
|
||||
communication.HandleBootstrap <- "restart"
|
||||
time.Sleep(2 * time.Second)
|
||||
occurence = 0
|
||||
}
|
||||
previousPacket = packageCounter.Load().(int64)
|
||||
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -66,7 +66,6 @@ func (s Stream) GetCodecs() []av.CodecData {
|
||||
|
||||
func (s Stream) ReadPackets(packetChannel chan av.Packet) {
|
||||
session := s.Open()
|
||||
fmt.Println("Start reading H264 packages from stream")
|
||||
for {
|
||||
packet, err := session.ReadPacket()
|
||||
if err != nil {
|
||||
|
||||
@@ -10,15 +10,12 @@ import (
|
||||
"time"
|
||||
|
||||
mqtt "github.com/eclipse/paho.mqtt.golang"
|
||||
geo "github.com/kellydunn/golang-geo"
|
||||
"github.com/kerberos-io/agent/machinery/src/capture"
|
||||
"github.com/kerberos-io/agent/machinery/src/log"
|
||||
"github.com/kerberos-io/agent/machinery/src/models"
|
||||
"github.com/kerberos-io/joy4/av/pubsub"
|
||||
|
||||
//"github.com/whorfin/go-libjpeg/jpeg"
|
||||
|
||||
geo "github.com/kellydunn/golang-geo"
|
||||
"github.com/kerberos-io/joy4/av"
|
||||
"github.com/kerberos-io/joy4/av/pubsub"
|
||||
"github.com/kerberos-io/joy4/cgo/ffmpeg"
|
||||
)
|
||||
|
||||
@@ -44,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()
|
||||
@@ -77,18 +75,21 @@ func ProcessMotion(motionCursor *pubsub.QueueCursor, configuration *models.Confi
|
||||
|
||||
// Calculate mask
|
||||
var polyObjects []geo.Polygon
|
||||
for _, polygon := range config.Region.Polygon {
|
||||
coords := polygon.Coordinates
|
||||
poly := geo.Polygon{}
|
||||
for _, c := range coords {
|
||||
x := c.X
|
||||
y := c.Y
|
||||
p := geo.NewPoint(x, y)
|
||||
if !poly.Contains(p) {
|
||||
poly.Add(p)
|
||||
|
||||
if config.Region != nil {
|
||||
for _, polygon := range config.Region.Polygon {
|
||||
coords := polygon.Coordinates
|
||||
poly := geo.Polygon{}
|
||||
for _, c := range coords {
|
||||
x := c.X
|
||||
y := c.Y
|
||||
p := geo.NewPoint(x, y)
|
||||
if !poly.Contains(p) {
|
||||
poly.Add(p)
|
||||
}
|
||||
}
|
||||
polyObjects = append(polyObjects, poly)
|
||||
}
|
||||
polyObjects = append(polyObjects, poly)
|
||||
}
|
||||
|
||||
bounds := img.Bounds()
|
||||
@@ -132,24 +133,29 @@ func ProcessMotion(motionCursor *pubsub.QueueCursor, configuration *models.Confi
|
||||
|
||||
// Check if within time interval
|
||||
detectMotion := true
|
||||
now := time.Now().In(loc)
|
||||
weekday := now.Weekday()
|
||||
hour := now.Hour()
|
||||
minute := now.Minute()
|
||||
second := now.Second()
|
||||
timeInterval := config.Timetable[int(weekday)]
|
||||
if timeInterval != nil {
|
||||
start1 := timeInterval.Start1
|
||||
end1 := timeInterval.End1
|
||||
start2 := timeInterval.Start2
|
||||
end2 := timeInterval.End2
|
||||
currentTimeInSeconds := hour*60*60 + minute*60 + second
|
||||
if (currentTimeInSeconds >= start1 && currentTimeInSeconds <= end1) ||
|
||||
(currentTimeInSeconds >= start2 && currentTimeInSeconds <= end2) {
|
||||
timeEnabled := config.Time
|
||||
if timeEnabled != "false" {
|
||||
now := time.Now().In(loc)
|
||||
weekday := now.Weekday()
|
||||
hour := now.Hour()
|
||||
minute := now.Minute()
|
||||
second := now.Second()
|
||||
if config.Timetable != nil && len(config.Timetable) > 0 {
|
||||
timeInterval := config.Timetable[int(weekday)]
|
||||
if timeInterval != nil {
|
||||
start1 := timeInterval.Start1
|
||||
end1 := timeInterval.End1
|
||||
start2 := timeInterval.Start2
|
||||
end2 := timeInterval.End2
|
||||
currentTimeInSeconds := hour*60*60 + minute*60 + second
|
||||
if (currentTimeInSeconds >= start1 && currentTimeInSeconds <= end1) ||
|
||||
(currentTimeInSeconds >= start2 && currentTimeInSeconds <= end2) {
|
||||
|
||||
} else {
|
||||
detectMotion = false
|
||||
log.Log.Debug("ProcessMotion: Time interval not valid, disabling motion detection.")
|
||||
} else {
|
||||
detectMotion = false
|
||||
log.Log.Info("ProcessMotion: Time interval not valid, disabling motion detection.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,8 +165,29 @@ func ProcessMotion(motionCursor *pubsub.QueueCursor, configuration *models.Confi
|
||||
isPixelChangeThresholdReached, changesToReturn = FindMotion(imageArray, coordinatesToCheck, pixelThreshold)
|
||||
if detectMotion && isPixelChangeThresholdReached {
|
||||
|
||||
if mqttClient != nil {
|
||||
mqttClient.Publish("kerberos/"+key+"/device/"+config.Key+"/motion", 2, false, "motion")
|
||||
// If offline mode is disabled, send a message to the hub
|
||||
if config.Offline != "true" {
|
||||
if mqttClient != nil {
|
||||
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/agent/"+deviceKey, 2, false, "motion")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if config.Capture.Recording != "false" {
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
package components
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
_ "image/png"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"reflect"
|
||||
@@ -18,17 +17,17 @@ import (
|
||||
"github.com/kerberos-io/agent/machinery/src/database"
|
||||
"github.com/kerberos-io/agent/machinery/src/log"
|
||||
"github.com/kerberos-io/agent/machinery/src/models"
|
||||
"gopkg.in/mgo.v2/bson"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
)
|
||||
|
||||
func GetImageFromFilePath() (image.Image, error) {
|
||||
snapshotDirectory := "./data/snapshots"
|
||||
func GetImageFromFilePath(configDirectory string) (image.Image, error) {
|
||||
snapshotDirectory := configDirectory + "/data/snapshots"
|
||||
files, err := ioutil.ReadDir(snapshotDirectory)
|
||||
if err == nil && len(files) > 1 {
|
||||
sort.Slice(files, func(i, j int) bool {
|
||||
return files[i].ModTime().Before(files[j].ModTime())
|
||||
})
|
||||
filePath := "./data/snapshots/" + files[1].Name()
|
||||
filePath := configDirectory + "/data/snapshots/" + files[1].Name()
|
||||
f, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -43,19 +42,18 @@ func GetImageFromFilePath() (image.Image, error) {
|
||||
// ReadUserConfig Reads the user configuration of the Kerberos Open Source instance.
|
||||
// This will return a models.User struct including the username, password,
|
||||
// selected language, and if the installation was completed or not.
|
||||
func ReadUserConfig() (userConfig models.User) {
|
||||
func ReadUserConfig(configDirectory string) (userConfig models.User) {
|
||||
for {
|
||||
jsonFile, err := os.Open("./data/config/user.json")
|
||||
jsonFile, err := os.Open(configDirectory + "/data/config/user.json")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
fmt.Println("Config file is not found " + "./data/config/user.json" + ", trying again in 5s.")
|
||||
log.Log.Error("Config file is not found " + configDirectory + "/data/config/user.json, trying again in 5s: " + err.Error())
|
||||
time.Sleep(5 * time.Second)
|
||||
} else {
|
||||
fmt.Println("Successfully Opened user.json")
|
||||
log.Log.Info("Successfully Opened user.json")
|
||||
byteValue, _ := ioutil.ReadAll(jsonFile)
|
||||
err = json.Unmarshal(byteValue, &userConfig)
|
||||
if err != nil {
|
||||
fmt.Println("JSON file not valid: " + err.Error())
|
||||
log.Log.Error("JSON file not valid: " + err.Error())
|
||||
} else {
|
||||
jsonFile.Close()
|
||||
break
|
||||
@@ -68,7 +66,7 @@ func ReadUserConfig() (userConfig models.User) {
|
||||
return
|
||||
}
|
||||
|
||||
func OpenConfig(configuration *models.Configuration) {
|
||||
func OpenConfig(configDirectory string, configuration *models.Configuration) {
|
||||
|
||||
// We are checking which deployment this is running, so we can load
|
||||
// into the configuration as expected.
|
||||
@@ -79,19 +77,52 @@ func OpenConfig(configuration *models.Configuration) {
|
||||
// Multiple agents have there configuration stored, and can benefit from
|
||||
// the concept of a global concept.
|
||||
|
||||
session := database.New().Copy()
|
||||
defer session.Close()
|
||||
db := session.DB(database.DatabaseName)
|
||||
collection := db.C("configuration")
|
||||
// Write to mongodb
|
||||
client := database.New()
|
||||
|
||||
collection.Find(bson.M{
|
||||
db := client.Database(database.DatabaseName)
|
||||
collection := db.Collection("configuration")
|
||||
|
||||
var globalConfig models.Config
|
||||
res := collection.FindOne(context.Background(), bson.M{
|
||||
"type": "global",
|
||||
}).One(&configuration.GlobalConfig)
|
||||
})
|
||||
|
||||
collection.Find(bson.M{
|
||||
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")
|
||||
res = collection.FindOne(context.Background(), bson.M{
|
||||
"type": "config",
|
||||
"name": os.Getenv("DEPLOYMENT_NAME"),
|
||||
}).One(&configuration.CustomConfig)
|
||||
"name": deploymentName,
|
||||
})
|
||||
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.
|
||||
// Read again from database but this store overwrite the same object.
|
||||
@@ -110,8 +141,13 @@ func OpenConfig(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
|
||||
@@ -126,6 +162,15 @@ func OpenConfig(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
|
||||
|
||||
@@ -136,9 +181,9 @@ func OpenConfig(configuration *models.Configuration) {
|
||||
|
||||
// Open device config
|
||||
for {
|
||||
jsonFile, err := os.Open("./data/config/config.json")
|
||||
jsonFile, err := os.Open(configDirectory + "/data/config/config.json")
|
||||
if err != nil {
|
||||
log.Log.Error("Config file is not found " + "./data/config/config.json" + ", trying again in 5s.")
|
||||
log.Log.Error("Config file is not found " + configDirectory + "/data/config/config.json" + ", trying again in 5s.")
|
||||
time.Sleep(5 * time.Second)
|
||||
} else {
|
||||
log.Log.Info("Successfully Opened config.json from " + configuration.Name)
|
||||
@@ -146,11 +191,11 @@ func OpenConfig(configuration *models.Configuration) {
|
||||
err = json.Unmarshal(byteValue, &configuration.Config)
|
||||
jsonFile.Close()
|
||||
if err != nil {
|
||||
fmt.Println("JSON file not valid: " + err.Error())
|
||||
log.Log.Error("JSON file not valid: " + err.Error())
|
||||
} else {
|
||||
err = json.Unmarshal(byteValue, &configuration.CustomConfig)
|
||||
if err != nil {
|
||||
fmt.Println("JSON file not valid: " + err.Error())
|
||||
log.Log.Error("JSON file not valid: " + err.Error())
|
||||
} else {
|
||||
break
|
||||
}
|
||||
@@ -179,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
|
||||
@@ -207,8 +252,7 @@ func OverrideWithEnvironmentVariables(configuration *models.Configuration) {
|
||||
|
||||
/* ONVIF connnection settings */
|
||||
case "AGENT_CAPTURE_IPCAMERA_ONVIF":
|
||||
isEnabled := value == " true"
|
||||
configuration.Config.Capture.IPCamera.ONVIF = isEnabled
|
||||
configuration.Config.Capture.IPCamera.ONVIF = value
|
||||
break
|
||||
case "AGENT_CAPTURE_IPCAMERA_ONVIF_XADDR":
|
||||
configuration.Config.Capture.IPCamera.ONVIFXAddr = value
|
||||
@@ -270,26 +314,82 @@ func OverrideWithEnvironmentVariables(configuration *models.Configuration) {
|
||||
}
|
||||
break
|
||||
|
||||
/* Cloud settings for persisting recordings */
|
||||
case "AGENT_CLOUD":
|
||||
configuration.Config.Cloud = value
|
||||
/* Conditions */
|
||||
|
||||
case "AGENT_TIME":
|
||||
configuration.Config.Time = value
|
||||
break
|
||||
case "AGENT_TIMETABLE":
|
||||
var timetable []*models.Timetable
|
||||
|
||||
// Convert value to timetable array with (start1, end1, start2, end2)
|
||||
// Where days are limited by ; and time by ,
|
||||
// su;mo;tu;we;th;fr;sa
|
||||
// 0,43199,43200,86400;0,43199,43200,86400
|
||||
|
||||
// Split days
|
||||
daysString := strings.Split(value, ";")
|
||||
for _, dayString := range daysString {
|
||||
// Split time
|
||||
timeString := strings.Split(dayString, ",")
|
||||
if len(timeString) == 4 {
|
||||
start1, err := strconv.ParseInt(timeString[0], 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
end1, err := strconv.ParseInt(timeString[1], 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
start2, err := strconv.ParseInt(timeString[2], 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
end2, err := strconv.ParseInt(timeString[3], 10, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
timetable = append(timetable, &models.Timetable{
|
||||
Start1: int(start1),
|
||||
End1: int(end1),
|
||||
Start2: int(start2),
|
||||
End2: int(end2),
|
||||
})
|
||||
}
|
||||
}
|
||||
configuration.Config.Timetable = timetable
|
||||
break
|
||||
|
||||
/* When connected and storing in Kerberos Hub (SAAS) */
|
||||
case "AGENT_HUB_URI":
|
||||
configuration.Config.HubURI = value
|
||||
break
|
||||
case "AGENT_HUB_KEY":
|
||||
configuration.Config.HubKey = value
|
||||
break
|
||||
case "AGENT_HUB_PRIVATE_KEY":
|
||||
configuration.Config.HubPrivateKey = value
|
||||
break
|
||||
case "AGENT_HUB_USERNAME":
|
||||
configuration.Config.S3.Username = value
|
||||
break
|
||||
case "AGENT_HUB_SITE":
|
||||
configuration.Config.HubSite = value
|
||||
case "AGENT_REGION_POLYGON":
|
||||
var coordinates []models.Coordinate
|
||||
|
||||
// Convert value to coordinates array
|
||||
// 0,0;1,1;2,2;3,3
|
||||
coordinatesString := strings.Split(value, ";")
|
||||
for _, coordinateString := range coordinatesString {
|
||||
coordinate := strings.Split(coordinateString, ",")
|
||||
if len(coordinate) == 2 {
|
||||
x, err := strconv.ParseFloat(coordinate[0], 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
y, err := strconv.ParseFloat(coordinate[1], 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
coordinates = append(coordinates, models.Coordinate{
|
||||
X: x,
|
||||
Y: y,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
configuration.Config.Region.Polygon = []models.Polygon{
|
||||
{
|
||||
Coordinates: coordinates,
|
||||
ID: "0",
|
||||
},
|
||||
}
|
||||
break
|
||||
|
||||
/* MQTT settings for bi-directional communication */
|
||||
@@ -317,6 +417,32 @@ func OverrideWithEnvironmentVariables(configuration *models.Configuration) {
|
||||
configuration.Config.TURNPassword = value
|
||||
break
|
||||
|
||||
/* Cloud settings for persisting recordings */
|
||||
case "AGENT_CLOUD":
|
||||
configuration.Config.Cloud = value
|
||||
break
|
||||
|
||||
case "AGENT_REMOVE_AFTER_UPLOAD":
|
||||
configuration.Config.RemoveAfterUpload = value
|
||||
break
|
||||
|
||||
/* When connected and storing in Kerberos Hub (SAAS) */
|
||||
case "AGENT_HUB_URI":
|
||||
configuration.Config.HubURI = value
|
||||
break
|
||||
case "AGENT_HUB_KEY":
|
||||
configuration.Config.HubKey = value
|
||||
break
|
||||
case "AGENT_HUB_PRIVATE_KEY":
|
||||
configuration.Config.HubPrivateKey = value
|
||||
break
|
||||
case "AGENT_HUB_SITE":
|
||||
configuration.Config.HubSite = value
|
||||
break
|
||||
case "AGENT_HUB_REGION":
|
||||
configuration.Config.S3.Region = value
|
||||
break
|
||||
|
||||
/* When storing in a Kerberos Vault */
|
||||
case "AGENT_KERBEROSVAULT_URI":
|
||||
configuration.Config.KStorage.URI = value
|
||||
@@ -334,24 +460,50 @@ func OverrideWithEnvironmentVariables(configuration *models.Configuration) {
|
||||
configuration.Config.KStorage.Directory = value
|
||||
break
|
||||
|
||||
/* When storing in dropbox */
|
||||
case "AGENT_DROPBOX_ACCESS_TOKEN":
|
||||
configuration.Config.Dropbox.AccessToken = value
|
||||
break
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func SaveConfig(config models.Config, configuration *models.Configuration, communication *models.Communication) error {
|
||||
func SaveConfig(configDirectory string, config models.Config, configuration *models.Configuration, communication *models.Communication) error {
|
||||
if !communication.IsConfiguring.IsSet() {
|
||||
communication.IsConfiguring.Set()
|
||||
|
||||
err := StoreConfig(config)
|
||||
err := StoreConfig(configDirectory, config)
|
||||
if err != nil {
|
||||
communication.IsConfiguring.UnSet()
|
||||
return err
|
||||
}
|
||||
|
||||
select {
|
||||
case communication.HandleBootstrap <- "restart":
|
||||
default:
|
||||
if communication.CameraConnected {
|
||||
select {
|
||||
case communication.HandleBootstrap <- "restart":
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
communication.IsConfiguring.UnSet()
|
||||
@@ -362,25 +514,38 @@ func SaveConfig(config models.Config, configuration *models.Configuration, commu
|
||||
}
|
||||
}
|
||||
|
||||
func StoreConfig(config models.Config) error {
|
||||
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
|
||||
session := database.New().Copy()
|
||||
defer session.Close()
|
||||
db := session.DB(database.DatabaseName)
|
||||
collection := db.C("configuration")
|
||||
client := database.New()
|
||||
|
||||
err := collection.Update(bson.M{
|
||||
db := client.Database(database.DatabaseName)
|
||||
collection := db.Collection("configuration")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
_, err := collection.UpdateOne(ctx, bson.M{
|
||||
"type": "config",
|
||||
"name": os.Getenv("DEPLOYMENT_NAME"),
|
||||
}, &config)
|
||||
}, bson.M{"$set": config})
|
||||
|
||||
return err
|
||||
|
||||
// Save into file
|
||||
} else if os.Getenv("DEPLOYMENT") == "" || os.Getenv("DEPLOYMENT") == "agent" {
|
||||
res, _ := json.MarshalIndent(config, "", "\t")
|
||||
err := ioutil.WriteFile("./data/config/config.json", res, 0644)
|
||||
err := ioutil.WriteFile(configDirectory+"/data/config/config.json", res, 0644)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,45 +1,55 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gopkg.in/mgo.v2"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
type DB struct {
|
||||
Session *mgo.Session
|
||||
Client *mongo.Client
|
||||
}
|
||||
|
||||
var _init_ctx sync.Once
|
||||
var _instance *DB
|
||||
var DatabaseName = "KerberosFactory"
|
||||
|
||||
func New() *mgo.Session {
|
||||
func New() *mongo.Client {
|
||||
|
||||
host := os.Getenv("MONGODB_HOST")
|
||||
database := os.Getenv("MONGODB_DATABASE_CREDENTIALS")
|
||||
databaseCredentials := os.Getenv("MONGODB_DATABASE_CREDENTIALS")
|
||||
replicaset := os.Getenv("MONGODB_REPLICASET")
|
||||
username := os.Getenv("MONGODB_USERNAME")
|
||||
password := os.Getenv("MONGODB_PASSWORD")
|
||||
authentication := "SCRAM-SHA-256"
|
||||
|
||||
_init_ctx.Do(func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
_instance = new(DB)
|
||||
mongoDBDialInfo := &mgo.DialInfo{
|
||||
Addrs: strings.Split(host, ","),
|
||||
Timeout: 3 * time.Second,
|
||||
Database: database,
|
||||
Username: username,
|
||||
Password: password,
|
||||
mongodbURI := fmt.Sprintf("mongodb://%s:%s@%s", username, password, host)
|
||||
if replicaset != "" {
|
||||
mongodbURI = fmt.Sprintf("%s/?replicaSet=%s", mongodbURI, replicaset)
|
||||
}
|
||||
session, err := mgo.DialWithInfo(mongoDBDialInfo)
|
||||
|
||||
client, err := mongo.Connect(ctx, options.Client().ApplyURI(mongodbURI).SetAuth(options.Credential{
|
||||
AuthMechanism: authentication,
|
||||
AuthSource: databaseCredentials,
|
||||
Username: username,
|
||||
Password: password,
|
||||
}))
|
||||
if err != nil {
|
||||
fmt.Printf("Error en mongo: %+v\n", err)
|
||||
fmt.Printf("Error setting up mongodb connection: %+v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
_instance.Session = session
|
||||
_instance.Client = client
|
||||
})
|
||||
|
||||
return _instance.Session
|
||||
return _instance.Client
|
||||
}
|
||||
|
||||
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...)
|
||||
}
|
||||
@@ -21,7 +21,7 @@ var Log = Logging{
|
||||
|
||||
var gologging = logging.MustGetLogger("gologger")
|
||||
|
||||
func ConfigureGoLogging(timezone *time.Location) {
|
||||
func ConfigureGoLogging(configDirectory string, timezone *time.Location) {
|
||||
// Logging
|
||||
var format = logging.MustStringFormatter(
|
||||
`%{color}%{time:15:04:05.000} %{shortfunc} ▶ %{level:.4s} %{id:03x}%{color:reset} %{message}`,
|
||||
@@ -32,7 +32,7 @@ func ConfigureGoLogging(timezone *time.Location) {
|
||||
stdBackend := logging.NewLogBackend(os.Stderr, "", 0)
|
||||
stdBackendLeveled := logging.NewBackendFormatter(stdBackend, format)
|
||||
fileBackend := logging.NewLogBackend(&lumberjack.Logger{
|
||||
Filename: "./data/log/machinery.txt",
|
||||
Filename: configDirectory + "/data/log/machinery.txt",
|
||||
MaxSize: 2, // megabytes
|
||||
Compress: true, // disabled by default
|
||||
}, "", 0)
|
||||
@@ -75,10 +75,10 @@ type Logging struct {
|
||||
Level string
|
||||
}
|
||||
|
||||
func (self *Logging) Init(timezone *time.Location) {
|
||||
func (self *Logging) Init(configDirectory string, timezone *time.Location) {
|
||||
switch self.Logger {
|
||||
case "go-logging":
|
||||
ConfigureGoLogging(timezone)
|
||||
ConfigureGoLogging(configDirectory, timezone)
|
||||
case "logrus":
|
||||
ConfigureLogrus(timezone)
|
||||
default:
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package models
|
||||
|
||||
type APIResponse struct {
|
||||
Data interface{} `json:"data" bson:"data"`
|
||||
Message interface{} `json:"message" bson:"message"`
|
||||
Data interface{} `json:"data" bson:"data"`
|
||||
Message interface{} `json:"message" bson:"message"`
|
||||
PTZFunctions interface{} `json:"ptz_functions" bson:"ptz_functions"`
|
||||
CanZoom bool `json:"can_zoom" bson:"can_zoom"`
|
||||
CanPanTilt bool `json:"can_pan_tilt" bson:"can_pan_tilt"`
|
||||
}
|
||||
|
||||
type OnvifCredentials struct {
|
||||
@@ -26,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"`
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
@@ -12,6 +13,8 @@ import (
|
||||
// The communication struct that is managing
|
||||
// all the communication between the different goroutines.
|
||||
type Communication struct {
|
||||
Context *context.Context
|
||||
CancelContext *context.CancelFunc
|
||||
PackageCounter *atomic.Value
|
||||
LastPacketTimer *atomic.Value
|
||||
CloudTimestamp *atomic.Value
|
||||
@@ -19,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
|
||||
@@ -34,4 +38,6 @@ type Communication struct {
|
||||
Decoder *ffmpeg.VideoDecoder
|
||||
SubDecoder *ffmpeg.VideoDecoder
|
||||
Image string
|
||||
CameraConnected bool
|
||||
HasBackChannel bool
|
||||
}
|
||||
|
||||
@@ -12,34 +12,37 @@ type Configuration struct {
|
||||
// Config is the highlevel struct which contains all the configuration of
|
||||
// your Kerberos Open Source instance.
|
||||
type Config struct {
|
||||
Type string `json:"type"`
|
||||
Key string `json:"key"`
|
||||
Name string `json:"name"`
|
||||
FriendlyName string `json:"friendly_name"`
|
||||
Time string `json:"time" bson:"time"`
|
||||
Offline string `json:"offline"`
|
||||
AutoClean string `json:"auto_clean"`
|
||||
MaxDirectorySize int64 `json:"max_directory_size"`
|
||||
Timezone string `json:"timezone,omitempty" bson:"timezone,omitempty"`
|
||||
Capture Capture `json:"capture"`
|
||||
Timetable []*Timetable `json:"timetable"`
|
||||
Region *Region `json:"region"`
|
||||
Cloud string `json:"cloud" bson:"cloud"`
|
||||
S3 *S3 `json:"s3,omitempty" bson:"s3,omitempty"`
|
||||
KStorage *KStorage `json:"kstorage,omitempty" bson:"kstorage,omitempty"`
|
||||
MQTTURI string `json:"mqtturi" bson:"mqtturi,omitempty"`
|
||||
MQTTUsername string `json:"mqtt_username" bson:"mqtt_username"`
|
||||
MQTTPassword string `json:"mqtt_password" bson:"mqtt_password"`
|
||||
STUNURI string `json:"stunuri" bson:"stunuri"`
|
||||
TURNURI string `json:"turnuri" bson:"turnuri"`
|
||||
TURNUsername string `json:"turn_username" bson:"turn_username"`
|
||||
TURNPassword string `json:"turn_password" bson:"turn_password"`
|
||||
HeartbeatURI string `json:"heartbeaturi" bson:"heartbeaturi"` /*obsolete*/
|
||||
HubURI string `json:"hub_uri" bson:"hub_uri"`
|
||||
HubKey string `json:"hub_key" bson:"hub_key"`
|
||||
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"`
|
||||
Type string `json:"type"`
|
||||
Key string `json:"key"`
|
||||
Name string `json:"name"`
|
||||
FriendlyName string `json:"friendly_name"`
|
||||
Time string `json:"time" bson:"time"`
|
||||
Offline string `json:"offline"`
|
||||
AutoClean string `json:"auto_clean"`
|
||||
RemoveAfterUpload string `json:"remove_after_upload"`
|
||||
MaxDirectorySize int64 `json:"max_directory_size"`
|
||||
Timezone string `json:"timezone"`
|
||||
Capture Capture `json:"capture"`
|
||||
Timetable []*Timetable `json:"timetable"`
|
||||
Region *Region `json:"region"`
|
||||
Cloud string `json:"cloud" bson:"cloud"`
|
||||
S3 *S3 `json:"s3,omitempty" bson:"s3,omitempty"`
|
||||
KStorage *KStorage `json:"kstorage,omitempty" bson:"kstorage,omitempty"`
|
||||
Dropbox *Dropbox `json:"dropbox,omitempty" bson:"dropbox,omitempty"`
|
||||
MQTTURI string `json:"mqtturi" bson:"mqtturi,omitempty"`
|
||||
MQTTUsername string `json:"mqtt_username" bson:"mqtt_username"`
|
||||
MQTTPassword string `json:"mqtt_password" bson:"mqtt_password"`
|
||||
STUNURI string `json:"stunuri" bson:"stunuri"`
|
||||
TURNURI string `json:"turnuri" bson:"turnuri"`
|
||||
TURNUsername string `json:"turn_username" bson:"turn_username"`
|
||||
TURNPassword string `json:"turn_password" bson:"turn_password"`
|
||||
HeartbeatURI string `json:"heartbeaturi" bson:"heartbeaturi"` /*obsolete*/
|
||||
HubURI string `json:"hub_uri" bson:"hub_uri"`
|
||||
HubKey string `json:"hub_key" bson:"hub_key"`
|
||||
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),
|
||||
@@ -50,10 +53,10 @@ type Capture struct {
|
||||
USBCamera USBCamera `json:"usbcamera"`
|
||||
RaspiCamera RaspiCamera `json:"raspicamera"`
|
||||
Recording string `json:"recording,omitempty"`
|
||||
Continuous string `json:"continuous,omitempty"`
|
||||
Snapshots string `json:"snapshots,omitempty"`
|
||||
Motion string `json:"motion,omitempty"`
|
||||
Liveview string `json:"liveview,omitempty"`
|
||||
Continuous string `json:"continuous,omitempty"`
|
||||
PostRecording int64 `json:"postrecording"`
|
||||
PreRecording int64 `json:"prerecording"`
|
||||
MaxLengthRecording int64 `json:"maxlengthrecording"`
|
||||
@@ -68,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 bool `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"`
|
||||
ONVIF string `json:"onvif,omitempty" bson:"onvif"`
|
||||
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*)
|
||||
@@ -147,3 +152,18 @@ type KStorage struct {
|
||||
Provider string `json:"provider,omitempty" bson:"provider,omitempty"`
|
||||
Directory string `json:"directory,omitempty" bson:"directory,omitempty"`
|
||||
}
|
||||
|
||||
// Dropbox integration
|
||||
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"`
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/kerberos-io/agent/machinery/src/log"
|
||||
"github.com/kerberos-io/agent/machinery/src/models"
|
||||
"github.com/kerberos-io/onvif/media"
|
||||
@@ -31,7 +32,8 @@ func HandleONVIFActions(configuration *models.Configuration, communication *mode
|
||||
json.Unmarshal(b, &ptzAction)
|
||||
|
||||
// Connect to Onvif device
|
||||
device, err := ConnectToOnvifDevice(configuration)
|
||||
cameraConfiguration := configuration.Config.Capture.IPCamera
|
||||
device, err := ConnectToOnvifDevice(&cameraConfiguration)
|
||||
if err == nil {
|
||||
|
||||
// Get token from the first profile
|
||||
@@ -39,18 +41,78 @@ func HandleONVIFActions(configuration *models.Configuration, communication *mode
|
||||
if err == nil {
|
||||
|
||||
// Get the configurations from the device
|
||||
configurations, err := GetConfigurationsFromDevice(device)
|
||||
configurations, err := GetPTZConfigurationsFromDevice(device)
|
||||
|
||||
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())
|
||||
}
|
||||
@@ -100,16 +162,13 @@ func HandleONVIFActions(configuration *models.Configuration, communication *mode
|
||||
log.Log.Debug("HandleONVIFActions: finished")
|
||||
}
|
||||
|
||||
func ConnectToOnvifDevice(configuration *models.Configuration) (*onvif.Device, error) {
|
||||
func ConnectToOnvifDevice(cameraConfiguration *models.IPCamera) (*onvif.Device, error) {
|
||||
log.Log.Debug("ConnectToOnvifDevice: started")
|
||||
|
||||
config := configuration.Config
|
||||
|
||||
// Get the capabilities of the ONVIF device
|
||||
device, err := onvif.NewDevice(onvif.DeviceParams{
|
||||
Xaddr: config.Capture.IPCamera.ONVIFXAddr,
|
||||
Username: config.Capture.IPCamera.ONVIFUsername,
|
||||
Password: config.Capture.IPCamera.ONVIFPassword,
|
||||
Xaddr: cameraConfiguration.ONVIFXAddr,
|
||||
Username: cameraConfiguration.ONVIFUsername,
|
||||
Password: cameraConfiguration.ONVIFPassword,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@@ -154,11 +213,11 @@ func GetTokenFromProfile(device *onvif.Device, profileId int) (xsd.ReferenceToke
|
||||
return profileToken, err
|
||||
}
|
||||
|
||||
func GetConfigurationsFromDevice(device *onvif.Device) (ptz.GetConfigurationsResponse, error) {
|
||||
func GetPTZConfigurationsFromDevice(device *onvif.Device) (ptz.GetConfigurationsResponse, error) {
|
||||
// We'll try to receive the PTZ configurations from the server
|
||||
var configurations ptz.GetConfigurationsResponse
|
||||
|
||||
// Get the configurations from the device
|
||||
// Get the PTZ configurations from the device
|
||||
resp, err := device.CallMethod(ptz.GetConfigurations{})
|
||||
if err == nil {
|
||||
defer resp.Body.Close()
|
||||
@@ -167,11 +226,11 @@ func GetConfigurationsFromDevice(device *onvif.Device) (ptz.GetConfigurationsRes
|
||||
stringBody := string(b)
|
||||
decodedXML, et, err := getXMLNode(stringBody, "GetConfigurationsResponse")
|
||||
if err != nil {
|
||||
log.Log.Error("GetConfigurationsFromDevice: " + err.Error())
|
||||
log.Log.Error("GetPTZConfigurationsFromDevice: " + err.Error())
|
||||
return configurations, err
|
||||
} else {
|
||||
if err := decodedXML.DecodeElement(&configurations, et); err != nil {
|
||||
log.Log.Error("GetConfigurationsFromDevice: " + err.Error())
|
||||
log.Log.Error("GetPTZConfigurationsFromDevice: " + err.Error())
|
||||
return configurations, err
|
||||
}
|
||||
}
|
||||
@@ -180,18 +239,83 @@ func GetConfigurationsFromDevice(device *onvif.Device) (ptz.GetConfigurationsRes
|
||||
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,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -200,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{
|
||||
@@ -227,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,
|
||||
@@ -300,6 +668,89 @@ func GetCapabilitiesFromDevice(device *onvif.Device) []string {
|
||||
return capabilities
|
||||
}
|
||||
|
||||
func GetPresetsFromDevice(device *onvif.Device) ([]models.OnvifActionPreset, error) {
|
||||
var presets []models.OnvifActionPreset
|
||||
var presetsResponse ptz.GetPresetsResponse
|
||||
|
||||
// Get token from the first profile
|
||||
token, err := GetTokenFromProfile(device, 0)
|
||||
if err == nil {
|
||||
resp, err := device.CallMethod(ptz.GetPresets{
|
||||
ProfileToken: token,
|
||||
})
|
||||
|
||||
defer resp.Body.Close()
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
if err == nil {
|
||||
stringBody := string(b)
|
||||
decodedXML, et, err := getXMLNode(stringBody, "GetPresetsResponse")
|
||||
if err != nil {
|
||||
log.Log.Error("GetPresetsFromDevice: " + err.Error())
|
||||
return presets, err
|
||||
} else {
|
||||
if err := decodedXML.DecodeElement(&presetsResponse, et); err != nil {
|
||||
log.Log.Error("GetPresetsFromDevice: " + err.Error())
|
||||
return presets, err
|
||||
}
|
||||
|
||||
for _, preset := range presetsResponse.Preset {
|
||||
p := models.OnvifActionPreset{
|
||||
Name: string(preset.Name),
|
||||
Token: string(preset.Token),
|
||||
}
|
||||
|
||||
presets = append(presets, p)
|
||||
}
|
||||
|
||||
return presets, err
|
||||
}
|
||||
} else {
|
||||
log.Log.Error("GetPresetsFromDevice: " + err.Error())
|
||||
}
|
||||
} else {
|
||||
log.Log.Error("GetPresetsFromDevice: " + err.Error())
|
||||
}
|
||||
|
||||
return presets, err
|
||||
}
|
||||
|
||||
func GoToPresetFromDevice(device *onvif.Device, presetName string) error {
|
||||
var goToPresetResponse ptz.GotoPresetResponse
|
||||
|
||||
// Get token from the first profile
|
||||
token, err := GetTokenFromProfile(device, 0)
|
||||
if err == nil {
|
||||
|
||||
resp, err := device.CallMethod(ptz.GotoPreset{
|
||||
ProfileToken: token,
|
||||
PresetToken: xsd.ReferenceToken(presetName),
|
||||
})
|
||||
|
||||
defer resp.Body.Close()
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
if err == nil {
|
||||
stringBody := string(b)
|
||||
decodedXML, et, err := getXMLNode(stringBody, "GotoPresetResponses")
|
||||
if err != nil {
|
||||
log.Log.Error("GoToPresetFromDevice: " + err.Error())
|
||||
return err
|
||||
} else {
|
||||
if err := decodedXML.DecodeElement(&goToPresetResponse, et); err != nil {
|
||||
log.Log.Error("GoToPresetFromDevice: " + err.Error())
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
log.Log.Error("GoToPresetFromDevice: " + err.Error())
|
||||
}
|
||||
} else {
|
||||
log.Log.Error("GoToPresetFromDevice: " + err.Error())
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func getXMLNode(xmlBody string, nodeName string) (*xml.Decoder, *xml.StartElement, error) {
|
||||
xmlBytes := bytes.NewBufferString(xmlBody)
|
||||
decodedXML := xml.NewDecoder(xmlBytes)
|
||||
@@ -317,3 +768,89 @@ func getXMLNode(xmlBody string, nodeName string) (*xml.Decoder, *xml.StartElemen
|
||||
}
|
||||
return nil, nil, errors.New("error in NodeName - username and password might be wrong")
|
||||
}
|
||||
|
||||
func GetPTZFunctionsFromDevice(configurations ptz.GetConfigurationsResponse) ([]string, bool, bool) {
|
||||
var functions []string
|
||||
canZoom := false
|
||||
canPanTilt := false
|
||||
|
||||
if configurations.PTZConfiguration.DefaultAbsolutePantTiltPositionSpace != "" {
|
||||
functions = append(functions, "AbsolutePanTiltMove")
|
||||
canPanTilt = true
|
||||
}
|
||||
if configurations.PTZConfiguration.DefaultAbsoluteZoomPositionSpace != "" {
|
||||
functions = append(functions, "AbsoluteZoomMove")
|
||||
canZoom = true
|
||||
}
|
||||
if configurations.PTZConfiguration.DefaultRelativePanTiltTranslationSpace != "" {
|
||||
functions = append(functions, "RelativePanTiltMove")
|
||||
canPanTilt = true
|
||||
}
|
||||
if configurations.PTZConfiguration.DefaultRelativeZoomTranslationSpace != "" {
|
||||
functions = append(functions, "RelativeZoomMove")
|
||||
canZoom = true
|
||||
}
|
||||
if configurations.PTZConfiguration.DefaultContinuousPanTiltVelocitySpace != "" {
|
||||
functions = append(functions, "ContinuousPanTiltMove")
|
||||
canPanTilt = true
|
||||
}
|
||||
if configurations.PTZConfiguration.DefaultContinuousZoomVelocitySpace != "" {
|
||||
functions = append(functions, "ContinuousZoomMove")
|
||||
canZoom = true
|
||||
}
|
||||
if configurations.PTZConfiguration.DefaultPTZSpeed != nil {
|
||||
functions = append(functions, "PTZSpeed")
|
||||
}
|
||||
if configurations.PTZConfiguration.DefaultPTZTimeout != "" {
|
||||
functions = append(functions, "PTZTimeout")
|
||||
}
|
||||
|
||||
return functions, canZoom, canPanTilt
|
||||
}
|
||||
|
||||
// VerifyOnvifConnection godoc
|
||||
// @Router /api/onvif/verify [post]
|
||||
// @ID verify-onvif
|
||||
// @Security Bearer
|
||||
// @securityDefinitions.apikey Bearer
|
||||
// @in header
|
||||
// @name Authorization
|
||||
// @Tags config
|
||||
// @Param cameraConfig body models.IPCamera true "Camera Config"
|
||||
// @Summary Will verify the ONVIF connectivity.
|
||||
// @Description Will verify the ONVIF connectivity.
|
||||
// @Success 200 {object} models.APIResponse
|
||||
func VerifyOnvifConnection(c *gin.Context) {
|
||||
var cameraConfig models.IPCamera
|
||||
err := c.BindJSON(&cameraConfig)
|
||||
if err == nil {
|
||||
device, err := ConnectToOnvifDevice(&cameraConfig)
|
||||
if err == nil {
|
||||
// Get the list of configurations
|
||||
configurations, err := GetPTZConfigurationsFromDevice(device)
|
||||
if err == nil {
|
||||
|
||||
// Check if can zoom and/or pan/tilt is supported
|
||||
ptzFunctions, canZoom, canPanTilt := GetPTZFunctionsFromDevice(configurations)
|
||||
c.JSON(200, models.APIResponse{
|
||||
Data: device,
|
||||
PTZFunctions: ptzFunctions,
|
||||
CanZoom: canZoom,
|
||||
CanPanTilt: canPanTilt,
|
||||
})
|
||||
} else {
|
||||
c.JSON(400, models.APIResponse{
|
||||
Message: "Something went wrong while getting the configurations " + err.Error(),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
c.JSON(400, models.APIResponse{
|
||||
Message: "Something went wrong while verifying the ONVIF connection " + err.Error(),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
c.JSON(400, models.APIResponse{
|
||||
Message: "Something went wrong while receiving the config " + err.Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,8 @@ func LoginToOnvif(c *gin.Context) {
|
||||
},
|
||||
}
|
||||
|
||||
device, err := onvif.ConnectToOnvifDevice(configuration)
|
||||
cameraConfiguration := configuration.Config.Capture.IPCamera
|
||||
device, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
|
||||
if err == nil {
|
||||
c.JSON(200, gin.H{
|
||||
"device": device,
|
||||
@@ -85,7 +86,8 @@ func GetOnvifCapabilities(c *gin.Context) {
|
||||
},
|
||||
}
|
||||
|
||||
device, err := onvif.ConnectToOnvifDevice(configuration)
|
||||
cameraConfiguration := configuration.Config.Capture.IPCamera
|
||||
device, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
|
||||
if err == nil {
|
||||
c.JSON(200, gin.H{
|
||||
"capabilities": onvif.GetCapabilitiesFromDevice(device),
|
||||
@@ -128,7 +130,8 @@ func DoOnvifPanTilt(c *gin.Context) {
|
||||
},
|
||||
}
|
||||
|
||||
device, err := onvif.ConnectToOnvifDevice(configuration)
|
||||
cameraConfiguration := configuration.Config.Capture.IPCamera
|
||||
device, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
|
||||
|
||||
if err == nil {
|
||||
// Get token from the first profile
|
||||
@@ -137,13 +140,13 @@ func DoOnvifPanTilt(c *gin.Context) {
|
||||
if err == nil {
|
||||
|
||||
// Get the configurations from the device
|
||||
configurations, err := onvif.GetConfigurationsFromDevice(device)
|
||||
ptzConfigurations, err := onvif.GetPTZConfigurationsFromDevice(device)
|
||||
|
||||
if err == nil {
|
||||
|
||||
pan := onvifPanTilt.Pan
|
||||
tilt := onvifPanTilt.Tilt
|
||||
err := onvif.ContinuousPanTilt(device, configurations, token, pan, tilt)
|
||||
err := onvif.ContinuousPanTilt(device, ptzConfigurations, token, pan, tilt)
|
||||
if err == nil {
|
||||
c.JSON(200, models.APIResponse{
|
||||
Message: "Successfully pan/tilted the camera",
|
||||
@@ -201,7 +204,8 @@ func DoOnvifZoom(c *gin.Context) {
|
||||
},
|
||||
}
|
||||
|
||||
device, err := onvif.ConnectToOnvifDevice(configuration)
|
||||
cameraConfiguration := configuration.Config.Capture.IPCamera
|
||||
device, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
|
||||
|
||||
if err == nil {
|
||||
// Get token from the first profile
|
||||
@@ -209,13 +213,13 @@ func DoOnvifZoom(c *gin.Context) {
|
||||
|
||||
if err == nil {
|
||||
|
||||
// Get the configurations from the device
|
||||
configurations, err := onvif.GetConfigurationsFromDevice(device)
|
||||
// Get the PTZ configurations from the device
|
||||
ptzConfigurations, err := onvif.GetPTZConfigurationsFromDevice(device)
|
||||
|
||||
if err == nil {
|
||||
|
||||
zoom := onvifZoom.Zoom
|
||||
err := onvif.ContinuousZoom(device, configurations, token, zoom)
|
||||
err := onvif.ContinuousZoom(device, ptzConfigurations, token, zoom)
|
||||
if err == nil {
|
||||
c.JSON(200, models.APIResponse{
|
||||
Message: "Successfully zoomed the camera",
|
||||
@@ -246,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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,16 +7,18 @@ import (
|
||||
jwt "github.com/appleboy/gin-jwt/v2"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/kerberos-io/agent/machinery/src/capture"
|
||||
"github.com/kerberos-io/agent/machinery/src/onvif"
|
||||
"github.com/kerberos-io/agent/machinery/src/routers/websocket"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware, configuration *models.Configuration, communication *models.Communication) *gin.RouterGroup {
|
||||
func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware, configDirectory string, configuration *models.Configuration, communication *models.Communication) *gin.RouterGroup {
|
||||
|
||||
r.GET("/ws", func(c *gin.Context) {
|
||||
websocket.WebsocketHandler(c, communication)
|
||||
@@ -39,7 +41,7 @@ func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware, configuratio
|
||||
var config models.Config
|
||||
err := c.BindJSON(&config)
|
||||
if err == nil {
|
||||
err := components.SaveConfig(config, configuration, communication)
|
||||
err := configService.SaveConfig(configDirectory, config, configuration, communication)
|
||||
if err == nil {
|
||||
c.JSON(200, gin.H{
|
||||
"data": "☄ Reconfiguring",
|
||||
@@ -62,23 +64,22 @@ func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware, configuratio
|
||||
|
||||
api.GET("/dashboard", func(c *gin.Context) {
|
||||
|
||||
// This will return the timestamp when the last packet was correctyl received
|
||||
// this is to calculate if the camera connection is still working.
|
||||
lastPacketReceived := int64(0)
|
||||
if communication.LastPacketTimer != nil && communication.LastPacketTimer.Load() != nil {
|
||||
lastPacketReceived = communication.LastPacketTimer.Load().(int64)
|
||||
}
|
||||
// Check if camera is online.
|
||||
cameraIsOnline := communication.CameraConnected
|
||||
|
||||
// If an agent is properly setup with Kerberos Hub, we will send
|
||||
// a ping to Kerberos Hub every 15seconds. On receiving a positive response
|
||||
// it will update the CloudTimestamp value.
|
||||
cloudTimestamp := int64(0)
|
||||
cloudIsOnline := false
|
||||
if communication.CloudTimestamp != nil && communication.CloudTimestamp.Load() != nil {
|
||||
cloudTimestamp = communication.CloudTimestamp.Load().(int64)
|
||||
timestamp := communication.CloudTimestamp.Load().(int64)
|
||||
if timestamp > 0 {
|
||||
cloudIsOnline = true
|
||||
}
|
||||
}
|
||||
|
||||
// The total number of recordings stored in the directory.
|
||||
recordingDirectory := "./data/recordings"
|
||||
recordingDirectory := configDirectory + "/data/recordings"
|
||||
numberOfRecordings := utils.NumberOfMP4sInDirectory(recordingDirectory)
|
||||
|
||||
// All days stored in this agent.
|
||||
@@ -99,8 +100,8 @@ func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware, configuratio
|
||||
|
||||
c.JSON(200, gin.H{
|
||||
"offlineMode": configuration.Config.Offline,
|
||||
"cameraOnline": lastPacketReceived,
|
||||
"cloudOnline": cloudTimestamp,
|
||||
"cameraOnline": cameraIsOnline,
|
||||
"cloudOnline": cloudIsOnline,
|
||||
"numberOfRecordings": numberOfRecordings,
|
||||
"days": days,
|
||||
"latestEvents": latestEvents,
|
||||
@@ -115,7 +116,7 @@ func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware, configuratio
|
||||
if eventFilter.NumberOfElements == 0 {
|
||||
eventFilter.NumberOfElements = 10
|
||||
}
|
||||
recordingDirectory := "./data/recordings"
|
||||
recordingDirectory := configDirectory + "/data/recordings"
|
||||
files, err := utils.ReadDirectory(recordingDirectory)
|
||||
if err == nil {
|
||||
events := utils.GetSortedDirectory(files)
|
||||
@@ -137,7 +138,7 @@ func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware, configuratio
|
||||
})
|
||||
|
||||
api.GET("/days", func(c *gin.Context) {
|
||||
recordingDirectory := "./data/recordings"
|
||||
recordingDirectory := configDirectory + "/data/recordings"
|
||||
files, err := utils.ReadDirectory(recordingDirectory)
|
||||
if err == nil {
|
||||
events := utils.GetSortedDirectory(files)
|
||||
@@ -165,7 +166,7 @@ func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware, configuratio
|
||||
var config models.Config
|
||||
err := c.BindJSON(&config)
|
||||
if err == nil {
|
||||
err := components.SaveConfig(config, configuration, communication)
|
||||
err := configService.SaveConfig(configDirectory, config, configuration, communication)
|
||||
if err == nil {
|
||||
c.JSON(200, gin.H{
|
||||
"data": "☄ Reconfiguring",
|
||||
@@ -196,12 +197,16 @@ func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware, configuratio
|
||||
})
|
||||
})
|
||||
|
||||
api.POST("/onvif/verify", func(c *gin.Context) {
|
||||
onvif.VerifyOnvifConnection(c)
|
||||
})
|
||||
|
||||
api.POST("/hub/verify", func(c *gin.Context) {
|
||||
cloud.VerifyHub(c)
|
||||
})
|
||||
|
||||
api.POST("/persistence/verify", func(c *gin.Context) {
|
||||
cloud.VerifyPersistence(c)
|
||||
cloud.VerifyPersistence(c, configDirectory)
|
||||
})
|
||||
|
||||
// Streaming handler
|
||||
@@ -211,7 +216,7 @@ func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware, configuratio
|
||||
// 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()
|
||||
img, err := configService.GetImageFromFilePath(configDirectory)
|
||||
return img, err
|
||||
}
|
||||
h := components.StartMotionJPEG(imageFunction, 80)
|
||||
@@ -223,6 +228,8 @@ func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware, configuratio
|
||||
// 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,6 +1,10 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
jwt "github.com/appleboy/gin-jwt/v2"
|
||||
"github.com/gin-contrib/pprof"
|
||||
"github.com/gin-gonic/contrib/static"
|
||||
@@ -10,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"
|
||||
@@ -33,7 +38,7 @@ import (
|
||||
// @in header
|
||||
// @name Authorization
|
||||
|
||||
func StartServer(configuration *models.Configuration, communication *models.Communication) {
|
||||
func StartServer(configDirectory string, configuration *models.Configuration, communication *models.Communication) {
|
||||
|
||||
// Initialize REST API
|
||||
r := gin.Default()
|
||||
@@ -55,15 +60,28 @@ func StartServer(configuration *models.Configuration, communication *models.Comm
|
||||
}
|
||||
|
||||
// Add all routes
|
||||
AddRoutes(r, authMiddleware, configuration, communication)
|
||||
AddRoutes(r, authMiddleware, configDirectory, configuration, communication)
|
||||
|
||||
// Update environment variables
|
||||
environmentVariables := configDirectory + "/www/env.js"
|
||||
if os.Getenv("AGENT_MODE") == "demo" {
|
||||
demoEnvironmentVariables := configDirectory + "/www/env.demo.js"
|
||||
// Move demo environment variables to environment variables
|
||||
err := os.Rename(demoEnvironmentVariables, environmentVariables)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Add static routes to UI
|
||||
r.Use(static.Serve("/", static.LocalFile("./www", true)))
|
||||
r.Use(static.Serve("/dashboard", static.LocalFile("./www", true)))
|
||||
r.Use(static.Serve("/media", static.LocalFile("./www", true)))
|
||||
r.Use(static.Serve("/settings", static.LocalFile("./www", true)))
|
||||
r.Use(static.Serve("/login", static.LocalFile("./www", true)))
|
||||
r.Handle("GET", "/file/*filepath", Files)
|
||||
r.Use(static.Serve("/", static.LocalFile(configDirectory+"/www", true)))
|
||||
r.Use(static.Serve("/dashboard", static.LocalFile(configDirectory+"/www", true)))
|
||||
r.Use(static.Serve("/media", static.LocalFile(configDirectory+"/www", true)))
|
||||
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, configuration)
|
||||
})
|
||||
|
||||
// Run the api on port
|
||||
err = r.Run(":" + configuration.Port)
|
||||
@@ -72,8 +90,50 @@ func StartServer(configuration *models.Configuration, communication *models.Comm
|
||||
}
|
||||
}
|
||||
|
||||
func Files(c *gin.Context) {
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
c.Header("Content-Type", "video/mp4")
|
||||
c.File("./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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,6 @@ import (
|
||||
"github.com/kerberos-io/agent/machinery/src/routers/http"
|
||||
)
|
||||
|
||||
func StartWebserver(configuration *models.Configuration, communication *models.Communication) {
|
||||
http.StartServer(configuration, communication)
|
||||
func StartWebserver(configDirectory string, configuration *models.Configuration, communication *models.Communication) {
|
||||
http.StartServer(configDirectory, configuration, communication)
|
||||
}
|
||||
|
||||
@@ -1,22 +1,70 @@
|
||||
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"
|
||||
)
|
||||
|
||||
func ConfigureMQTT(configuration *models.Configuration, communication *models.Communication) mqtt.Client {
|
||||
// We'll cache the MQTT settings to know if we need to reinitialize the MQTT client connection.
|
||||
// If we update the configuration but no new MQTT settings are provided, we don't need to restart it.
|
||||
var PREV_MQTTURI string
|
||||
var PREV_MQTTUsername string
|
||||
var PREV_MQTTPassword string
|
||||
var PREV_HubKey string
|
||||
var PREV_AgentKey string
|
||||
|
||||
func HasMQTTClientModified(configuration *models.Configuration) bool {
|
||||
MTTURI := configuration.Config.MQTTURI
|
||||
MTTUsername := configuration.Config.MQTTUsername
|
||||
MQTTPassword := configuration.Config.MQTTPassword
|
||||
HubKey := configuration.Config.HubKey
|
||||
AgentKey := configuration.Config.Key
|
||||
if PREV_MQTTURI != MTTURI || PREV_MQTTUsername != MTTUsername || PREV_MQTTPassword != MQTTPassword || PREV_HubKey != HubKey || PREV_AgentKey != AgentKey {
|
||||
log.Log.Info("HasMQTTClientModified: MQTT settings have been modified, restarting MQTT client.")
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
// Set the MQTT settings.
|
||||
PREV_MQTTURI = configuration.Config.MQTTURI
|
||||
PREV_MQTTUsername = configuration.Config.MQTTUsername
|
||||
PREV_MQTTPassword = configuration.Config.MQTTPassword
|
||||
PREV_HubKey = configuration.Config.HubKey
|
||||
PREV_AgentKey = configuration.Config.Key
|
||||
|
||||
if config.Offline == "true" {
|
||||
log.Log.Info("ConfigureMQTT: not starting as running in Offline mode.")
|
||||
} else {
|
||||
@@ -78,27 +126,11 @@ func ConfigureMQTT(configuration *models.Configuration, communication *models.Co
|
||||
webrtc.CandidateArrays = make(map[string](chan string))
|
||||
|
||||
opts.OnConnect = func(c mqtt.Client) {
|
||||
|
||||
// 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)
|
||||
@@ -113,96 +145,373 @@ 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) {
|
||||
select {
|
||||
case communication.HandleLiveSD <- time.Now().Unix():
|
||||
default:
|
||||
}
|
||||
log.Log.Info("MQTTListenerHandleLiveSD: received request to livestream.")
|
||||
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) {
|
||||
log.Log.Info("MQTTListenerHandleLiveHDHandshake: received request to setup webrtc.")
|
||||
var sdp models.SDPPayload
|
||||
json.Unmarshal(msg.Payload(), &sdp)
|
||||
select {
|
||||
case communication.HandleLiveHDHandshake <- sdp:
|
||||
default:
|
||||
}
|
||||
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) {
|
||||
alive := string(msg.Payload())
|
||||
communication.HandleLiveHDKeepalive <- alive
|
||||
log.Log.Info("MQTTListenerHandleLiveHDKeepalive: Received keepalive: " + alive)
|
||||
})
|
||||
}
|
||||
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) {
|
||||
peerCount := string(msg.Payload())
|
||||
communication.HandleLiveHDPeers <- peerCount
|
||||
log.Log.Info("MQTTListenerHandleLiveHDPeers: Number of peers listening: " + peerCount)
|
||||
})
|
||||
}
|
||||
// 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)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
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) {
|
||||
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()
|
||||
}
|
||||
log.Log.Info("MQTTListenerHandleLiveHDCandidates: " + string(msg.Payload()))
|
||||
channel <- string(msg.Payload())
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
var onvifAction models.OnvifAction
|
||||
json.Unmarshal(msg.Payload(), &onvifAction)
|
||||
communication.HandleONVIF <- onvifAction
|
||||
log.Log.Info("MQTTListenerHandleONVIF: Received an action - " + onvifAction.Action)
|
||||
})
|
||||
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 {
|
||||
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
|
||||
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/" + config.HubKey + "/device/" + config.Key + "/request-live")
|
||||
mqttClient.Unsubscribe(config.Key + "/register")
|
||||
mqttClient.Unsubscribe("kerberos/webrtc/keepalivehub/" + config.Key)
|
||||
mqttClient.Unsubscribe("kerberos/webrtc/peers/" + config.Key)
|
||||
mqttClient.Unsubscribe("candidate/cloud")
|
||||
mqttClient.Unsubscribe("kerberos/onvif/" + config.Key)
|
||||
// Cleanup all subscriptions
|
||||
// New methods
|
||||
mqttClient.Unsubscribe("kerberos/agent/" + PREV_HubKey)
|
||||
mqttClient.Disconnect(1000)
|
||||
mqttClient = nil
|
||||
log.Log.Info("DisconnectMQTT: MQTT client disconnected.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package websocket
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
@@ -52,6 +51,7 @@ func WebsocketHandler(c *gin.Context, communication *models.Communication) {
|
||||
w := c.Writer
|
||||
r := c.Request
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
|
||||
// error handling here
|
||||
if err == nil {
|
||||
defer conn.Close()
|
||||
@@ -84,28 +84,29 @@ func WebsocketHandler(c *gin.Context, communication *models.Communication) {
|
||||
_, exists := sockets[clientID].Cancels["stream-sd"]
|
||||
if exists {
|
||||
sockets[clientID].Cancels["stream-sd"]()
|
||||
delete(sockets[clientID].Cancels, "stream-sd")
|
||||
} else {
|
||||
fmt.Println("Streaming sd does not exists for " + clientID)
|
||||
log.Log.Error("Streaming sd does not exists for " + clientID)
|
||||
}
|
||||
|
||||
case "stream-sd":
|
||||
startStrean := Message{
|
||||
ClientID: clientID,
|
||||
MessageType: "stream-sd",
|
||||
Message: map[string]string{
|
||||
"message": "Start streaming low resolution",
|
||||
},
|
||||
}
|
||||
sockets[clientID].WriteJson(startStrean)
|
||||
if communication.CameraConnected {
|
||||
_, exists := sockets[clientID].Cancels["stream-sd"]
|
||||
if exists {
|
||||
log.Log.Info("Already streaming sd for " + clientID)
|
||||
} else {
|
||||
startStream := Message{
|
||||
ClientID: clientID,
|
||||
MessageType: "stream-sd",
|
||||
Message: map[string]string{
|
||||
"message": "Start streaming low resolution",
|
||||
},
|
||||
}
|
||||
sockets[clientID].WriteJson(startStream)
|
||||
|
||||
_, exists := sockets[clientID].Cancels["stream-sd"]
|
||||
if exists {
|
||||
fmt.Println("Already streaming sd for " + clientID)
|
||||
} else {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
sockets[clientID].Cancels["stream-sd"] = cancel
|
||||
go ForwardSDStream(ctx, clientID, sockets[clientID], communication)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
sockets[clientID].Cancels["stream-sd"] = cancel
|
||||
go ForwardSDStream(ctx, clientID, sockets[clientID], communication)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,5 +175,14 @@ logreader:
|
||||
|
||||
frame.Free()
|
||||
|
||||
// Close socket for streaming
|
||||
_, exists := connection.Cancels["stream-sd"]
|
||||
if exists {
|
||||
delete(connection.Cancels, "stream-sd")
|
||||
} else {
|
||||
log.Log.Error("Streaming sd does not exists for " + clientID)
|
||||
}
|
||||
|
||||
// Send stop streaming message
|
||||
log.Log.Info("ForwardSDStream: stop sending streaming over websocket")
|
||||
}
|
||||
|
||||
247
machinery/src/rtsp/client.go
Normal file
247
machinery/src/rtsp/client.go
Normal file
@@ -0,0 +1,247 @@
|
||||
package rtsp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/jpeg"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/bluenviron/gortsplib/v3"
|
||||
"github.com/bluenviron/gortsplib/v3/pkg/base"
|
||||
"github.com/bluenviron/gortsplib/v3/pkg/formats"
|
||||
"github.com/bluenviron/gortsplib/v3/pkg/formats/rtph265"
|
||||
"github.com/bluenviron/gortsplib/v3/pkg/url"
|
||||
"github.com/bluenviron/mediacommon/pkg/codecs/h264"
|
||||
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
func CreateClient() {
|
||||
c := &gortsplib.Client{
|
||||
OnRequest: func(req *base.Request) {
|
||||
//log.Log.Info(logger.Debug, "c->s %v", req)
|
||||
},
|
||||
OnResponse: func(res *base.Response) {
|
||||
//s.Log(logger.Debug, "s->c %v", res)
|
||||
},
|
||||
OnTransportSwitch: func(err error) {
|
||||
//s.Log(logger.Warn, err.Error())
|
||||
},
|
||||
OnPacketLost: func(err error) {
|
||||
//s.Log(logger.Warn, err.Error())
|
||||
},
|
||||
OnDecodeError: func(err error) {
|
||||
//s.Log(logger.Warn, err.Error())
|
||||
},
|
||||
}
|
||||
|
||||
u, err := url.Parse("rtsp://admin:admin@192.168.1.111") //"rtsp://seing:bud-edPTQc@109.159.199.103:554/rtsp/defaultPrimary?mtu=1440&streamType=m") //
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = c.Start(u.Scheme, u.Host)
|
||||
if err != nil {
|
||||
//return err
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
medias, baseURL, _, err := c.Describe(u)
|
||||
if err != nil {
|
||||
//return err
|
||||
}
|
||||
fmt.Println(medias)
|
||||
|
||||
// find the H264 media and format
|
||||
var forma *formats.H265
|
||||
medi := medias.FindFormat(&forma)
|
||||
if medi == nil {
|
||||
panic("media not found")
|
||||
}
|
||||
|
||||
// setup RTP/H264 -> H264 decoder
|
||||
rtpDec := forma.CreateDecoder()
|
||||
// setup H264 -> MPEG-TS muxer
|
||||
//pegtsMuxer, err := newMPEGTSMuxer(forma.SPS, forma.PPS)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// setup H264 -> raw frames decoder
|
||||
/*h264RawDec, err := newH264Decoder()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer h264RawDec.close()
|
||||
|
||||
// if SPS and PPS are present into the SDP, send them to the decoder
|
||||
if forma.SPS != nil {
|
||||
h264RawDec.decode(forma.SPS)
|
||||
}
|
||||
if forma.PPS != nil {
|
||||
h264RawDec.decode(forma.PPS)
|
||||
}*/
|
||||
|
||||
readErr := make(chan error)
|
||||
go func() {
|
||||
readErr <- func() error {
|
||||
// Get codecs
|
||||
for _, medi := range medias {
|
||||
for _, forma := range medi.Formats {
|
||||
fmt.Println(forma)
|
||||
}
|
||||
}
|
||||
|
||||
err = c.SetupAll(medias, baseURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, medi := range medias {
|
||||
for _, forma := range medi.Formats {
|
||||
c.OnPacketRTP(medi, forma, func(pkt *rtp.Packet) {
|
||||
|
||||
au, pts, err := rtpDec.Decode(pkt)
|
||||
if err != nil {
|
||||
if err != rtph265.ErrNonStartingPacketAndNoPrevious && err != rtph265.ErrMorePacketsNeeded {
|
||||
log.Printf("ERR: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
for _, nalu := range au {
|
||||
log.Printf("received NALU with PTS %v and size %d\n", pts, len(nalu))
|
||||
}
|
||||
|
||||
/*// extract access unit from RTP packets
|
||||
// DecodeUntilMarker is necessary for the DTS extractor to work
|
||||
if pkt.PayloadType == 96 {
|
||||
au, pts, err := rtpDec.DecodeUntilMarker(pkt)
|
||||
|
||||
if err != nil {
|
||||
if err != rtph264.ErrNonStartingPacketAndNoPrevious && err != rtph264.ErrMorePacketsNeeded {
|
||||
log.Printf("ERR: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// encode the access unit into MPEG-TS
|
||||
mpegtsMuxer.encode(au, pts)
|
||||
|
||||
for _, nalu := range au {
|
||||
// convert NALUs into RGBA frames
|
||||
img, err := h264RawDec.decode(nalu)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// wait for a frame
|
||||
if img == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// convert frame to JPEG and save to file
|
||||
err = saveToFile(img)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
_, err = c.Play(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Wait()
|
||||
}()
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case err := <-readErr:
|
||||
fmt.Println(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func saveToFile(img image.Image) error {
|
||||
// create file
|
||||
fname := strconv.FormatInt(time.Now().UnixNano()/int64(time.Millisecond), 10) + ".jpg"
|
||||
f, err := os.Create(fname)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
log.Println("saving", fname)
|
||||
|
||||
// convert to jpeg
|
||||
return jpeg.Encode(f, img, &jpeg.Options{
|
||||
Quality: 60,
|
||||
})
|
||||
}
|
||||
|
||||
// extract SPS and PPS without decoding RTP packets
|
||||
func rtpH264ExtractSPSPPS(pkt *rtp.Packet) ([]byte, []byte) {
|
||||
if len(pkt.Payload) < 1 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
typ := h264.NALUType(pkt.Payload[0] & 0x1F)
|
||||
|
||||
switch typ {
|
||||
case h264.NALUTypeSPS:
|
||||
return pkt.Payload, nil
|
||||
|
||||
case h264.NALUTypePPS:
|
||||
return nil, pkt.Payload
|
||||
|
||||
case h264.NALUTypeSTAPA:
|
||||
payload := pkt.Payload[1:]
|
||||
var sps []byte
|
||||
var pps []byte
|
||||
|
||||
for len(payload) > 0 {
|
||||
if len(payload) < 2 {
|
||||
break
|
||||
}
|
||||
|
||||
size := uint16(payload[0])<<8 | uint16(payload[1])
|
||||
payload = payload[2:]
|
||||
|
||||
if size == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
if int(size) > len(payload) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
nalu := payload[:size]
|
||||
payload = payload[size:]
|
||||
|
||||
typ = h264.NALUType(nalu[0] & 0x1F)
|
||||
|
||||
switch typ {
|
||||
case h264.NALUTypeSPS:
|
||||
sps = nalu
|
||||
|
||||
case h264.NALUTypePPS:
|
||||
pps = nalu
|
||||
}
|
||||
}
|
||||
|
||||
return sps, pps
|
||||
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
140
machinery/src/rtsp/h264decoder.go
Normal file
140
machinery/src/rtsp/h264decoder.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package rtsp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// #cgo pkg-config: libavcodec libavutil libswscale
|
||||
// #include <libavcodec/avcodec.h>
|
||||
// #include <libavutil/imgutils.h>
|
||||
// #include <libswscale/swscale.h>
|
||||
import "C"
|
||||
|
||||
func frameData(frame *C.AVFrame) **C.uint8_t {
|
||||
return (**C.uint8_t)(unsafe.Pointer(&frame.data[0]))
|
||||
}
|
||||
|
||||
func frameLineSize(frame *C.AVFrame) *C.int {
|
||||
return (*C.int)(unsafe.Pointer(&frame.linesize[0]))
|
||||
}
|
||||
|
||||
// h264Decoder is a wrapper around ffmpeg's H264 decoder.
|
||||
type h264Decoder struct {
|
||||
codecCtx *C.AVCodecContext
|
||||
srcFrame *C.AVFrame
|
||||
swsCtx *C.struct_SwsContext
|
||||
dstFrame *C.AVFrame
|
||||
dstFramePtr []uint8
|
||||
}
|
||||
|
||||
// newH264Decoder allocates a new h264Decoder.
|
||||
func newH264Decoder() (*h264Decoder, error) {
|
||||
codec := C.avcodec_find_decoder(C.AV_CODEC_ID_H264)
|
||||
if codec == nil {
|
||||
return nil, fmt.Errorf("avcodec_find_decoder() failed")
|
||||
}
|
||||
|
||||
codecCtx := C.avcodec_alloc_context3(codec)
|
||||
if codecCtx == nil {
|
||||
return nil, fmt.Errorf("avcodec_alloc_context3() failed")
|
||||
}
|
||||
|
||||
res := C.avcodec_open2(codecCtx, codec, nil)
|
||||
if res < 0 {
|
||||
C.avcodec_close(codecCtx)
|
||||
return nil, fmt.Errorf("avcodec_open2() failed")
|
||||
}
|
||||
|
||||
srcFrame := C.av_frame_alloc()
|
||||
if srcFrame == nil {
|
||||
C.avcodec_close(codecCtx)
|
||||
return nil, fmt.Errorf("av_frame_alloc() failed")
|
||||
}
|
||||
|
||||
return &h264Decoder{
|
||||
codecCtx: codecCtx,
|
||||
srcFrame: srcFrame,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// close closes the decoder.
|
||||
func (d *h264Decoder) close() {
|
||||
if d.dstFrame != nil {
|
||||
C.av_frame_free(&d.dstFrame)
|
||||
}
|
||||
|
||||
if d.swsCtx != nil {
|
||||
C.sws_freeContext(d.swsCtx)
|
||||
}
|
||||
|
||||
C.av_frame_free(&d.srcFrame)
|
||||
C.avcodec_close(d.codecCtx)
|
||||
}
|
||||
|
||||
func (d *h264Decoder) decode(nalu []byte) (image.Image, error) {
|
||||
nalu = append([]uint8{0x00, 0x00, 0x00, 0x01}, []uint8(nalu)...)
|
||||
|
||||
// send frame to decoder
|
||||
var avPacket C.AVPacket
|
||||
avPacket.data = (*C.uint8_t)(C.CBytes(nalu))
|
||||
defer C.free(unsafe.Pointer(avPacket.data))
|
||||
avPacket.size = C.int(len(nalu))
|
||||
res := C.avcodec_send_packet(d.codecCtx, &avPacket)
|
||||
if res < 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// receive frame if available
|
||||
res = C.avcodec_receive_frame(d.codecCtx, d.srcFrame)
|
||||
if res < 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// if frame size has changed, allocate needed objects
|
||||
if d.dstFrame == nil || d.dstFrame.width != d.srcFrame.width || d.dstFrame.height != d.srcFrame.height {
|
||||
if d.dstFrame != nil {
|
||||
C.av_frame_free(&d.dstFrame)
|
||||
}
|
||||
|
||||
if d.swsCtx != nil {
|
||||
C.sws_freeContext(d.swsCtx)
|
||||
}
|
||||
|
||||
d.dstFrame = C.av_frame_alloc()
|
||||
d.dstFrame.format = C.AV_PIX_FMT_RGBA
|
||||
d.dstFrame.width = d.srcFrame.width
|
||||
d.dstFrame.height = d.srcFrame.height
|
||||
d.dstFrame.color_range = C.AVCOL_RANGE_JPEG
|
||||
res = C.av_frame_get_buffer(d.dstFrame, 1)
|
||||
if res < 0 {
|
||||
return nil, fmt.Errorf("av_frame_get_buffer() err")
|
||||
}
|
||||
|
||||
d.swsCtx = C.sws_getContext(d.srcFrame.width, d.srcFrame.height, C.AV_PIX_FMT_YUV420P,
|
||||
d.dstFrame.width, d.dstFrame.height, (int32)(d.dstFrame.format), C.SWS_BILINEAR, nil, nil, nil)
|
||||
if d.swsCtx == nil {
|
||||
return nil, fmt.Errorf("sws_getContext() err")
|
||||
}
|
||||
|
||||
dstFrameSize := C.av_image_get_buffer_size((int32)(d.dstFrame.format), d.dstFrame.width, d.dstFrame.height, 1)
|
||||
d.dstFramePtr = (*[1 << 30]uint8)(unsafe.Pointer(d.dstFrame.data[0]))[:dstFrameSize:dstFrameSize]
|
||||
}
|
||||
|
||||
// convert frame from YUV420 to RGB
|
||||
res = C.sws_scale(d.swsCtx, frameData(d.srcFrame), frameLineSize(d.srcFrame),
|
||||
0, d.srcFrame.height, frameData(d.dstFrame), frameLineSize(d.dstFrame))
|
||||
if res < 0 {
|
||||
return nil, fmt.Errorf("sws_scale() err")
|
||||
}
|
||||
|
||||
// embed frame into an image.Image
|
||||
return &image.RGBA{
|
||||
Pix: d.dstFramePtr,
|
||||
Stride: 4 * (int)(d.dstFrame.width),
|
||||
Rect: image.Rectangle{
|
||||
Max: image.Point{(int)(d.dstFrame.width), (int)(d.dstFrame.height)},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
15
machinery/src/rtsp/mp4muxer.go
Normal file
15
machinery/src/rtsp/mp4muxer.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package rtsp
|
||||
|
||||
// mp4Muxer allows to save a H264 stream into a Mp4 file.
|
||||
type mp4Muxer struct {
|
||||
sps []byte
|
||||
pps []byte
|
||||
}
|
||||
|
||||
// newMp4Muxer allocates a mp4Muxer.
|
||||
func newMp4Muxer(sps []byte, pps []byte) (*mp4Muxer, error) {
|
||||
return &mp4Muxer{
|
||||
sps: sps,
|
||||
pps: pps,
|
||||
}, nil
|
||||
}
|
||||
173
machinery/src/rtsp/mpegtsmuxer.go
Normal file
173
machinery/src/rtsp/mpegtsmuxer.go
Normal file
@@ -0,0 +1,173 @@
|
||||
package rtsp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/asticode/go-astits"
|
||||
"github.com/bluenviron/mediacommon/pkg/codecs/h264"
|
||||
)
|
||||
|
||||
// mpegtsMuxer allows to save a H264 stream into a MPEG-TS file.
|
||||
type mpegtsMuxer struct {
|
||||
sps []byte
|
||||
pps []byte
|
||||
|
||||
f *os.File
|
||||
b *bufio.Writer
|
||||
mux *astits.Muxer
|
||||
dtsExtractor *h264.DTSExtractor
|
||||
firstIDRReceived bool
|
||||
startDTS time.Duration
|
||||
}
|
||||
|
||||
// newMPEGTSMuxer allocates a mpegtsMuxer.
|
||||
func newMPEGTSMuxer(sps []byte, pps []byte) (*mpegtsMuxer, error) {
|
||||
f, err := os.Create("mystream.ts")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b := bufio.NewWriter(f)
|
||||
|
||||
mux := astits.NewMuxer(context.Background(), b)
|
||||
mux.AddElementaryStream(astits.PMTElementaryStream{
|
||||
ElementaryPID: 256,
|
||||
StreamType: astits.StreamTypeH264Video,
|
||||
})
|
||||
mux.SetPCRPID(256)
|
||||
|
||||
return &mpegtsMuxer{
|
||||
sps: sps,
|
||||
pps: pps,
|
||||
f: f,
|
||||
b: b,
|
||||
mux: mux,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// close closes all the mpegtsMuxer resources.
|
||||
func (e *mpegtsMuxer) close() {
|
||||
e.b.Flush()
|
||||
e.f.Close()
|
||||
}
|
||||
|
||||
// encode encodes a H264 access unit into MPEG-TS.
|
||||
func (e *mpegtsMuxer) encode(au [][]byte, pts time.Duration) error {
|
||||
// prepend an AUD. This is required by some players
|
||||
filteredNALUs := [][]byte{
|
||||
{byte(h264.NALUTypeAccessUnitDelimiter), 240},
|
||||
}
|
||||
|
||||
nonIDRPresent := false
|
||||
idrPresent := false
|
||||
|
||||
for _, nalu := range au {
|
||||
typ := h264.NALUType(nalu[0] & 0x1F)
|
||||
switch typ {
|
||||
case h264.NALUTypeSPS:
|
||||
e.sps = append([]byte(nil), nalu...)
|
||||
continue
|
||||
|
||||
case h264.NALUTypePPS:
|
||||
e.pps = append([]byte(nil), nalu...)
|
||||
continue
|
||||
|
||||
case h264.NALUTypeAccessUnitDelimiter:
|
||||
continue
|
||||
|
||||
case h264.NALUTypeIDR:
|
||||
idrPresent = true
|
||||
|
||||
case h264.NALUTypeNonIDR:
|
||||
nonIDRPresent = true
|
||||
}
|
||||
|
||||
filteredNALUs = append(filteredNALUs, nalu)
|
||||
}
|
||||
|
||||
au = filteredNALUs
|
||||
|
||||
if !nonIDRPresent && !idrPresent {
|
||||
return nil
|
||||
}
|
||||
|
||||
// add SPS and PPS before every group that contains an IDR
|
||||
if idrPresent {
|
||||
au = append([][]byte{e.sps, e.pps}, au...)
|
||||
}
|
||||
|
||||
var dts time.Duration
|
||||
|
||||
if !e.firstIDRReceived {
|
||||
// skip samples silently until we find one with a IDR
|
||||
if !idrPresent {
|
||||
return nil
|
||||
}
|
||||
|
||||
e.firstIDRReceived = true
|
||||
e.dtsExtractor = h264.NewDTSExtractor()
|
||||
|
||||
var err error
|
||||
dts, err = e.dtsExtractor.Extract(au, pts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
e.startDTS = dts
|
||||
dts = 0
|
||||
pts -= e.startDTS
|
||||
|
||||
} else {
|
||||
var err error
|
||||
dts, err = e.dtsExtractor.Extract(au, pts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dts -= e.startDTS
|
||||
pts -= e.startDTS
|
||||
}
|
||||
|
||||
oh := &astits.PESOptionalHeader{
|
||||
MarkerBits: 2,
|
||||
}
|
||||
|
||||
if dts == pts {
|
||||
oh.PTSDTSIndicator = astits.PTSDTSIndicatorOnlyPTS
|
||||
oh.PTS = &astits.ClockReference{Base: int64(pts.Seconds() * 90000)}
|
||||
} else {
|
||||
oh.PTSDTSIndicator = astits.PTSDTSIndicatorBothPresent
|
||||
oh.DTS = &astits.ClockReference{Base: int64(dts.Seconds() * 90000)}
|
||||
oh.PTS = &astits.ClockReference{Base: int64(pts.Seconds() * 90000)}
|
||||
}
|
||||
|
||||
// encode into Annex-B
|
||||
annexb, err := h264.AnnexBMarshal(au)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// write TS packet
|
||||
_, err = e.mux.WriteData(&astits.MuxerData{
|
||||
PID: 256,
|
||||
AdaptationField: &astits.PacketAdaptationField{
|
||||
RandomAccessIndicator: idrPresent,
|
||||
},
|
||||
PES: &astits.PESData{
|
||||
Header: &astits.PESHeader{
|
||||
OptionalHeader: oh,
|
||||
StreamID: 224, // video
|
||||
},
|
||||
Data: annexb,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Println("wrote TS packet")
|
||||
return nil
|
||||
}
|
||||
@@ -9,11 +9,13 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
"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"
|
||||
)
|
||||
@@ -38,6 +40,17 @@ const (
|
||||
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
|
||||
)
|
||||
|
||||
func PrintASCIIArt() {
|
||||
asciiArt := ` _ __ _ _
|
||||
| |/ /___ _ __| |__ ___ _ __ ___ ___ (_) ___
|
||||
| ' // _ \ '__| '_ \ / _ \ '__/ _ \/ __| | |/ _ \
|
||||
| . \ __/ | | |_) | __/ | | (_) \__ \_| | (_) |
|
||||
|_|\_\___|_| |_.__/ \___|_| \___/|___(_)_|\___/
|
||||
|
||||
`
|
||||
fmt.Println(asciiArt)
|
||||
}
|
||||
|
||||
func DirSize(path string) (int64, error) {
|
||||
var size int64
|
||||
err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error {
|
||||
@@ -98,30 +111,46 @@ func CountDigits(i int64) (count int) {
|
||||
return count
|
||||
}
|
||||
|
||||
func CheckDataDirectoryPermissions() error {
|
||||
recordingsDirectory := "./data/recordings"
|
||||
configDirectory := "./data/config"
|
||||
snapshotsDirectory := "./data/snapshots"
|
||||
func CheckDataDirectoryPermissions(configDirectory string) error {
|
||||
recordingsDirectory := configDirectory + "/data/recordings"
|
||||
configurationDirectory := configDirectory + "/data/config"
|
||||
snapshotsDirectory := configDirectory + "/data/snapshots"
|
||||
cloudDirectory := configDirectory + "/data/cloud"
|
||||
|
||||
err := CheckDirectoryPermissions(recordingsDirectory)
|
||||
if err == nil {
|
||||
err = CheckDirectoryPermissions(configurationDirectory)
|
||||
if err == nil {
|
||||
err = CheckDirectoryPermissions(snapshotsDirectory)
|
||||
if err == nil {
|
||||
err = CheckDirectoryPermissions(cloudDirectory)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Log.Error("Checking data directory permissions: " + err.Error())
|
||||
return err
|
||||
}
|
||||
err = CheckDirectoryPermissions(configDirectory)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = CheckDirectoryPermissions(snapshotsDirectory)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Log.Info("Checking data directory permissions: OK")
|
||||
return nil
|
||||
}
|
||||
|
||||
func CheckDirectoryPermissions(directory string) error {
|
||||
// Check if the directory exists
|
||||
if _, err := os.Stat(directory); os.IsNotExist(err) {
|
||||
return errors.New("Directory does not exist, " + directory)
|
||||
}
|
||||
|
||||
// Try to create a file
|
||||
file := directory + "/.test"
|
||||
f, err := os.Create(file)
|
||||
defer f.Close()
|
||||
if f != nil {
|
||||
defer f.Close()
|
||||
}
|
||||
|
||||
// We will remove the file if it was created
|
||||
if err == nil {
|
||||
err := os.Remove(file)
|
||||
if err == nil {
|
||||
@@ -136,7 +165,6 @@ func CheckDirectoryPermissions(directory string) error {
|
||||
func ReadDirectory(directory string) ([]os.FileInfo, error) {
|
||||
ff, err := ioutil.ReadDir(directory)
|
||||
if err != nil {
|
||||
log.Log.Error(err.Error())
|
||||
return []os.FileInfo{}, nil
|
||||
}
|
||||
return ff, err
|
||||
@@ -271,3 +299,99 @@ func CreateFragmentedMP4(fullName string, fragmentedDuration int64) {
|
||||
os.Remove(fullName)
|
||||
os.Rename(fullName+"f.mp4", fullName)
|
||||
}
|
||||
|
||||
func PrintEnvironmentVariables() {
|
||||
// Print environment variables that include "AGENT_" as a prefix.
|
||||
environmentVariables := ""
|
||||
for _, e := range os.Environ() {
|
||||
if strings.Contains(e, "AGENT_") {
|
||||
pair := strings.Split(e, "=")
|
||||
environmentVariables = environmentVariables + pair[0] + "=" + pair[1] + " "
|
||||
}
|
||||
}
|
||||
log.Log.Info("Printing out environmentVariables (AGENT_...): " + environmentVariables)
|
||||
}
|
||||
|
||||
func PrintConfiguration(configuration *models.Configuration) {
|
||||
// We will print out the struct.
|
||||
if configuration == nil {
|
||||
log.Log.Info("Configuration is nil")
|
||||
return
|
||||
}
|
||||
config := configuration.Config
|
||||
// Iterate over the struct and printout the values.
|
||||
v := reflect.ValueOf(config)
|
||||
typeOfS := v.Type()
|
||||
configurationVariables := ""
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
key := typeOfS.Field(i).Name
|
||||
value := v.Field(i).Interface()
|
||||
// Convert to string.
|
||||
configurationVariables = configurationVariables + key + ": " + fmt.Sprintf("%v", value) + " "
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ var (
|
||||
CandidateArrays map[string](chan string)
|
||||
peerConnectionCount int64
|
||||
peerConnections map[string]*pionWebRTC.PeerConnection
|
||||
encoder *ffmpeg.VideoEncoder
|
||||
//encoder *ffmpeg.VideoEncoder
|
||||
)
|
||||
|
||||
type WebRTC struct {
|
||||
@@ -40,7 +40,8 @@ type WebRTC struct {
|
||||
PacketsCount chan int
|
||||
}
|
||||
|
||||
func init() {
|
||||
// No longer used, is for transcoding, might comeback on this!
|
||||
/*func init() {
|
||||
// Encoder is created for once and for all.
|
||||
var err error
|
||||
encoder, err = ffmpeg.NewVideoEncoderByCodecType(av.H264)
|
||||
@@ -55,7 +56,7 @@ func init() {
|
||||
encoder.SetPixelFormat(av.I420)
|
||||
encoder.SetBitrate(1000000) // 1MB
|
||||
encoder.SetGopSize(30 / 1) // 1s
|
||||
}
|
||||
}*/
|
||||
|
||||
func CreateWebRTC(name string, stunServers []string, turnServers []string, turnServersUsername string, turnServersCredential string) *WebRTC {
|
||||
return &WebRTC{
|
||||
@@ -86,19 +87,37 @@ func (w WebRTC) CreateOffer(sd []byte) pionWebRTC.SessionDescription {
|
||||
return offer
|
||||
}
|
||||
|
||||
func InitializeWebRTCConnection(configuration *models.Configuration, communication *models.Communication, mqttClient mqtt.Client, track *pionWebRTC.TrackLocalStaticSample, handshake models.SDPPayload, candidates chan string) {
|
||||
func RegisterCandidates(key string, candidate models.ReceiveHDCandidatesPayload) {
|
||||
|
||||
// Set lock
|
||||
CandidatesMutex.Lock()
|
||||
defer CandidatesMutex.Unlock()
|
||||
|
||||
channel := CandidateArrays[key]
|
||||
if channel == nil {
|
||||
channel = make(chan string)
|
||||
CandidateArrays[key] = channel
|
||||
}
|
||||
log.Log.Info("HandleReceiveHDCandidates: " + candidate.Candidate)
|
||||
channel <- candidate.Candidate
|
||||
}
|
||||
|
||||
func InitializeWebRTCConnection(configuration *models.Configuration, communication *models.Communication, mqttClient mqtt.Client, videoTrack *pionWebRTC.TrackLocalStaticSample, audioTrack *pionWebRTC.TrackLocalStaticSample, handshake models.RequestHDStreamPayload, candidates chan string) {
|
||||
|
||||
config := configuration.Config
|
||||
|
||||
name := config.Key
|
||||
deviceKey := config.Key
|
||||
stunServers := []string{config.STUNURI}
|
||||
turnServers := []string{config.TURNURI}
|
||||
turnServersUsername := config.TURNUsername
|
||||
turnServersCredential := config.TURNPassword
|
||||
|
||||
// Set variables
|
||||
hubKey := handshake.HubKey
|
||||
sessionDescription := handshake.SessionDescription
|
||||
|
||||
// Create WebRTC object
|
||||
w := CreateWebRTC(name, stunServers, turnServers, turnServersUsername, turnServersCredential)
|
||||
sd, err := w.DecodeSessionDescription(handshake.Sdp)
|
||||
w := CreateWebRTC(deviceKey, stunServers, turnServers, turnServersUsername, turnServersCredential)
|
||||
sd, err := w.DecodeSessionDescription(sessionDescription)
|
||||
|
||||
if err == nil {
|
||||
|
||||
@@ -127,15 +146,13 @@ func InitializeWebRTCConnection(configuration *models.Configuration, communicati
|
||||
|
||||
if err == nil && peerConnection != nil {
|
||||
|
||||
if _, err = peerConnection.AddTrack(track); err != nil {
|
||||
if _, err = peerConnection.AddTrack(videoTrack); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
_, err = peerConnection.AddTransceiverFromTrack(track,
|
||||
pionWebRTC.RtpTransceiverInit{
|
||||
Direction: pionWebRTC.RTPTransceiverDirectionSendonly,
|
||||
},
|
||||
)
|
||||
if _, err = peerConnection.AddTrack(audioTrack); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -143,8 +160,11 @@ func InitializeWebRTCConnection(configuration *models.Configuration, communicati
|
||||
|
||||
peerConnection.OnICEConnectionStateChange(func(connectionState pionWebRTC.ICEConnectionState) {
|
||||
if connectionState == pionWebRTC.ICEConnectionStateDisconnected {
|
||||
CandidatesMutex.Lock()
|
||||
defer CandidatesMutex.Unlock()
|
||||
|
||||
atomic.AddInt64(&peerConnectionCount, -1)
|
||||
peerConnections[handshake.Cuuid] = nil
|
||||
peerConnections[handshake.SessionID] = nil
|
||||
close(candidates)
|
||||
close(w.PacketsCount)
|
||||
if err := peerConnection.Close(); err != nil {
|
||||
@@ -153,9 +173,12 @@ func InitializeWebRTCConnection(configuration *models.Configuration, communicati
|
||||
} else if connectionState == pionWebRTC.ICEConnectionStateConnected {
|
||||
atomic.AddInt64(&peerConnectionCount, 1)
|
||||
} else if connectionState == pionWebRTC.ICEConnectionStateChecking {
|
||||
// Iterate over the candidates and send them to the remote client
|
||||
// Non blocking channel
|
||||
for candidate := range candidates {
|
||||
log.Log.Info("InitializeWebRTCConnection: Received candidate.")
|
||||
if candidateErr := peerConnection.AddICECandidate(pionWebRTC.ICECandidateInit{Candidate: string(candidate)}); candidateErr != nil {
|
||||
log.Log.Error("InitializeWebRTCConnection: something went wrong while adding candidate: " + candidateErr.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -168,7 +191,6 @@ func InitializeWebRTCConnection(configuration *models.Configuration, communicati
|
||||
panic(err)
|
||||
}
|
||||
|
||||
//gatherCompletePromise := pionWebRTC.GatheringCompletePromise(peerConnection)
|
||||
answer, err := peerConnection.CreateAnswer(nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -179,8 +201,9 @@ func InitializeWebRTCConnection(configuration *models.Configuration, communicati
|
||||
// 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
|
||||
}
|
||||
@@ -188,25 +211,60 @@ func InitializeWebRTCConnection(configuration *models.Configuration, communicati
|
||||
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 {
|
||||
@@ -214,12 +272,29 @@ func InitializeWebRTCConnection(configuration *models.Configuration, communicati
|
||||
}
|
||||
}
|
||||
|
||||
func NewVideoTrack() *pionWebRTC.TrackLocalStaticSample {
|
||||
outboundVideoTrack, _ := pionWebRTC.NewTrackLocalStaticSample(pionWebRTC.RTPCodecCapability{MimeType: "video/h264"}, "video", "pion124")
|
||||
func NewVideoTrack(codecs []av.CodecData) *pionWebRTC.TrackLocalStaticSample {
|
||||
var mimeType string
|
||||
mimeType = pionWebRTC.MimeTypeH264
|
||||
outboundVideoTrack, _ := pionWebRTC.NewTrackLocalStaticSample(pionWebRTC.RTPCodecCapability{MimeType: mimeType}, "video", "pion124")
|
||||
return outboundVideoTrack
|
||||
}
|
||||
|
||||
func WriteToTrack(livestreamCursor *pubsub.QueueCursor, configuration *models.Configuration, communication *models.Communication, mqttClient mqtt.Client, track *pionWebRTC.TrackLocalStaticSample, codecs []av.CodecData, decoder *ffmpeg.VideoDecoder, decoderMutex *sync.Mutex) {
|
||||
func NewAudioTrack(codecs []av.CodecData) *pionWebRTC.TrackLocalStaticSample {
|
||||
var mimeType string
|
||||
for _, codec := range codecs {
|
||||
if codec.Type().String() == "OPUS" {
|
||||
mimeType = pionWebRTC.MimeTypeOpus
|
||||
} else if codec.Type().String() == "PCM_MULAW" {
|
||||
mimeType = pionWebRTC.MimeTypePCMU
|
||||
} else if codec.Type().String() == "PCM_ALAW" {
|
||||
mimeType = pionWebRTC.MimeTypePCMA
|
||||
}
|
||||
}
|
||||
outboundAudioTrack, _ := pionWebRTC.NewTrackLocalStaticSample(pionWebRTC.RTPCodecCapability{MimeType: mimeType}, "audio", "pion124")
|
||||
return outboundAudioTrack
|
||||
}
|
||||
|
||||
func WriteToTrack(livestreamCursor *pubsub.QueueCursor, configuration *models.Configuration, communication *models.Communication, mqttClient mqtt.Client, videoTrack *pionWebRTC.TrackLocalStaticSample, audioTrack *pionWebRTC.TrackLocalStaticSample, codecs []av.CodecData, decoder *ffmpeg.VideoDecoder, decoderMutex *sync.Mutex) {
|
||||
|
||||
config := configuration.Config
|
||||
|
||||
@@ -230,13 +305,10 @@ func WriteToTrack(livestreamCursor *pubsub.QueueCursor, configuration *models.Co
|
||||
// Later when we read a packet we need to figure out which track to send it to.
|
||||
videoIdx := -1
|
||||
audioIdx := -1
|
||||
log.Log.Info("WriteToTrack: listing codecs.")
|
||||
for i, codec := range codecs {
|
||||
log.Log.Info("WriteToTrack: codec - " + codec.Type().String() + " found.")
|
||||
log.Log.Info(codec.Type().String())
|
||||
if codec.Type().String() == "H264" && videoIdx < 0 {
|
||||
videoIdx = i
|
||||
} else if codec.Type().String() == "PCM_MULAW" && audioIdx < 0 {
|
||||
} else if (codec.Type().String() == "OPUS" || codec.Type().String() == "PCM_MULAW" || codec.Type().String() == "PCM_ALAW") && audioIdx < 0 {
|
||||
audioIdx = i
|
||||
}
|
||||
}
|
||||
@@ -345,16 +417,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)
|
||||
@@ -366,13 +431,17 @@ func WriteToTrack(livestreamCursor *pubsub.QueueCursor, configuration *models.Co
|
||||
log.Log.Info("WriteToTrack: Error marshalling frame, " + err.Error())
|
||||
}
|
||||
} else {
|
||||
if err := track.WriteSample(sample); err != nil && err != io.ErrClosedPipe {
|
||||
fmt.Println("WriteToTrack: something went wrong while writing sample: " + err.Error())
|
||||
if err := videoTrack.WriteSample(sample); err != nil && err != io.ErrClosedPipe {
|
||||
log.Log.Error("WriteToTrack: something went wrong while writing sample: " + err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
case audioIdx:
|
||||
//log.Log.Info("WriteToTrack: not writing audio for the moment.")
|
||||
// We will send the audio
|
||||
sample := pionMedia.Sample{Data: pkt.Data, Duration: pkt.Time}
|
||||
if err := audioTrack.WriteSample(sample); err != nil && err != io.ErrClosedPipe {
|
||||
log.Log.Error("WriteToTrack: something went wrong while writing sample: " + err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -381,6 +450,7 @@ func WriteToTrack(livestreamCursor *pubsub.QueueCursor, configuration *models.Co
|
||||
p.Close()
|
||||
}
|
||||
}
|
||||
|
||||
peerConnectionCount = 0
|
||||
log.Log.Info("WriteToTrack: stop writing to track.")
|
||||
}
|
||||
|
||||
6
snap/hooks/configure
vendored
Normal file
6
snap/hooks/configure
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
#!/bin/sh -e
|
||||
|
||||
cp -R $SNAP/data $SNAP_COMMON/
|
||||
cp -R $SNAP/www $SNAP_COMMON/
|
||||
cp -R $SNAP/version $SNAP_COMMON/
|
||||
cp -R $SNAP/mp4fragment $SNAP_COMMON/
|
||||
23
snap/snapcraft.yaml
Normal file
23
snap/snapcraft.yaml
Normal file
@@ -0,0 +1,23 @@
|
||||
name: kerberosio # you probably want to 'snapcraft register <name>'
|
||||
base: core22 # the base snap is the execution environment for this snap
|
||||
version: '3.0.0' # just for humans, typically '1.2+git' or '1.3.2'
|
||||
summary: A stand-alone open source video surveillance system # 79 char long summary
|
||||
description: |
|
||||
Kerberos Agent is an isolated and scalable video (surveillance) management
|
||||
agent made available as Open Source under the MIT License. This means that
|
||||
all the source code is available for you or your company, and you can use,
|
||||
transform and distribute the source code; as long you keep a reference of
|
||||
the original license. Kerberos Agent can be used for commercial usage.
|
||||
|
||||
grade: stable # stable # must be 'stable' to release into candidate/stable channels
|
||||
confinement: strict # use 'strict' once you have the right plugs and slots
|
||||
environment:
|
||||
GIN_MODE: release
|
||||
apps:
|
||||
agent:
|
||||
command: main -config /var/snap/kerberosio/common
|
||||
plugs: [ network, network-bind ]
|
||||
parts:
|
||||
agent:
|
||||
source: . #https://github.com/kerberos-io/agent/releases/download/21c0e01/agent-amd64.tar
|
||||
plugin: dump
|
||||
@@ -21,9 +21,11 @@
|
||||
"react/forbid-prop-types": "off",
|
||||
"no-case-declarations": "off",
|
||||
"camelcase": "off",
|
||||
"dot-notation": "off",
|
||||
"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": [
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"private": false,
|
||||
"dependencies": {
|
||||
"@giantmachines/redux-websocket": "^1.5.1",
|
||||
"@kerberos-io/ui": "1.70.0",
|
||||
"@kerberos-io/ui": "^1.76.0",
|
||||
"@material-ui/core": "^4.12.4",
|
||||
"@material-ui/icons": "^4.11.3",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
(function (window) {
|
||||
window['env'] = window['env'] || {};
|
||||
// Environment variables
|
||||
window['env']['apiUrl'] = '${FACTORY_API_URL}';
|
||||
window['env']['mode'] = 'demo';
|
||||
})(this);
|
||||
@@ -1,5 +1,5 @@
|
||||
(function (window) {
|
||||
window['env'] = window['env'] || {};
|
||||
// Environment variables
|
||||
window['env']['apiUrl'] = 'http://api.factory.kerberos.io';
|
||||
window['env']['mode'] = 'release';
|
||||
})(this);
|
||||
@@ -19,7 +19,7 @@
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<script src="assets/env.js"></script>
|
||||
<script src="%PUBLIC_URL%/env.js"></script>
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
"configure": "Konfigurieren"
|
||||
},
|
||||
"buttons": {
|
||||
"save": "Speichern"
|
||||
"save": "Speichern",
|
||||
"verify_connection": "Verify Connection"
|
||||
},
|
||||
"navigation": {
|
||||
"profile": "Profil",
|
||||
@@ -69,7 +70,10 @@
|
||||
"verify_persistence_error": "Beim überprüfen der Speichereinstellungen ist ein Fehler aufgetreten",
|
||||
"verify_camera": "Überprüfen der Kameraeinstellungen.",
|
||||
"verify_camera_success": "Die Kameraeinstellungen wurden erfolgreich überprüft.",
|
||||
"verify_camera_error": "Beim überprüfen der Kameraeinstellungen ist ein Fehler aufgetreten"
|
||||
"verify_camera_error": "Beim überprüfen der Kameraeinstellungen ist ein Fehler aufgetreten",
|
||||
"verify_onvif": "Verifying your ONVIF settings.",
|
||||
"verify_onvif_success": "ONVIF settings are successfully verified.",
|
||||
"verify_onvif_error": "Something went wrong while verifying the ONVIF settings"
|
||||
},
|
||||
"overview": {
|
||||
"general": "Allgemein",
|
||||
@@ -81,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",
|
||||
@@ -186,19 +199,26 @@
|
||||
"kerberoshub_description_region": "Die Region in der die Aufnahmen gespeichert werden sollen.",
|
||||
"kerberoshub_bucket": "Bucket",
|
||||
"kerberoshub_description_bucket": "Der Bucket in dem die Aufnahmen gespeichert werden.",
|
||||
"kerberoshub_username": "Benutzername/Verzeichnis",
|
||||
"kerberoshub_username": "Benutzername/Verzeichnis (should match Kerberos Hub username)",
|
||||
"kerberoshub_description_username": "Der Benutzername des Kerberos Hub Accounts.",
|
||||
"kerberosvault_apiurl": "Kerberos Vault API URL",
|
||||
"kerberosvault_description_apiurl": "Die Kerberos Vault API URL.",
|
||||
"kerberosvault_provider": "Anbieter",
|
||||
"kerberosvault_description_provider": "Der Anbieter zu dem die Aufnahmen gesendet werden.",
|
||||
"kerberosvault_directory": "Verzeichnis",
|
||||
"kerberosvault_directory": "Verzeichnis (should match Kerberos Hub username)",
|
||||
"kerberosvault_description_directory": "Das Uneterverzeichnis in dem die Aufnahmen gespeichert werden sollen.",
|
||||
"kerberosvault_accesskey": "Zugriffsschlüssel",
|
||||
"kerberosvault_description_accesskey": "Der Zugriffsschlüssel des Kerberos Vault Accounts..",
|
||||
"kerberosvault_secretkey": "Geheimer Schlüssel",
|
||||
"kerberosvault_description_secretkey": "Der geheime Schlüssel des Kerberos Vault Accounts.",
|
||||
"verify_connection": "Verbindung prüfen"
|
||||
"verify_connection": "Verbindung prüfen",
|
||||
"dropbox_directory": "Directory",
|
||||
"dropbox_description_directory": "The sub directory where the recordings will be stored in your Dropbox account.",
|
||||
"dropbox_accesstoken": "Access token",
|
||||
"dropbox_description_accesstoken": "The access token of your Dropbox account/app.",
|
||||
"remove_after_upload": "Once recordings are uploaded to some persistence, you might want to remove them from the local Kerberos Agent.",
|
||||
"remove_after_upload_description": "Remove recordings after they are uploaded successfully.",
|
||||
"remove_after_upload_enabled": "Enabled delete on upload"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
"configure": "Configure"
|
||||
},
|
||||
"buttons": {
|
||||
"save": "Save"
|
||||
"save": "Save",
|
||||
"verify_connection": "Verify Connection"
|
||||
},
|
||||
"navigation": {
|
||||
"profile": "Profile",
|
||||
@@ -22,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",
|
||||
@@ -69,7 +70,10 @@
|
||||
"verify_persistence_error": "Something went wrong while verifying the persistence",
|
||||
"verify_camera": "Verifying your camera settings.",
|
||||
"verify_camera_success": "Camera settings are successfully verified.",
|
||||
"verify_camera_error": "Something went wrong while verifying the camera settings"
|
||||
"verify_camera_error": "Something went wrong while verifying the camera settings",
|
||||
"verify_onvif": "Verifying your ONVIF settings.",
|
||||
"verify_onvif_success": "ONVIF settings are successfully verified.",
|
||||
"verify_onvif_error": "Something went wrong while verifying the ONVIF settings"
|
||||
},
|
||||
"overview": {
|
||||
"general": "General",
|
||||
@@ -81,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",
|
||||
@@ -138,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.",
|
||||
@@ -186,19 +199,26 @@
|
||||
"kerberoshub_description_region": "The region we are storing our recordings in.",
|
||||
"kerberoshub_bucket": "Bucket",
|
||||
"kerberoshub_description_bucket": "The bucket we are storing our recordings in.",
|
||||
"kerberoshub_username": "Username/Directory",
|
||||
"kerberoshub_username": "Username/Directory (should match Kerberos Hub username)",
|
||||
"kerberoshub_description_username": "The username of your Kerberos Hub account.",
|
||||
"kerberosvault_apiurl": "Kerberos Vault API URL",
|
||||
"kerberosvault_description_apiurl": "The Kerberos Vault API",
|
||||
"kerberosvault_provider": "Provider",
|
||||
"kerberosvault_description_provider": "The provider to which your recordings will be send.",
|
||||
"kerberosvault_directory": "Directory",
|
||||
"kerberosvault_directory": "Directory (should match Kerberos Hub username)",
|
||||
"kerberosvault_description_directory": "Sub directory the recordings will be stored in your provider.",
|
||||
"kerberosvault_accesskey": "Access key",
|
||||
"kerberosvault_description_accesskey": "The access key of your Kerberos Vault account.",
|
||||
"kerberosvault_secretkey": "Secret key",
|
||||
"kerberosvault_description_secretkey": "The secret key of your Kerberos Vault account.",
|
||||
"verify_connection": "Verify Connection"
|
||||
"dropbox_directory": "Directory",
|
||||
"dropbox_description_directory": "The sub directory where the recordings will be stored in your Dropbox account.",
|
||||
"dropbox_accesstoken": "Access token",
|
||||
"dropbox_description_accesstoken": "The access token of your Dropbox account/app.",
|
||||
"verify_connection": "Verify Connection",
|
||||
"remove_after_upload": "Once recordings are uploaded to some persistence, you might want to remove them from the local Kerberos Agent.",
|
||||
"remove_after_upload_description": "Remove recordings after they are uploaded successfully.",
|
||||
"remove_after_upload_enabled": "Enabled delete on upload"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
"configure": "Configure"
|
||||
},
|
||||
"buttons": {
|
||||
"save": "Save"
|
||||
"save": "Save",
|
||||
"verify_connection": "Verify Connection"
|
||||
},
|
||||
"navigation": {
|
||||
"profile": "Perfil",
|
||||
@@ -69,7 +70,10 @@
|
||||
"verify_persistence_error": "Something went wrong while verifying the persistence",
|
||||
"verify_camera": "Verifying your camera settings.",
|
||||
"verify_camera_success": "Camera settings are successfully verified.",
|
||||
"verify_camera_error": "Something went wrong while verifying the camera settings"
|
||||
"verify_camera_error": "Something went wrong while verifying the camera settings",
|
||||
"verify_onvif": "Verifying your ONVIF settings.",
|
||||
"verify_onvif_success": "ONVIF settings are successfully verified.",
|
||||
"verify_onvif_error": "Something went wrong while verifying the ONVIF settings"
|
||||
},
|
||||
"overview": {
|
||||
"general": "General",
|
||||
@@ -81,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",
|
||||
@@ -186,19 +199,26 @@
|
||||
"kerberoshub_description_region": "The region we are storing our recordings in.",
|
||||
"kerberoshub_bucket": "Bucket",
|
||||
"kerberoshub_description_bucket": "The bucket we are storing our recordings in.",
|
||||
"kerberoshub_username": "Username/Directory",
|
||||
"kerberoshub_username": "Username/Directory (should match Kerberos Hub username)",
|
||||
"kerberoshub_description_username": "The username of your Kerberos Hub account.",
|
||||
"kerberosvault_apiurl": "Kerberos Vault API URL",
|
||||
"kerberosvault_description_apiurl": "The Kerberos Vault API",
|
||||
"kerberosvault_provider": "Provider",
|
||||
"kerberosvault_description_provider": "The provider to which your recordings will be send.",
|
||||
"kerberosvault_directory": "Directory",
|
||||
"kerberosvault_directory": "Directory (should match Kerberos Hub username)",
|
||||
"kerberosvault_description_directory": "Sub directory the recordings will be stored in your provider.",
|
||||
"kerberosvault_accesskey": "Access key",
|
||||
"kerberosvault_description_accesskey": "The access key of your Kerberos Vault account.",
|
||||
"kerberosvault_secretkey": "Secret key",
|
||||
"kerberosvault_description_secretkey": "The secret key of your Kerberos Vault account.",
|
||||
"verify_connection": "Verify Connection"
|
||||
"dropbox_directory": "Directory",
|
||||
"dropbox_description_directory": "The sub directory where the recordings will be stored in your Dropbox account.",
|
||||
"dropbox_accesstoken": "Access token",
|
||||
"dropbox_description_accesstoken": "The access token of your Dropbox account/app.",
|
||||
"verify_connection": "Verify Connection",
|
||||
"remove_after_upload": "Once recordings are uploaded to some persistence, you might want to remove them from the local Kerberos Agent.",
|
||||
"remove_after_upload_description": "Remove recordings after they are uploaded successfully.",
|
||||
"remove_after_upload_enabled": "Enabled delete on upload"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +69,10 @@
|
||||
"verify_persistence_error": "Quelque chose s'est mal déroulé lors de la vérification de la persistance",
|
||||
"verify_camera": "Vérifier les paramètres de votre caméra.",
|
||||
"verify_camera_success": "Les paramètres de la caméra sont vérifiés avec succès.",
|
||||
"verify_camera_error": "Quelque chose s'est mal déroulé lors de la vérification des paramètres de la caméra"
|
||||
"verify_camera_error": "Quelque chose s'est mal déroulé lors de la vérification des paramètres de la caméra",
|
||||
"verify_onvif": "Verifying your ONVIF settings.",
|
||||
"verify_onvif_success": "ONVIF settings are successfully verified.",
|
||||
"verify_onvif_error": "Something went wrong while verifying the ONVIF settings"
|
||||
},
|
||||
"overview": {
|
||||
"general": "Général",
|
||||
@@ -81,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",
|
||||
@@ -186,19 +198,26 @@
|
||||
"kerberoshub_description_region": "La région dans laquelle nous stockons vos enregistrements.",
|
||||
"kerberoshub_bucket": "Compartiment",
|
||||
"kerberoshub_description_bucket": "Le compartiment dans lequel nous stockons vos enregistrements.",
|
||||
"kerberoshub_username": "Utilisateur/Répertoire",
|
||||
"kerberoshub_username": "Utilisateur/Répertoire (should match Kerberos Hub username)",
|
||||
"kerberoshub_description_username": "Le nom d'utilisateur de votre compte Kerberos Hub.",
|
||||
"kerberosvault_apiurl": "URL de l'API Kerberos Vault",
|
||||
"kerberosvault_description_apiurl": "L'API Kerberos Vault",
|
||||
"kerberosvault_provider": "Fournisseur",
|
||||
"kerberosvault_description_provider": "Le fournisseur auquel vos enregistrements seront envoyés.",
|
||||
"kerberosvault_directory": "Répertoire",
|
||||
"kerberosvault_directory": "Répertoire (should match Kerberos Hub username)",
|
||||
"kerberosvault_description_directory": "Le sous-répertoire dans lequel les enregistrements seront stockés chez votre fournisseur.",
|
||||
"kerberosvault_accesskey": "Clé d'accès",
|
||||
"kerberosvault_description_accesskey": "La clé d'accès de votre compte Kerberos Vault.",
|
||||
"kerberosvault_secretkey": "Clé secrète",
|
||||
"kerberosvault_description_secretkey": "La clé secrète de votre compte Kerberos Vault.",
|
||||
"verify_connection": "Vérifier la connexion"
|
||||
"dropbox_directory": "Directory",
|
||||
"dropbox_description_directory": "The sub directory where the recordings will be stored in your Dropbox account.",
|
||||
"dropbox_accesstoken": "Access token",
|
||||
"dropbox_description_accesstoken": "The access token of your Dropbox account/app.",
|
||||
"verify_connection": "Vérifier la connexion",
|
||||
"remove_after_upload": "Once recordings are uploaded to some persistence, you might want to remove them from the local Kerberos Agent.",
|
||||
"remove_after_upload_description": "Remove recordings after they are uploaded successfully.",
|
||||
"remove_after_upload_enabled": "Enabled delete on upload"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
224
ui/public/locales/ja/translation.json
Normal file
224
ui/public/locales/ja/translation.json
Normal file
@@ -0,0 +1,224 @@
|
||||
{
|
||||
"breadcrumb": {
|
||||
"watch_recordings": "録画を見る",
|
||||
"configure": "設定"
|
||||
},
|
||||
"buttons": {
|
||||
"save": "保存",
|
||||
"verify_connection": "Verify Connection"
|
||||
},
|
||||
"navigation": {
|
||||
"profile": "プロフィール",
|
||||
"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 エージェントが正しく構成されていることを確認してください。",
|
||||
"motion_detected": "モーションが検出されました",
|
||||
"live_view": "ライブビュー",
|
||||
"loading_live_view": "ライブビューを読み込んでいます",
|
||||
"loading_live_view_description": "ライブ ビューをロードしています。お待ちください。",
|
||||
"time": "時間",
|
||||
"description": "説明",
|
||||
"name": "名前"
|
||||
},
|
||||
"recordings": {
|
||||
"title": "録画",
|
||||
"heading": "すべての録音を 1 か所に",
|
||||
"search_media": "メディアを検索"
|
||||
},
|
||||
"settings": {
|
||||
"title": "設定",
|
||||
"heading": "搭載カメラ",
|
||||
"submenu": {
|
||||
"all": "全て",
|
||||
"overview": "概要",
|
||||
"camera": "カメラ",
|
||||
"recording": "録音",
|
||||
"streaming": "ストリーミング",
|
||||
"conditions": "条件",
|
||||
"persistence": "持続性"
|
||||
},
|
||||
"info": {
|
||||
"kerberos_hub_demo": "Kerberos Hub のデモ環境を見て、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": "Verifying your ONVIF settings.",
|
||||
"verify_onvif_success": "ONVIF settings are successfully verified.",
|
||||
"verify_onvif_error": "Something went wrong while verifying the ONVIF settings"
|
||||
},
|
||||
"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": "Sub RTSP url (ライブストリーミングに使用)",
|
||||
"sub_rtsp_h264": "カメラの低解像度へのセカンダリ RTSP 接続。",
|
||||
"onvif": "ONVIF",
|
||||
"description_onvif": "ONVIF 機能と通信するための資格情報",
|
||||
"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 時間またはモーション ベースの録画を行います。",
|
||||
"max_duration": "動画の最大再生時間 (秒)",
|
||||
"description_max_duration": "録音の最大継続時間。",
|
||||
"pre_recording": "事前録画 (キー フレームのバッファリング)",
|
||||
"description_pre_recording": "イベントが発生する数秒前。",
|
||||
"post_recording": "ポストレコーディング (秒)",
|
||||
"description_post_recording": "イベントが発生してからの秒数。",
|
||||
"threshold": "記録閾値(ピクセル)",
|
||||
"description_threshold": "記録するために変更されたピクセル数",
|
||||
"autoclean": "オートクリーン",
|
||||
"description_autoclean": "特定のストレージ容量 (MB) に達したときに、Kerberos エージェントが記録をクリーンアップできるかどうかを指定します。",
|
||||
"autoclean_enable": "自動クリーニングを有効にする",
|
||||
"autoclean_description_enable": "容量に達したら、最も古い記録を削除します。",
|
||||
"autoclean_max_directory_size": "最大ディレクトリ サイズ (MB)",
|
||||
"autoclean_description_max_directory_size": "保存された録音の最大 MB。",
|
||||
"fragmentedrecordings": "断片化された録音",
|
||||
"description_fragmentedrecordings": "録音が断片化されている場合、HLS ストリームに適しています。",
|
||||
"fragmentedrecordings_enable": "断片化を有効にする",
|
||||
"fragmentedrecordings_description_enable": "HLS には断片化された録音が必要です。",
|
||||
"fragmentedrecordings_duration": "フラグメント期間",
|
||||
"fragmentedrecordings_description_duration": "1 つのフラグメントの持続時間。"
|
||||
},
|
||||
"streaming": {
|
||||
"stun_turn": "WebRTCのSTUN/TURN",
|
||||
"description_stun_turn": "フル解像度のライブ ストリーミングには、WebRTC の概念を使用します。",
|
||||
"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": "MQTT を介して h264 ストリームを転送する",
|
||||
"stun_turn_transcode": "トランスコード ストリーム",
|
||||
"stun_turn_description_transcode": "ストリームを低解像度に変換する",
|
||||
"stun_turn_downscale": "解像度のダウンスケール (% または元の解像度)",
|
||||
"mqtt": "MQTT",
|
||||
"description_mqtt": "それらの通信にはMQTT ブローカーが使用されます。",
|
||||
"description2_mqtt": "たとえば、ライブストリーミングや ONVIF (PTZ) 機能を実現するために、Kerberos エージェントに送信します。",
|
||||
"mqtt_brokeruri": "ブローカー URI",
|
||||
"mqtt_username": "ユーザー名",
|
||||
"mqtt_password": "パスワード"
|
||||
},
|
||||
"conditions": {
|
||||
"timeofinterest": "特定の時間",
|
||||
"description_timeofinterest": "特定の時間間隔 (タイムゾーンに基づく) の間のみ録画を行います。",
|
||||
"timeofinterest_enabled": "有効",
|
||||
"timeofinterest_description_enabled": "有効にすると、時間枠を指定できます",
|
||||
"sunday": "日曜日",
|
||||
"monday": "月曜日",
|
||||
"tuesday": "火曜日",
|
||||
"wednesday": "水曜日",
|
||||
"thursday": "木曜日",
|
||||
"friday": "金曜日",
|
||||
"saturday": "土曜日",
|
||||
"externalcondition": "外部条件",
|
||||
"description_externalcondition": "外部 Web サービスに応じて、記録を有効または無効にすることができます。",
|
||||
"regionofinterest": "検出領域",
|
||||
"description_regionofinterest": "1 つまたは複数の領域を定義すると、定義した領域でのみモーションが追跡されます。"
|
||||
},
|
||||
"persistence": {
|
||||
"kerberoshub": "ケルベロス ハブ",
|
||||
"description_kerberoshub": "Kerberos エージェントはハートビートを中央に送信できます。",
|
||||
"description2_kerberoshub": "インストール。",
|
||||
"persistence": "持続性",
|
||||
"saasoffering": "Kerberos ハブ (SAAS オファリング)",
|
||||
"description_persistence": "録音を保存する機能を持つことは、すべての始まりです。",
|
||||
"description2_persistence": "、またはサードパーティのプロバイダ",
|
||||
"select_persistence": "永続性を選択",
|
||||
"kerberoshub_proxyurl": "Kerberos ハブ プロキシ URL",
|
||||
"kerberoshub_description_proxyurl": "記録をアップロードするためのプロキシ エンドポイント。",
|
||||
"kerberoshub_apiurl": "ケルベロス ハブ 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 Hub で Kerberos エージェントが属しているサイト ID。",
|
||||
"kerberoshub_region": "領域",
|
||||
"kerberoshub_description_region": "録音を保存しているリージョン。",
|
||||
"kerberoshub_bucket": "bucket",
|
||||
"kerberoshub_description_bucket": "録音を保存しているbucket",
|
||||
"kerberoshub_username": "ユーザー名/ディレクトリ (should match Kerberos Hub username)",
|
||||
"kerberoshub_description_username": "Kerberos Hub アカウントのユーザー名。",
|
||||
"kerberosvault_apiurl": "Kerberos ボールト API URL",
|
||||
"kerberosvault_description_apiurl": "Kerberos ボールト API",
|
||||
"kerberosvault_provider": "プロバイダ",
|
||||
"kerberosvault_description_provider": "録音の送信先のプロバイダー。",
|
||||
"kerberosvault_directory": "ディレクトリ (should match Kerberos Hub username)",
|
||||
"kerberosvault_description_directory": "録音がプロバイダーに保存されるサブディレクトリ。",
|
||||
"kerberosvault_accesskey": "アクセスキー",
|
||||
"kerberosvault_description_accesskey": "Kerberos Vault アカウントのアクセス キー。",
|
||||
"kerberosvault_secretkey": "秘密鍵",
|
||||
"kerberosvault_description_secretkey": "Kerberos Vault アカウントの秘密鍵。",
|
||||
"dropbox_directory": "Directory",
|
||||
"dropbox_description_directory": "The sub directory where the recordings will be stored in your Dropbox account.",
|
||||
"dropbox_accesstoken": "Access token",
|
||||
"dropbox_description_accesstoken": "The access token of your Dropbox account/app.",
|
||||
"verify_connection": "接続の確認",
|
||||
"remove_after_upload": "Once recordings are uploaded to some persistence, you might want to remove them from the local Kerberos Agent.",
|
||||
"remove_after_upload_description": "Remove recordings after they are uploaded successfully.",
|
||||
"remove_after_upload_enabled": "Enabled delete on upload"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,8 @@
|
||||
"configure": "Instellingen"
|
||||
},
|
||||
"buttons": {
|
||||
"save": "Opslaan"
|
||||
"save": "Opslaan",
|
||||
"verify_connection": "Verifieer Connectie"
|
||||
},
|
||||
"navigation": {
|
||||
"profile": "Profiel",
|
||||
@@ -69,7 +70,10 @@
|
||||
"verify_persistence_error": "Er ging iets fout tijdens het controleren van de opslag instellingen",
|
||||
"verify_camera": "We controleren de camera instellingen.",
|
||||
"verify_camera_success": "De camera instellingen zijn gevalideerd.",
|
||||
"verify_camera_error": "Er ging iets mis met de camera instellingen."
|
||||
"verify_camera_error": "Er ging iets mis met de camera instellingen.",
|
||||
"verify_onvif": "Verifying your ONVIF settings.",
|
||||
"verify_onvif_success": "ONVIF settings are successfully verified.",
|
||||
"verify_onvif_error": "Something went wrong while verifying the ONVIF settings"
|
||||
},
|
||||
"overview": {
|
||||
"general": "Algemeen",
|
||||
@@ -81,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",
|
||||
@@ -187,19 +200,26 @@
|
||||
"kerberoshub_description_region": "De regio waar jouw opnames worden opgeslagen.",
|
||||
"kerberoshub_bucket": "Bucket",
|
||||
"kerberoshub_description_bucket": "De bucket opslag locatie",
|
||||
"kerberoshub_username": "Gebruikersnaam/Map",
|
||||
"kerberoshub_username": "Gebruikersnaam/Map (moet overeenkomen met de Kerberos Hub username)",
|
||||
"kerberoshub_description_username": "De gebruikersnaam van jouw Kerberos Hub account.",
|
||||
"kerberosvault_apiurl": "Kerberos Vault API URL",
|
||||
"kerberosvault_description_apiurl": "De Kerberos Vault API",
|
||||
"kerberosvault_provider": "Provider",
|
||||
"kerberosvault_description_provider": "De provider verantwoordelijk voor de opnames op te slaan.",
|
||||
"kerberosvault_directory": "Map",
|
||||
"kerberosvault_directory": "Map (moet overeenkomen met de Kerberos Hub username)",
|
||||
"kerberosvault_description_directory": "Sub map waarin de opnames worden opgeslagen.",
|
||||
"kerberosvault_accesskey": "Access key",
|
||||
"kerberosvault_description_accesskey": "De access key van jouw Kerberos Vault account.",
|
||||
"kerberosvault_secretkey": "Secret key",
|
||||
"kerberosvault_description_secretkey": "De secret key van jouw Kerberos Vault account.",
|
||||
"verify_connection": "Verifieer verbinding"
|
||||
"dropbox_directory": "Directory",
|
||||
"dropbox_description_directory": "The sub directory where the recordings will be stored in your Dropbox account.",
|
||||
"dropbox_accesstoken": "Access token",
|
||||
"dropbox_description_accesstoken": "The access token of your Dropbox account/app.",
|
||||
"verify_connection": "Verifieer verbinding",
|
||||
"remove_after_upload": "Wanneer opnames opgeslagen zijn, kan je ze verwijderen in de lokale Kerberos Agent.",
|
||||
"remove_after_upload_description": "Verwijder opnames wanneer ze zijn geupload naar een opslag locatie.",
|
||||
"remove_after_upload_enabled": "Activeer verwijderen na uploaden"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
"configure": "Configure"
|
||||
},
|
||||
"buttons": {
|
||||
"save": "Save"
|
||||
"save": "Save",
|
||||
"verify_connection": "Verify Connection"
|
||||
},
|
||||
"navigation": {
|
||||
"profile": "Profil",
|
||||
@@ -69,7 +70,10 @@
|
||||
"verify_persistence_error": "Something went wrong while verifying the persistence",
|
||||
"verify_camera": "Verifying your camera settings.",
|
||||
"verify_camera_success": "Camera settings are successfully verified.",
|
||||
"verify_camera_error": "Something went wrong while verifying the camera settings"
|
||||
"verify_camera_error": "Something went wrong while verifying the camera settings",
|
||||
"verify_onvif": "Verifying your ONVIF settings.",
|
||||
"verify_onvif_success": "ONVIF settings are successfully verified.",
|
||||
"verify_onvif_error": "Something went wrong while verifying the ONVIF settings"
|
||||
},
|
||||
"overview": {
|
||||
"general": "General",
|
||||
@@ -81,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",
|
||||
@@ -186,19 +199,26 @@
|
||||
"kerberoshub_description_region": "The region we are storing our recordings in.",
|
||||
"kerberoshub_bucket": "Bucket",
|
||||
"kerberoshub_description_bucket": "The bucket we are storing our recordings in.",
|
||||
"kerberoshub_username": "Username/Directory",
|
||||
"kerberoshub_username": "Username/Directory (should match Kerberos Hub username)",
|
||||
"kerberoshub_description_username": "The username of your Kerberos Hub account.",
|
||||
"kerberosvault_apiurl": "Kerberos Vault API URL",
|
||||
"kerberosvault_description_apiurl": "The Kerberos Vault API",
|
||||
"kerberosvault_provider": "Provider",
|
||||
"kerberosvault_description_provider": "The provider to which your recordings will be send.",
|
||||
"kerberosvault_directory": "Directory",
|
||||
"kerberosvault_directory": "Directory (should match Kerberos Hub username)",
|
||||
"kerberosvault_description_directory": "Sub directory the recordings will be stored in your provider.",
|
||||
"kerberosvault_accesskey": "Access key",
|
||||
"kerberosvault_description_accesskey": "The access key of your Kerberos Vault account.",
|
||||
"kerberosvault_secretkey": "Secret key",
|
||||
"kerberosvault_description_secretkey": "The secret key of your Kerberos Vault account.",
|
||||
"verify_connection": "Verify Connection"
|
||||
"dropbox_directory": "Directory",
|
||||
"dropbox_description_directory": "The sub directory where the recordings will be stored in your Dropbox account.",
|
||||
"dropbox_accesstoken": "Access token",
|
||||
"dropbox_description_accesstoken": "The access token of your Dropbox account/app.",
|
||||
"verify_connection": "Verify Connection",
|
||||
"remove_after_upload": "Once recordings are uploaded to some persistence, you might want to remove them from the local Kerberos Agent.",
|
||||
"remove_after_upload_description": "Remove recordings after they are uploaded successfully.",
|
||||
"remove_after_upload_enabled": "Enabled delete on upload"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"breadcrumb": {
|
||||
"watch_recordings": "Watch recordings",
|
||||
"configure": "Configure"
|
||||
"watch_recordings": "Assistir gravações",
|
||||
"configure": "Configurar"
|
||||
},
|
||||
"buttons": {
|
||||
"save": "Save"
|
||||
"save": "Salvar",
|
||||
"verify_connection": "Verify Connection"
|
||||
},
|
||||
"navigation": {
|
||||
"profile": "Perfil",
|
||||
@@ -12,7 +13,7 @@
|
||||
"management": "Gestão",
|
||||
"dashboard": "Painel",
|
||||
"recordings": "Gravações",
|
||||
"settings": "Definições",
|
||||
"settings": "Configurações",
|
||||
"help_support": "Ajuda e suporte",
|
||||
"swagger": "Swagger API",
|
||||
"documentation": "Documentação",
|
||||
@@ -21,184 +22,203 @@
|
||||
"choose_language": "Escolha o seu idioma"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
"heading": "Overview of your video surveilance",
|
||||
"number_of_days": "Number of days",
|
||||
"total_recordings": "Total recordings",
|
||||
"connected": "Connected",
|
||||
"not_connected": "Not connected",
|
||||
"offline_mode": "Offline mode",
|
||||
"latest_events": "Latest events",
|
||||
"configure_connection": "Configure connection",
|
||||
"no_events": "No events",
|
||||
"no_events_description": "No recordings where found, make sure your Kerberos Agent is properly configured.",
|
||||
"motion_detected": "Motion was detected",
|
||||
"live_view": "Live view",
|
||||
"loading_live_view": "Loading live view",
|
||||
"loading_live_view_description": "Hold on we are loading your live view here. If you didn't configure your camera connection, update it on the settings pages.",
|
||||
"title": "Painel",
|
||||
"heading": "Visão geral de sua videovigilância",
|
||||
"number_of_days": "Número de dias",
|
||||
"total_recordings": "Total de gravações",
|
||||
"connected": "Conectado",
|
||||
"not_connected": "Desconectado",
|
||||
"offline_mode": "Modo Offline",
|
||||
"latest_events": "Ultimos eventos",
|
||||
"configure_connection": "Configurar conexão",
|
||||
"no_events": "Nenhum evento",
|
||||
"no_events_description": "Nenhuma gravação foi encontrada, certifique-se de que seu Kerberos Agent esteja configurado corretamente.",
|
||||
"motion_detected": "Movimento detectado",
|
||||
"live_view": "Visualização ao vivo",
|
||||
"loading_live_view": "Carregando visualização ao vivo",
|
||||
"loading_live_view_description": "Aguarde, estamos carregando sua visualização ao vivo. Se você não configurou a conexão de sua câmera, atualize-a nas páginas de configurações.",
|
||||
"time": "Time",
|
||||
"description": "Description",
|
||||
"name": "Name"
|
||||
"description": "Descrição",
|
||||
"name": "Nome"
|
||||
},
|
||||
"recordings": {
|
||||
"title": "Recordings",
|
||||
"heading": "All your recordings in a single place",
|
||||
"search_media": "Search media"
|
||||
"title": "Gravações",
|
||||
"heading": "Todas as suas gravações em um único lugar",
|
||||
"search_media": "Pesquisar mídia"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"heading": "Onboard your camera",
|
||||
"title": "Configurações",
|
||||
"heading": "Integre sua câmera",
|
||||
"submenu": {
|
||||
"all": "All",
|
||||
"overview": "Overview",
|
||||
"camera": "Camera",
|
||||
"recording": "Recording",
|
||||
"streaming": "Streaming",
|
||||
"conditions": "Conditions",
|
||||
"persistence": "Persistence"
|
||||
"all": "Todas",
|
||||
"overview": "Visão geral",
|
||||
"camera": "Câmera",
|
||||
"recording": "Gravação",
|
||||
"streaming": "Transmissão",
|
||||
"conditions": "Condições",
|
||||
"persistence": "Armazenamento"
|
||||
},
|
||||
"info": {
|
||||
"kerberos_hub_demo": "Have a look at our Kerberos Hub demo environment, to see Kerberos Hub in action!",
|
||||
"configuration_updated_success": "Your configuration have been updated successfully.",
|
||||
"configuration_updated_error": "Something went wrong while saving.",
|
||||
"verify_hub": "Verifying your Kerberos Hub settings.",
|
||||
"verify_hub_success": "Kerberos Hub settings are successfully verified.",
|
||||
"verify_hub_error": "Something went wrong while verifying Kerberos Hub",
|
||||
"verify_persistence": "Verifying your persistence settings.",
|
||||
"verify_persistence_success": "Persistence settings are successfully verified.",
|
||||
"verify_persistence_error": "Something went wrong while verifying the persistence",
|
||||
"verify_camera": "Verifying your camera settings.",
|
||||
"verify_camera_success": "Camera settings are successfully verified.",
|
||||
"verify_camera_error": "Something went wrong while verifying the camera settings"
|
||||
"kerberos_hub_demo": "Dê uma olhada em nosso ambiente de demonstração do Kerberos Hub para ver o Kerberos Hub em ação!",
|
||||
"configuration_updated_success": "Sua configuração foi atualizada com sucesso.",
|
||||
"configuration_updated_error": "Ocorreu um erro durante o salvamento.",
|
||||
"verify_hub": "Verificando as configurações do Kerberos Hub.",
|
||||
"verify_hub_success": "As configurações do Kerberos Hub foram verificadas com sucesso.",
|
||||
"verify_hub_error": "Algo deu errado ao verificar o Kerberos Hub",
|
||||
"verify_persistence": "Verificando suas configurações de armazenamento.",
|
||||
"verify_persistence_success": "As configurações de armazenamento foram verificadas com sucesso.",
|
||||
"verify_persistence_error": "Algo deu errado ao verificar o armazenamento",
|
||||
"verify_camera": "Verificando as configurações de sua câmera.",
|
||||
"verify_camera_success": "As configurações da câmera foram verificadas com sucesso.",
|
||||
"verify_camera_error": "Algo deu errado ao verificar as configurações da câmera.",
|
||||
"verify_onvif": "Verifying your ONVIF settings.",
|
||||
"verify_onvif_success": "ONVIF settings are successfully verified.",
|
||||
"verify_onvif_error": "Something went wrong while verifying the ONVIF settings"
|
||||
},
|
||||
"overview": {
|
||||
"general": "General",
|
||||
"description_general": "General settings for your Kerberos Agent",
|
||||
"key": "Key",
|
||||
"camera_name": "Camera name",
|
||||
"timezone": "Timezone",
|
||||
"select_timezone": "Select a timezone",
|
||||
"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"
|
||||
"general": "Geral",
|
||||
"description_general": "Configurações gerais para seu agente Kerberos",
|
||||
"key": "Chave",
|
||||
"camera_name": "Nome da câmera",
|
||||
"timezone": "Fuso horário",
|
||||
"select_timezone": "Selecione a timezone",
|
||||
"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",
|
||||
"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",
|
||||
"description_camera": "Camera settings are required to make a connection to your camera of choice.",
|
||||
"only_h264": "Currently only H264 RTSP streams are supported.",
|
||||
"rtsp_url": "RTSP url",
|
||||
"rtsp_h264": "A H264 RTSP connection to your camera.",
|
||||
"sub_rtsp_url": "Sub RTSP url (used for livestreaming)",
|
||||
"sub_rtsp_h264": "A secondary RTSP connection to the low resolution of your camera.",
|
||||
"camera": "Câmera",
|
||||
"description_camera": "As configurações da câmera são necessárias para fazer uma conexão com a câmera de sua escolha.",
|
||||
"only_h264": "Atualmente, apenas streams H264 RTSP são suportados.",
|
||||
"rtsp_url": "Url RTSP",
|
||||
"rtsp_h264": "Uma conexão H264 RTSP para sua câmera.",
|
||||
"sub_rtsp_url": "Sub RTSP URL(usado para transmissão ao vivo)",
|
||||
"sub_rtsp_h264": "Uma conexão RTSP secundária para a baixa resolução de sua câmera.",
|
||||
"onvif": "ONVIF",
|
||||
"description_onvif": "Credentials to communicate with ONVIF capabilities. These are used for PTZ or other capabilities provided by the camera.",
|
||||
"description_onvif": "Credenciais para se comunicar com recursos ONVIF. Eles são usados para PTZ ou outros recursos fornecidos pela câmera.",
|
||||
"onvif_xaddr": "ONVIF xaddr",
|
||||
"onvif_username": "ONVIF username",
|
||||
"onvif_password": "ONVIF password",
|
||||
"verify_connection": "Verify Connection",
|
||||
"verify_sub_connection": "Verify Sub Connection"
|
||||
"onvif_username": "ONVIF usuario (username)",
|
||||
"onvif_password": "ONVIF senha (password)",
|
||||
"verify_connection": "Verificar conexão",
|
||||
"verify_sub_connection": "Verificar subconexão"
|
||||
},
|
||||
"recording": {
|
||||
"recording": "Recording",
|
||||
"description_recording": "Specify how you would like to make recordings. Having a continuous 24/7 setup or a motion based recording.",
|
||||
"continuous_recording": "Continuous recording",
|
||||
"description_continuous_recording": "Make 24/7 or motion based recordings.",
|
||||
"max_duration": "max video duration (seconds)",
|
||||
"description_max_duration": "The maximum duration of a recording.",
|
||||
"pre_recording": "pre recording (key frames buffered)",
|
||||
"description_pre_recording": "Seconds before an event occurred.",
|
||||
"post_recording": "post recording (seconds)",
|
||||
"description_post_recording": "Seconds after an event occurred.",
|
||||
"threshold": "Recording threshold (pixels)",
|
||||
"description_threshold": "The number of pixels changed to record",
|
||||
"autoclean": "Auto clean",
|
||||
"description_autoclean": "Specify if the Kerberos Agent can cleanup recordings when a specific storage capacity (MB) is reached. This will remove the oldest recordings when the capacity is reached.",
|
||||
"autoclean_enable": "Enable auto clean",
|
||||
"autoclean_description_enable": "Remove oldest recording when capacity reached.",
|
||||
"autoclean_max_directory_size": "Maximum directory size (MB)",
|
||||
"autoclean_description_max_directory_size": "The maximum MB's of recordings stored.",
|
||||
"fragmentedrecordings": "Fragmented recordings",
|
||||
"description_fragmentedrecordings": "When recordings are fragmented they are suitable for an HLS stream. When turned on the MP4 container will look a bit different.",
|
||||
"fragmentedrecordings_enable": "Enable fragmentation",
|
||||
"fragmentedrecordings_description_enable": "Fragmented recordings are required for HLS.",
|
||||
"fragmentedrecordings_duration": "fragment duration",
|
||||
"fragmentedrecordings_description_duration": "Duration of a single fragment."
|
||||
"recording": "Gravação",
|
||||
"description_recording": "Especifique como você gostaria de fazer gravações. Gravar o tempo todo (24/7) ou baseada na detecção de movimento.",
|
||||
"continuous_recording": "Gravação contínua",
|
||||
"description_continuous_recording": "Faça gravações 24/7 ou baseadas na detecção de movimento.",
|
||||
"max_duration": "duração máxima do vídeo (segundos)",
|
||||
"description_max_duration": "A duração máxima de uma gravação.",
|
||||
"pre_recording": "pré-gravação (key frames em buffer)",
|
||||
"description_pre_recording": "Segundos antes de um evento ocorrer.",
|
||||
"post_recording": "pós gravação (segundos)",
|
||||
"description_post_recording": "Segundos após a ocorrência de um evento.",
|
||||
"threshold": "Limite de gravação (pixels)",
|
||||
"description_threshold": "O número de pixels alterados para gravar",
|
||||
"autoclean": "Limpeza automática",
|
||||
"description_autoclean": "Especifique se o Agente Kerberos pode limpar gravações quando uma capacidade de armazenamento específica (MB) é atingida. Isso removerá as gravações mais antigas quando a capacidade for atingida.",
|
||||
"autoclean_enable": "Ativar limpeza automática",
|
||||
"autoclean_description_enable": "Remova a gravação mais antiga quando a capacidade for atingida.",
|
||||
"autoclean_max_directory_size": "Tamanho máximo do diretório (MB)",
|
||||
"autoclean_description_max_directory_size": "O máximo de MB de gravações armazenadas.",
|
||||
"fragmentedrecordings": "Gravações Fragmentadas",
|
||||
"description_fragmentedrecordings": "Quando as gravações são fragmentadas, elas são adequadas para um fluxo HLS. Quando ativado, o contêiner MP4 parecerá um pouco diferente.",
|
||||
"fragmentedrecordings_enable": "Ativar fragmentação",
|
||||
"fragmentedrecordings_description_enable": "Gravações fragmentadas são necessárias para HLS.",
|
||||
"fragmentedrecordings_duration": "Duração do fragmento",
|
||||
"fragmentedrecordings_description_duration": "Duração de um único fragmento."
|
||||
},
|
||||
"streaming": {
|
||||
"stun_turn": "STUN/TURN for WebRTC",
|
||||
"description_stun_turn": "For full-resolution livestreaming we use the concept of WebRTC. One of the key capabilities is the ICE-candidate feature, which allows NAT traversal using the concepts of STUN/TURN.",
|
||||
"stun_server": "STUN server",
|
||||
"turn_server": "TURN server",
|
||||
"turn_username": "Username",
|
||||
"turn_password": "Password",
|
||||
"stun_turn_forward": "Forwarding and transcoding",
|
||||
"stun_turn_description_forward": "Optimisations and enhancements for TURN/STUN communication.",
|
||||
"stun_turn_webrtc": "Forwarding to WebRTC broker",
|
||||
"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": "STUN/TURN para WebRTC",
|
||||
"description_stun_turn": "Para transmissão ao vivo de resolução total, usamos o conceito de WebRTC. Um dos principais recursos é o recurso ICE-candidate, que permite a travessia de NAT usando os conceitos de STUN/TURN.",
|
||||
"stun_server": "Servidor STUN",
|
||||
"turn_server": "Servidor TURN",
|
||||
"turn_username": "Usuario",
|
||||
"turn_password": "Senha",
|
||||
"stun_turn_forward": "Encaminhamento e transcodificação",
|
||||
"stun_turn_description_forward": "Otimizações e melhorias para a comunicação TURN/STUN.",
|
||||
"stun_turn_webrtc": "Encaminhamento para broker WebRTC",
|
||||
"stun_turn_description_webrtc": "Encaminhar stream h264 através do MQTT",
|
||||
"stun_turn_transcode": "Transcodificar stream",
|
||||
"stun_turn_description_transcode": "Converter stream para uma resolução mais baixa",
|
||||
"stun_turn_downscale": "Resolução reduzida (em % ou resolução original)",
|
||||
"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.",
|
||||
"mqtt_brokeruri": "Broker Uri",
|
||||
"mqtt_username": "Username",
|
||||
"mqtt_password": "Password"
|
||||
"description_mqtt": "Um broker MQTT é usado para se comunicar de",
|
||||
"description2_mqtt": "para o Kerberos Agent, para obter, por exemplo, recursos de transmissão ao vivo ou ONVIF (PTZ).",
|
||||
"mqtt_brokeruri": "Url Broker",
|
||||
"mqtt_username": "Usuario",
|
||||
"mqtt_password": "Senha"
|
||||
},
|
||||
"conditions": {
|
||||
"timeofinterest": "Time Of Interest",
|
||||
"description_timeofinterest": "Only make recordings between specific time intervals (based on timezone).",
|
||||
"timeofinterest_enabled": "Enabled",
|
||||
"timeofinterest_description_enabled": "If enabled you can specify time windows",
|
||||
"sunday": "Sunday",
|
||||
"monday": "Monday",
|
||||
"tuesday": "Tuesday",
|
||||
"wednesday": "Wednesday",
|
||||
"thursday": "Thursday",
|
||||
"friday": "Friday",
|
||||
"saturday": "Saturday",
|
||||
"externalcondition": "External Condition",
|
||||
"description_externalcondition": "Depending on an external webservice recording can be enabled or disabled.",
|
||||
"regionofinterest": "Region Of Interest",
|
||||
"description_regionofinterest": "By defining one or more regions, motion will be tracked only in the regions you have defined."
|
||||
"timeofinterest": "Tempo de interesse",
|
||||
"description_timeofinterest": "Apenas faça gravações entre intervalos de tempo específicos (com base no fuso horário).",
|
||||
"timeofinterest_enabled": "Habilitado",
|
||||
"timeofinterest_description_enabled": "Se ativado, você pode especificar os intervalos de tempo",
|
||||
"sunday": "Domingo",
|
||||
"monday": "Segunda",
|
||||
"tuesday": "Terça",
|
||||
"wednesday": "Quarta",
|
||||
"thursday": "Quinta",
|
||||
"friday": "Sexta",
|
||||
"saturday": "Sabado",
|
||||
"externalcondition": "Condição Externa",
|
||||
"description_externalcondition": "Dependendo de um serviço de web(API) externo, a gravação pode ser habilitada ou desabilitada.",
|
||||
"regionofinterest": "Região de interesse",
|
||||
"description_regionofinterest": "Ao definir uma ou mais regiões, o movimento será rastreado apenas nas regiões definidas por você."
|
||||
},
|
||||
"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": "Persistence",
|
||||
"saasoffering": "Kerberos Hub (SAAS offering)",
|
||||
"description_persistence": "Having the ability to store your recordings is the beginning of everything. You can choose between our",
|
||||
"description2_persistence": ", or a 3rd party provider",
|
||||
"select_persistence": "Select a persistence",
|
||||
"kerberoshub_proxyurl": "Kerberos Hub Proxy URL",
|
||||
"kerberoshub_description_proxyurl": "The Proxy endpoint for uploading your recordings.",
|
||||
"kerberoshub_apiurl": "Kerberos Hub API URL",
|
||||
"kerberoshub_description_apiurl": "The API endpoint for uploading your recordings.",
|
||||
"kerberoshub_publickey": "Public key",
|
||||
"kerberoshub_description_publickey": "The public key granted to your Kerberos Hub account.",
|
||||
"kerberoshub_privatekey": "Private key",
|
||||
"kerberoshub_description_privatekey": "The private key granted to your Kerberos Hub account.",
|
||||
"kerberoshub_site": "Site",
|
||||
"kerberoshub_description_site": "The site ID the Kerberos Agents are belonging to in Kerberos Hub.",
|
||||
"kerberoshub_region": "Region",
|
||||
"kerberoshub_description_region": "The region we are storing our recordings in.",
|
||||
"description_kerberoshub": "Agentes Kerberos podem enviar sinais constantes para a central",
|
||||
"description2_kerberoshub": "instalação. Os sinais e outras informações relevantes são sincronizadas com o Kerberos Hub para mostrar informações em tempo real sobre seu cenário de vídeo.",
|
||||
"persistence": "Armazenamento",
|
||||
"saasoffering": "Kerberos Hub (oferta SAAS)",
|
||||
"description_persistence": "Ter a capacidade de armazenar suas gravações é o começo de tudo. Você pode escolher entre nossos",
|
||||
"description2_persistence": ", ou um provedor terceirizado",
|
||||
"select_persistence": "Selecione um provedor de armazenamento",
|
||||
"kerberoshub_proxyurl": "Url proxy para Kerberos Hub",
|
||||
"kerberoshub_description_proxyurl": "O endpoint Proxy para enviar suas gravações.",
|
||||
"kerberoshub_apiurl": "Url de API do Kerberos Hub",
|
||||
"kerberoshub_description_apiurl": "O endpoint da API para enviar suas gravações.",
|
||||
"kerberoshub_publickey": "Chave pública (Public key)",
|
||||
"kerberoshub_description_publickey": "A chave pública concedida à sua conta do Kerberos Hub.",
|
||||
"kerberoshub_privatekey": "Chave privada (Private key)",
|
||||
"kerberoshub_description_privatekey": "A chave privada concedida à sua conta do Kerberos Hub.",
|
||||
"kerberoshub_site": "Local",
|
||||
"kerberoshub_description_site": "O ID do local ao qual os Agentes Kerberos pertencem no Hub Kerberos.",
|
||||
"kerberoshub_region": "Região",
|
||||
"kerberoshub_description_region": "A região em que estamos armazenando nossas gravações.",
|
||||
"kerberoshub_bucket": "Bucket",
|
||||
"kerberoshub_description_bucket": "The bucket we are storing our recordings in.",
|
||||
"kerberoshub_username": "Username/Directory",
|
||||
"kerberoshub_description_username": "The username of your Kerberos Hub account.",
|
||||
"kerberosvault_apiurl": "Kerberos Vault API URL",
|
||||
"kerberosvault_description_apiurl": "The Kerberos Vault API",
|
||||
"kerberosvault_provider": "Provider",
|
||||
"kerberosvault_description_provider": "The provider to which your recordings will be send.",
|
||||
"kerberosvault_directory": "Directory",
|
||||
"kerberosvault_description_directory": "Sub directory the recordings will be stored in your provider.",
|
||||
"kerberosvault_accesskey": "Access key",
|
||||
"kerberosvault_description_accesskey": "The access key of your Kerberos Vault account.",
|
||||
"kerberosvault_secretkey": "Secret key",
|
||||
"kerberosvault_description_secretkey": "The secret key of your Kerberos Vault account.",
|
||||
"verify_connection": "Verify Connection"
|
||||
"kerberoshub_description_bucket": "O bucket no qual estamos armazenando nossas gravações.",
|
||||
"kerberoshub_username": "Nome de usuário/diretório (should match Kerberos Hub username)",
|
||||
"kerberoshub_description_username": "O nome de usuário da sua conta do Kerberos Hub.",
|
||||
"kerberosvault_apiurl": "Url da API do Kerberos Vault",
|
||||
"kerberosvault_description_apiurl": "a API Kerberos Vault",
|
||||
"kerberosvault_provider": "Provedor",
|
||||
"kerberosvault_description_provider": "O provedor para o qual suas gravações serão enviadas.",
|
||||
"kerberosvault_directory": "Diretório (should match Kerberos Hub username)",
|
||||
"kerberosvault_description_directory": "Subdiretório as gravações serão armazenadas em seu provedor.",
|
||||
"kerberosvault_accesskey": "Chave de acesso(Access key)",
|
||||
"kerberosvault_description_accesskey": "A chave de acesso da sua conta do Kerberos Vault.",
|
||||
"kerberosvault_secretkey": "Chave secreta(Secret key)",
|
||||
"kerberosvault_description_secretkey": "A chave secreta da sua conta do Kerberos Vault.",
|
||||
"dropbox_directory": "Directory",
|
||||
"dropbox_description_directory": "The sub directory where the recordings will be stored in your Dropbox account.",
|
||||
"dropbox_accesstoken": "Access token",
|
||||
"dropbox_description_accesstoken": "The access token of your Dropbox account/app.",
|
||||
"verify_connection": "Verificar conexão",
|
||||
"remove_after_upload": "Once recordings are uploaded to some persistence, you might want to remove them from the local Kerberos Agent.",
|
||||
"remove_after_upload_description": "Remove recordings after they are uploaded successfully.",
|
||||
"remove_after_upload_enabled": "Enabled delete on upload"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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": "Включено удаление при выгрузке"
|
||||
}
|
||||
}
|
||||
}
|
||||
217
ui/public/locales/zh/translation.json
Normal file
217
ui/public/locales/zh/translation.json
Normal file
@@ -0,0 +1,217 @@
|
||||
{
|
||||
"breadcrumb": {
|
||||
"watch_recordings": "观看录像",
|
||||
"configure": "配置"
|
||||
},
|
||||
"buttons": {
|
||||
"save": "保存",
|
||||
"verify_connection": "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 演示环境,了解 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": "Verifying your ONVIF settings.",
|
||||
"verify_onvif_success": "ONVIF settings are successfully verified.",
|
||||
"verify_onvif_error": "Something went wrong while verifying the ONVIF settings"
|
||||
},
|
||||
"overview": {
|
||||
"general": "常规",
|
||||
"description_general": "Kerberos Agent 常规设置",
|
||||
"key": "Key",
|
||||
"camera_name": "相机名称",
|
||||
"timezone": "时区",
|
||||
"select_timezone": "选择时区",
|
||||
"advanced_configuration": "高级配置",
|
||||
"description_advanced_configuration": "启用或禁用 Kerberos Agent 特定部分详细配置选项",
|
||||
"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 网址",
|
||||
"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": "指定录制方式。具有连续的 24/7 全天候设置 或 基于运动的录制。",
|
||||
"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 代理是否可以在达到特定存储容量(MB)时清理录制文件。这将在达到容量时删除最旧的录制文件。",
|
||||
"autoclean_enable": "启用自动清理",
|
||||
"autoclean_description_enable": "启用自动清理",
|
||||
"autoclean_max_directory_size": "最大目录大小(MB)",
|
||||
"autoclean_description_max_directory_size": "存储录像最大的 MB 数",
|
||||
"fragmentedrecordings": "碎片录制",
|
||||
"description_fragmentedrecordings": "当录制文件碎片化时,它们适用于 HLS 流。打开 MP4 容器时,看起来会有点不同。",
|
||||
"fragmentedrecordings_enable": "启用碎片",
|
||||
"fragmentedrecordings_description_enable": "HLS 需要分段录制。",
|
||||
"fragmentedrecordings_duration": "片段持续时间",
|
||||
"fragmentedrecordings_description_duration": "单个片段的持续时间"
|
||||
},
|
||||
"streaming": {
|
||||
"stun_turn": "WebRTC 的 STUN/TURN",
|
||||
"description_stun_turn": "对于全分辨率直播,我们使用 WebRTC 的概念。其中一个关键功能是 ICE 候选功能,它允许使用 STUN/TURN 的概念进行 NAT 遍历。",
|
||||
"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": "通过 MQTT 转发 h264 流",
|
||||
"stun_turn_transcode": "转码流",
|
||||
"stun_turn_description_transcode": "将流转换为较低分辨率",
|
||||
"stun_turn_downscale": "缩小分辨率(以%或原始分辨率为单位)",
|
||||
"mqtt": "MQTT",
|
||||
"description_mqtt": "MQTT 代理用于从",
|
||||
"description2_mqtt": "到 Kerberos Agent 进行通信, 以实现例如实时流式传输或 ONVIF (PTZ) 功能。",
|
||||
"mqtt_brokeruri": "代理 URI",
|
||||
"mqtt_username": "账户",
|
||||
"mqtt_password": "密码"
|
||||
},
|
||||
"conditions": {
|
||||
"timeofinterest": "兴趣时间",
|
||||
"description_timeofinterest": "仅在特定时间间隔(基于时区)之间进行录制。",
|
||||
"timeofinterest_enabled": "启用",
|
||||
"timeofinterest_description_enabled": "如果启用,您可以指定时间窗口",
|
||||
"sunday": "周日",
|
||||
"monday": "周一",
|
||||
"tuesday": "周二",
|
||||
"wednesday": "周三",
|
||||
"thursday": "周四",
|
||||
"friday": "周五",
|
||||
"saturday": "周六",
|
||||
"externalcondition": "外界条件",
|
||||
"description_externalcondition": "根据外部 Web 服务,可以启用或禁用录制。",
|
||||
"regionofinterest": "兴趣地区",
|
||||
"description_regionofinterest": "通过定义一个或多个区域,将仅在您定义的区域中跟踪运动。"
|
||||
},
|
||||
"persistence": {
|
||||
"kerberoshub": "Kerberos Hub",
|
||||
"description_kerberoshub": "Kerberos Agents 可以将检测信号发送到中央",
|
||||
"description2_kerberoshub": "进行安装。检测信号和其他相关信息将同步到 Kerberos Hub,以显示有关视频环境的实时信息。",
|
||||
"persistence": "持久化存储",
|
||||
"saasoffering": "Kerberos Hub (SAAS 产品)",
|
||||
"description_persistence": "能够存储您的录像是一切的开始。您可以在我们的",
|
||||
"description2_persistence": ", 或第三方提供商之间进行选择。",
|
||||
"select_persistence": "选择持久化存储",
|
||||
"kerberoshub_proxyurl": "Kerberos Hub 代理 URL",
|
||||
"kerberoshub_description_proxyurl": "用于上传您录像的代理端点",
|
||||
"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 Agents 在 Kerberos Hub 中所属的站点 ID",
|
||||
"kerberoshub_region": "区域",
|
||||
"kerberoshub_description_region": "存储录像的区域",
|
||||
"kerberoshub_bucket": "Bucket",
|
||||
"kerberoshub_description_bucket": "存储录像的桶",
|
||||
"kerberoshub_username": "账户/目录 (should match Kerberos Hub username)",
|
||||
"kerberoshub_description_username": "您的 Kerberos Hub 帐户的用户名",
|
||||
"kerberosvault_apiurl": "Kerberos Vault API URL",
|
||||
"kerberosvault_description_apiurl": "Kerberos Vault API",
|
||||
"kerberosvault_provider": "供应商",
|
||||
"kerberosvault_description_provider": "您的录像将会被发送到的提供商",
|
||||
"kerberosvault_directory": "目录 (should match Kerberos Hub username)",
|
||||
"kerberosvault_description_directory": "录像将存储在提供商中的子目录",
|
||||
"kerberosvault_accesskey": "访问密钥",
|
||||
"kerberosvault_description_accesskey": "Kerberos Vault 帐户的访问密钥",
|
||||
"kerberosvault_secretkey": "密钥",
|
||||
"kerberosvault_description_secretkey": "Kerberos Vault 帐户的密钥",
|
||||
"verify_connection": "验证连接"
|
||||
}
|
||||
}
|
||||
}
|
||||
220
ui/src/App.jsx
220
ui/src/App.jsx
@@ -38,6 +38,16 @@ class App extends React.Component {
|
||||
dispatchGetDashboardInformation();
|
||||
dispatchConnect();
|
||||
|
||||
const connectInterval = interval(1000);
|
||||
this.connectionSubscription = connectInterval.subscribe(() => {
|
||||
const { connected } = this.props;
|
||||
if (connected) {
|
||||
// Already connected
|
||||
} else {
|
||||
dispatchConnect();
|
||||
}
|
||||
});
|
||||
|
||||
const interval$ = interval(5000);
|
||||
this.subscription = interval$.subscribe(() => {
|
||||
dispatchGetDashboardInformation();
|
||||
@@ -64,6 +74,7 @@ class App extends React.Component {
|
||||
|
||||
componentWillUnmount() {
|
||||
this.subscription.unsubscribe();
|
||||
this.connectionSubscription.unsubscribe();
|
||||
const message = {
|
||||
client_id: uuid(),
|
||||
message_type: 'goodbye',
|
||||
@@ -82,111 +93,122 @@ class App extends React.Component {
|
||||
const { children, username, dashboard, dispatchLogout } = this.props;
|
||||
const cloudOnline = this.getCurrentTimestamp() - dashboard.cloudOnline < 30;
|
||||
return (
|
||||
<div id="page-root">
|
||||
<Sidebar logo={logo} title="Kerberos Agent" version="v1-beta" mobile>
|
||||
<Profilebar
|
||||
username={username}
|
||||
email="support@kerberos.io"
|
||||
userrole={t('navigation.admin')}
|
||||
logout={dispatchLogout}
|
||||
/>
|
||||
<Navigation>
|
||||
<NavigationSection title={t('navigation.management')} />
|
||||
<NavigationGroup>
|
||||
<NavigationItem
|
||||
title={t('navigation.dashboard')}
|
||||
icon="dashboard"
|
||||
link="dashboard"
|
||||
/>
|
||||
<NavigationItem
|
||||
title={t('navigation.recordings')}
|
||||
icon="media"
|
||||
link="media"
|
||||
/>
|
||||
<NavigationItem
|
||||
title={t('navigation.settings')}
|
||||
icon="preferences"
|
||||
link="settings"
|
||||
/>
|
||||
</NavigationGroup>
|
||||
<NavigationSection title={t('navigation.help_support')} />
|
||||
<NavigationGroup>
|
||||
<NavigationItem
|
||||
title={t('navigation.swagger')}
|
||||
icon="api"
|
||||
external
|
||||
link={`${config.URL}/swagger/index.html`}
|
||||
/>
|
||||
<NavigationItem
|
||||
title={t('navigation.documentation')}
|
||||
icon="book"
|
||||
external
|
||||
link="https://doc.kerberos.io/agent/announcement"
|
||||
/>
|
||||
<NavigationItem
|
||||
title="Kerberos Hub"
|
||||
icon="cloud"
|
||||
external
|
||||
link="https://app.kerberos.io"
|
||||
/>
|
||||
<NavigationItem
|
||||
title={t('navigation.ui_library')}
|
||||
icon="paint"
|
||||
external
|
||||
link="https://ui.kerberos.io/"
|
||||
/>
|
||||
<NavigationItem
|
||||
title="Github"
|
||||
icon="github-nav"
|
||||
external
|
||||
link="https://github.com/kerberos-io/agent"
|
||||
/>
|
||||
</NavigationGroup>
|
||||
<NavigationSection title={t('navigation.layout')} />
|
||||
<NavigationGroup>
|
||||
<LanguageSelect />
|
||||
</NavigationGroup>
|
||||
|
||||
<NavigationSection title="Websocket" />
|
||||
<NavigationGroup>
|
||||
<div className="websocket-badge">
|
||||
<Badge
|
||||
title={connected ? 'connected' : 'disconnected'}
|
||||
status={connected ? 'success' : 'warning'}
|
||||
<>
|
||||
{config.MODE !== 'release' && (
|
||||
<div className={`environment ${config.MODE}`}>
|
||||
Environment: {config.MODE}
|
||||
</div>
|
||||
)}
|
||||
<div id="page-root">
|
||||
<Sidebar logo={logo} title="Kerberos Agent" version="v1-beta" mobile>
|
||||
<Profilebar
|
||||
username={username}
|
||||
email="support@kerberos.io"
|
||||
userrole={t('navigation.admin')}
|
||||
logout={dispatchLogout}
|
||||
/>
|
||||
<Navigation>
|
||||
<NavigationSection title={t('navigation.management')} />
|
||||
<NavigationGroup>
|
||||
<NavigationItem
|
||||
title={t('navigation.dashboard')}
|
||||
icon="dashboard"
|
||||
link="dashboard"
|
||||
/>
|
||||
</div>
|
||||
</NavigationGroup>
|
||||
</Navigation>
|
||||
</Sidebar>
|
||||
<Main>
|
||||
<Gradient />
|
||||
<NavigationItem
|
||||
title={t('navigation.recordings')}
|
||||
icon="media"
|
||||
link="media"
|
||||
/>
|
||||
<NavigationItem
|
||||
title={t('navigation.settings')}
|
||||
icon="preferences"
|
||||
link="settings"
|
||||
/>
|
||||
</NavigationGroup>
|
||||
<NavigationSection title={t('navigation.help_support')} />
|
||||
<NavigationGroup>
|
||||
<NavigationItem
|
||||
title={t('navigation.swagger')}
|
||||
icon="api"
|
||||
external
|
||||
link={`${config.URL}/swagger/index.html`}
|
||||
/>
|
||||
<NavigationItem
|
||||
title={t('navigation.documentation')}
|
||||
icon="book"
|
||||
external
|
||||
link="https://doc.kerberos.io/agent/announcement"
|
||||
/>
|
||||
<NavigationItem
|
||||
title="Kerberos Hub"
|
||||
icon="cloud"
|
||||
external
|
||||
link="https://app.kerberos.io"
|
||||
/>
|
||||
<NavigationItem
|
||||
title={t('navigation.ui_library')}
|
||||
icon="paint"
|
||||
external
|
||||
link="https://ui.kerberos.io/"
|
||||
/>
|
||||
<NavigationItem
|
||||
title="Github"
|
||||
icon="github-nav"
|
||||
external
|
||||
link="https://github.com/kerberos-io/agent"
|
||||
/>
|
||||
</NavigationGroup>
|
||||
<NavigationSection title={t('navigation.layout')} />
|
||||
<NavigationGroup>
|
||||
<LanguageSelect />
|
||||
</NavigationGroup>
|
||||
|
||||
{!cloudOnline && (
|
||||
<a href="https://app.kerberos.io" target="_blank" rel="noreferrer">
|
||||
<div className="cloud-not-installed">
|
||||
<div>
|
||||
<Icon label="cloud" />
|
||||
Activate Kerberos Hub, and make your cameras and recordings
|
||||
available through a secured cloud!
|
||||
<NavigationSection title="Websocket" />
|
||||
<NavigationGroup>
|
||||
<div className="websocket-badge">
|
||||
<Badge
|
||||
title={connected ? 'connected' : 'disconnected'}
|
||||
status={connected ? 'success' : 'warning'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
)}
|
||||
</NavigationGroup>
|
||||
</Navigation>
|
||||
</Sidebar>
|
||||
<Main>
|
||||
<Gradient />
|
||||
|
||||
{dashboard.offlineMode === 'true' && (
|
||||
<Link to="/settings">
|
||||
<div className="offline-mode">
|
||||
<div>
|
||||
<Icon label="info" />
|
||||
Attention! Kerberos is currently running in Offline mode.
|
||||
{!cloudOnline && (
|
||||
<a
|
||||
href="https://app.kerberos.io"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<div className="cloud-not-installed">
|
||||
<div>
|
||||
<Icon label="cloud" />
|
||||
Activate Kerberos Hub, and make your cameras and recordings
|
||||
available through a secured cloud!
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
</a>
|
||||
)}
|
||||
|
||||
<MainBody>{children}</MainBody>
|
||||
</Main>
|
||||
</div>
|
||||
{dashboard.offlineMode === 'true' && (
|
||||
<Link to="/settings">
|
||||
<div className="offline-mode">
|
||||
<div>
|
||||
<Icon label="info" />
|
||||
Attention! Kerberos is currently running in Offline mode.
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<MainBody>{children}</MainBody>
|
||||
</Main>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,20 @@
|
||||
@import "./app.variables";
|
||||
|
||||
.grid-container {
|
||||
row-gap: size(2);
|
||||
}
|
||||
|
||||
.main {
|
||||
padding-top: size(7);
|
||||
|
||||
@media (min-width: 800px) {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.MuiPopover-root {
|
||||
z-index: 99999999 !important;
|
||||
}
|
||||
|
||||
.offline-mode, .cloud-not-installed {
|
||||
background: var(--upper-gradient);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
doGetConfig,
|
||||
doSaveConfig,
|
||||
doVerifyOnvif,
|
||||
doVerifyHub,
|
||||
doVerifyPersistence,
|
||||
doGetKerberosAgentTags,
|
||||
@@ -39,6 +40,28 @@ export const updateRegion = (id, polygon) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const verifyOnvif = (config, onSuccess, onError) => {
|
||||
return (dispatch) => {
|
||||
doVerifyOnvif(
|
||||
config,
|
||||
(data) => {
|
||||
dispatch({
|
||||
type: 'VERIFY_ONVIF',
|
||||
});
|
||||
if (onSuccess) {
|
||||
onSuccess(data);
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
const { message } = error;
|
||||
if (onError) {
|
||||
onError(message);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
export const verifyCamera = (streamType, config, onSuccess, onError) => {
|
||||
return (dispatch) => {
|
||||
doVerifyCamera(
|
||||
|
||||
@@ -91,6 +91,26 @@ export function doVerifyHub(config, onSuccess, onError) {
|
||||
});
|
||||
}
|
||||
|
||||
export function doVerifyOnvif(config, onSuccess, onError) {
|
||||
const endpoint = API.post(`onvif/verify`, {
|
||||
...config,
|
||||
});
|
||||
endpoint
|
||||
.then((res) => {
|
||||
if (res.status !== 200) {
|
||||
throw new Error(res.data);
|
||||
}
|
||||
return res.data;
|
||||
})
|
||||
.then((data) => {
|
||||
onSuccess(data);
|
||||
})
|
||||
.catch((e) => {
|
||||
const { data } = e.response;
|
||||
onError(data);
|
||||
});
|
||||
}
|
||||
|
||||
export function doVerifyCamera(streamType, config, onSuccess, onError) {
|
||||
const cameraStreams = {
|
||||
rtsp: '',
|
||||
|
||||
@@ -46,11 +46,3 @@ export function doCheckIfInstalled(onSuccess, onError) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* export function doAuth(onSuccess, onError) {
|
||||
|
||||
}
|
||||
|
||||
export function doRefreshToken(onSuccess, onError) {
|
||||
|
||||
} */
|
||||
|
||||
@@ -20,8 +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]) {
|
||||
|
||||
@@ -7,14 +7,17 @@ const websocketprotocol = protocol === 'http:' ? 'ws:' : 'wss:';
|
||||
|
||||
const dev = {
|
||||
ENV: 'dev',
|
||||
// Comment the below lines, when using codespaces or other special DNS names (which you can't control)
|
||||
HOSTNAME: hostname,
|
||||
API_URL: `${protocol}//${hostname}:8080/api`,
|
||||
URL: `${protocol}//${hostname}:8080`,
|
||||
WS_URL: `${websocketprotocol}//${hostname}:8080/ws`,
|
||||
|
||||
API_URL: `${protocol}//${hostname}:80/api`,
|
||||
URL: `${protocol}//${hostname}:80`,
|
||||
WS_URL: `${websocketprotocol}//${hostname}:80/ws`,
|
||||
MODE: window['env']['mode'],
|
||||
// Uncomment, and comment the above lines, when using codespaces or other special DNS names (which you can't control)
|
||||
// HOSTNAME: externalHost,
|
||||
// API_URL: `${protocol}//${externalHost}/api`,
|
||||
// URL: `${protocol}//${externalHost}`,
|
||||
// WS_URL: `${websocketprotocol}//${externalHost}/ws`,
|
||||
};
|
||||
|
||||
const prod = {
|
||||
@@ -23,6 +26,7 @@ const prod = {
|
||||
API_URL: `${protocol}//${host}/api`,
|
||||
URL: `${protocol}//${host}`,
|
||||
WS_URL: `${websocketprotocol}//${host}/ws`,
|
||||
MODE: window['env']['mode'],
|
||||
};
|
||||
|
||||
const config = process.env.REACT_APP_ENVIRONMENT === 'production' ? prod : dev;
|
||||
|
||||
@@ -14,7 +14,7 @@ i18n
|
||||
escapeValue: false,
|
||||
},
|
||||
load: 'languageOnly',
|
||||
whitelist: ['de', 'en', 'nl', 'fr', 'pl', 'es', 'pt'],
|
||||
whitelist: ['de', 'en', 'nl', 'fr', 'pl', 'es', 'pt', 'ja', 'ru'],
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
|
||||
@@ -50,7 +50,9 @@ function getAuthState() {
|
||||
}
|
||||
}
|
||||
|
||||
const reduxWebsocketMiddleware = reduxWebsocket();
|
||||
const reduxWebsocketMiddleware = reduxWebsocket({
|
||||
reconnectOnClose: true,
|
||||
});
|
||||
|
||||
const store = createStore(
|
||||
rootReducer(history),
|
||||
|
||||
@@ -144,3 +144,55 @@ body {
|
||||
padding: 0;
|
||||
font-family: 'Inter', sans-serif; // use regular Inter font by default..
|
||||
}
|
||||
|
||||
.environment.develop {
|
||||
background: repeating-linear-gradient(
|
||||
-55deg,
|
||||
#222,
|
||||
#222 10px,
|
||||
#035e1f 10px,
|
||||
#035e1f 20px
|
||||
);
|
||||
text-align: center;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
padding: size(1) 0;
|
||||
}
|
||||
|
||||
.environment.demo {
|
||||
background: repeating-linear-gradient(
|
||||
-55deg,
|
||||
#222,
|
||||
#222 10px,
|
||||
#035e1f 10px,
|
||||
#035e1f 20px
|
||||
);
|
||||
text-align: center;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.environment.staging {
|
||||
background: repeating-linear-gradient(
|
||||
-55deg,
|
||||
#222,
|
||||
#222 10px,
|
||||
#6d0e0e 10px,
|
||||
#6d0e0e 20px
|
||||
);
|
||||
text-align: center;
|
||||
color: white;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.environment.acceptance {
|
||||
background: repeating-linear-gradient(
|
||||
-55deg,
|
||||
#222,
|
||||
#222 10px,
|
||||
#8b8203 10px,
|
||||
#8b8203 20px
|
||||
);
|
||||
text-align: center;
|
||||
color: white;
|
||||
padding: 12px 0;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { Link, withRouter } from 'react-router-dom';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import { send } from '@giantmachines/redux-websocket';
|
||||
import { connect } from 'react-redux';
|
||||
import { interval } from 'rxjs';
|
||||
import {
|
||||
Breadcrumb,
|
||||
KPI,
|
||||
@@ -34,7 +35,9 @@ class Dashboard extends React.Component {
|
||||
liveviewLoaded: false,
|
||||
open: false,
|
||||
currentRecording: '',
|
||||
initialised: false,
|
||||
};
|
||||
this.initialiseLiveview = this.initialiseLiveview.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -46,27 +49,11 @@ class Dashboard extends React.Component {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const { connected } = this.props;
|
||||
if (connected === true) {
|
||||
const { dispatchSend } = this.props;
|
||||
const message = {
|
||||
message_type: 'stream-sd',
|
||||
};
|
||||
dispatchSend(message);
|
||||
}
|
||||
this.initialiseLiveview();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { connected: connectedPrev } = prevProps;
|
||||
const { connected } = this.props;
|
||||
if (connectedPrev === false && connected === true) {
|
||||
const { dispatchSend } = this.props;
|
||||
const message = {
|
||||
message_type: 'stream-sd',
|
||||
};
|
||||
dispatchSend(message);
|
||||
}
|
||||
componentDidUpdate() {
|
||||
this.initialiseLiveview();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@@ -75,6 +62,9 @@ class Dashboard extends React.Component {
|
||||
liveview[0].remove();
|
||||
}
|
||||
|
||||
if (this.requestStreamSubscription) {
|
||||
this.requestStreamSubscription.unsubscribe();
|
||||
}
|
||||
const { dispatchSend } = this.props;
|
||||
const message = {
|
||||
message_type: 'stop-sd',
|
||||
@@ -93,6 +83,30 @@ class Dashboard extends React.Component {
|
||||
return Math.round(Date.now() / 1000);
|
||||
}
|
||||
|
||||
initialiseLiveview() {
|
||||
const { initialised } = this.state;
|
||||
if (!initialised) {
|
||||
const message = {
|
||||
message_type: 'stream-sd',
|
||||
};
|
||||
const { connected, dispatchSend } = this.props;
|
||||
if (connected) {
|
||||
dispatchSend(message);
|
||||
}
|
||||
|
||||
const requestStreamInterval = interval(2000);
|
||||
this.requestStreamSubscription = requestStreamInterval.subscribe(() => {
|
||||
const { connected: isConnected } = this.props;
|
||||
if (isConnected) {
|
||||
dispatchSend(message);
|
||||
}
|
||||
});
|
||||
this.setState({
|
||||
initialised: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
openModal(file) {
|
||||
this.setState({
|
||||
open: true,
|
||||
@@ -106,17 +120,16 @@ class Dashboard extends React.Component {
|
||||
|
||||
// We check if the camera was getting a valid frame
|
||||
// during the last 5 seconds, otherwise we assume the camera is offline.
|
||||
const isCameraOnline =
|
||||
this.getCurrentTimestamp() - dashboard.cameraOnline < 15;
|
||||
const isCameraOnline = dashboard.cameraOnline;
|
||||
|
||||
// We check if a connection is made to Kerberos Hub, or if Offline mode
|
||||
// has been turned on.
|
||||
const cloudOnline = this.getCurrentTimestamp() - dashboard.cloudOnline < 30;
|
||||
const isCloudOnline = dashboard.cloudOnline;
|
||||
let cloudConnection = t('dashboard.not_connected');
|
||||
if (dashboard.offlineMode === 'true') {
|
||||
cloudConnection = t('dashboard.offline_mode');
|
||||
} else {
|
||||
cloudConnection = cloudOnline
|
||||
cloudConnection = isCloudOnline
|
||||
? t('dashboard.connected')
|
||||
: t('dashboard.not_connected');
|
||||
}
|
||||
@@ -176,7 +189,7 @@ class Dashboard extends React.Component {
|
||||
subtitle={cloudConnection}
|
||||
footer="Cloud"
|
||||
icon={
|
||||
cloudOnline && dashboard.offlineMode !== 'true'
|
||||
isCloudOnline && dashboard.offlineMode !== 'true'
|
||||
? 'circle-check-big'
|
||||
: 'circle-cross-big'
|
||||
}
|
||||
@@ -220,14 +233,21 @@ class Dashboard extends React.Component {
|
||||
id="cells1"
|
||||
bodycells={[
|
||||
<>
|
||||
<div className="time">
|
||||
<div
|
||||
className="time"
|
||||
onClick={() =>
|
||||
this.openModal(
|
||||
`${config.URL}/file/${event.key}`
|
||||
)
|
||||
}
|
||||
>
|
||||
<Ellipse status="success" />{' '}
|
||||
<p data-tip="10m and 5s ago">{event.time}</p>
|
||||
</div>
|
||||
</>,
|
||||
<>
|
||||
<p
|
||||
className="pointer"
|
||||
className="pointer event-description"
|
||||
onClick={() =>
|
||||
this.openModal(
|
||||
`${config.URL}/file/${event.key}`
|
||||
@@ -290,7 +310,7 @@ class Dashboard extends React.Component {
|
||||
</div>
|
||||
<div>
|
||||
<h2>{t('dashboard.live_view')}</h2>
|
||||
{!liveviewLoaded && (
|
||||
{(!liveviewLoaded || !isCameraOnline) && (
|
||||
<SetupBox
|
||||
btnicon="preferences"
|
||||
btnlabel={t('dashboard.configure_connection')}
|
||||
@@ -300,7 +320,12 @@ class Dashboard extends React.Component {
|
||||
text={t('dashboard.loading_live_view_description')}
|
||||
/>
|
||||
)}
|
||||
<div style={{ visibility: liveviewLoaded ? 'visible' : 'hidden' }}>
|
||||
<div
|
||||
style={{
|
||||
visibility:
|
||||
liveviewLoaded && isCameraOnline ? 'visible' : 'hidden',
|
||||
}}
|
||||
>
|
||||
<ImageCard
|
||||
imageSrc={`data:image/png;base64, ${
|
||||
images.length ? images[0] : ''
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@import "../../app.variables";
|
||||
|
||||
#dashboard {
|
||||
|
||||
hr {
|
||||
@@ -15,6 +17,13 @@
|
||||
video {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
p.event-description {
|
||||
display: none;
|
||||
@media (min-width: 800px) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
|
||||
@@ -49,60 +49,98 @@ class Login extends React.Component {
|
||||
const { loginError, error } = this.props;
|
||||
|
||||
return (
|
||||
<LandingLayout
|
||||
title="Kerberos Agent"
|
||||
version={config.VERSION}
|
||||
description="Video surveillance for everyone"
|
||||
>
|
||||
<section className="login-body">
|
||||
<Block>
|
||||
<form onSubmit={this.handleSubmit} noValidate>
|
||||
<BlockHeader>
|
||||
<div>
|
||||
<Icon label="login" /> <h4>Login</h4>
|
||||
</div>
|
||||
</BlockHeader>
|
||||
{loginError && (
|
||||
<AlertMessage
|
||||
message={error}
|
||||
onClick={() => this.hideMessage()}
|
||||
/>
|
||||
)}
|
||||
<BlockBody>
|
||||
<Input
|
||||
label="username or email"
|
||||
placeholder="Your username/email"
|
||||
readonly={false}
|
||||
disabled={false}
|
||||
type="text"
|
||||
name="username"
|
||||
iconleft="accounts"
|
||||
/>
|
||||
<Input
|
||||
label="password"
|
||||
placeholder="Your password"
|
||||
readonly={false}
|
||||
disabled={false}
|
||||
type="password"
|
||||
name="password"
|
||||
iconleft="locked"
|
||||
iconright="activity"
|
||||
seperate
|
||||
/>
|
||||
</BlockBody>
|
||||
<BlockFooter>
|
||||
<p />
|
||||
<Button
|
||||
buttonType="submit"
|
||||
type="submit"
|
||||
icon="logout"
|
||||
label="Login"
|
||||
/>
|
||||
</BlockFooter>
|
||||
</form>
|
||||
</Block>
|
||||
</section>
|
||||
</LandingLayout>
|
||||
<>
|
||||
{config.MODE !== 'release' && (
|
||||
<div className={`environment ${config.MODE}`}>
|
||||
Environment: {config.MODE}
|
||||
</div>
|
||||
)}
|
||||
<LandingLayout
|
||||
title="Kerberos Agent"
|
||||
version={config.VERSION}
|
||||
description="Video surveillance for everyone"
|
||||
>
|
||||
<section className="login-body">
|
||||
<Block>
|
||||
<form onSubmit={this.handleSubmit} noValidate>
|
||||
<BlockHeader>
|
||||
<div>
|
||||
<Icon label="login" /> <h4>Login</h4>
|
||||
</div>
|
||||
</BlockHeader>
|
||||
{loginError && (
|
||||
<AlertMessage
|
||||
message={error}
|
||||
onClick={() => this.hideMessage()}
|
||||
/>
|
||||
)}
|
||||
<BlockBody>
|
||||
{config.MODE === 'demo' && (
|
||||
<>
|
||||
<Input
|
||||
label="username or email"
|
||||
placeholder="Your username/email"
|
||||
readonly
|
||||
disabled={false}
|
||||
value="root"
|
||||
type="text"
|
||||
name="username"
|
||||
iconleft="accounts"
|
||||
/>
|
||||
<Input
|
||||
label="password"
|
||||
placeholder="Your password"
|
||||
readonly
|
||||
disabled={false}
|
||||
value="root"
|
||||
type="password"
|
||||
name="password"
|
||||
iconleft="locked"
|
||||
iconright="activity"
|
||||
seperate
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{config.MODE !== 'demo' && (
|
||||
<>
|
||||
<Input
|
||||
label="username or email"
|
||||
placeholder="Your username/email"
|
||||
readonly={false}
|
||||
disabled={false}
|
||||
type="text"
|
||||
name="username"
|
||||
iconleft="accounts"
|
||||
/>
|
||||
<Input
|
||||
label="password"
|
||||
placeholder="Your password"
|
||||
readonly={false}
|
||||
disabled={false}
|
||||
type="password"
|
||||
name="password"
|
||||
iconleft="locked"
|
||||
iconright="activity"
|
||||
seperate
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</BlockBody>
|
||||
<BlockFooter>
|
||||
<p />
|
||||
<Button
|
||||
buttonType="submit"
|
||||
type="submit"
|
||||
icon="logout"
|
||||
label="Login"
|
||||
/>
|
||||
</BlockFooter>
|
||||
</form>
|
||||
</Block>
|
||||
</section>
|
||||
</LandingLayout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,8 @@ import PropTypes from 'prop-types';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import {
|
||||
Breadcrumb,
|
||||
VideoContainer,
|
||||
VideoCard,
|
||||
ControlBar,
|
||||
Button,
|
||||
Input,
|
||||
Modal,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
@@ -105,17 +102,7 @@ class Media extends React.Component {
|
||||
</Link>
|
||||
</Breadcrumb>
|
||||
|
||||
<ControlBar>
|
||||
<Input
|
||||
iconleft="search"
|
||||
onChange={() => {}}
|
||||
placeholder={t('recordings.search_media')}
|
||||
layout="controlbar"
|
||||
type="text"
|
||||
/>
|
||||
</ControlBar>
|
||||
|
||||
<VideoContainer cols={4} isVideoWall={false}>
|
||||
<div className="stats grid-container --four-columns">
|
||||
{events.map((event) => (
|
||||
<div
|
||||
key={event.key}
|
||||
@@ -135,7 +122,7 @@ class Media extends React.Component {
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</VideoContainer>
|
||||
</div>
|
||||
{open && (
|
||||
<Modal>
|
||||
<ModalHeader
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
updateRegion,
|
||||
removeRegion,
|
||||
saveConfig,
|
||||
verifyOnvif,
|
||||
verifyCamera,
|
||||
verifyHub,
|
||||
verifyPersistence,
|
||||
@@ -40,6 +41,8 @@ class Settings extends React.Component {
|
||||
|
||||
KERBEROS_HUB = 's3'; // @TODO needs to change
|
||||
|
||||
DROPBOX = 'dropbox';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
@@ -61,6 +64,9 @@ class Settings extends React.Component {
|
||||
verifyCameraSuccess: false,
|
||||
verifyCameraError: false,
|
||||
verifyCameraMessage: '',
|
||||
verifyOnvifSuccess: false,
|
||||
verifyOnvifError: false,
|
||||
verifyOnvifErrorMessage: '',
|
||||
loading: false,
|
||||
loadingHub: false,
|
||||
loadingCamera: false,
|
||||
@@ -74,6 +80,10 @@ class Settings extends React.Component {
|
||||
label: 'Kerberos Vault',
|
||||
value: this.KERBEROS_VAULT,
|
||||
},
|
||||
{
|
||||
label: 'Dropbox',
|
||||
value: this.DROPBOX,
|
||||
},
|
||||
];
|
||||
|
||||
this.tags = {
|
||||
@@ -95,6 +105,7 @@ class Settings extends React.Component {
|
||||
'gcp',
|
||||
'aws',
|
||||
'minio',
|
||||
'dropbox',
|
||||
],
|
||||
};
|
||||
this.timezones = [];
|
||||
@@ -120,6 +131,7 @@ class Settings extends React.Component {
|
||||
this.onAddRegion = this.onAddRegion.bind(this);
|
||||
this.onUpdateRegion = this.onUpdateRegion.bind(this);
|
||||
this.onDeleteRegion = this.onDeleteRegion.bind(this);
|
||||
this.verifyONVIF = this.verifyONVIF.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -217,13 +229,15 @@ class Settings extends React.Component {
|
||||
|
||||
calculateTimetable(timetable) {
|
||||
this.timetable = timetable;
|
||||
for (let i = 0; i < timetable.length; i += 1) {
|
||||
const time = timetable[i];
|
||||
const { start1, start2, end1, end2 } = time;
|
||||
this.timetable[i].start1Full = this.convertSecondsToHourMinute(start1);
|
||||
this.timetable[i].start2Full = this.convertSecondsToHourMinute(start2);
|
||||
this.timetable[i].end1Full = this.convertSecondsToHourMinute(end1);
|
||||
this.timetable[i].end2Full = this.convertSecondsToHourMinute(end2);
|
||||
if (this.timetable) {
|
||||
for (let i = 0; i < timetable.length; i += 1) {
|
||||
const time = timetable[i];
|
||||
const { start1, start2, end1, end2 } = time;
|
||||
this.timetable[i].start1Full = this.convertSecondsToHourMinute(start1);
|
||||
this.timetable[i].start2Full = this.convertSecondsToHourMinute(start2);
|
||||
this.timetable[i].end1Full = this.convertSecondsToHourMinute(end1);
|
||||
this.timetable[i].end2Full = this.convertSecondsToHourMinute(end2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,6 +279,8 @@ class Settings extends React.Component {
|
||||
verifyHubError: false,
|
||||
configSuccess: false,
|
||||
configError: false,
|
||||
verifyOnvifSuccess: false,
|
||||
verifyOnvifError: false,
|
||||
});
|
||||
|
||||
if (config) {
|
||||
@@ -286,6 +302,52 @@ class Settings extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
verifyONVIF() {
|
||||
const { config, dispatchVerifyOnvif } = this.props;
|
||||
|
||||
// Get camera configuration (subset of config).
|
||||
const cameraConfig = {
|
||||
onvif_xaddr: config.config.capture.ipcamera.onvif_xaddr,
|
||||
onvif_username: config.config.capture.ipcamera.onvif_username,
|
||||
onvif_password: config.config.capture.ipcamera.onvif_password,
|
||||
};
|
||||
|
||||
this.setState({
|
||||
verifyOnvifSuccess: false,
|
||||
verifyOnvifError: false,
|
||||
verifyOnvifErrorMessage: '',
|
||||
verifyCameraSuccess: false,
|
||||
verifyCameraError: false,
|
||||
verifyCameraErrorMessage: '',
|
||||
configSuccess: false,
|
||||
configError: false,
|
||||
loadingCamera: false,
|
||||
loadingOnvif: true,
|
||||
});
|
||||
|
||||
if (config) {
|
||||
dispatchVerifyOnvif(
|
||||
cameraConfig,
|
||||
() => {
|
||||
this.setState({
|
||||
verifyOnvifSuccess: true,
|
||||
verifyOnvifError: false,
|
||||
verifyOnvifErrorMessage: '',
|
||||
loadingOnvif: false,
|
||||
});
|
||||
},
|
||||
(error) => {
|
||||
this.setState({
|
||||
verifyOnvifSuccess: false,
|
||||
verifyOnvifError: true,
|
||||
verifyOnvifErrorMessage: error,
|
||||
loadingOnvif: false,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
verifyHubSettings() {
|
||||
const { config, dispatchVerifyHub } = this.props;
|
||||
if (config) {
|
||||
@@ -308,6 +370,8 @@ class Settings extends React.Component {
|
||||
verifyCameraSuccess: false,
|
||||
verifyCameraError: false,
|
||||
verifyCameraErrorMessage: '',
|
||||
verifyOnvifSuccess: false,
|
||||
verifyOnvifError: false,
|
||||
loadingHub: true,
|
||||
});
|
||||
|
||||
@@ -353,6 +417,8 @@ class Settings extends React.Component {
|
||||
persistenceError: false,
|
||||
verifyCameraSuccess: false,
|
||||
verifyCameraError: false,
|
||||
verifyOnvifSuccess: false,
|
||||
verifyOnvifError: false,
|
||||
verifyCameraErrorMessage: '',
|
||||
loading: true,
|
||||
});
|
||||
@@ -393,6 +459,7 @@ class Settings extends React.Component {
|
||||
this.setState({
|
||||
configSuccess: false,
|
||||
configError: false,
|
||||
loadingCamera: true,
|
||||
verifyPersistenceSuccess: false,
|
||||
verifyPersistenceError: false,
|
||||
verifyHubSuccess: false,
|
||||
@@ -401,9 +468,10 @@ class Settings extends React.Component {
|
||||
verifyCameraSuccess: false,
|
||||
verifyCameraError: false,
|
||||
verifyCameraErrorMessage: '',
|
||||
verifyOnvifSuccess: false,
|
||||
verifyOnvifError: false,
|
||||
hubSuccess: false,
|
||||
hubError: false,
|
||||
loadingCamera: true,
|
||||
});
|
||||
|
||||
dispatchVerifyCamera(
|
||||
@@ -444,6 +512,10 @@ class Settings extends React.Component {
|
||||
verifyCameraSuccess,
|
||||
verifyCameraError,
|
||||
verifyCameraErrorMessage,
|
||||
loadingOnvif,
|
||||
verifyOnvifSuccess,
|
||||
verifyOnvifError,
|
||||
verifyOnvifErrorMessage,
|
||||
loadingCamera,
|
||||
loading,
|
||||
loadingHub,
|
||||
@@ -643,10 +715,23 @@ class Settings extends React.Component {
|
||||
type="alert"
|
||||
message={`${t(
|
||||
'settings.info.verify_camera_error'
|
||||
)} :${verifyCameraErrorMessage}`}
|
||||
)}: ${verifyCameraErrorMessage}`}
|
||||
/>
|
||||
)}
|
||||
|
||||
{loadingOnvif && (
|
||||
<InfoBar type="loading" message={t('settings.info.verify_onvif')} />
|
||||
)}
|
||||
{verifyOnvifSuccess && (
|
||||
<InfoBar
|
||||
type="success"
|
||||
message={t('settings.info.verify_onvif_success')}
|
||||
/>
|
||||
)}
|
||||
{verifyOnvifError && (
|
||||
<InfoBar type="alert" message={verifyOnvifErrorMessage} />
|
||||
)}
|
||||
|
||||
{loadingHub && (
|
||||
<InfoBar type="loading" message={t('settings.info.verify_hub')} />
|
||||
)}
|
||||
@@ -725,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
|
||||
@@ -1094,7 +1197,7 @@ class Settings extends React.Component {
|
||||
noPadding
|
||||
label={t('settings.camera.onvif_xaddr')}
|
||||
value={config.capture.ipcamera.onvif_xaddr}
|
||||
placeholder="http://x.x.x.x/onvif/device_service"
|
||||
placeholder="x.x.x.x:yyyy"
|
||||
onChange={(value) =>
|
||||
this.onUpdateField(
|
||||
'capture.ipcamera',
|
||||
@@ -1134,6 +1237,12 @@ class Settings extends React.Component {
|
||||
/>
|
||||
</BlockBody>
|
||||
<BlockFooter>
|
||||
<Button
|
||||
label={t('buttons.verify_connection')}
|
||||
type="default"
|
||||
icon="verify"
|
||||
onClick={this.verifyONVIF}
|
||||
/>
|
||||
<Button
|
||||
label={t('buttons.save')}
|
||||
type="default"
|
||||
@@ -1148,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
|
||||
@@ -1966,6 +2145,32 @@ class Settings extends React.Component {
|
||||
<h4>{t('settings.persistence.persistence')}</h4>
|
||||
</BlockHeader>
|
||||
<BlockBody>
|
||||
<p>{t('settings.persistence.remove_after_upload')}</p>
|
||||
<div className="toggle-wrapper">
|
||||
<Toggle
|
||||
on={config.remove_after_upload === 'true'}
|
||||
disabled={false}
|
||||
onClick={(event) =>
|
||||
this.onUpdateToggle(
|
||||
'',
|
||||
'remove_after_upload',
|
||||
event,
|
||||
config
|
||||
)
|
||||
}
|
||||
/>
|
||||
<div>
|
||||
<span>
|
||||
{t('settings.persistence.remove_after_upload_enabled')}
|
||||
</span>
|
||||
<p>
|
||||
{t(
|
||||
'settings.persistence.remove_after_upload_description'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
{t('settings.persistence.description_persistence')}{' '}
|
||||
<a
|
||||
@@ -1997,17 +2202,6 @@ class Settings extends React.Component {
|
||||
/>
|
||||
{config.cloud === this.KERBEROS_HUB && (
|
||||
<>
|
||||
<Input
|
||||
noPadding
|
||||
label={t('settings.persistence.kerberoshub_proxyurl')}
|
||||
placeholder={t(
|
||||
'settings.persistence.kerberoshub_description_proxyurl'
|
||||
)}
|
||||
value={config.s3 ? config.s3.proxyuri : ''}
|
||||
onChange={(value) =>
|
||||
this.onUpdateField('s3', 'proxyuri', value, config.s3)
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
noPadding
|
||||
label={t('settings.persistence.kerberoshub_region')}
|
||||
@@ -2019,28 +2213,6 @@ class Settings extends React.Component {
|
||||
this.onUpdateField('s3', 'region', value, config.s3)
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
noPadding
|
||||
label={t('settings.persistence.kerberoshub_bucket')}
|
||||
placeholder={t(
|
||||
'settings.persistence.kerberoshub_description_bucket'
|
||||
)}
|
||||
value={config.s3 ? config.s3.bucket : ''}
|
||||
onChange={(value) =>
|
||||
this.onUpdateField('s3', 'bucket', value, config.s3)
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
noPadding
|
||||
label={t('settings.persistence.kerberoshub_username')}
|
||||
placeholder={t(
|
||||
'settings.persistence.kerberoshub_description_username'
|
||||
)}
|
||||
value={config.s3 ? config.s3.username : ''}
|
||||
onChange={(value) =>
|
||||
this.onUpdateField('s3', 'username', value, config.s3)
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{config.cloud === this.KERBEROS_VAULT && (
|
||||
@@ -2139,6 +2311,44 @@ class Settings extends React.Component {
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{config.cloud === this.DROPBOX && (
|
||||
<>
|
||||
<Input
|
||||
noPadding
|
||||
label={t('settings.persistence.dropbox_directory')}
|
||||
placeholder={t(
|
||||
'settings.persistence.dropbox_description_directory'
|
||||
)}
|
||||
value={config.dropbox ? config.dropbox.directory : ''}
|
||||
onChange={(value) =>
|
||||
this.onUpdateField(
|
||||
'dropbox',
|
||||
'directory',
|
||||
value,
|
||||
config.dropbox
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
noPadding
|
||||
label={t('settings.persistence.dropbox_accesstoken')}
|
||||
placeholder={t(
|
||||
'settings.persistence.dropbox_description_accesstoken'
|
||||
)}
|
||||
value={
|
||||
config.dropbox ? config.dropbox.access_token : ''
|
||||
}
|
||||
onChange={(value) =>
|
||||
this.onUpdateField(
|
||||
'dropbox',
|
||||
'access_token',
|
||||
value,
|
||||
config.dropbox
|
||||
)
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</BlockBody>
|
||||
<BlockFooter>
|
||||
<Button
|
||||
@@ -2172,6 +2382,8 @@ const mapStateToProps = (state /* , ownProps */) => ({
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch /* , ownProps */) => ({
|
||||
dispatchVerifyOnvif: (config, success, error) =>
|
||||
dispatch(verifyOnvif(config, success, error)),
|
||||
dispatchVerifyCamera: (streamType, config, success, error) =>
|
||||
dispatch(verifyCamera(streamType, config, success, error)),
|
||||
dispatchVerifyHub: (config, success, error) =>
|
||||
@@ -2199,6 +2411,7 @@ Settings.propTypes = {
|
||||
dispatchUpdateRegion: PropTypes.func.isRequired,
|
||||
dispatchRemoveRegion: PropTypes.func.isRequired,
|
||||
dispatchVerifyCamera: PropTypes.func.isRequired,
|
||||
dispatchVerifyOnvif: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default withTranslation()(
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.table-container table {
|
||||
border-spacing: 0 12px;
|
||||
width: 100%;
|
||||
|
||||
351
ui/yarn.lock
351
ui/yarn.lock
@@ -174,7 +174,7 @@
|
||||
dependencies:
|
||||
"@babel/types" "^7.18.9"
|
||||
|
||||
"@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.18.6":
|
||||
"@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.18.6":
|
||||
version "7.18.6"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e"
|
||||
integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==
|
||||
@@ -1056,6 +1056,13 @@
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.11"
|
||||
|
||||
"@babel/runtime@^7.18.3", "@babel/runtime@^7.21.0":
|
||||
version "7.21.0"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673"
|
||||
integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.11"
|
||||
|
||||
"@babel/template@^7.18.10", "@babel/template@^7.18.6", "@babel/template@^7.3.3":
|
||||
version "7.18.10"
|
||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.10.tgz#6f9134835970d1dbf0835c0d100c9f38de0c5e71"
|
||||
@@ -1206,11 +1213,151 @@
|
||||
resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-2.0.2.tgz#1bfafe4b7ed0f3e4105837e056e0a89b108ebe36"
|
||||
integrity sha512-IkpVW/ehM1hWKln4fCA3NzJU8KwD+kIOvPZA4cqxoJHtE21CCzjyp+Kxbu0i5I4tBNOlXPL9mjwnWlL0VEG4Fg==
|
||||
|
||||
"@date-io/core@^2.15.0", "@date-io/core@^2.16.0":
|
||||
version "2.16.0"
|
||||
resolved "https://registry.yarnpkg.com/@date-io/core/-/core-2.16.0.tgz#7871bfc1d9bca9aa35ad444a239505589d0f22f6"
|
||||
integrity sha512-DYmSzkr+jToahwWrsiRA2/pzMEtz9Bq1euJwoOuYwuwIYXnZFtHajY2E6a1VNVDc9jP8YUXK1BvnZH9mmT19Zg==
|
||||
|
||||
"@date-io/date-fns@^2.15.0":
|
||||
version "2.16.0"
|
||||
resolved "https://registry.yarnpkg.com/@date-io/date-fns/-/date-fns-2.16.0.tgz#bd5e09b6ecb47ee55e593fc3a87e7b2caaa3da40"
|
||||
integrity sha512-bfm5FJjucqlrnQcXDVU5RD+nlGmL3iWgkHTq3uAZWVIuBu6dDmGa3m8a6zo2VQQpu8ambq9H22UyUpn7590joA==
|
||||
dependencies:
|
||||
"@date-io/core" "^2.16.0"
|
||||
|
||||
"@date-io/dayjs@^2.15.0":
|
||||
version "2.16.0"
|
||||
resolved "https://registry.yarnpkg.com/@date-io/dayjs/-/dayjs-2.16.0.tgz#0d2c254ad8db1306fdc4b8eda197cb53c9af89dc"
|
||||
integrity sha512-y5qKyX2j/HG3zMvIxTobYZRGnd1FUW2olZLS0vTj7bEkBQkjd2RO7/FEwDY03Z1geVGlXKnzIATEVBVaGzV4Iw==
|
||||
dependencies:
|
||||
"@date-io/core" "^2.16.0"
|
||||
|
||||
"@date-io/luxon@^2.15.0":
|
||||
version "2.16.1"
|
||||
resolved "https://registry.yarnpkg.com/@date-io/luxon/-/luxon-2.16.1.tgz#b08786614cb58831c729a15807753011e4acb966"
|
||||
integrity sha512-aeYp5K9PSHV28946pC+9UKUi/xMMYoaGelrpDibZSgHu2VWHXrr7zWLEr+pMPThSs5vt8Ei365PO+84pCm37WQ==
|
||||
dependencies:
|
||||
"@date-io/core" "^2.16.0"
|
||||
|
||||
"@date-io/moment@^2.15.0":
|
||||
version "2.16.1"
|
||||
resolved "https://registry.yarnpkg.com/@date-io/moment/-/moment-2.16.1.tgz#ec6e0daa486871e0e6412036c6f806842a0eeed4"
|
||||
integrity sha512-JkxldQxUqZBfZtsaCcCMkm/dmytdyq5pS1RxshCQ4fHhsvP5A7gSqPD22QbVXMcJydi3d3v1Y8BQdUKEuGACZQ==
|
||||
dependencies:
|
||||
"@date-io/core" "^2.16.0"
|
||||
|
||||
"@emotion/babel-plugin@^11.10.6":
|
||||
version "11.10.6"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.10.6.tgz#a68ee4b019d661d6f37dec4b8903255766925ead"
|
||||
integrity sha512-p2dAqtVrkhSa7xz1u/m9eHYdLi+en8NowrmXeF/dKtJpU8lCWli8RUAati7NcSl0afsBott48pdnANuD0wh9QQ==
|
||||
dependencies:
|
||||
"@babel/helper-module-imports" "^7.16.7"
|
||||
"@babel/runtime" "^7.18.3"
|
||||
"@emotion/hash" "^0.9.0"
|
||||
"@emotion/memoize" "^0.8.0"
|
||||
"@emotion/serialize" "^1.1.1"
|
||||
babel-plugin-macros "^3.1.0"
|
||||
convert-source-map "^1.5.0"
|
||||
escape-string-regexp "^4.0.0"
|
||||
find-root "^1.1.0"
|
||||
source-map "^0.5.7"
|
||||
stylis "4.1.3"
|
||||
|
||||
"@emotion/cache@^11.10.5":
|
||||
version "11.10.5"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.10.5.tgz#c142da9351f94e47527ed458f7bbbbe40bb13c12"
|
||||
integrity sha512-dGYHWyzTdmK+f2+EnIGBpkz1lKc4Zbj2KHd4cX3Wi8/OWr5pKslNjc3yABKH4adRGCvSX4VDC0i04mrrq0aiRA==
|
||||
dependencies:
|
||||
"@emotion/memoize" "^0.8.0"
|
||||
"@emotion/sheet" "^1.2.1"
|
||||
"@emotion/utils" "^1.2.0"
|
||||
"@emotion/weak-memoize" "^0.3.0"
|
||||
stylis "4.1.3"
|
||||
|
||||
"@emotion/hash@^0.8.0":
|
||||
version "0.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413"
|
||||
integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==
|
||||
|
||||
"@emotion/hash@^0.9.0":
|
||||
version "0.9.0"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.9.0.tgz#c5153d50401ee3c027a57a177bc269b16d889cb7"
|
||||
integrity sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ==
|
||||
|
||||
"@emotion/is-prop-valid@^1.2.0":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.2.0.tgz#7f2d35c97891669f7e276eb71c83376a5dc44c83"
|
||||
integrity sha512-3aDpDprjM0AwaxGE09bOPkNxHpBd+kA6jty3RnaEXdweX1DF1U3VQpPYb0g1IStAuK7SVQ1cy+bNBBKp4W3Fjg==
|
||||
dependencies:
|
||||
"@emotion/memoize" "^0.8.0"
|
||||
|
||||
"@emotion/memoize@^0.8.0":
|
||||
version "0.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.8.0.tgz#f580f9beb67176fa57aae70b08ed510e1b18980f"
|
||||
integrity sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==
|
||||
|
||||
"@emotion/react@^11.10.4":
|
||||
version "11.10.6"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.10.6.tgz#dbe5e650ab0f3b1d2e592e6ab1e006e75fd9ac11"
|
||||
integrity sha512-6HT8jBmcSkfzO7mc+N1L9uwvOnlcGoix8Zn7srt+9ga0MjREo6lRpuVX0kzo6Jp6oTqDhREOFsygN6Ew4fEQbw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.18.3"
|
||||
"@emotion/babel-plugin" "^11.10.6"
|
||||
"@emotion/cache" "^11.10.5"
|
||||
"@emotion/serialize" "^1.1.1"
|
||||
"@emotion/use-insertion-effect-with-fallbacks" "^1.0.0"
|
||||
"@emotion/utils" "^1.2.0"
|
||||
"@emotion/weak-memoize" "^0.3.0"
|
||||
hoist-non-react-statics "^3.3.1"
|
||||
|
||||
"@emotion/serialize@^1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.1.1.tgz#0595701b1902feded8a96d293b26be3f5c1a5cf0"
|
||||
integrity sha512-Zl/0LFggN7+L1liljxXdsVSVlg6E/Z/olVWpfxUTxOAmi8NU7YoeWeLfi1RmnB2TATHoaWwIBRoL+FvAJiTUQA==
|
||||
dependencies:
|
||||
"@emotion/hash" "^0.9.0"
|
||||
"@emotion/memoize" "^0.8.0"
|
||||
"@emotion/unitless" "^0.8.0"
|
||||
"@emotion/utils" "^1.2.0"
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@emotion/sheet@^1.2.1":
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.2.1.tgz#0767e0305230e894897cadb6c8df2c51e61a6c2c"
|
||||
integrity sha512-zxRBwl93sHMsOj4zs+OslQKg/uhF38MB+OMKoCrVuS0nyTkqnau+BM3WGEoOptg9Oz45T/aIGs1qbVAsEFo3nA==
|
||||
|
||||
"@emotion/styled@^11.10.4":
|
||||
version "11.10.6"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-11.10.6.tgz#d886afdc51ef4d66c787ebde848f3cc8b117ebba"
|
||||
integrity sha512-OXtBzOmDSJo5Q0AFemHCfl+bUueT8BIcPSxu0EGTpGk6DmI5dnhSzQANm1e1ze0YZL7TDyAyy6s/b/zmGOS3Og==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.18.3"
|
||||
"@emotion/babel-plugin" "^11.10.6"
|
||||
"@emotion/is-prop-valid" "^1.2.0"
|
||||
"@emotion/serialize" "^1.1.1"
|
||||
"@emotion/use-insertion-effect-with-fallbacks" "^1.0.0"
|
||||
"@emotion/utils" "^1.2.0"
|
||||
|
||||
"@emotion/unitless@^0.8.0":
|
||||
version "0.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.8.0.tgz#a4a36e9cbdc6903737cd20d38033241e1b8833db"
|
||||
integrity sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==
|
||||
|
||||
"@emotion/use-insertion-effect-with-fallbacks@^1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.0.tgz#ffadaec35dbb7885bd54de3fa267ab2f860294df"
|
||||
integrity sha512-1eEgUGmkaljiBnRMTdksDV1W4kUnmwgp7X9G8B++9GYwl1lUdqSndSriIrTJ0N7LQaoauY9JJ2yhiOYK5+NI4A==
|
||||
|
||||
"@emotion/utils@^1.2.0":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.2.0.tgz#9716eaccbc6b5ded2ea5a90d65562609aab0f561"
|
||||
integrity sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw==
|
||||
|
||||
"@emotion/weak-memoize@^0.3.0":
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz#ea89004119dc42db2e1dba0f97d553f7372f6fcb"
|
||||
integrity sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg==
|
||||
|
||||
"@eslint/eslintrc@^0.4.3":
|
||||
version "0.4.3"
|
||||
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c"
|
||||
@@ -1568,14 +1715,20 @@
|
||||
"@jridgewell/resolve-uri" "^3.0.3"
|
||||
"@jridgewell/sourcemap-codec" "^1.4.10"
|
||||
|
||||
"@kerberos-io/ui@1.70.0":
|
||||
version "1.70.0"
|
||||
resolved "https://registry.yarnpkg.com/@kerberos-io/ui/-/ui-1.70.0.tgz#377f42725cc98d02af43e133cf66f3c10852caf0"
|
||||
integrity sha512-Sq7gxNAXUFtjUNUpKBKogbvnJ03l/oxkf7943I0GpAzI0n+YdWNxlGpShB5HxsiarRsb+HphwZjAZZY+WupUwA==
|
||||
"@kerberos-io/ui@^1.71.0":
|
||||
version "1.71.0"
|
||||
resolved "https://registry.yarnpkg.com/@kerberos-io/ui/-/ui-1.71.0.tgz#06914c94e8b0982068d2099acf8158917a511bfc"
|
||||
integrity sha512-pHCTn/iQTcQEPoCK82eJHGRn6BgzW3wgV4C+mNqdKOtLTquxL+vh7molEgC66tl3DGf7HyjSNa8LuoxYbt9TEg==
|
||||
dependencies:
|
||||
"@emotion/react" "^11.10.4"
|
||||
"@emotion/styled" "^11.10.4"
|
||||
"@mui/icons-material" "^5.10.3"
|
||||
"@mui/material" "^5.10.3"
|
||||
"@mui/x-date-pickers" "^5.0.0-beta.7"
|
||||
"@svgr/webpack" "^5.5.0"
|
||||
"@types/react-router-dom" "^5.1.7"
|
||||
md5 "^2.3.0"
|
||||
moment "^2.29.4"
|
||||
react "^17.0.2"
|
||||
react-dom "^17.0.2"
|
||||
react-router-dom "^5.2.0"
|
||||
@@ -1664,6 +1817,117 @@
|
||||
prop-types "^15.7.2"
|
||||
react-is "^16.8.0 || ^17.0.0"
|
||||
|
||||
"@mui/base@5.0.0-alpha.121":
|
||||
version "5.0.0-alpha.121"
|
||||
resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-alpha.121.tgz#24a238a32fc0386efcfefaf88d80712a0e00560c"
|
||||
integrity sha512-8nJRY76UqlJV+q/Yzo0tgGfPWEOa+4N9rjO81fMmcJqP0I6m54hLDXsjvMg4tvelY5eKHXUK6Tb7en+GHfTqZA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.21.0"
|
||||
"@emotion/is-prop-valid" "^1.2.0"
|
||||
"@mui/types" "^7.2.3"
|
||||
"@mui/utils" "^5.11.13"
|
||||
"@popperjs/core" "^2.11.6"
|
||||
clsx "^1.2.1"
|
||||
prop-types "^15.8.1"
|
||||
react-is "^18.2.0"
|
||||
|
||||
"@mui/core-downloads-tracker@^5.11.13":
|
||||
version "5.11.13"
|
||||
resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.11.13.tgz#0419a88ae98b3eaff2a633229d23ee04c8868be1"
|
||||
integrity sha512-lx+GXBR9h/ApZsEP728tl0pyZyuajto+VnBgsoAzw1d5+CbmOo8ZWieKwVUGxZlPT1wMYNUYS5NtKzCli0xYjw==
|
||||
|
||||
"@mui/icons-material@^5.10.3":
|
||||
version "5.11.11"
|
||||
resolved "https://registry.yarnpkg.com/@mui/icons-material/-/icons-material-5.11.11.tgz#d4e01bd405b0dac779f5e060586277f91f3acb6e"
|
||||
integrity sha512-Eell3ADmQVE8HOpt/LZ3zIma8JSvPh3XgnhwZLT0k5HRqZcd6F/QDHc7xsWtgz09t+UEFvOYJXjtrwKmLdwwpw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.21.0"
|
||||
|
||||
"@mui/material@^5.10.3":
|
||||
version "5.11.13"
|
||||
resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.11.13.tgz#bbbdc74a78d4a0e6257a7bca3b8fa38ac8ad3915"
|
||||
integrity sha512-2CnSj43F+159LbGmTLLQs5xbGYMiYlpTByQhP7c7cMX6opbScctBFE1PuyElpAmwW8Ag9ysfZH1d1MFAmJQkjg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.21.0"
|
||||
"@mui/base" "5.0.0-alpha.121"
|
||||
"@mui/core-downloads-tracker" "^5.11.13"
|
||||
"@mui/system" "^5.11.13"
|
||||
"@mui/types" "^7.2.3"
|
||||
"@mui/utils" "^5.11.13"
|
||||
"@types/react-transition-group" "^4.4.5"
|
||||
clsx "^1.2.1"
|
||||
csstype "^3.1.1"
|
||||
prop-types "^15.8.1"
|
||||
react-is "^18.2.0"
|
||||
react-transition-group "^4.4.5"
|
||||
|
||||
"@mui/private-theming@^5.11.13":
|
||||
version "5.11.13"
|
||||
resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-5.11.13.tgz#7841acc7e0d85e3aad223b1a0fad11be9349ef01"
|
||||
integrity sha512-PJnYNKzW5LIx3R+Zsp6WZVPs6w5sEKJ7mgLNnUXuYB1zo5aX71FVLtV7geyPXRcaN2tsoRNK7h444ED0t7cIjA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.21.0"
|
||||
"@mui/utils" "^5.11.13"
|
||||
prop-types "^15.8.1"
|
||||
|
||||
"@mui/styled-engine@^5.11.11":
|
||||
version "5.11.11"
|
||||
resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.11.11.tgz#9084c331fdcff2210ec33adf37f34e94d67202e4"
|
||||
integrity sha512-wV0UgW4lN5FkDBXefN8eTYeuE9sjyQdg5h94vtwZCUamGQEzmCOtir4AakgmbWMy0x8OLjdEUESn9wnf5J9MOg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.21.0"
|
||||
"@emotion/cache" "^11.10.5"
|
||||
csstype "^3.1.1"
|
||||
prop-types "^15.8.1"
|
||||
|
||||
"@mui/system@^5.11.13":
|
||||
version "5.11.13"
|
||||
resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.11.13.tgz#67941e401d2dde8ac6c078738dbecd193ed9074f"
|
||||
integrity sha512-OWP0Alp6C8ufnGm9+CZcl3d+OoRXL2PnrRT5ohaMLxvGL9OfNcL2t4JOjMmA0k1UAGd6E/Ygbu5lEPrZSDlvCg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.21.0"
|
||||
"@mui/private-theming" "^5.11.13"
|
||||
"@mui/styled-engine" "^5.11.11"
|
||||
"@mui/types" "^7.2.3"
|
||||
"@mui/utils" "^5.11.13"
|
||||
clsx "^1.2.1"
|
||||
csstype "^3.1.1"
|
||||
prop-types "^15.8.1"
|
||||
|
||||
"@mui/types@^7.2.3":
|
||||
version "7.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.2.3.tgz#06faae1c0e2f3a31c86af6f28b3a4a42143670b9"
|
||||
integrity sha512-tZ+CQggbe9Ol7e/Fs5RcKwg/woU+o8DCtOnccX6KmbBc7YrfqMYEYuaIcXHuhpT880QwNkZZ3wQwvtlDFA2yOw==
|
||||
|
||||
"@mui/utils@^5.10.3", "@mui/utils@^5.11.13":
|
||||
version "5.11.13"
|
||||
resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.11.13.tgz#8d7317f221e8973200764820fa7f2a622dbc7150"
|
||||
integrity sha512-5ltA58MM9euOuUcnvwFJqpLdEugc9XFsRR8Gt4zZNb31XzMfSKJPR4eumulyhsOTK1rWf7K4D63NKFPfX0AxqA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.21.0"
|
||||
"@types/prop-types" "^15.7.5"
|
||||
"@types/react-is" "^16.7.1 || ^17.0.0"
|
||||
prop-types "^15.8.1"
|
||||
react-is "^18.2.0"
|
||||
|
||||
"@mui/x-date-pickers@^5.0.0-beta.7":
|
||||
version "5.0.20"
|
||||
resolved "https://registry.yarnpkg.com/@mui/x-date-pickers/-/x-date-pickers-5.0.20.tgz#7b4e5b5a214a8095937ba7d82bb82acd6f270d72"
|
||||
integrity sha512-ERukSeHIoNLbI1C2XRhF9wRhqfsr+Q4B1SAw2ZlU7CWgcG8UBOxgqRKDEOVAIoSWL+DWT6GRuQjOKvj6UXZceA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.18.9"
|
||||
"@date-io/core" "^2.15.0"
|
||||
"@date-io/date-fns" "^2.15.0"
|
||||
"@date-io/dayjs" "^2.15.0"
|
||||
"@date-io/luxon" "^2.15.0"
|
||||
"@date-io/moment" "^2.15.0"
|
||||
"@mui/utils" "^5.10.3"
|
||||
"@types/react-transition-group" "^4.4.5"
|
||||
clsx "^1.2.1"
|
||||
prop-types "^15.7.2"
|
||||
react-transition-group "^4.4.5"
|
||||
rifm "^0.12.1"
|
||||
|
||||
"@nodelib/fs.scandir@2.1.5":
|
||||
version "2.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
|
||||
@@ -1700,6 +1964,11 @@
|
||||
schema-utils "^3.0.0"
|
||||
source-map "^0.7.3"
|
||||
|
||||
"@popperjs/core@^2.11.6":
|
||||
version "2.11.6"
|
||||
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.6.tgz#cee20bd55e68a1720bdab363ecf0c821ded4cd45"
|
||||
integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==
|
||||
|
||||
"@rollup/plugin-babel@^5.2.0":
|
||||
version "5.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz#04bc0608f4aa4b2e4b1aebf284344d0f68fda283"
|
||||
@@ -2135,7 +2404,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.0.tgz#ea03e9f0376a4446f44797ca19d9c46c36e352dc"
|
||||
integrity sha512-RI1L7N4JnW5gQw2spvL7Sllfuf1SaHdrZpCHiBlCXjIlufi1SMNnbu2teze3/QE67Fg2tBlH7W+mi4hVNk4p0A==
|
||||
|
||||
"@types/prop-types@*":
|
||||
"@types/prop-types@*", "@types/prop-types@^15.7.5":
|
||||
version "15.7.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
|
||||
integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==
|
||||
@@ -2155,6 +2424,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
|
||||
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
|
||||
|
||||
"@types/react-is@^16.7.1 || ^17.0.0":
|
||||
version "17.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-is/-/react-is-17.0.3.tgz#2d855ba575f2fc8d17ef9861f084acc4b90a137a"
|
||||
integrity sha512-aBTIWg1emtu95bLTLx0cpkxwGW3ueZv71nE2YFBpL8k/z5czEW8yYpOo8Dp+UUAFAtKwNaOsh/ioSeQnWlZcfw==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-redux@^7.1.20":
|
||||
version "7.1.24"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.24.tgz#6caaff1603aba17b27d20f8ad073e4c077e975c0"
|
||||
@@ -2182,7 +2458,7 @@
|
||||
"@types/history" "^4.7.11"
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-transition-group@^4.2.0":
|
||||
"@types/react-transition-group@^4.2.0", "@types/react-transition-group@^4.4.5":
|
||||
version "4.4.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.5.tgz#aae20dcf773c5aa275d5b9f7cdbca638abc5e416"
|
||||
integrity sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==
|
||||
@@ -3256,7 +3532,7 @@ cliui@^7.0.2:
|
||||
strip-ansi "^6.0.0"
|
||||
wrap-ansi "^7.0.0"
|
||||
|
||||
clsx@^1.0.4:
|
||||
clsx@^1.0.4, clsx@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
|
||||
integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
|
||||
@@ -3416,6 +3692,11 @@ convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0:
|
||||
dependencies:
|
||||
safe-buffer "~5.1.1"
|
||||
|
||||
convert-source-map@^1.5.0:
|
||||
version "1.9.0"
|
||||
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f"
|
||||
integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==
|
||||
|
||||
cookie-signature@1.0.6:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
|
||||
@@ -3715,6 +3996,11 @@ csstype@^3.0.2:
|
||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.0.tgz#4ddcac3718d787cf9df0d1b7d15033925c8f29f2"
|
||||
integrity sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==
|
||||
|
||||
csstype@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.1.tgz#841b532c45c758ee546a11d5bd7b7b473c8c30b9"
|
||||
integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==
|
||||
|
||||
damerau-levenshtein@^1.0.8:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7"
|
||||
@@ -4784,6 +5070,11 @@ find-cache-dir@^3.3.1:
|
||||
make-dir "^3.0.2"
|
||||
pkg-dir "^4.1.0"
|
||||
|
||||
find-root@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4"
|
||||
integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==
|
||||
|
||||
find-up@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73"
|
||||
@@ -5132,7 +5423,7 @@ history@4.10.1, history@^4.9.0:
|
||||
tiny-warning "^1.0.0"
|
||||
value-equal "^1.0.1"
|
||||
|
||||
hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
|
||||
hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
|
||||
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
|
||||
@@ -6755,6 +7046,11 @@ mkdirp@~0.5.1:
|
||||
dependencies:
|
||||
minimist "^1.2.6"
|
||||
|
||||
moment@^2.29.4:
|
||||
version "2.29.4"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108"
|
||||
integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==
|
||||
|
||||
ms@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||
@@ -8001,7 +8297,7 @@ react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0:
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
|
||||
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
|
||||
|
||||
react-is@^18.0.0:
|
||||
react-is@^18.0.0, react-is@^18.2.0:
|
||||
version "18.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
|
||||
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==
|
||||
@@ -8128,7 +8424,7 @@ react-tooltip@^4.2.21:
|
||||
prop-types "^15.7.2"
|
||||
uuid "^7.0.3"
|
||||
|
||||
react-transition-group@^4.4.0:
|
||||
react-transition-group@^4.4.0, react-transition-group@^4.4.5:
|
||||
version "4.4.5"
|
||||
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1"
|
||||
integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==
|
||||
@@ -8388,6 +8684,11 @@ reusify@^1.0.4:
|
||||
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
|
||||
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
|
||||
|
||||
rifm@^0.12.1:
|
||||
version "0.12.1"
|
||||
resolved "https://registry.yarnpkg.com/rifm/-/rifm-0.12.1.tgz#8fa77f45b7f1cda2a0068787ac821f0593967ac4"
|
||||
integrity sha512-OGA1Bitg/dSJtI/c4dh90svzaUPt228kzFsUkJbtA2c964IqEAwWXeL9ZJi86xWv3j5SMqRvGULl7bA6cK0Bvg==
|
||||
|
||||
rimraf@^3.0.0, rimraf@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
|
||||
@@ -8657,6 +8958,11 @@ setprototypeof@1.2.0:
|
||||
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
|
||||
integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
|
||||
|
||||
sha-1@0.1.1:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/sha-1/-/sha-1-0.1.1.tgz#2a39304bf41bbab11dd9efb7474ec25b1a92c257"
|
||||
integrity sha512-dexizf3hB7d4Jq6Cd0d/NYQiqgEqIfZIpuMfwPfvSb6h06DZKmHyUe55jYwpHC12R42wpqXO6ouhiBpRzIcD/g==
|
||||
|
||||
shebang-command@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
|
||||
@@ -8783,6 +9089,11 @@ source-map@0.6.1, source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, sourc
|
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
|
||||
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
|
||||
|
||||
source-map@^0.5.7:
|
||||
version "0.5.7"
|
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
|
||||
integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==
|
||||
|
||||
source-map@^0.7.3:
|
||||
version "0.7.4"
|
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656"
|
||||
@@ -9006,6 +9317,11 @@ stylehacks@^5.1.0:
|
||||
browserslist "^4.16.6"
|
||||
postcss-selector-parser "^6.0.4"
|
||||
|
||||
stylis@4.1.3:
|
||||
version "4.1.3"
|
||||
resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.1.3.tgz#fd2fbe79f5fed17c55269e16ed8da14c84d069f7"
|
||||
integrity sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA==
|
||||
|
||||
supports-color@^5.3.0:
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
|
||||
@@ -9486,6 +9802,11 @@ utils-merge@1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
|
||||
integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
|
||||
|
||||
uuid@3.3.2:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
|
||||
integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==
|
||||
|
||||
uuid@^7.0.3:
|
||||
version "7.0.3"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b"
|
||||
@@ -9496,6 +9817,14 @@ uuid@^8.3.2:
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
|
||||
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
|
||||
|
||||
uuidv4@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/uuidv4/-/uuidv4-2.0.0.tgz#3ec764288f9e9c4e40f8027ad309c2c528be2976"
|
||||
integrity sha512-sAUlwUVepcVk6bwnaW/oi6LCwMdueako5QQzRr90ioAVVcms6p1mV0PaSxK8gyAC4CRvKddsk217uUpZUbKd2Q==
|
||||
dependencies:
|
||||
sha-1 "0.1.1"
|
||||
uuid "3.3.2"
|
||||
|
||||
v8-compile-cache@^2.0.3:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
|
||||
|
||||
Reference in New Issue
Block a user