Compare commits

..

1 Commits

Author SHA1 Message Date
Cedric Verstraeten
f1ac4f53b6 add logging + friendly_name might be empty 2023-09-12 15:05:22 +02:00
117 changed files with 4357 additions and 17782 deletions

View File

@@ -5,7 +5,7 @@ version: 2
jobs:
machinery:
docker:
- image: kerberos/base:0a50dc9
- image: kerberos/base:91ab4d4
working_directory: /go/src/github.com/{{ORG_NAME}}/{{REPO_NAME}}
steps:
- checkout

View File

@@ -1,26 +1,2 @@
FROM mcr.microsoft.com/devcontainers/go:1.24-bookworm
# Install node environment
RUN apt-get update && \
apt-get install -y --no-install-recommends \
nodejs \
npm \
&& rm -rf /var/lib/apt/lists/*
# Install ffmpeg
RUN apt-get update && \
apt-get install -y --no-install-recommends \
ffmpeg \
libavcodec-extra \
libavutil-dev \
libavformat-dev \
libavfilter-dev \
libavdevice-dev \
libswscale-dev \
libswresample-dev \
&& rm -rf /var/lib/apt/lists/*
USER vscode
# Install go swagger
RUN go install github.com/swaggo/swag/cmd/swag@latest
FROM kerberos/devcontainer:b2bc659
LABEL AUTHOR=Kerberos.io

View File

@@ -1,24 +1,33 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/python
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.245.2/containers/docker-existing-dockerfile
{
"name": "go:1.24-bookworm",
"runArgs": [
"--name=agent",
"--network=host"
"name": "A Dockerfile containing FFmpeg, OpenCV, Go and Yarn",
// Sets the run context to one level up instead of the .devcontainer folder.
"context": "..",
// Update the 'dockerFile' property if you aren't using the standard 'Dockerfile' filename.
"dockerFile": "./Dockerfile",
// Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [
3000,
80
],
"dockerFile": "Dockerfile",
// 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": {}
},
"customizations": {
"vscode": {
"extensions": [
"GitHub.copilot",
"ms-azuretools.vscode-docker",
"mongodb.mongodb-vscode"
"ms-kubernetes-tools.vscode-kubernetes-tools",
"GitHub.copilot"
]
}
},
"forwardPorts": [
3000,
8080
],
"postCreateCommand": "cd ui && yarn install && yarn build && cd ../machinery && go mod download"
// Uncomment when using a ptrace-based debugger like C++, Go, and Rust
// "runArgs": [ "--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined" ],
// Uncomment to use the Docker CLI from inside the container. See https://aka.ms/vscode-remote/samples/docker-from-docker.
// "mounts": [ "source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind" ],
// Uncomment to connect as a non-root user if you've added one. See https://aka.ms/vscode-remote/containers/non-root.
// "remoteUser": "vscode"
}

58
.github/workflows/docker-dev.yml vendored Normal file
View File

@@ -0,0 +1,58 @@
name: Docker development build
on:
push:
branches: [ develop ]
jobs:
build-amd64:
runs-on: ubuntu-latest
strategy:
matrix:
architecture: [amd64]
steps:
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Available platforms
run: echo ${{ steps.buildx.outputs.platforms }}
- name: Run Buildx
run: docker buildx build --platform linux/${{matrix.architecture}} -t kerberos/agent-dev:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7) --push .
- name: Create new and append to manifest
run: docker buildx imagetools create -t kerberos/agent-dev:$(echo $GITHUB_SHA | cut -c1-7) kerberos/agent-dev:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7)
- name: Create new and append to latest manifest
run: docker buildx imagetools create -t kerberos/agent-dev:latest kerberos/agent-dev:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7)
build-other:
runs-on: ubuntu-latest
strategy:
matrix:
#architecture: [arm64, arm/v7, arm/v6]
architecture: [arm64, arm/v7]
steps:
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Available platforms
run: echo ${{ steps.buildx.outputs.platforms }}
- name: Run Buildx
run: docker buildx build --platform linux/${{matrix.architecture}} -t kerberos/agent-dev:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7) --push .
- name: Create new and append to manifest
run: docker buildx imagetools create --append -t kerberos/agent-dev:$(echo $GITHUB_SHA | cut -c1-7) kerberos/agent-dev:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7)
- name: Create new and append to manifest latest
run: docker buildx imagetools create --append -t kerberos/agent-dev:latest kerberos/agent-dev:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7)

54
.github/workflows/docker-nightly.yml vendored Normal file
View File

@@ -0,0 +1,54 @@
name: Docker nightly build
on:
# Triggers the workflow every day at 9PM (CET).
schedule:
- cron: "0 22 * * *"
jobs:
build-amd64:
runs-on: ubuntu-latest
strategy:
matrix:
architecture: [amd64]
steps:
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Available platforms
run: echo ${{ steps.buildx.outputs.platforms }}
- name: Run Buildx
run: docker buildx build --platform linux/${{matrix.architecture}} -t kerberos/agent-nightly:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7) --push .
- name: Create new and append to manifest
run: docker buildx imagetools create -t kerberos/agent-nightly:$(echo $GITHUB_SHA | cut -c1-7) kerberos/agent-nightly:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7)
build-other:
runs-on: ubuntu-latest
strategy:
matrix:
architecture: [arm64, arm/v7, arm/v6]
steps:
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Available platforms
run: echo ${{ steps.buildx.outputs.platforms }}
- name: Run Buildx
run: docker buildx build --platform linux/${{matrix.architecture}} -t kerberos/agent-nightly:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7) --push .
- name: Create new and append to manifest
run: docker buildx imagetools create --append -t kerberos/agent-nightly:$(echo $GITHUB_SHA | cut -c1-7) kerberos/agent-nightly:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7)

114
.github/workflows/docker.yml vendored Normal file
View File

@@ -0,0 +1,114 @@
name: Docker master build
on:
push:
branches: [ master ]
env:
REPO: kerberos/agent
jobs:
build-amd64:
runs-on: ubuntu-latest
permissions:
contents: write
strategy:
matrix:
architecture: [amd64]
steps:
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Checkout
uses: actions/checkout@v3
- uses: benjlevesque/short-sha@v2.1
id: short-sha
with:
length: 7
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Available platforms
run: echo ${{ steps.buildx.outputs.platforms }}
- name: Run Buildx
run: docker buildx build --platform linux/$(echo ${{matrix.architecture}} | tr - /) -t $REPO-arch:arch-${{matrix.architecture}}-${{steps.short-sha.outputs.sha}} --push .
- name: Create new and append to manifest
run: docker buildx imagetools create -t $REPO:${{ steps.short-sha.outputs.sha }} $REPO-arch:arch-${{matrix.architecture}}-${{steps.short-sha.outputs.sha}}
- name: Create new and append to manifest latest
run: docker buildx imagetools create -t $REPO:latest $REPO-arch:arch-${{matrix.architecture}}-${{steps.short-sha.outputs.sha}}
- name: Run Buildx with output
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 }}
message: "Release ${{ steps.short-sha.outputs.sha }}"
- name: Create a release
uses: ncipollo/release-action@v1
with:
latest: true
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:
contents: write
needs: build-amd64
strategy:
matrix:
architecture: [arm64, arm-v7, arm-v6]
#architecture: [arm64, arm-v7]
steps:
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Checkout
uses: actions/checkout@v3
- uses: benjlevesque/short-sha@v2.1
id: short-sha
with:
length: 7
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Available platforms
run: echo ${{ steps.buildx.outputs.platforms }}
- name: Run Buildx
run: docker buildx build --platform linux/$(echo ${{matrix.architecture}} | tr - /) -t $REPO-arch:arch-${{matrix.architecture}}-${{steps.short-sha.outputs.sha}} --push .
- name: Create new and append to manifest
run: docker buildx imagetools create --append -t $REPO:${{ steps.short-sha.outputs.sha }} $REPO-arch:arch-${{matrix.architecture}}-${{steps.short-sha.outputs.sha}}
- name: Create new and append to manifest latest
run: docker buildx imagetools create --append -t $REPO:latest $REPO-arch:arch-${{matrix.architecture}}-${{steps.short-sha.outputs.sha}}
- name: Run Buildx with output
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
- name: Create a release
uses: ncipollo/release-action@v1
with:
latest: true
allowUpdates: true
name: ${{ steps.short-sha.outputs.sha }}
tag: ${{ steps.short-sha.outputs.sha }}
artifacts: "agent-${{matrix.architecture}}.tar"

View File

@@ -2,37 +2,35 @@ name: Go
on:
push:
branches: [develop, master]
branches: [ develop, master ]
pull_request:
branches: [develop, master]
branches: [ develop, master ]
jobs:
build:
name: Build
runs-on: ubuntu-latest
container:
image: kerberos/base:eb6b088
image: kerberos/base:70d69dc
strategy:
matrix:
#No longer supported Go versions.
#go-version: ['1.17', '1.18', '1.19', '1.20', '1.21']
go-version: ["1.24"]
go-version: [1.17, 1.18, 1.19]
steps:
- name: Set up Go ${{ matrix.go-version }}
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
- name: Check out code into the Go module directory
uses: actions/checkout@v4
- name: Set up git ownershi
run: git config --system --add safe.directory /__w/agent/agent
- name: Get dependencies
run: cd machinery && go mod download
- name: Build
run: cd machinery && go build -v ./...
- name: Vet
run: cd machinery && go vet -v ./...
- name: Test
run: cd machinery && go test -v ./...
- name: Set up Go ${{ matrix.go-version }}
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
- name: Check out code into the Go module directory
uses: actions/checkout@v3
- name: Get dependencies
run: cd machinery && go mod download
- name: Build
run: cd machinery && go build -v ./...
- name: Vet
run: cd machinery && go vet -v ./...
- name: Test
run: cd machinery && go test -v ./...

View File

@@ -1,51 +0,0 @@
name: Create User Story Issue
on:
workflow_dispatch:
inputs:
issue_title:
description: 'Title for the issue'
required: true
issue_description:
description: 'Brief description of the feature'
required: true
complexity:
description: 'Complexity of the feature'
required: true
type: choice
options:
- 'Low'
- 'Medium'
- 'High'
default: 'Medium'
duration:
description: 'Estimated duration'
required: true
type: choice
options:
- '1 day'
- '3 days'
- '1 week'
- '2 weeks'
- '1 month'
default: '1 week'
jobs:
create-issue:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: Create Issue with User Story
uses: cedricve/llm-create-issue-user-story@main
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
azure_openai_api_key: ${{ secrets.AZURE_OPENAI_API_KEY }}
azure_openai_endpoint: ${{ secrets.AZURE_OPENAI_ENDPOINT }}
azure_openai_version: ${{ secrets.AZURE_OPENAI_VERSION }}
openai_model: ${{ secrets.OPENAI_MODEL }}
issue_title: ${{ github.event.inputs.issue_title }}
issue_description: ${{ github.event.inputs.issue_description }}
complexity: ${{ github.event.inputs.complexity }}
duration: ${{ github.event.inputs.duration }}
labels: 'user-story,feature'
assignees: ${{ github.actor }}

View File

@@ -1,60 +0,0 @@
name: Nightly build
on:
# Triggers the workflow every day at 9PM (CET).
schedule:
- cron: "0 22 * * *"
# Allows manual triggering from the Actions tab.
workflow_dispatch:
jobs:
nightly-build-amd64:
runs-on: ubuntu-latest
strategy:
matrix:
architecture: [amd64]
steps:
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Checkout
uses: actions/checkout@v4
with:
ref: master
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Available platforms
run: echo ${{ steps.buildx.outputs.platforms }}
- name: Run Buildx
run: docker buildx build --platform linux/${{matrix.architecture}} -t kerberos/agent-nightly:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7) --push .
- name: Create new and append to manifest
run: docker buildx imagetools create -t kerberos/agent-nightly:$(echo $GITHUB_SHA | cut -c1-7) kerberos/agent-nightly:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7)
nightly-build-other:
runs-on: ubuntu-latest
strategy:
matrix:
architecture: [arm64, arm/v7, arm/v6]
steps:
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Checkout
uses: actions/checkout@v4
with:
ref: master
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Available platforms
run: echo ${{ steps.buildx.outputs.platforms }}
- name: Run Buildx
run: docker buildx build --platform linux/${{matrix.architecture}} -t kerberos/agent-nightly:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7) --push .
- name: Create new and append to manifest
run: docker buildx imagetools create --append -t kerberos/agent-nightly:$(echo $GITHUB_SHA | cut -c1-7) kerberos/agent-nightly:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7)

View File

@@ -1,75 +0,0 @@
name: Build pull request
on:
pull_request:
types: [opened, synchronize]
env:
REPO: kerberos/agent
jobs:
build-amd64:
runs-on: ubuntu-24.04
permissions:
contents: write
strategy:
matrix:
architecture: [amd64]
steps:
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Checkout
uses: actions/checkout@v3
- uses: benjlevesque/short-sha@v2.1
id: short-sha
with:
length: 7
- name: Run Build
run: |
docker build -t ${{matrix.architecture}} .
CID=$(docker create ${{matrix.architecture}})
docker cp ${CID}:/home/agent ./output-${{matrix.architecture}}
docker rm ${CID}
- name: Strip binary
run: tar -cf agent-${{matrix.architecture}}.tar -C output-${{matrix.architecture}} . && rm -rf output-${{matrix.architecture}}
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: agent-${{matrix.architecture}}.tar
path: agent-${{matrix.architecture}}.tar
build-arm64:
runs-on: ubuntu-24.04-arm
permissions:
contents: write
strategy:
matrix:
architecture: [arm64]
steps:
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Checkout
uses: actions/checkout@v3
- uses: benjlevesque/short-sha@v2.1
id: short-sha
with:
length: 7
- name: Run Build
run: |
docker build -t ${{matrix.architecture}} -f Dockerfile.arm64 .
CID=$(docker create ${{matrix.architecture}})
docker cp ${CID}:/home/agent ./output-${{matrix.architecture}}
docker rm ${CID}
- name: Strip binary
run: tar -cf agent-${{matrix.architecture}}.tar -C output-${{matrix.architecture}} . && rm -rf output-${{matrix.architecture}}
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: agent-${{matrix.architecture}}.tar
path: agent-${{matrix.architecture}}.tar

View File

@@ -1,26 +0,0 @@
name: Autofill PR description
on: pull_request
env:
ORGANIZATION: uugai
PROJECT: ${{ github.event.repository.name }}
PR_NUMBER: ${{ github.event.number }}
jobs:
openai-pr-description:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Autofill PR description if empty using OpenAI
uses: cedricve/azureopenai-pr-description@master
with:
github_token: ${{ secrets.TOKEN }}
openai_api_key: ${{ secrets.OPENAI_API_KEY }}
azure_openai_api_key: ${{ secrets.AZURE_OPENAI_API_KEY }}
azure_openai_endpoint: ${{ secrets.AZURE_OPENAI_ENDPOINT }}
azure_openai_version: ${{ secrets.AZURE_OPENAI_VERSION }}
openai_model: ${{ secrets.OPENAI_MODEL }}
pull_request_url: https://pr${{ env.PR_NUMBER }}.api.kerberos.lol
overwrite_description: true

View File

@@ -1,130 +0,0 @@
name: Create a new release
on:
release:
types: [created]
workflow_dispatch:
inputs:
tag:
description: "Tag for the Docker image"
required: true
default: "test"
env:
REPO: kerberos/agent
jobs:
build-amd64:
runs-on: ubuntu-24.04
permissions:
contents: write
strategy:
matrix:
architecture: [amd64]
steps:
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Checkout
uses: actions/checkout@v3
- uses: benjlevesque/short-sha@v2.1
id: short-sha
with:
length: 7
- name: Run Build
run: |
docker build --provenance=false --build-arg VERSION=${{github.event.inputs.tag || github.ref_name}} -t ${{matrix.architecture}} .
CID=$(docker create ${{matrix.architecture}})
docker cp ${CID}:/home/agent ./output-${{matrix.architecture}}
docker rm ${CID}
- name: Strip binary
run: tar -cf agent-${{matrix.architecture}}.tar -C output-${{matrix.architecture}} . && rm -rf output-${{matrix.architecture}}
- name: Build and push Docker image
run: |
docker tag ${{matrix.architecture}} $REPO-arch:arch-${{matrix.architecture}}-${{github.event.inputs.tag || github.ref_name}}
docker push $REPO-arch:arch-${{matrix.architecture}}-${{github.event.inputs.tag || github.ref_name}}
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: agent-${{matrix.architecture}}.tar
path: agent-${{matrix.architecture}}.tar
build-arm64:
runs-on: ubuntu-24.04-arm
permissions:
contents: write
strategy:
matrix:
architecture: [arm64]
steps:
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Checkout
uses: actions/checkout@v3
- uses: benjlevesque/short-sha@v2.1
id: short-sha
with:
length: 7
- name: Run Build
run: |
docker build --provenance=false --build-arg VERSION=${{github.event.inputs.tag || github.ref_name}} -t ${{matrix.architecture}} -f Dockerfile.arm64 .
CID=$(docker create ${{matrix.architecture}})
docker cp ${CID}:/home/agent ./output-${{matrix.architecture}}
docker rm ${CID}
- name: Strip binary
run: tar -cf agent-${{matrix.architecture}}.tar -C output-${{matrix.architecture}} . && rm -rf output-${{matrix.architecture}}
- name: Build and push Docker image
run: |
docker tag ${{matrix.architecture}} $REPO-arch:arch-${{matrix.architecture}}-${{github.event.inputs.tag || github.ref_name}}
docker push $REPO-arch:arch-${{matrix.architecture}}-${{github.event.inputs.tag || github.ref_name}}
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: agent-${{matrix.architecture}}.tar
path: agent-${{matrix.architecture}}.tar
create-manifest:
runs-on: ubuntu-24.04
needs: [build-amd64, build-arm64]
steps:
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Create and push multi-arch manifest
run: |
docker manifest create $REPO:${{ github.event.inputs.tag || github.ref_name }} \
$REPO-arch:arch-amd64-${{github.event.inputs.tag || github.ref_name}} \
$REPO-arch:arch-arm64-${{github.event.inputs.tag || github.ref_name}}
docker manifest push $REPO:${{ github.event.inputs.tag || github.ref_name }}
- name: Create and push latest manifest
run: |
docker manifest create $REPO:latest \
$REPO-arch:arch-amd64-${{github.event.inputs.tag || github.ref_name}} \
$REPO-arch:arch-arm64-${{github.event.inputs.tag || github.ref_name}}
docker manifest push $REPO:latest
if: github.event.inputs.tag == 'test'
create-release:
runs-on: ubuntu-24.04
needs: [build-amd64, build-arm64]
permissions:
contents: write
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
- name: Create a release
uses: ncipollo/release-action@v1
with:
latest: true
allowUpdates: true
name: ${{ github.event.inputs.tag || github.ref_name }}
tag: ${{ github.event.inputs.tag || github.ref_name }}
generateReleaseNotes: false
omitBodyDuringUpdate: true
artifacts: "agent-*.tar/agent-*.tar"

8
.gitignore vendored
View File

@@ -1,8 +1,6 @@
ui/node_modules
ui/build
ui/public/assets/env.js
.DS_Store
__debug*
.idea
machinery/www
yarn.lock
@@ -12,7 +10,5 @@ machinery/data/recordings
machinery/data/snapshots
machinery/test*
machinery/init-dev.sh
machinery/.env.local
machinery/vendor
deployments/docker/private-docker-compose.yaml
video.mp4
machinery/.env
deployments/docker/private-docker-compose.yaml

19
.travis.yml Normal file
View File

@@ -0,0 +1,19 @@
language: go
go:
- 1.12.x
- 1.13.x
- 1.14.x
- 1.15.x
- tip
before_install:
- cd machinery
- go mod download
script:
- go vet
- go test -race -coverprofile=coverage.txt -covermode=atomic
after_success:
- bash <(curl -s https://codecov.io/bash)

33
.vscode/launch.json vendored
View File

@@ -1,33 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch Golang",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/machinery/main.go",
"args": [
"-action",
"run",
"-port",
"8080"
],
"envFile": "${workspaceFolder}/machinery/.env.local",
"buildFlags": "--tags dynamic",
},
{
"name": "Launch React",
"type": "node",
"request": "launch",
"cwd": "${workspaceFolder}/ui",
"runtimeExecutable": "yarn",
"runtimeArgs": [
"start"
],
}
]
}

View File

@@ -1,8 +1,6 @@
ARG BASE_IMAGE_VERSION=amd64-ddbe40e
ARG VERSION=0.0.0
FROM kerberos/base:${BASE_IMAGE_VERSION} AS build-machinery
LABEL AUTHOR=uug.ai
FROM kerberos/base:dc12d68 AS build-machinery
LABEL AUTHOR=Kerberos.io
ENV GOROOT=/usr/local/go
ENV GOPATH=/go
@@ -12,7 +10,7 @@ ENV GOSUMDB=off
##########################################
# Installing some additional dependencies.
RUN apt-get upgrade -y && apt-get update && apt-get install -y --fix-missing --no-install-recommends \
RUN apt-get upgrade -y && apt-get update && apt-get install -y --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/*
@@ -22,7 +20,6 @@ RUN apt-get upgrade -y && apt-get update && apt-get install -y --fix-missing --n
RUN mkdir -p /go/src/github.com/kerberos-io/agent
COPY machinery /go/src/github.com/kerberos-io/agent/machinery
RUN rm -rf /go/src/github.com/kerberos-io/agent/machinery/.env
##################################################################
# Get the latest commit hash, so we know which version we're running
@@ -35,8 +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 && \
VERSION=$(cd /go/src/github.com/kerberos-io/agent && git describe --tags --always 2>/dev/null || echo "${VERSION}") && \
go build -tags timetzdata,netgo,osusergo --ldflags "-s -w -X github.com/kerberos-io/agent/machinery/src/utils.VERSION=${VERSION} -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 && \
@@ -46,7 +42,8 @@ RUN cd /go/src/github.com/kerberos-io/agent/machinery && \
mkdir -p /agent/data/log && \
mkdir -p /agent/data/recordings && \
mkdir -p /agent/data/capture-test && \
mkdir -p /agent/data/config
mkdir -p /agent/data/config && \
rm -rf /go/src/gitlab.com/
####################################
# Let's create a /dist folder containing just the files necessary for runtime.
@@ -60,6 +57,18 @@ RUN cp -r /agent ./
RUN /dist/agent/main version
###############################################
# Build Bento4 -> we want fragmented mp4 files
ENV BENTO4_VERSION 1.6.0-639
RUN cd /tmp && git clone https://github.com/axiomatic-systems/Bento4 && cd Bento4 && \
git checkout tags/v${BENTO4_VERSION} && \
cd Build && \
cmake -DCMAKE_BUILD_TYPE=Release .. && \
make && \
mv /tmp/Bento4/Build/mp4fragment /dist/agent/ && \
rm -rf /tmp/Bento4
FROM node:18.14.0-alpine3.16 AS build-ui
RUN apk update && apk upgrade --available && sync
@@ -101,6 +110,7 @@ RUN apk update && apk add ca-certificates curl libstdc++ libc6-compat --no-cache
# Try running agent
RUN mv /agent/* /home/agent/
RUN cp /home/agent/mp4fragment /usr/local/bin/
RUN /home/agent/main version
#######################
@@ -137,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", "-action", "run", "-port", "80"]
CMD ["./main", "-action", "run", "-port", "80"]

View File

@@ -1,140 +0,0 @@
ARG BASE_IMAGE_VERSION=arm64-ddbe40e
ARG VERSION=0.0.0
FROM kerberos/base:${BASE_IMAGE_VERSION} AS build-machinery
LABEL AUTHOR=uug.ai
ENV GOROOT=/usr/local/go
ENV GOPATH=/go
ENV PATH=$GOPATH/bin:$GOROOT/bin:/usr/local/lib:$PATH
ENV GOSUMDB=off
##########################################
# Installing some additional dependencies.
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/*
##############################################################################
# Copy all the relevant source code in the Docker image, so we can build this.
RUN mkdir -p /go/src/github.com/kerberos-io/agent
COPY machinery /go/src/github.com/kerberos-io/agent/machinery
RUN rm -rf /go/src/github.com/kerberos-io/agent/machinery/.env
##################################################################
# Get the latest commit hash, so we know which version we're running
COPY .git /go/src/github.com/kerberos-io/agent/.git
RUN cd /go/src/github.com/kerberos-io/agent/.git && git log --format="%H" -n 1 | head -c7 > /go/src/github.com/kerberos-io/agent/machinery/version
RUN cat /go/src/github.com/kerberos-io/agent/machinery/version
##################
# Build Machinery
RUN cd /go/src/github.com/kerberos-io/agent/machinery && \
go mod download && \
VERSION=$(cd /go/src/github.com/kerberos-io/agent && git describe --tags --always 2>/dev/null || echo "${VERSION}") && \
go build -tags timetzdata,netgo,osusergo --ldflags "-s -w -X github.com/kerberos-io/agent/machinery/src/utils.VERSION=${VERSION} -extldflags '-static -latomic'" main.go && \
mkdir -p /agent && \
mv main /agent && \
mv version /agent && \
mv data /agent && \
mkdir -p /agent/data/cloud && \
mkdir -p /agent/data/snapshots && \
mkdir -p /agent/data/log && \
mkdir -p /agent/data/recordings && \
mkdir -p /agent/data/capture-test && \
mkdir -p /agent/data/config
####################################
# Let's create a /dist folder containing just the files necessary for runtime.
# Later, it will be copied as the / (root) of the output image.
WORKDIR /dist
RUN cp -r /agent ./
####################################################################################
# This will collect dependent libraries so they're later copied to the final image.
RUN /dist/agent/main version
FROM node:18.14.0-alpine3.16 AS build-ui
RUN apk update && apk upgrade --available && sync
########################
# Build Web (React app)
RUN mkdir -p /go/src/github.com/kerberos-io/agent/machinery/www
COPY ui /go/src/github.com/kerberos-io/agent/ui
RUN cd /go/src/github.com/kerberos-io/agent/ui && rm -rf yarn.lock && yarn config set network-timeout 300000 && \
yarn && yarn build
####################################
# Let's create a /dist folder containing just the files necessary for runtime.
# Later, it will be copied as the / (root) of the output image.
WORKDIR /dist
RUN mkdir -p ./agent && cp -r /go/src/github.com/kerberos-io/agent/machinery/www ./agent/
############################################
# Publish main binary to GitHub release
FROM alpine:latest
############################
# Protect by non-root user.
RUN addgroup -S kerberosio && adduser -S agent -G kerberosio && addgroup agent video
#################################
# Copy files from previous images
COPY --chown=0:0 --from=build-machinery /dist /
COPY --chown=0:0 --from=build-ui /dist /
RUN apk update && apk add ca-certificates curl libstdc++ libc6-compat --no-cache && rm -rf /var/cache/apk/*
##################
# Try running agent
RUN mv /agent/* /home/agent/
RUN /home/agent/main version
#######################
# Make template config
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
RUN apk add libcap && setcap 'cap_net_bind_service=+ep' /home/agent/main
###################
# Run non-root user
USER agent
######################################
# By default the app runs on port 80
EXPOSE 80
######################################
# Check if agent is still running
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", "-action", "run", "-port", "80"]

271
README.md
View File

@@ -17,23 +17,20 @@
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
[![donate](https://brianmacdonald.github.io/Ethonate/svg/eth-donate-blue.svg)](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>
[![Discord Shield](https://discordapp.com/api/guilds/1039619181731135499/widget.png?style=shield)](https://discord.gg/Bj77Vqfp2G)
[![kerberosio](https://snapcraft.io/kerberosio/badge.svg)](https://snapcraft.io/kerberosio)
[![Slack invite](https://img.shields.io/badge/join%20kerberos.io%20on%20slack-grey?style=for-the-badge&logo=slack)](https://joinslack.kerberos.io/)
[**Docker Hub**](https://hub.docker.com/r/kerberos/agent) | [**Documentation**](https://doc.kerberos.io) | [**Website**](https://kerberos.io) | [**View Demo**](https://demo.kerberos.io)
> Before you continue, this repository discusses one of the components of the Kerberos.io stack, the Kerberos Agent, in depth. If you are [looking for an end-to-end deployment guide have a look here](https://github.com/kerberos-io/deployment).
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 (which was not the case for v2). Read more [about the license here](LICENSE).
![Kerberos Agent go through UI](./assets/img/kerberos-agent-overview.gif)
## :thinking: Prerequisites
- An IP camera which supports a RTSP H264 or H265 encoded stream,
- (or) a USB camera, Raspberry Pi camera or other camera, that [you can transform to a valid RTSP H264 or H265 stream](https://github.com/kerberos-io/camera-to-rtsp).
- Any hardware (ARMv6, ARMv7, ARM64, AMD64) 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.
- An IP camera which supports a RTSP H264 encoded stream,
- (or) a USB camera, Raspberry Pi camera or other camera, that [you can tranform to a valid RTSP H264 stream](https://github.com/kerberos-io/camera-to-rtsp).
- 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?
@@ -49,46 +46,41 @@ There are a myriad of cameras out there (USB, IP and other cameras), and it migh
### Introduction
1. [A world of Kerberos Agents](#a-world-of-kerberos-agents)
3. [A world of Kerberos Agents](#a-world-of-kerberos-agents)
### Running and automation
1. [How to run and deploy a Kerberos Agent](#how-to-run-and-deploy-a-kerberos-agent)
2. [Access the Kerberos Agent](#access-the-kerberos-agent)
3. [Configure and persist with volume mounts](#configure-and-persist-with-volume-mounts)
4. [Configure with environment variables](#configure-with-environment-variables)
### Insights
1. [Encryption](#encryption)
2. [H264 vs H265](#h264-vs-h265)
4. [How to run and deploy a Kerberos Agent](#how-to-run-and-deploy-a-kerberos-agent)
5. [Access the Kerberos Agent](#access-the-kerberos-agent)
6. [Configure and persist with volume mounts](#configure-and-persist-with-volume-mounts)
7. [Configure with environment variables](#configure-with-environment-variables)
### Contributing
1. [Contribute with Codespaces](#contribute-with-codespaces)
2. [Develop and build](#develop-and-build)
3. [Building from source](#building-from-source)
4. [Building for Docker](#building-for-docker)
8. [Contribute with Codespaces](#contribute-with-codespaces)
9. [Develop and build](#develop-and-build)
10. [Building from source](#building-from-source)
11. [Building for Docker](#building-for-docker)
### Varia
1. [Support our project](#support-our-project)
1. [What is new?](#what-is-new)
1. [Contributors](#contributors)
12. [Support our project](#support-our-project)
13. [What is new?](#what-is-new)
14. [Contributors](#contributors)
## Quickstart - Docker
The easiest way to get your Kerberos Agent up and running is to use our public image on [Docker hub](https://hub.docker.com/r/kerberos/agent). Once you have selected a specific tag, run `docker` command below, which will open the web interface of your Kerberos agent on port `80`, and off you go. For a more configurable and persistent deployment have a look at [Running and automating a Kerberos Agent](#running-and-automating-a-kerberos-agent).
The easiest to get your Kerberos Agent up and running is to use our public image on [Docker hub](https://hub.docker.com/r/kerberos/agent). Once you have selected a specific tag, run below `docker` command, which will open the web interface of your Kerberos agent on port `80`, and off you go. For a more configurable and persistent deployment have a look at [Running and automating a Kerberos Agent](#running-and-automating-a-kerberos-agent).
docker run -p 80:80 --name mycamera -d --restart=always kerberos/agent:latest
If you want to connect to a USB or Raspberry Pi camera, [you'll need to run our side car container](https://github.com/kerberos-io/camera-to-rtsp) which proxies the camera to an RTSP stream. In that case you'll want to configure the Kerberos Agent container to run in the host network, so it can connect directly to the RTSP sidecar.
If you want to connect to an USB or Raspberry Pi camera, [you'll need to run our side car container](https://github.com/kerberos-io/camera-to-rtsp) which proxy the camera to an RTSP stream. In that case you'll want to configure the Kerberos Agent container to run in the host network, so it can connect directly to the RTSP sidecar.
docker run --network=host --name mycamera -d --restart=always kerberos/agent:latest
## Quickstart - Balena
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 much 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).
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).
[![deploy with balena](https://balena.io/deploy.svg)](https://dashboard.balena-cloud.com/deploy?repoUrl=https://github.com/kerberos-io/balena-agent)
@@ -104,37 +96,31 @@ Once installed you can find your Kerberos Agent configration at `/var/snap/kerbe
## A world of Kerberos Agents
The Kerberos Agent is an isolated and scalable video (surveillance) management agent with a strong focus on user experience, scalability, resilience, extension and integration. Next to the Kerberos Agent, Kerberos.io provides many other tools such as [Kerberos Factory](https://github.com/kerberos-io/factory), [Kerberos Vault](https://github.com/kerberos-io/vault), and [Kerberos Hub](https://github.com/kerberos-io/hub) to provide additional capabilities: bring your own cloud, bring your own storage, central overview, live streaming, machine learning, etc.
The Kerberos Agent is an isolated and scalable video (surveillance) management agent with a strong focus on user experience, scalability, resilience, extension and integration. Next to the Kerberos Agent, Kerberos.io provides many other tools such as [Kerberos Factory](https://github.com/kerberos-io/factory), [Kerberos Vault](https://github.com/kerberos-io/vault) and [Kerberos Hub](https://github.com/kerberos-io/hub) to provide additional capabilities: bring your own cloud, bring your own storage, central overview, live streaming, machine learning etc.
[![Deployment Agent](./assets/img/edge-deployment-agent.svg)](https://github.com/kerberos-io/deployment)
As mentioned above Kerberos.io applies the concept of agents. An agent is running next to (or on) your camera, and is processing a single camera feed. It applies motion based or continuous recording and makes those recordings available through a user friendly web interface. A Kerberos Agent allows you to connect to other cloud services or integrate with custom applications. Kerberos Agent is used for personal applications and scales to enterprise production level deployments. Learn more about the [deployment strategies here](<(https://github.com/kerberos-io/deployment)>).
As mentioned above Kerberos.io applies the concept of agents. An agent is running next to (or on) your camera, and is processing a single camera feed. It applies motion based or continuous recording and make those recordings available through a user friendly web interface. A Kerberos Agent allows you to connect to other cloud services or integrates with custom applications. Kerberos Agent is used for personal usage and scales to enterprise production level deployments.
This repository contains everything you'll need to know about our core product, Kerberos Agent. Below you'll find a brief list of features and functions.
- Low memory and CPU usage.
- Simplified and modern user interface.
- Multi architecture (ARMv6, ARMv7, ARM64, AMD64)
- Multi stream, for example recording in H265, live streaming and motion detection in H264.
- Multi camera support: IP Cameras (H264 and H265), USB cameras and Raspberry Pi Cameras [through a RTSP proxy](https://github.com/kerberos-io/camera-to-rtsp).
- 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).
- Low resolution streaming through MQTT and high resolution streaming through WebRTC (only supports H264/PCM).
- Backchannel audio from Kerberos Hub to IP camera (requires PCM ULAW codec)
- Audio (AAC) and video (H264/H265) recording in MP4 container.
- End-to-end encryption through MQTT using RSA and AES (livestreaming, ONVIF, remote configuration, etc)
- Conditional recording: offline mode, motion region, time table, continuous recording, webhook condition etc.
- Post- and pre-recording for motion detection.
- Encryption at rest using AES-256-CBC.
- Ability to create fragmented recordings, and streaming through HLS fMP4.
- Primary and secondary stream setup (record full-res, stream low-res).
- Low resolution streaming through MQTT and full resolution streaming through WebRTC.
- Ability to specifiy conditions: offline mode, motion region, time table, continuous recording, etc.
- Post- and pre-recording on motion detection.
- Ability to create fragmented recordings, and streaming though HLS fMP4.
- [Deploy where you want](#how-to-run-and-deploy-a-kerberos-agent) with the tools you use: `docker`, `docker compose`, `ansible`, `terraform`, `kubernetes`, etc.
- Cloud storage/persistance: Kerberos Hub, Kerberos Vault and Dropbox. [(WIP: Minio, Storj, Google Drive, FTP etc.)](https://github.com/kerberos-io/agent/issues/95)
- Outputs: trigger an integration (Webhooks, MQTT, Script, etc) when a specific event (motion detection or start recording ) occurs
- 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
A Kerberos Agent, as previously mentioned, is a container. You can deploy it using various methods and automation tools, including `docker`, `docker compose`, `kubernetes` and more. To streamline your Kerberos.io experience, we provide concrete deployment examples to speed up your Kerberos.io journey
As described before a Kerberos Agent is a container, which can be deployed through various ways and automation tools such as `docker`, `docker compose`, `kubernetes` and the list goes on. To simplify your life we have come with concrete and working examples of deployments to help you speed up your Kerberos.io journey.
We have documented the different deployment models [in the `deployments` directory](https://github.com/kerberos-io/agent/tree/master/deployments) of this repository. There you'll learn and find how to deploy using:
@@ -148,7 +134,7 @@ We have documented the different deployment models [in the `deployments` directo
- [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 store all configuration and recordings within 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.
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.
## Access the Kerberos Agent
@@ -163,23 +149,23 @@ The default username and password for the Kerberos Agent is:
## 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 of the deployment models and tools described above](#running-and-automating-a-kerberos-agent).
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).
You attach a volume to your container by leveraging the `-v` option. To mount your own configuration file and recordings folder, run the following commands:
You attach a volume to your container by leveraging the `-v` option. To mount your own configuration file and recordings folder, execute as following:
docker run -p 80:80 --name mycamera \
-v $(pwd)/agent/config:/home/agent/data/config \
-v $(pwd)/agent/recordings:/home/agent/data/recordings \
-d --restart=always kerberos/agent:latest
More examples for each deployment and automation tool [can be found in the deployment section](https://github.com/kerberos-io/agent/tree/master/deployments). Be sure to verify the permissions of the directory/volume you are attaching. More information in [this issue](https://github.com/kerberos-io/agent/issues/80).
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
Next to attaching the configuration file, it is also possible to override the configuration with environment variables. This makes deploying with `docker compose` or `kubernetes` much easier and more scalable. Using this approach, we simplify automation through `ansible` and `terraform`.
Next to attaching the configuration file, it is also possible to override the configuration with environment variables. This makes deployments easier when leveraging `docker compose` or `kubernetes` deployments much easier and scalable. Using this approach we simplify automation through `ansible` and `terraform`.
docker run -p 80:80 --name mycamera \
-e AGENT_NAME=mycamera \
@@ -190,122 +176,63 @@ Next to attaching the configuration file, it is also possible to override the co
| Name | Description | Default Value |
| --------------------------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------ |
| `LOG_LEVEL` | Level for logging, could be "info", "warning", "debug", "error" or "fatal". | "info" |
| `LOG_OUTPUT` | Logging output format "json" or "text". | "text" |
| `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_BASE_WIDTH` | Force a specific width resolution for live view processing. | "" |
| `AGENT_CAPTURE_IPCAMERA_BASE_HEIGHT` | Force a specific height resolution for live view processing. | "" |
| `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` | An 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_REALTIME_PROCESSING` | If `AGENT_REALTIME_PROCESSING` set to `true`, the agent will send key frames to the topic | "" |
| `AGENT_REALTIME_PROCESSING_TOPIC` | The topic to which keyframes will be sent in base64 encoded format. | "" |
| `AGENT_STUN_URI` | When using WebRTC, you'll need to provide a STUN server. | "stun:turn.kerberos.io:8443" |
| `AGENT_FORCE_TURN` | Force using a TURN server, by generating relay candidates only. | "false" |
| `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_ENCRYPTION` | Turning on/off encryption of traffic from your Kerberos Agent to Kerberos Hub. | "true" |
| `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 Kerberos vault, where the recordings will be stored. | "" |
| `AGENT_KERBEROSVAULT_SECONDARY_URI` | The Kerberos Vault API url. | "https://vault.domain.com/api" |
| `AGENT_KERBEROSVAULT_SECONDARY_ACCESS_KEY` | The access key of a secondary Kerberos Vault account. | "" |
| `AGENT_KERBEROSVAULT_SECONDARY_SECRET_KEY` | The secret key of a secondary Kerberos Vault account. | "" |
| `AGENT_KERBEROSVAULT_SECONDARY_PROVIDER` | A secondary Kerberos Vault provider you have created (optional). | "" |
| `AGENT_KERBEROSVAULT_SECONDARY_DIRECTORY` | The directory, in the secondary Kerberos vault, where the recordings will be stored. | "" |
| `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 Dropbox, where the recordings will be stored. | "" |
| `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 decrypt and sign requests send over MQTT. | "" |
| `AGENT_ENCRYPTION_SYMMETRIC_KEY` | The symmetric key (AES) to encrypt and decrypt requests sent over MQTT. | "" |
| `AGENT_SIGNING` | Enable 'true' or disable 'false' for signing recordings. | "true" |
| `AGENT_SIGNING_PRIVATE_KEY` | The private key (RSA) to sign the recordings fingerprint to validate origin. | "" - uses default one if empty |
## 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 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
Or 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
## H264 vs H265
If we talk about video encoders and decoders (codecs) there are 2 major video codecs on the market: H264 and H265. Taking into account your use case, you might use one over the other. We will provide an (not complete) overview of the advantages and disadvantages of each codec in the field of video surveillance and video analytics. If you would like to know more, you should look for additional resources on the internet (or if you like to read physical items, books still exists nowadays).
- H264 (also known as AVC or MPEG-4 Part 10)
- Is the most common one and most widely supported for IP cameras.
- Supported in the majority of browsers, operating system, and third-party applications.
- Can be embedded in commercial and 3rd party applications.
- Different levels of compression (high, medium, low, ..)
- Better quality / compression ratio, shows less artifacts at medium compression ratios.
- Does support technologies such as WebRTC
- H265 (also known as HEVC)
- Is not supported on legacy cameras, though becoming rapidly available on "newer" IP cameras.
- Might not always be supported due to licensing. For example not supported in browers on a Linux distro.
- Requires licensing when embedding in a commercial product (be careful).
- Higher levels of compression (50% more than H264).
- H265 shows artifacts in motion based environments (which is less with H264).
- Recording the same video (resolution, duration and FPS) in H264 and H265 will result in approx 50% the file size.
- Not supported in technologies such as WebRTC
Conclusion: depending on the use case you might choose one over the other, and you can use both at the same time. For example you can use H264 (main stream) for livestreaming, and H265 (sub stream) for recording. If you wish to play recordings in a cross-platform and cross-browser environment, you might opt for H264 for better support.
| `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. | "" |
## Contribute with Codespaces
One of the major blockers for letting you contribute to an Open Source project is to set up your local development machine. Why? Because you might already have some tools and libraries installed that are used for other projects, and the libraries you would need for Kerberos Agent, for example FFmpeg, might require a different version. Welcome to dependency hell...
One of the major blockers for letting you contribute to an Open Source project is to setup your local development machine. Why? Because you might have already some tools and libraries installed that are used for other projects, and the libraries you would need for Kerberos Agent, for example FFmpeg, might require a different version. Welcome to the dependency hell..
By leveraging codespaces, which the Kerberos Agent repo supports, you will be able to set up the required development environment in a few minutes. By opening the `<> Code` tab on the top of the page, you will be able to create a codespace, [using the Kerberos Devcontainer](https://github.com/kerberos-io/devcontainer) base image. This image requires all the relevant dependencies: FFmpeg, OpenCV, Golang, Node, Yarn, etc.
By leveraging codespaces, which the Kerberos Agent repo supports, you will be able to setup the required development environment in a few minutes. By opening the `<> Code` tab on the top of the page, you will be able to create a codespace, [using the Kerberos Devcontainer](https://github.com/kerberos-io/devcontainer) base image. This image requires all the relevant dependencies: FFmpeg, OpenCV, Golang, Node, Yarn, etc.
![Kerberos Agent codespace](assets/img/codespace.png)
@@ -332,7 +259,7 @@ On opening of the GitHub Codespace, some dependencies will be installed. Once th
WS_URL: `${websocketprotocol}//${externalHost}/ws`,
};
Go and open two terminals: one for the `ui` project and one for the `machinery` project.
Go and open two terminals one for the `ui` project and one for the `machinery` project.
1. Terminal A:
@@ -348,11 +275,11 @@ Once executed, a popup will show up mentioning `portforwarding`. You should see
![Codespace make public](./assets/img/codespace-make-public.png)
As mentioned above, copy the hostname of the `machinery` DNS name, and paste it in the `ui/src/config.json` file. Once done, reload the `ui` page in your browser, and you should be able to access the login page with the default credentials `root` and `root`.
As mentioned above, copy the hostname of the `machinery` DNS name, and past it in the `ui/src/config.json` file. Once done reload, the `ui` page in your browser, and you should be able to access the login page with the default credentials `root` and `root`.
## Develop and build
The Kerberos Agent is divided in two parts: a `machinery` and `web` part. Both parts live in this repository in their relative folders. For development or running the application on your local machine, you have to run both the `machinery` and the `web` as described below. When running in production everything is shipped as only one artifact, read more about this at [Building for production](#building-for-production).
Kerberos Agent is divided in two parts a `machinery` and `web`. Both parts live in this repository in their relative folders. For development or running the application on your local machine, you have to run both the `machinery` and the `web` as described below. When running in production everything is shipped as only one artifact, read more about this at [Building for production](#building-for-production).
### UI
@@ -366,13 +293,13 @@ This will start a webserver and launches the web app on port `3000`.
![login-agent](./assets/img/agent-login.gif)
Once signed in you'll see the dashboard page. After successfull configuration of your agent, you'll should see a live view and possible events recorded to disk.
Once signed in you'll see the dashboard page showing up. After successfull configuration of your agent, you'll should see a live view and possible events recorded to disk.
![dashboard-agent](./assets/img/agent-dashboard.png)
### Machinery
The `machinery` is a **Golang** project which delivers two functions: it acts as the Kerberos Agent which is doing all the heavy lifting with camera processing and other kinds of logic and on the other hand it acts as a webserver (Rest API) that allows communication from the web (React) or any other custom application. The API is documented using `swagger`.
The `machinery` is a **Golang** project which delivers two functions: it acts as the Kerberos Agent which is doing all the heavy lifting with camera processing and other kinds of logic, on the other hand it acts as a webserver (Rest API) that allows communication from the web (React) or any other custom application. The API is documented using `swagger`.
You can simply run the `machinery` using following commands.
@@ -380,13 +307,13 @@ You can simply run the `machinery` using following commands.
cd machinery
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 built in.
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.
![VSCode desktop](./assets/img/vscode-desktop.png)
## Building from source
Running Kerberos Agent in production only requires a single binary to run. Nevertheless, we have two parts: the `machinery` and the `web`, we merge them during build time. So this is what happens.
Running Kerberos Agent in production only require a single binary to run. Nevertheless, we have two parts, the `machinery` and the `web`, we merge them during build time. So this is what happens.
### UI
@@ -397,7 +324,7 @@ To build the Kerberos Agent web app, you simply have to run the `build` command
### Machinery
Building the `machinery` is also super easy 🚀, by using `go build` you can create a single binary which ships it all; thank you Golang. After building you will end up with a binary called `main`, this is what contains everything you need to run Kerberos Agent.
Building the `machinery` is also super easy 🚀, by using `go build` you can create a single binary which ships it all; thank you Golang. After building you will endup with a binary called `main`, this is what contains everything you need to run Kerberos Agent.
Remember the build step of the `web` part, during build time we move the build directory to the `machinery` directory. Inside the `machinery` web server [we reference the](https://github.com/kerberos-io/agent/blob/master/machinery/src/routers/http/Server.go#L44) `build` directory. This makes it possible to just a have single web server that runs it all.
@@ -406,8 +333,8 @@ Remember the build step of the `web` part, during build time we move the build d
## Building for Docker
Inside the root of this `agent` repository, you will find a `Dockerfile`. This file contains the instructions for building and shipping a **Kerberos Agent**. Important to note is that you start from a prebuilt base image, `kerberos/base:xxx`.
This base image already contains a couple of tools, such as Golang, FFmpeg and OpenCV. We do this for faster compilation times.
Inside the root of this `agent` repository, you will find a `Dockerfile`. This file contains the instructions for building and shipping **Kerberos Agent**. Important to note is that start from a prebuild base image, `kerberos/base:xxx`.
This base image contains already a couple of tools, such as Golang, FFmpeg and OpenCV. We do this for faster compilation times.
By running the `docker build` command, you will create the Kerberos Agent Docker image. After building you can simply run the image as a Docker container.
@@ -423,7 +350,7 @@ Read more about this [at the FAQ](#faq) below.
## Contributors
This project exists thanks to all the people who contribute. Bravo!
This project exists thanks to all the people who contribute.
<a href="https://github.com/kerberos-io/agent/graphs/contributors">
<img src="https://contrib.rocks/image?repo=kerberos-io/agent" />

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 696 KiB

10
build.sh Normal file
View File

@@ -0,0 +1,10 @@
export version=0.0.1
export name=agent
docker build -t $name .
docker tag $name kerberos/$name:$version
docker push kerberos/$name:$version
docker tag $name kerberos/$name:latest
docker push kerberos/$name:latest

View File

@@ -9,7 +9,7 @@ Kerberos Agents are now also shipped as static binaries. Within the Docker image
You can run the binary as following on port `8080`:
main -action=run -port=80
main run cameraname 8080
## Systemd
@@ -18,7 +18,7 @@ When running on a Linux OS you might consider to auto-start the Kerberos Agent u
[Unit]
Wants=network.target
[Service]
ExecStart=/home/pi/agent/main -action=run -port=80
ExecStart=/home/pi/agent/main run camera 80
WorkingDirectory=/home/pi/agent/
[Install]
WantedBy=multi-user.target

View File

@@ -36,12 +36,12 @@ You attach a volume to your container by leveraging the `-v` option. To mount yo
docker run -p 80:80 --name mycamera \
-v $(pwd)/agent/config:/home/agent/data/config \
-v $(pwd)/agent/recordings:/home/agent/data/recordings \
-d --restart=always kerberos/agent:latest
-v $(pwd)/agent/recordings:/home/agent/data/recordings\
-d --restart=alwayskerberos/agent:latest
### Override with environment variables
Next to attaching the configuration file, it is also possible to override the configuration with environment variables. This makes deployments when leveraging `docker compose` or `kubernetes` much easier and more scalable. Using this approach we simplify automation through `ansible` and `terraform`. You'll find [the full list of environment variables on the main README.md file](https://github.com/kerberos-io/agent#override-with-environment-variables).
Next to attaching the configuration file, it is also possible to override the configuration with environment variables. This makes deployments easier when leveraging `docker compose` or `kubernetes` deployments much easier and scalable. Using this approach we simplify automation through `ansible` and `terraform`. You'll find [the full list of environment variables on the main README.md file](https://github.com/kerberos-io/agent#override-with-environment-variables).
### 2. Running multiple containers with Docker compose

View File

@@ -1,38 +1,35 @@
version: "3.9"
x-common-variables: &common-variables
# Add variables here to add them to all agents
AGENT_HUB_KEY: "xxxxx" # The access key linked to your account in Kerberos Hub.
AGENT_HUB_PRIVATE_KEY: "xxxxx" # The secret access key linked to your account in Kerberos Hub.
# find full list of environment variables here: https://github.com/kerberos-io/agent#override-with-environment-variables
services:
kerberos-agent1:
image: "kerberos/agent:latest"
ports:
- "8081:80"
environment:
<<: *common-variables
AGENT_NAME: agent1
AGENT_CAPTURE_IPCAMERA_RTSP: rtsp://username:password@x.x.x.x/Streaming/Channels/101 # Hikvision camera RTSP url example
AGENT_KEY: "1"
- AGENT_NAME=agent1
- AGENT_CAPTURE_IPCAMERA_RTSP=rtsp://x.x.x.x:554/Streaming/Channels/101
- AGENT_HUB_KEY=xxx
- AGENT_HUB_PRIVATE_KEY=xxx
- AGENT_CAPTURE_CONTINUOUS=true
- AGENT_CAPTURE_PRERECORDING=10
- AGENT_CAPTURE_POSTRECORDING=10
- AGENT_CAPTURE_MAXLENGTH=60
- AGENT_CAPTURE_PIXEL_CHANGE=150
# find full list of environment variables here: https://github.com/kerberos-io/agent#override-with-environment-variables
kerberos-agent2:
image: "kerberos/agent:latest"
ports:
- "8082:80"
environment:
<<: *common-variables
AGENT_NAME: agent2
AGENT_CAPTURE_IPCAMERA_RTSP: rtsp://username:password@x.x.x.x/channel1 # Linksys camera RTSP url example
AGENT_KEY: "2"
- AGENT_NAME=agent2
- AGENT_CAPTURE_IPCAMERA_RTSP=rtsp://x.x.x.x:554/Streaming/Channels/101
- AGENT_HUB_KEY=yyy
- AGENT_HUB_PRIVATE_KEY=yyy
kerberos-agent3:
image: "kerberos/agent:latest"
ports:
- "8083:80"
environment:
<<: *common-variables
AGENT_NAME: agent3
AGENT_CAPTURE_IPCAMERA_RTSP: rtsp://username:password@x.x.x.x/cam/realmonitor?channel=1&subtype=1 # Dahua camera RTSP url example
AGENT_KEY: "3"
networks:
default:
name: cluster-net
external: true
- AGENT_NAME=agent3
- AGENT_CAPTURE_IPCAMERA_RTSP=rtsp://x.x.x.x:554/Streaming/Channels/101
- AGENT_HUB_KEY=zzz
- AGENT_HUB_PRIVATE_KEY=zzz

View File

@@ -16,7 +16,7 @@ spec:
spec:
containers:
- name: agent
image: kerberos/agent:3.2.3
image: kerberos/agent:latest
ports:
- containerPort: 80
protocol: TCP
@@ -50,4 +50,4 @@ spec:
- port: 80
targetPort: 80
selector:
app: agent
app: agent

BIN
machinery/.DS_Store vendored

Binary file not shown.

View File

@@ -1,31 +0,0 @@
AGENT_NAME=camera-name
AGENT_KEY=uniq-camera-id
AGENT_TIMEZONE=Europe/Brussels
#AGENT_CAPTURE_CONTINUOUS=true
#AGENT_CAPTURE_IPCAMERA_RTSP=rtsp://fake.kerberos.io/stream
#AGENT_CAPTURE_IPCAMERA_SUB_RTSP=rtsp://fake.kerberos.io/stream
AGENT_CAPTURE_IPCAMERA_ONVIF_XADDR=x.x.x.x
AGENT_CAPTURE_IPCAMERA_ONVIF_USERNAME=xxx
AGENT_CAPTURE_IPCAMERA_ONVIF_PASSWORD=xxx
AGENT_HUB_URI=https://api.cloud.kerberos.io
AGENT_HUB_KEY=AKIXxxx4JBEI
AGENT_HUB_PRIVATE_KEY=DIOXxxxAlYpaxxxxXioL0txxx
AGENT_HUB_SITE=681xxxxxxx9bcda5
# By default will send to Hub (=S3), if you wish to send to Kerberos Vault, set to "kstorage"
AGENT_CLOUD=s3
AGENT_KERBEROSVAULT_URI=
AGENT_KERBEROSVAULT_PROVIDER=
AGENT_KERBEROSVAULT_DIRECTORY=
AGENT_KERBEROSVAULT_ACCESS_KEY=
AGENT_KERBEROSVAULT_SECRET_KEY=
AGENT_KERBEROSVAULT_MAX_RETRIES=10
AGENT_KERBEROSVAULT_TIMEOUT=120
AGENT_KERBEROSVAULT_SECONDARY_URI=
AGENT_KERBEROSVAULT_SECONDARY_PROVIDER=
AGENT_KERBEROSVAULT_SECONDARY_DIRECTORY=
AGENT_KERBEROSVAULT_SECONDARY_ACCESS_KEY=
AGENT_KERBEROSVAULT_SECONDARY_SECRET_KEY=
# Open telemetry tracing endpoint
OTEL_EXPORTER_OTLP_ENDPOINT=

18
machinery/.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,18 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch Package",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "main.go",
"args": ["-action", "run"],
"envFile": "${workspaceFolder}/.env",
"buildFlags": "--tags dynamic",
},
]
}

View File

@@ -1,22 +1,25 @@
{
"type": "",
"key": "",
"name": "agent",
"key": "okkey",
"name": "rtsp2",
"time": "false",
"offline": "false",
"auto_clean": "true",
"auto_clean": "100",
"remove_after_upload": "true",
"max_directory_size": 100,
"timezone": "Africa/Ceuta",
"max_directory_size": 20,
"timezone": "Europe/Brussels",
"capture": {
"name": "",
"ipcamera": {
"rtsp": "",
"sub_rtsp": "",
"width": 640,
"height": 480,
"fps": "",
"base_width": 640,
"base_height": 0
"rtsp": "rtsp://fake.kerberos.io/stream",
"sub_rtsp": "rtsp://kellyvd:cedricve123@192.168.1.68:554/Streaming/Channels/102?transportmode=multicast\u0026profile=Profile_1",
"onvif": "true",
"onvif_xaddr": "192.168.1.68",
"onvif_username": "kellyvd",
"onvif_password": "cedricve123"
},
"usbcamera": {
"device": ""
@@ -24,28 +27,27 @@
"raspicamera": {
"device": ""
},
"continuous": "false",
"recording": "true",
"snapshots": "true",
"liveview": "true",
"liveview_chunking": "false",
"motion": "true",
"postrecording": 20,
"prerecording": 10,
"maxlengthrecording": 30,
"liveview": "true",
"continuous": "false",
"postrecording": 5,
"prerecording": 5,
"maxlengthrecording": 10,
"transcodingwebrtc": "",
"transcodingresolution": 0,
"forwardwebrtc": "",
"fragmented": "false",
"fragmentedduration": 8,
"fragmentedduration": 10,
"pixelChangeThreshold": 150
},
"timetable": [
{
"start1": 0,
"end1": 43199,
"start2": 43200,
"end2": 86400
"end1": 0,
"start2": 0,
"end2": 0
},
{
"start1": 0,
@@ -92,34 +94,118 @@
"x2": 800,
"y2": 640
},
"polygon": []
"polygon": [
{
"id": "1694456798869",
"coordinates": [
{
"x": 458.8176559163439,
"y": 420.6597577161105
},
{
"x": 541.8959724597637,
"y": 422.62130444776193
},
{
"x": 540.2578560606811,
"y": 446.03551153214977
},
{
"x": 459.51092569845395,
"y": 445.07041692205803
}
]
},
{
"id": "1694455955284",
"coordinates": [
{
"x": 82.63849814838939,
"y": 122.83665165528404
},
{
"x": 259.18591194149286,
"y": 122.83665165528404
},
{
"x": 259.18591194149286,
"y": 232.42716889666335
},
{
"x": 82.63849814838939,
"y": 232.42716889666335
}
]
},
{
"id": "1694455953115",
"coordinates": [
{
"x": 200.7130606007035,
"y": 171.25495957161723
},
{
"x": 445.73676749725524,
"y": 171.25495957161723
},
{
"x": 445.73676749725524,
"y": 297.5911664681689
},
{
"x": 200.7130606007035,
"y": 297.5911664681689
}
]
},
{
"id": "0",
"coordinates": [
{
"x": 351.8431271737717,
"y": 65.62379883848743
},
{
"x": 609.6534108617149,
"y": 65.62379883848743
},
{
"x": 609.6534108617149,
"y": 246.50145841295546
},
{
"x": 351.8431271737717,
"y": 246.50145841295546
}
]
}
]
},
"cloud": "s3",
"s3": {
"proxyuri": "http://proxy.kerberos.io",
"bucket": "kerberosaccept",
"region": "eu-west-1"
"region": "eu-west1",
"username": "cedricve"
},
"kstorage": {
"uri": "https://api.vault.kerberos.io",
"access_key": "pDCQve0XL1DXwy2x",
"secret_access_key": "GTpus90EKrKYuplqL3g!2oiQ@s",
"directory": "cedricve"
},
"kstorage": {},
"kstorage_secondary": {},
"dropbox": {},
"mqtturi": "tcp://mqtt.kerberos.io:1883",
"mqtt_username": "",
"mqtt_password": "",
"stunuri": "stun:turn.kerberos.io:8443",
"turn_force": "false",
"turnuri": "turn:turn.kerberos.io:8443",
"turn_username": "username1",
"turn_password": "password1",
"heartbeaturi": "",
"hub_encryption": "true",
"hub_uri": "https://api.cloud.kerberos.io",
"hub_key": "",
"hub_private_key": "",
"hub_uri": "http://localhost:8081",
"hub_key": "AKIA5V6EGLUWXDU4JBEI",
"hub_private_key": "DIOXToTpAlYpa4atf7aWtxHd9xEw/Fk4XioL0tek",
"hub_site": "",
"condition_uri": "",
"encryption": {},
"signing": {},
"realtimeprocessing": "false",
"realtimeprocessing_topic": ""
}
"condition_uri": ""
}

View File

@@ -1,4 +1,5 @@
// Package docs Code generated by swaggo/swag. DO NOT EDIT
// Package docs GENERATED BY SWAG; DO NOT EDIT
// This file was generated by swaggo/swag
package docs
import "github.com/swaggo/swag"
@@ -28,7 +29,7 @@ const docTemplate = `{
"post": {
"description": "Will return the ONVIF capabilities for the specific camera.",
"tags": [
"onvif"
"camera"
],
"summary": "Will return the ONVIF capabilities for the specific camera.",
"operationId": "camera-onvif-capabilities",
@@ -57,7 +58,7 @@ const docTemplate = `{
"post": {
"description": "Will activate the desired ONVIF preset.",
"tags": [
"onvif"
"camera"
],
"summary": "Will activate the desired ONVIF preset.",
"operationId": "camera-onvif-gotopreset",
@@ -82,45 +83,11 @@ const docTemplate = `{
}
}
},
"/api/camera/onvif/inputs": {
"post": {
"security": [
{
"Bearer": []
}
],
"description": "Will get the digital inputs from the ONVIF device.",
"tags": [
"onvif"
],
"summary": "Will get the digital inputs from the ONVIF device.",
"operationId": "get-digital-inputs",
"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/login": {
"post": {
"description": "Try to login into ONVIF supported camera.",
"tags": [
"onvif"
"camera"
],
"summary": "Try to login into ONVIF supported camera.",
"operationId": "camera-onvif-login",
@@ -145,86 +112,11 @@ const docTemplate = `{
}
}
},
"/api/camera/onvif/outputs": {
"post": {
"security": [
{
"Bearer": []
}
],
"description": "Will get the relay outputs from the ONVIF device.",
"tags": [
"onvif"
],
"summary": "Will get the relay outputs from the ONVIF device.",
"operationId": "get-relay-outputs",
"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/outputs/{output}": {
"post": {
"security": [
{
"Bearer": []
}
],
"description": "Will trigger the relay output from the ONVIF device.",
"tags": [
"onvif"
],
"summary": "Will trigger the relay output from the ONVIF device.",
"operationId": "trigger-relay-output",
"parameters": [
{
"description": "OnvifCredentials",
"name": "config",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.OnvifCredentials"
}
},
{
"type": "string",
"description": "Output",
"name": "output",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.APIResponse"
}
}
}
}
},
"/api/camera/onvif/pantilt": {
"post": {
"description": "Panning or/and tilting the camera using a direction (x,y).",
"tags": [
"onvif"
"camera"
],
"summary": "Panning or/and tilting the camera.",
"operationId": "camera-onvif-pantilt",
@@ -253,7 +145,7 @@ const docTemplate = `{
"post": {
"description": "Will return the ONVIF presets for the specific camera.",
"tags": [
"onvif"
"camera"
],
"summary": "Will return the ONVIF presets for the specific camera.",
"operationId": "camera-onvif-presets",
@@ -278,45 +170,11 @@ const docTemplate = `{
}
}
},
"/api/camera/onvif/verify": {
"post": {
"security": [
{
"Bearer": []
}
],
"description": "Will verify the ONVIF connectivity.",
"tags": [
"onvif"
],
"summary": "Will verify the ONVIF connectivity.",
"operationId": "verify-onvif",
"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.",
"tags": [
"onvif"
"camera"
],
"summary": "Zooming in or out the camera.",
"operationId": "camera-onvif-zoom",
@@ -341,90 +199,6 @@ const docTemplate = `{
}
}
},
"/api/camera/record": {
"post": {
"description": "Make a recording.",
"tags": [
"camera"
],
"summary": "Make a recording.",
"operationId": "camera-record",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.APIResponse"
}
}
}
}
},
"/api/camera/restart": {
"post": {
"description": "Restart the agent.",
"tags": [
"camera"
],
"summary": "Restart the agent.",
"operationId": "camera-restart",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.APIResponse"
}
}
}
}
},
"/api/camera/snapshot/base64": {
"get": {
"description": "Get a snapshot from the camera in base64.",
"tags": [
"camera"
],
"summary": "Get a snapshot from the camera in base64.",
"operationId": "snapshot-base64",
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/camera/snapshot/jpeg": {
"get": {
"description": "Get a snapshot from the camera in jpeg format.",
"tags": [
"camera"
],
"summary": "Get a snapshot from the camera in jpeg format.",
"operationId": "snapshot-jpeg",
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/camera/stop": {
"post": {
"description": "Stop the agent.",
"tags": [
"camera"
],
"summary": "Stop the agent.",
"operationId": "camera-stop",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.APIResponse"
}
}
}
}
},
"/api/camera/verify/{streamType}": {
"post": {
"description": "This method will validate a specific profile connection from an RTSP camera, and try to get the codec.",
@@ -465,75 +239,6 @@ const docTemplate = `{
}
}
},
"/api/config": {
"get": {
"description": "Get the current configuration.",
"tags": [
"config"
],
"summary": "Get the current configuration.",
"operationId": "config",
"responses": {
"200": {
"description": "OK"
}
}
},
"post": {
"description": "Update the current configuration.",
"tags": [
"config"
],
"summary": "Update the current configuration.",
"operationId": "config",
"parameters": [
{
"description": "Configuration",
"name": "config",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.Config"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/dashboard": {
"get": {
"description": "Get all information showed on the dashboard.",
"tags": [
"general"
],
"summary": "Get all information showed on the dashboard.",
"operationId": "dashboard",
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/days": {
"get": {
"description": "Get all days stored in the recordings directory.",
"tags": [
"general"
],
"summary": "Get all days stored in the recordings directory.",
"operationId": "days",
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/hub/verify": {
"post": {
"security": [
@@ -543,7 +248,7 @@ const docTemplate = `{
],
"description": "Will verify the hub connectivity.",
"tags": [
"persistence"
"config"
],
"summary": "Will verify the hub connectivity.",
"operationId": "verify-hub",
@@ -568,32 +273,6 @@ const docTemplate = `{
}
}
},
"/api/latest-events": {
"post": {
"description": "Get the latest recordings (events) from the recordings directory.",
"tags": [
"general"
],
"summary": "Get the latest recordings (events) from the recordings directory.",
"operationId": "latest-events",
"parameters": [
{
"description": "Event filter",
"name": "eventFilter",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.EventFilter"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/login": {
"post": {
"description": "Get Authorization token.",
@@ -623,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": [
@@ -632,7 +345,7 @@ const docTemplate = `{
],
"description": "Will verify the persistence.",
"tags": [
"persistence"
"config"
],
"summary": "Will verify the persistence.",
"operationId": "verify-persistence",
@@ -792,9 +505,6 @@ const docTemplate = `{
"dropbox": {
"$ref": "#/definitions/models.Dropbox"
},
"encryption": {
"$ref": "#/definitions/models.Encryption"
},
"friendly_name": {
"type": "string"
},
@@ -802,9 +512,6 @@ const docTemplate = `{
"description": "obsolete",
"type": "string"
},
"hub_encryption": {
"type": "string"
},
"hub_key": {
"type": "string"
},
@@ -841,12 +548,6 @@ const docTemplate = `{
"offline": {
"type": "string"
},
"realtimeprocessing": {
"type": "string"
},
"realtimeprocessing_topic": {
"type": "string"
},
"region": {
"$ref": "#/definitions/models.Region"
},
@@ -871,9 +572,6 @@ const docTemplate = `{
"timezone": {
"type": "string"
},
"turn_force": {
"type": "string"
},
"turn_password": {
"type": "string"
},
@@ -910,49 +608,12 @@ const docTemplate = `{
}
}
},
"models.Encryption": {
"type": "object",
"properties": {
"enabled": {
"type": "string"
},
"fingerprint": {
"type": "string"
},
"private_key": {
"type": "string"
},
"recordings": {
"type": "string"
},
"symmetric_key": {
"type": "string"
}
}
},
"models.EventFilter": {
"type": "object",
"properties": {
"number_of_elements": {
"type": "integer"
},
"timestamp_offset_end": {
"type": "integer"
},
"timestamp_offset_start": {
"type": "integer"
}
}
},
"models.IPCamera": {
"type": "object",
"properties": {
"fps": {
"type": "string"
},
"height": {
"type": "integer"
},
"onvif": {
"type": "string"
},
@@ -968,20 +629,8 @@ const docTemplate = `{
"rtsp": {
"type": "string"
},
"sub_fps": {
"type": "string"
},
"sub_height": {
"type": "integer"
},
"sub_rtsp": {
"type": "string"
},
"sub_width": {
"type": "integer"
},
"width": {
"type": "integer"
}
}
},
@@ -1186,8 +835,6 @@ var SwaggerInfo = &swag.Spec{
Description: "This is the API for using and configure Kerberos Agent.",
InfoInstanceName: "swagger",
SwaggerTemplate: docTemplate,
LeftDelim: "{{",
RightDelim: "}}",
}
func init() {

View File

@@ -21,7 +21,7 @@
"post": {
"description": "Will return the ONVIF capabilities for the specific camera.",
"tags": [
"onvif"
"camera"
],
"summary": "Will return the ONVIF capabilities for the specific camera.",
"operationId": "camera-onvif-capabilities",
@@ -50,7 +50,7 @@
"post": {
"description": "Will activate the desired ONVIF preset.",
"tags": [
"onvif"
"camera"
],
"summary": "Will activate the desired ONVIF preset.",
"operationId": "camera-onvif-gotopreset",
@@ -75,45 +75,11 @@
}
}
},
"/api/camera/onvif/inputs": {
"post": {
"security": [
{
"Bearer": []
}
],
"description": "Will get the digital inputs from the ONVIF device.",
"tags": [
"onvif"
],
"summary": "Will get the digital inputs from the ONVIF device.",
"operationId": "get-digital-inputs",
"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/login": {
"post": {
"description": "Try to login into ONVIF supported camera.",
"tags": [
"onvif"
"camera"
],
"summary": "Try to login into ONVIF supported camera.",
"operationId": "camera-onvif-login",
@@ -138,86 +104,11 @@
}
}
},
"/api/camera/onvif/outputs": {
"post": {
"security": [
{
"Bearer": []
}
],
"description": "Will get the relay outputs from the ONVIF device.",
"tags": [
"onvif"
],
"summary": "Will get the relay outputs from the ONVIF device.",
"operationId": "get-relay-outputs",
"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/outputs/{output}": {
"post": {
"security": [
{
"Bearer": []
}
],
"description": "Will trigger the relay output from the ONVIF device.",
"tags": [
"onvif"
],
"summary": "Will trigger the relay output from the ONVIF device.",
"operationId": "trigger-relay-output",
"parameters": [
{
"description": "OnvifCredentials",
"name": "config",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.OnvifCredentials"
}
},
{
"type": "string",
"description": "Output",
"name": "output",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.APIResponse"
}
}
}
}
},
"/api/camera/onvif/pantilt": {
"post": {
"description": "Panning or/and tilting the camera using a direction (x,y).",
"tags": [
"onvif"
"camera"
],
"summary": "Panning or/and tilting the camera.",
"operationId": "camera-onvif-pantilt",
@@ -246,7 +137,7 @@
"post": {
"description": "Will return the ONVIF presets for the specific camera.",
"tags": [
"onvif"
"camera"
],
"summary": "Will return the ONVIF presets for the specific camera.",
"operationId": "camera-onvif-presets",
@@ -271,45 +162,11 @@
}
}
},
"/api/camera/onvif/verify": {
"post": {
"security": [
{
"Bearer": []
}
],
"description": "Will verify the ONVIF connectivity.",
"tags": [
"onvif"
],
"summary": "Will verify the ONVIF connectivity.",
"operationId": "verify-onvif",
"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.",
"tags": [
"onvif"
"camera"
],
"summary": "Zooming in or out the camera.",
"operationId": "camera-onvif-zoom",
@@ -334,90 +191,6 @@
}
}
},
"/api/camera/record": {
"post": {
"description": "Make a recording.",
"tags": [
"camera"
],
"summary": "Make a recording.",
"operationId": "camera-record",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.APIResponse"
}
}
}
}
},
"/api/camera/restart": {
"post": {
"description": "Restart the agent.",
"tags": [
"camera"
],
"summary": "Restart the agent.",
"operationId": "camera-restart",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.APIResponse"
}
}
}
}
},
"/api/camera/snapshot/base64": {
"get": {
"description": "Get a snapshot from the camera in base64.",
"tags": [
"camera"
],
"summary": "Get a snapshot from the camera in base64.",
"operationId": "snapshot-base64",
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/camera/snapshot/jpeg": {
"get": {
"description": "Get a snapshot from the camera in jpeg format.",
"tags": [
"camera"
],
"summary": "Get a snapshot from the camera in jpeg format.",
"operationId": "snapshot-jpeg",
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/camera/stop": {
"post": {
"description": "Stop the agent.",
"tags": [
"camera"
],
"summary": "Stop the agent.",
"operationId": "camera-stop",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.APIResponse"
}
}
}
}
},
"/api/camera/verify/{streamType}": {
"post": {
"description": "This method will validate a specific profile connection from an RTSP camera, and try to get the codec.",
@@ -458,75 +231,6 @@
}
}
},
"/api/config": {
"get": {
"description": "Get the current configuration.",
"tags": [
"config"
],
"summary": "Get the current configuration.",
"operationId": "config",
"responses": {
"200": {
"description": "OK"
}
}
},
"post": {
"description": "Update the current configuration.",
"tags": [
"config"
],
"summary": "Update the current configuration.",
"operationId": "config",
"parameters": [
{
"description": "Configuration",
"name": "config",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.Config"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/dashboard": {
"get": {
"description": "Get all information showed on the dashboard.",
"tags": [
"general"
],
"summary": "Get all information showed on the dashboard.",
"operationId": "dashboard",
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/days": {
"get": {
"description": "Get all days stored in the recordings directory.",
"tags": [
"general"
],
"summary": "Get all days stored in the recordings directory.",
"operationId": "days",
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/hub/verify": {
"post": {
"security": [
@@ -536,7 +240,7 @@
],
"description": "Will verify the hub connectivity.",
"tags": [
"persistence"
"config"
],
"summary": "Will verify the hub connectivity.",
"operationId": "verify-hub",
@@ -561,32 +265,6 @@
}
}
},
"/api/latest-events": {
"post": {
"description": "Get the latest recordings (events) from the recordings directory.",
"tags": [
"general"
],
"summary": "Get the latest recordings (events) from the recordings directory.",
"operationId": "latest-events",
"parameters": [
{
"description": "Event filter",
"name": "eventFilter",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/models.EventFilter"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/login": {
"post": {
"description": "Get Authorization token.",
@@ -616,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": [
@@ -625,7 +337,7 @@
],
"description": "Will verify the persistence.",
"tags": [
"persistence"
"config"
],
"summary": "Will verify the persistence.",
"operationId": "verify-persistence",
@@ -785,9 +497,6 @@
"dropbox": {
"$ref": "#/definitions/models.Dropbox"
},
"encryption": {
"$ref": "#/definitions/models.Encryption"
},
"friendly_name": {
"type": "string"
},
@@ -795,9 +504,6 @@
"description": "obsolete",
"type": "string"
},
"hub_encryption": {
"type": "string"
},
"hub_key": {
"type": "string"
},
@@ -834,12 +540,6 @@
"offline": {
"type": "string"
},
"realtimeprocessing": {
"type": "string"
},
"realtimeprocessing_topic": {
"type": "string"
},
"region": {
"$ref": "#/definitions/models.Region"
},
@@ -864,9 +564,6 @@
"timezone": {
"type": "string"
},
"turn_force": {
"type": "string"
},
"turn_password": {
"type": "string"
},
@@ -903,49 +600,12 @@
}
}
},
"models.Encryption": {
"type": "object",
"properties": {
"enabled": {
"type": "string"
},
"fingerprint": {
"type": "string"
},
"private_key": {
"type": "string"
},
"recordings": {
"type": "string"
},
"symmetric_key": {
"type": "string"
}
}
},
"models.EventFilter": {
"type": "object",
"properties": {
"number_of_elements": {
"type": "integer"
},
"timestamp_offset_end": {
"type": "integer"
},
"timestamp_offset_start": {
"type": "integer"
}
}
},
"models.IPCamera": {
"type": "object",
"properties": {
"fps": {
"type": "string"
},
"height": {
"type": "integer"
},
"onvif": {
"type": "string"
},
@@ -961,20 +621,8 @@
"rtsp": {
"type": "string"
},
"sub_fps": {
"type": "string"
},
"sub_height": {
"type": "integer"
},
"sub_rtsp": {
"type": "string"
},
"sub_width": {
"type": "integer"
},
"width": {
"type": "integer"
}
}
},

View File

@@ -88,15 +88,11 @@ definitions:
type: string
dropbox:
$ref: '#/definitions/models.Dropbox'
encryption:
$ref: '#/definitions/models.Encryption'
friendly_name:
type: string
heartbeaturi:
description: obsolete
type: string
hub_encryption:
type: string
hub_key:
type: string
hub_private_key:
@@ -121,10 +117,6 @@ definitions:
type: string
offline:
type: string
realtimeprocessing:
type: string
realtimeprocessing_topic:
type: string
region:
$ref: '#/definitions/models.Region'
remove_after_upload:
@@ -141,8 +133,6 @@ definitions:
type: array
timezone:
type: string
turn_force:
type: string
turn_password:
type: string
turn_username:
@@ -166,34 +156,10 @@ definitions:
directory:
type: string
type: object
models.Encryption:
properties:
enabled:
type: string
fingerprint:
type: string
private_key:
type: string
recordings:
type: string
symmetric_key:
type: string
type: object
models.EventFilter:
properties:
number_of_elements:
type: integer
timestamp_offset_end:
type: integer
timestamp_offset_start:
type: integer
type: object
models.IPCamera:
properties:
fps:
type: string
height:
type: integer
onvif:
type: string
onvif_password:
@@ -204,16 +170,8 @@ definitions:
type: string
rtsp:
type: string
sub_fps:
type: string
sub_height:
type: integer
sub_rtsp:
type: string
sub_width:
type: integer
width:
type: integer
type: object
models.KStorage:
properties:
@@ -363,7 +321,7 @@ paths:
$ref: '#/definitions/models.APIResponse'
summary: Will return the ONVIF capabilities for the specific camera.
tags:
- onvif
- camera
/api/camera/onvif/gotopreset:
post:
description: Will activate the desired ONVIF preset.
@@ -382,28 +340,7 @@ paths:
$ref: '#/definitions/models.APIResponse'
summary: Will activate the desired ONVIF preset.
tags:
- onvif
/api/camera/onvif/inputs:
post:
description: Will get the digital inputs from the ONVIF device.
operationId: get-digital-inputs
parameters:
- description: OnvifCredentials
in: body
name: config
required: true
schema:
$ref: '#/definitions/models.OnvifCredentials'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.APIResponse'
security:
- Bearer: []
summary: Will get the digital inputs from the ONVIF device.
tags:
- onvif
- camera
/api/camera/onvif/login:
post:
description: Try to login into ONVIF supported camera.
@@ -422,54 +359,7 @@ paths:
$ref: '#/definitions/models.APIResponse'
summary: Try to login into ONVIF supported camera.
tags:
- onvif
/api/camera/onvif/outputs:
post:
description: Will get the relay outputs from the ONVIF device.
operationId: get-relay-outputs
parameters:
- description: OnvifCredentials
in: body
name: config
required: true
schema:
$ref: '#/definitions/models.OnvifCredentials'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.APIResponse'
security:
- Bearer: []
summary: Will get the relay outputs from the ONVIF device.
tags:
- onvif
/api/camera/onvif/outputs/{output}:
post:
description: Will trigger the relay output from the ONVIF device.
operationId: trigger-relay-output
parameters:
- description: OnvifCredentials
in: body
name: config
required: true
schema:
$ref: '#/definitions/models.OnvifCredentials'
- description: Output
in: path
name: output
required: true
type: string
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.APIResponse'
security:
- Bearer: []
summary: Will trigger the relay output from the ONVIF device.
tags:
- onvif
- camera
/api/camera/onvif/pantilt:
post:
description: Panning or/and tilting the camera using a direction (x,y).
@@ -488,7 +378,7 @@ paths:
$ref: '#/definitions/models.APIResponse'
summary: Panning or/and tilting the camera.
tags:
- onvif
- camera
/api/camera/onvif/presets:
post:
description: Will return the ONVIF presets for the specific camera.
@@ -507,28 +397,7 @@ paths:
$ref: '#/definitions/models.APIResponse'
summary: Will return the ONVIF presets for the specific camera.
tags:
- onvif
/api/camera/onvif/verify:
post:
description: Will verify the ONVIF connectivity.
operationId: verify-onvif
parameters:
- description: OnvifCredentials
in: body
name: config
required: true
schema:
$ref: '#/definitions/models.OnvifCredentials'
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.APIResponse'
security:
- Bearer: []
summary: Will verify the ONVIF connectivity.
tags:
- onvif
- camera
/api/camera/onvif/zoom:
post:
description: Zooming in or out the camera.
@@ -547,62 +416,6 @@ paths:
$ref: '#/definitions/models.APIResponse'
summary: Zooming in or out the camera.
tags:
- onvif
/api/camera/record:
post:
description: Make a recording.
operationId: camera-record
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.APIResponse'
summary: Make a recording.
tags:
- camera
/api/camera/restart:
post:
description: Restart the agent.
operationId: camera-restart
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.APIResponse'
summary: Restart the agent.
tags:
- camera
/api/camera/snapshot/base64:
get:
description: Get a snapshot from the camera in base64.
operationId: snapshot-base64
responses:
"200":
description: OK
summary: Get a snapshot from the camera in base64.
tags:
- camera
/api/camera/snapshot/jpeg:
get:
description: Get a snapshot from the camera in jpeg format.
operationId: snapshot-jpeg
responses:
"200":
description: OK
summary: Get a snapshot from the camera in jpeg format.
tags:
- camera
/api/camera/stop:
post:
description: Stop the agent.
operationId: camera-stop
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.APIResponse'
summary: Stop the agent.
tags:
- camera
/api/camera/verify/{streamType}:
post:
@@ -632,52 +445,6 @@ paths:
summary: Validate a specific RTSP profile camera connection.
tags:
- camera
/api/config:
get:
description: Get the current configuration.
operationId: config
responses:
"200":
description: OK
summary: Get the current configuration.
tags:
- config
post:
description: Update the current configuration.
operationId: config
parameters:
- description: Configuration
in: body
name: config
required: true
schema:
$ref: '#/definitions/models.Config'
responses:
"200":
description: OK
summary: Update the current configuration.
tags:
- config
/api/dashboard:
get:
description: Get all information showed on the dashboard.
operationId: dashboard
responses:
"200":
description: OK
summary: Get all information showed on the dashboard.
tags:
- general
/api/days:
get:
description: Get all days stored in the recordings directory.
operationId: days
responses:
"200":
description: OK
summary: Get all days stored in the recordings directory.
tags:
- general
/api/hub/verify:
post:
description: Will verify the hub connectivity.
@@ -698,24 +465,7 @@ paths:
- Bearer: []
summary: Will verify the hub connectivity.
tags:
- persistence
/api/latest-events:
post:
description: Get the latest recordings (events) from the recordings directory.
operationId: latest-events
parameters:
- description: Event filter
in: body
name: eventFilter
required: true
schema:
$ref: '#/definitions/models.EventFilter'
responses:
"200":
description: OK
summary: Get the latest recordings (events) from the recordings directory.
tags:
- general
- config
/api/login:
post:
description: Get Authorization token.
@@ -735,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.
@@ -755,7 +526,7 @@ paths:
- Bearer: []
summary: Will verify the persistence.
tags:
- persistence
- config
securityDefinitions:
Bearer:
in: header

View File

@@ -1,140 +1,145 @@
module github.com/kerberos-io/agent/machinery
go 1.24.2
go 1.19
replace google.golang.org/genproto => google.golang.org/genproto v0.0.0-20250519155744-55703ea1f237
// replace github.com/kerberos-io/joy4 v1.0.57 => ../../../../github.com/kerberos-io/joy4
// replace github.com/kerberos-io/onvif v0.0.6 => ../../../../github.com/kerberos-io/onvif
require (
github.com/Eyevinn/mp4ff v0.48.0
github.com/InVisionApp/conjungo v1.1.0
github.com/appleboy/gin-jwt/v2 v2.10.3
github.com/bluenviron/gortsplib/v4 v4.14.1
github.com/bluenviron/mediacommon v1.14.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/dromara/carbon/v2 v2.6.8
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.5.0
github.com/elastic/go-sysinfo v1.15.3
github.com/gin-contrib/cors v1.7.5
github.com/gin-contrib/pprof v1.5.3
github.com/gin-gonic/contrib v0.0.0-20250521004450-2b1292699c15
github.com/gin-gonic/gin v1.10.1
github.com/gofrs/uuid v4.4.0+incompatible
github.com/golang-jwt/jwt/v4 v4.5.2
github.com/gorilla/websocket v1.5.3
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/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.64
github.com/kerberos-io/onvif v1.0.0
github.com/kerberos-io/joy4 v1.0.58
github.com/kerberos-io/onvif v0.0.7
github.com/minio/minio-go/v6 v6.0.57
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
github.com/nsmith5/mjpeg v0.0.0-20200913181537-54b8ada0e53e
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/pion/interceptor v0.1.40
github.com/pion/rtp v1.8.19
github.com/pion/webrtc/v4 v4.1.2
github.com/sirupsen/logrus v1.9.3
github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.0
github.com/swaggo/swag v1.16.4
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 v1.4.0
go.mongodb.org/mongo-driver v1.17.3
go.opentelemetry.io/otel v1.36.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0
go.opentelemetry.io/otel/sdk v1.36.0
go.opentelemetry.io/otel/trace v1.36.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
go.mongodb.org/mongo-driver v1.7.5
gopkg.in/DataDog/dd-trace-go.v1 v1.46.0
gopkg.in/natefinch/lumberjack.v2 v2.0.0
)
require (
github.com/DataDog/datadog-agent/pkg/obfuscate v0.0.0-20211129110424-6491aa3bf583 // indirect
github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.42.0-rc.1 // indirect
github.com/DataDog/datadog-go v4.8.2+incompatible // indirect
github.com/DataDog/datadog-go/v5 v5.0.2 // indirect
github.com/DataDog/go-tuf v0.3.0--fix-localmeta-fork // indirect
github.com/DataDog/gostackparse v0.5.0 // indirect
github.com/DataDog/sketches-go v1.2.1 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
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/beevik/etree v1.2.0 // indirect
github.com/bluenviron/mediacommon/v2 v2.2.0 // indirect
github.com/bytedance/sonic v1.13.2 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cenkalti/backoff/v5 v5.0.2 // 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
github.com/clbanning/mxj/v2 v2.7.0 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/elastic/go-windows v1.0.2 // indirect
github.com/dgraph-io/ristretto v0.1.0 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/elastic/go-windows v1.0.0 // indirect
github.com/elgs/gostrgen v0.0.0-20161222160715-9d61ae07eeae // indirect
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gin-contrib/sse v1.0.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.6 // indirect
github.com/go-openapi/spec v0.20.4 // indirect
github.com/go-openapi/swag v0.19.15 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.26.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
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/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
github.com/icholy/digest v0.1.23 // 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/juju/errors v1.0.0 // indirect
github.com/klauspost/compress v1.16.7 // indirect
github.com/klauspost/compress v1.15.0 // indirect
github.com/klauspost/cpuid v1.2.3 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/kylelemons/go-gypsy v1.0.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/lib/pq v1.10.7 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/minio/md5-simd v1.1.0 // indirect
github.com/minio/sha256-simd v0.1.1 // indirect
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/montanaflynn/stats v0.7.1 // indirect
github.com/nxadm/tail v1.4.11 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pion/datachannel v1.5.10 // indirect
github.com/pion/dtls/v3 v3.0.6 // indirect
github.com/pion/ice/v4 v4.0.10 // indirect
github.com/pion/logging v0.2.3 // indirect
github.com/pion/mdns/v2 v2.0.7 // 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
github.com/pion/dtls/v2 v2.1.5 // indirect
github.com/pion/ice/v2 v2.2.12 // indirect
github.com/pion/interceptor v0.1.11 // indirect
github.com/pion/logging v0.2.2 // indirect
github.com/pion/mdns v0.0.5 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/rtcp v1.2.15 // indirect
github.com/pion/sctp v1.8.39 // indirect
github.com/pion/sdp/v3 v3.0.13 // indirect
github.com/pion/srtp/v3 v3.0.5 // indirect
github.com/pion/stun/v3 v3.0.0 // indirect
github.com/pion/transport/v3 v3.0.7 // indirect
github.com/pion/turn/v4 v4.0.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
github.com/wlynxg/anet v0.0.5 // indirect
github.com/pion/rtcp v1.2.10 // 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
github.com/pion/stun v0.3.5 // indirect
github.com/pion/transport v0.14.1 // indirect
github.com/pion/turn/v2 v2.0.8 // indirect
github.com/pion/udp v0.1.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
github.com/richardartoul/molecule v1.0.1-0.20221107223329-32cfee06a052 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.4.0 // indirect
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.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // 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
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel/metric v1.36.0 // indirect
go.opentelemetry.io/proto/otlp v1.6.0 // indirect
golang.org/x/arch v0.16.0 // indirect
golang.org/x/crypto v0.38.0 // indirect
golang.org/x/net v0.40.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sync v0.14.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.25.0 // indirect
golang.org/x/tools v0.30.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect
google.golang.org/grpc v1.72.1 // indirect
google.golang.org/protobuf v1.36.6 // 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.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.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
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
howett.net/plist v0.0.0-20181124034731-591f970eefbb // indirect
inet.af/netaddr v0.0.0-20220617031823-097006376321 // indirect
)

File diff suppressed because it is too large Load Diff

View File

@@ -3,70 +3,53 @@ package main
import (
"context"
"flag"
"fmt"
"os"
"time"
"github.com/kerberos-io/agent/machinery/src/capture"
"github.com/kerberos-io/agent/machinery/src/components"
"github.com/kerberos-io/agent/machinery/src/log"
"github.com/kerberos-io/agent/machinery/src/models"
"github.com/kerberos-io/agent/machinery/src/onvif"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/sdk/resource"
"go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
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"
"gopkg.in/DataDog/dd-trace-go.v1/profiler"
)
var VERSION = utils.VERSION
func startTracing(agentKey string, otelEndpoint string) (*trace.TracerProvider, error) {
serviceName := "agent-" + agentKey
headers := map[string]string{
"content-type": "application/json",
}
exporter, err := otlptrace.New(
context.Background(),
otlptracehttp.NewClient(
otlptracehttp.WithEndpoint(otelEndpoint),
otlptracehttp.WithHeaders(headers),
otlptracehttp.WithInsecure(),
),
)
if err != nil {
return nil, fmt.Errorf("creating new exporter: %w", err)
}
tracerprovider := trace.NewTracerProvider(
trace.WithBatcher(
exporter,
trace.WithMaxExportBatchSize(trace.DefaultMaxExportBatchSize),
trace.WithBatchTimeout(trace.DefaultScheduleDelay*time.Millisecond),
trace.WithMaxExportBatchSize(trace.DefaultMaxExportBatchSize),
),
trace.WithResource(
resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String(serviceName),
attribute.String("environment", "develop"),
),
),
)
otel.SetTracerProvider(tracerprovider)
return tracerprovider, nil
}
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" {
tracer.Start()
defer tracer.Stop()
} else {
service := os.Getenv("DATADOG_AGENT_SERVICE")
environment := os.Getenv("DATADOG_AGENT_ENVIRONMENT")
log.Log.Info("Starting Datadog Agent with service: " + service + " and environment: " + environment)
rules := []tracer.SamplingRule{tracer.RateRule(1)}
tracer.Start(
tracer.WithSamplingRules(rules),
tracer.WithService(service),
tracer.WithEnv(environment),
)
defer tracer.Stop()
err := profiler.Start(
profiler.WithService(service),
profiler.WithEnv(environment),
profiler.WithProfileTypes(
profiler.CPUProfile,
profiler.HeapProfile,
),
)
if err != nil {
log.Log.Fatal(err.Error())
}
defer profiler.Stop()
}
}
// Start the show ;)
// We'll parse the flags (named variables), and start the agent.
@@ -84,56 +67,20 @@ func main() {
flag.StringVar(&timeout, "timeout", "2000", "Number of milliseconds to wait for the ONVIF discovery to complete")
flag.Parse()
// Specify the level of loggin: "info", "warning", "debug", "error" or "fatal."
logLevel := os.Getenv("LOG_LEVEL")
if logLevel == "" {
logLevel = "info"
}
// Specify the output formatter of the log: "text" or "json".
logOutput := os.Getenv("LOG_OUTPUT")
if logOutput == "" {
logOutput = "text"
}
// Specify the timezone of the log: "UTC" or "Local".
timezone, _ := time.LoadLocation("CET")
log.Log.Init(logLevel, logOutput, configDirectory, timezone)
log.Log.Init(configDirectory, timezone)
switch action {
case "version":
{
log.Log.Info("main.Main(): You are currrently running Kerberos Agent " + VERSION)
}
log.Log.Info("You are currrently running Kerberos Agent " + VERSION)
case "discover":
{
// Convert duration to int
timeout, err := time.ParseDuration(timeout + "ms")
if err != nil {
log.Log.Fatal("main.Main(): could not parse timeout: " + err.Error())
return
}
onvif.Discover(timeout)
}
case "decrypt":
{
log.Log.Info("main.Main(): Decrypting: " + flag.Arg(0) + " with key: " + flag.Arg(1))
symmetricKey := []byte(flag.Arg(1))
if len(symmetricKey) == 0 {
log.Log.Fatal("main.Main(): symmetric key should not be empty")
return
}
if len(symmetricKey) != 32 {
log.Log.Fatal("main.Main(): symmetric key should be 32 bytes")
return
}
utils.Decrypt(flag.Arg(0), symmetricKey)
}
log.Log.Info(timeout)
case "run":
{
// Print Agent ASCII art
// Print Kerberos.io ASCII art
utils.PrintASCIIArt()
// Print the environment variables which include "AGENT_" as prefix.
@@ -146,29 +93,12 @@ func main() {
configuration.Name = name
configuration.Port = port
// Open this configuration either from Agent or Factory.
// Open this configuration either from Kerberos Agent or Kerberos Factory.
configService.OpenConfig(configDirectory, &configuration)
// We will override the configuration with the environment variables
configService.OverrideWithEnvironmentVariables(&configuration)
// Start OpenTelemetry tracing
if otelEndpoint := os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT"); otelEndpoint == "" {
log.Log.Info("main.Main(): No OpenTelemetry endpoint provided, skipping tracing")
} else {
log.Log.Info("main.Main(): Starting OpenTelemetry tracing with endpoint: " + otelEndpoint)
agentKey := configuration.Config.Key
traceProvider, err := startTracing(agentKey, otelEndpoint)
if err != nil {
log.Log.Error("traceprovider: " + err.Error())
}
defer func() {
if err := traceProvider.Shutdown(context.Background()); err != nil {
log.Log.Error("traceprovider: " + err.Error())
}
}()
}
// Printing final configuration
utils.PrintConfiguration(&configuration)
@@ -178,7 +108,7 @@ func main() {
// Set timezone
timezone, _ := time.LoadLocation(configuration.Config.Timezone)
log.Log.Init(logLevel, logOutput, configDirectory, timezone)
log.Log.Init(configDirectory, timezone)
// Check if we have a device Key or not, if not
// we will generate one.
@@ -187,9 +117,9 @@ func main() {
configuration.Config.Key = key
err := configService.StoreConfig(configDirectory, configuration.Config)
if err == nil {
log.Log.Info("main.Main(): updated unique key for agent to: " + key)
log.Log.Info("Main: updated unique key for agent to: " + key)
} else {
log.Log.Info("main.Main(): something went wrong while trying to store key: " + key)
log.Log.Info("Main: something went wrong while trying to store key: " + key)
}
}
@@ -197,28 +127,18 @@ func main() {
// This is used to restart the agent when the configuration is updated.
ctx, cancel := context.WithCancel(context.Background())
// We create a capture object, this will contain all the streaming clients.
// And allow us to extract media from within difference places in the agent.
capture := capture.Capture{
RTSPClient: nil,
RTSPSubClient: nil,
}
// Bootstrapping the agent
communication := models.Communication{
Context: &ctx,
CancelContext: &cancel,
HandleBootstrap: make(chan string, 1),
}
go components.Bootstrap(ctx, configDirectory, &configuration, &communication, &capture)
go components.Bootstrap(configDirectory, &configuration, &communication)
// Start the REST API.
routers.StartWebserver(configDirectory, &configuration, &communication, &capture)
routers.StartWebserver(configDirectory, &configuration, &communication)
}
default:
{
log.Log.Error("main.Main(): Sorry I don't understand :(")
}
log.Log.Error("Main: Sorry I don't understand :(")
}
}

View File

@@ -0,0 +1 @@
package api

View File

@@ -0,0 +1,150 @@
package capture
import (
"context"
"strconv"
"sync"
"time"
"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/kerberos-io/joy4/av"
"github.com/kerberos-io/joy4/av/avutil"
"github.com/kerberos-io/joy4/cgo/ffmpeg"
"github.com/kerberos-io/joy4/format"
)
func OpenRTSP(ctx context.Context, url string) (av.DemuxCloser, []av.CodecData, error) {
format.RegisterAll()
infile, err := avutil.Open(ctx, url)
if err == nil {
streams, errstreams := infile.Streams()
return infile, streams, errstreams
}
return nil, []av.CodecData{}, err
}
func GetVideoStream(streams []av.CodecData) (av.CodecData, error) {
var videoStream av.CodecData
for _, stream := range streams {
if stream.Type().IsAudio() {
//astream := stream.(av.AudioCodecData)
} else if stream.Type().IsVideo() {
videoStream = stream
}
}
return videoStream, nil
}
func GetVideoDecoder(decoder *ffmpeg.VideoDecoder, streams []av.CodecData) {
// Load video codec
var vstream av.VideoCodecData
for _, stream := range streams {
if stream.Type().IsAudio() {
//astream := stream.(av.AudioCodecData)
} else if stream.Type().IsVideo() {
vstream = stream.(av.VideoCodecData)
}
}
err := ffmpeg.NewVideoDecoder(decoder, vstream)
if err != nil {
log.Log.Error("GetVideoDecoder: " + err.Error())
}
}
func DecodeImage(frame *ffmpeg.VideoFrame, pkt av.Packet, decoder *ffmpeg.VideoDecoder, decoderMutex *sync.Mutex) (*ffmpeg.VideoFrame, error) {
decoderMutex.Lock()
img, err := decoder.Decode(frame, pkt.Data)
decoderMutex.Unlock()
return img, err
}
func HandleStream(infile av.DemuxCloser, queue *pubsub.Queue, communication *models.Communication) { //, wg *sync.WaitGroup) {
log.Log.Debug("HandleStream: started")
var err error
loop:
for {
// This will check if we need to stop the thread,
// because of a reconfiguration.
select {
case <-communication.HandleStream:
break loop
default:
}
var pkt av.Packet
if pkt, err = infile.ReadPacket(); err != nil { // sometimes this throws an end of file..
log.Log.Error("HandleStream: " + err.Error())
time.Sleep(1 * time.Second)
}
// Could be that a decode is throwing errors.
if len(pkt.Data) > 0 {
queue.WritePacket(pkt)
// This will check if we need to stop the thread,
// because of a reconfiguration.
select {
case <-communication.HandleStream:
break loop
default:
}
if pkt.IsKeyFrame {
// Increment packets, so we know the device
// is not blocking.
r := communication.PackageCounter.Load().(int64)
log.Log.Info("HandleStream: packet size " + strconv.Itoa(len(pkt.Data)))
communication.PackageCounter.Store((r + 1) % 1000)
communication.LastPacketTimer.Store(time.Now().Unix())
}
}
}
queue.Close()
log.Log.Debug("HandleStream: finished")
}
func HandleSubStream(infile av.DemuxCloser, queue *pubsub.Queue, communication *models.Communication) { //, wg *sync.WaitGroup) {
log.Log.Debug("HandleSubStream: started")
var err error
loop:
for {
// This will check if we need to stop the thread,
// because of a reconfiguration.
select {
case <-communication.HandleSubStream:
break loop
default:
}
var pkt av.Packet
if pkt, err = infile.ReadPacket(); err != nil { // sometimes this throws an end of file..
log.Log.Error("HandleSubStream: " + err.Error())
time.Sleep(1 * time.Second)
}
// Could be that a decode is throwing errors.
if len(pkt.Data) > 0 {
queue.WritePacket(pkt)
// This will check if we need to stop the thread,
// because of a reconfiguration.
select {
case <-communication.HandleSubStream:
break loop
default:
}
}
}
queue.Close()
log.Log.Debug("HandleSubStream: finished")
}

View File

@@ -1,72 +0,0 @@
package capture
import (
"context"
"image"
"github.com/kerberos-io/agent/machinery/src/models"
"github.com/kerberos-io/agent/machinery/src/packets"
)
type Capture struct {
RTSPClient *Golibrtsp
RTSPSubClient *Golibrtsp
RTSPBackChannelClient *Golibrtsp
}
func (c *Capture) SetMainClient(rtspUrl string) *Golibrtsp {
c.RTSPClient = &Golibrtsp{
Url: rtspUrl,
}
return c.RTSPClient
}
func (c *Capture) SetSubClient(rtspUrl string) *Golibrtsp {
c.RTSPSubClient = &Golibrtsp{
Url: rtspUrl,
}
return c.RTSPSubClient
}
func (c *Capture) SetBackChannelClient(rtspUrl string) *Golibrtsp {
c.RTSPBackChannelClient = &Golibrtsp{
Url: rtspUrl,
}
return c.RTSPBackChannelClient
}
// RTSPClient is a interface that abstracts the RTSP client implementation.
type RTSPClient interface {
// Connect to the RTSP server.
Connect(ctx context.Context, otelContext context.Context) error
// Connect to a backchannel RTSP server.
ConnectBackChannel(ctx context.Context, otelContext context.Context) error
// Start the RTSP client, and start reading packets.
Start(ctx context.Context, streamType string, queue *packets.Queue, configuration *models.Configuration, communication *models.Communication) error
// Start the RTSP client, and start reading packets.
StartBackChannel(ctx context.Context, otelContext context.Context) error
// Decode a packet into a image.
DecodePacket(pkt packets.Packet) (image.YCbCr, error)
// Decode a packet into a image.
DecodePacketRaw(pkt packets.Packet) (image.Gray, error)
// Write a packet to the RTSP server.
WritePacket(pkt packets.Packet) error
// Close the connection to the RTSP server.
Close(ctx context.Context) error
// Get a list of streams from the RTSP server.
GetStreams() ([]packets.Stream, error)
// Get a list of video streams from the RTSP server.
GetVideoStreams() ([]packets.Stream, error)
// Get a list of audio streams from the RTSP server.
GetAudioStreams() ([]packets.Stream, error)
}

File diff suppressed because it is too large Load Diff

View File

@@ -3,21 +3,18 @@ package capture
import (
"context"
"encoding/base64"
"image"
"os"
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/kerberos-io/agent/machinery/src/conditions"
"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/packets"
"github.com/kerberos-io/agent/machinery/src/utils"
"github.com/kerberos-io/agent/machinery/src/video"
"go.opentelemetry.io/otel/trace"
"github.com/kerberos-io/joy4/av/pubsub"
"github.com/kerberos-io/joy4/format/mp4"
"github.com/kerberos-io/joy4/av"
)
func CleanupRecordingDirectory(configDirectory string, configuration *models.Configuration) {
@@ -54,76 +51,48 @@ func CleanupRecordingDirectory(configDirectory string, configuration *models.Con
}
}
func HandleRecordStream(queue *packets.Queue, configDirectory string, configuration *models.Configuration, communication *models.Communication, rtspClient RTSPClient) {
func HandleRecordStream(queue *pubsub.Queue, configDirectory string, configuration *models.Configuration, communication *models.Communication, streams []av.CodecData) {
config := configuration.Config
loc, _ := time.LoadLocation(config.Timezone)
if config.Capture.Recording == "false" {
log.Log.Info("capture.main.HandleRecordStream(): disabled, we will not record anything.")
log.Log.Info("HandleRecordStream: disabled, we will not record anything.")
} else {
log.Log.Debug("capture.main.HandleRecordStream(): started")
log.Log.Debug("HandleRecordStream: started")
preRecording := config.Capture.PreRecording * 1000
postRecording := config.Capture.PostRecording * 1000 // number of seconds to record.
maxRecordingPeriod := config.Capture.MaxLengthRecording * 1000 // maximum number of seconds to record.
recordingPeriod := config.Capture.PostRecording // number of seconds to record.
maxRecordingPeriod := config.Capture.MaxLengthRecording // maximum number of seconds to record.
// We will calculate the maxRecordingPeriod based on the preRecording and postRecording values.
if maxRecordingPeriod == 0 {
// If maxRecordingPeriod is not set, we will use the preRecording and postRecording values
maxRecordingPeriod = preRecording + postRecording
}
if maxRecordingPeriod < preRecording+postRecording {
log.Log.Error("capture.main.HandleRecordStream(): maxRecordingPeriod is less than preRecording + postRecording, this is not allowed. Setting maxRecordingPeriod to preRecording + postRecording.")
maxRecordingPeriod = preRecording + postRecording
}
if config.FriendlyName != "" {
config.Name = config.FriendlyName
}
// Get the audio and video codec from the camera.
// We only expect one audio and one video codec.
// If there are multiple audio or video streams, we will use the first one.
audioCodec := ""
videoCodec := ""
audioStreams, _ := rtspClient.GetAudioStreams()
videoStreams, _ := rtspClient.GetVideoStreams()
if len(audioStreams) > 0 {
audioCodec = audioStreams[0].Name
config.Capture.IPCamera.SampleRate = audioStreams[0].SampleRate
config.Capture.IPCamera.Channels = audioStreams[0].Channels
}
if len(videoStreams) > 0 {
videoCodec = videoStreams[0].Name
}
// Synchronise the last synced time
now := time.Now().Unix()
startRecording := now
timestamp := now
// Check if continuous recording.
if config.Capture.Continuous == "true" {
//var cws *cacheWriterSeeker
var mp4Video *video.MP4
var videoTrack uint32
var audioTrack uint32
var name string
// Do not do anything!
log.Log.Info("capture.main.HandleRecordStream(continuous): start recording")
log.Log.Info("HandleRecordStream: Start continuous recording ")
loc, _ := time.LoadLocation(config.Timezone)
now = time.Now().Unix()
timestamp = now
start := false
var name string
var myMuxer *mp4.Muxer
var file *os.File
var err error
// If continuous record the full length
postRecording = maxRecordingPeriod
recordingPeriod = maxRecordingPeriod
// Recording file name
fullName := ""
var startRecording int64 = 0 // start recording timestamp in milliseconds
// Get as much packets we need.
//for pkt := range packets {
var cursorError error
var pkt packets.Packet
var nextPkt packets.Packet
var pkt av.Packet
var nextPkt av.Packet
recordingStatus := "idle"
recordingCursor := queue.Oldest()
@@ -135,101 +104,33 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
nextPkt, cursorError = recordingCursor.ReadPacket()
now := time.Now().UnixMilli()
now := time.Now().Unix()
if start && // If already recording and current frame is a keyframe and we should stop recording
nextPkt.IsKeyFrame && (startRecording+postRecording-now <= 0 || now-startRecording > maxRecordingPeriod-500) {
nextPkt.IsKeyFrame && (timestamp+recordingPeriod-now <= 0 || now-startRecording >= maxRecordingPeriod) {
pts := convertPTS(pkt.TimeLegacy)
if pkt.IsVideo {
// Write the last packet
if err := mp4Video.AddSampleToTrack(videoTrack, pkt.IsKeyFrame, pkt.Data, pts); err != nil {
log.Log.Error("capture.main.HandleRecordStream(continuous): " + err.Error())
}
} else if pkt.IsAudio {
// Write the last packet
if pkt.Codec == "AAC" {
if err := mp4Video.AddSampleToTrack(audioTrack, pkt.IsKeyFrame, pkt.Data, pts); err != nil {
log.Log.Error("capture.main.HandleRecordStream(continuous): " + err.Error())
}
} else if pkt.Codec == "PCM_MULAW" {
// TODO: transcode to AAC, some work to do..
log.Log.Debug("capture.main.HandleRecordStream(continuous): no AAC audio codec detected, skipping audio track.")
}
// Write the last packet
if err := myMuxer.WritePacket(pkt); err != nil {
log.Log.Error(err.Error())
}
// Close mp4
if len(mp4Video.SPSNALUs) == 0 && len(configuration.Config.Capture.IPCamera.SPSNALUs) > 0 {
mp4Video.SPSNALUs = configuration.Config.Capture.IPCamera.SPSNALUs
// This will write the trailer a well.
if err := myMuxer.WriteTrailerWithPacket(nextPkt); err != nil {
log.Log.Error(err.Error())
}
if len(mp4Video.PPSNALUs) == 0 && len(configuration.Config.Capture.IPCamera.PPSNALUs) > 0 {
mp4Video.PPSNALUs = configuration.Config.Capture.IPCamera.PPSNALUs
}
if len(mp4Video.VPSNALUs) == 0 && len(configuration.Config.Capture.IPCamera.VPSNALUs) > 0 {
mp4Video.VPSNALUs = configuration.Config.Capture.IPCamera.VPSNALUs
}
if (videoCodec == "H264" && (len(mp4Video.SPSNALUs) == 0 || len(mp4Video.PPSNALUs) == 0)) ||
(videoCodec == "H265" && (len(mp4Video.VPSNALUs) == 0 || len(mp4Video.SPSNALUs) == 0 || len(mp4Video.PPSNALUs) == 0)) {
log.Log.Warning("capture.main.HandleRecordStream(continuous): closing MP4 without full parameter sets, moov may be incomplete")
}
mp4Video.Close(&config)
log.Log.Info("capture.main.HandleRecordStream(continuous): recording finished: file save: " + name)
log.Log.Info("HandleRecordStream: Recording finished: file save: " + name)
// Cleanup muxer
start = false
myMuxer.Close()
myMuxer = nil
file.Close()
file = nil
// Update the name of the recording with the duration.
// We will update the name of the recording with the duration in milliseconds.
if mp4Video.VideoTotalDuration > 0 {
duration := mp4Video.VideoTotalDuration
// Update the name with the duration in milliseconds.
startRecordingSeconds := startRecording / 1000 // convert to seconds
startRecordingMilliseconds := startRecording % 1000 // convert to milliseconds
s := strconv.FormatInt(startRecordingSeconds, 10) + "_" +
strconv.Itoa(len(strconv.FormatInt(startRecordingMilliseconds, 10))) + "-" +
strconv.FormatInt(startRecordingMilliseconds, 10) + "_" +
config.Name + "_" +
"0-0-0-0" + "_" + // region coordinates, we
"-1" + "_" + // token
strconv.FormatInt(int64(duration), 10) // + "_" + // duration of recording
//utils.VERSION // version of the agent
oldName := name
name = s + ".mp4"
fullName = configDirectory + "/data/recordings/" + name
log.Log.Info("capture.main.HandleRecordStream(motiondetection): renamed file from: " + oldName + " to: " + name)
// Rename the file to the new name.
err := os.Rename(
configDirectory+"/data/recordings/"+oldName,
configDirectory+"/data/recordings/"+s+".mp4")
if err != nil {
log.Log.Error("capture.main.HandleRecordStream(motiondetection): error renaming file: " + err.Error())
}
} else {
log.Log.Info("capture.main.HandleRecordStream(continuous): no video data recorded, not renaming file.")
}
// 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("capture.main.HandleRecordStream(continuous): error writing file: " + err.Error())
}
} else {
log.Log.Error("capture.main.HandleRecordStream(continuous): error encrypting file: " + err.Error())
}
} else {
log.Log.Error("capture.main.HandleRecordStream(continuous): error reading file: " + err.Error())
}
// Check if need to convert to fragmented using bento
if config.Capture.Fragmented == "true" && config.Capture.FragmentedDuration > 0 {
utils.CreateFragmentedMP4(fullName, config.Capture.FragmentedDuration)
}
// Create a symbol link.
@@ -245,16 +146,33 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
// If not yet started and a keyframe, let's make a recording
if !start && pkt.IsKeyFrame {
// We might have different conditions enabled such as time window or uri response.
// We'll validate those conditions and if not valid we'll not do anything.
valid, err := conditions.Validate(loc, configuration)
if !valid && err != nil {
log.Log.Debug("capture.main.HandleRecordStream(continuous): " + err.Error() + ".")
time.Sleep(5 * time.Second)
continue
// Check if within time interval
nowInTimezone := time.Now().In(loc)
weekday := nowInTimezone.Weekday()
hour := nowInTimezone.Hour()
minute := nowInTimezone.Minute()
second := nowInTimezone.Second()
timeEnabled := config.Time
timeInterval := config.Timetable[int(weekday)]
if timeEnabled == "true" && 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 {
log.Log.Debug("HandleRecordStream: Disabled: no continuous recording at this moment. Not within specified time interval.")
time.Sleep(5 * time.Second)
continue
}
}
start = true
timestamp = now
// timestamp_microseconds_instanceName_regionCoordinates_numberOfChanges_token
// 1564859471_6-474162_oprit_577-283-727-375_1153_27.mp4
@@ -265,90 +183,55 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
// - Number of changes
// - Token
startRecording = pkt.CurrentTime
startRecordingSeconds := startRecording / 1000 // convert to seconds
startRecordingMilliseconds := startRecording % 1000 // convert to milliseconds
s := strconv.FormatInt(startRecordingSeconds, 10) + "_" + // start timestamp in seconds
strconv.Itoa(len(strconv.FormatInt(startRecordingMilliseconds, 10))) + "-" + // length of milliseconds
strconv.FormatInt(startRecordingMilliseconds, 10) + "_" + // milliseconds
config.Name + "_" + // device name
"0-0-0-0" + "_" + // region coordinates, we will not use this for continuous recording
"0" + "_" + // token
"0" + "_" //+ // duration of recording in milliseconds
//utils.VERSION // version of the agent
startRecording = time.Now().Unix() // we mark the current time when the record started.ss
s := strconv.FormatInt(startRecording, 10) + "_" +
"6" + "-" +
"967003" + "_" +
config.Name + "_" +
"200-200-400-400" + "_0_" +
"769"
name = s + ".mp4"
fullName = configDirectory + "/data/recordings/" + name
// Running...
log.Log.Info("capture.main.HandleRecordStream(continuous): recording started")
log.Log.Info("Recording started")
// Get width and height from the camera.
width := configuration.Config.Capture.IPCamera.Width
height := configuration.Config.Capture.IPCamera.Height
// Get SPS and PPS NALUs from the camera.
spsNALUS := configuration.Config.Capture.IPCamera.SPSNALUs
ppsNALUS := configuration.Config.Capture.IPCamera.PPSNALUs
vpsNALUS := configuration.Config.Capture.IPCamera.VPSNALUs
if len(spsNALUS) == 0 || len(ppsNALUS) == 0 {
log.Log.Warning("capture.main.HandleRecordStream(continuous): missing SPS/PPS at recording start")
}
// Create a video file, and set the dimensions.
mp4Video = video.NewMP4(fullName, spsNALUS, ppsNALUS, vpsNALUS, configuration.Config.Capture.MaxLengthRecording)
mp4Video.SetWidth(width)
mp4Video.SetHeight(height)
if videoCodec == "H264" {
videoTrack = mp4Video.AddVideoTrack("H264")
} else if videoCodec == "H265" {
videoTrack = mp4Video.AddVideoTrack("H265")
}
if audioCodec == "AAC" {
audioTrack = mp4Video.AddAudioTrack("AAC")
} else if audioCodec == "PCM_MULAW" {
log.Log.Debug("capture.main.HandleRecordStream(continuous): no AAC audio codec detected, skipping audio track.")
file, err = os.Create(fullName)
if err == nil {
myMuxer = mp4.NewMuxer(file)
}
pts := convertPTS(pkt.TimeLegacy)
if pkt.IsVideo {
if err := mp4Video.AddSampleToTrack(videoTrack, pkt.IsKeyFrame, pkt.Data, pts); err != nil {
log.Log.Error("capture.main.HandleRecordStream(continuous): " + err.Error())
}
} else if pkt.IsAudio {
if pkt.Codec == "AAC" {
if err := mp4Video.AddSampleToTrack(audioTrack, pkt.IsKeyFrame, pkt.Data, pts); err != nil {
log.Log.Error("capture.main.HandleRecordStream(continuous): " + err.Error())
}
} else if pkt.Codec == "PCM_MULAW" {
// TODO: transcode to AAC, some work to do..
// We might need to use ffmpeg to transcode the audio to AAC.
// For now we will skip the audio track.
log.Log.Debug("capture.main.HandleRecordStream(continuous): no AAC audio codec detected, skipping audio track.")
}
log.Log.Info("HandleRecordStream: composing recording")
log.Log.Info("HandleRecordStream: write header")
// Creating the file, might block sometimes.
if err := myMuxer.WriteHeader(streams); err != nil {
log.Log.Error(err.Error())
}
if err := myMuxer.WritePacket(pkt); err != nil {
log.Log.Error(err.Error())
}
recordingStatus = "started"
} else if start {
if err := myMuxer.WritePacket(pkt); err != nil {
log.Log.Error(err.Error())
}
pts := convertPTS(pkt.TimeLegacy)
if pkt.IsVideo {
// New method using new mp4 library
if err := mp4Video.AddSampleToTrack(videoTrack, pkt.IsKeyFrame, pkt.Data, pts); err != nil {
log.Log.Error("capture.main.HandleRecordStream(continuous): " + err.Error())
}
} else if pkt.IsAudio {
if pkt.Codec == "AAC" {
if err := mp4Video.AddSampleToTrack(audioTrack, pkt.IsKeyFrame, pkt.Data, pts); err != nil {
log.Log.Error("capture.main.HandleRecordStream(continuous): " + err.Error())
}
} else if pkt.Codec == "PCM_MULAW" {
// TODO: transcode to AAC, some work to do..
log.Log.Debug("capture.main.HandleRecordStream(continuous): no AAC audio codec detected, skipping audio track.")
// We will sync to file every keyframe.
if pkt.IsKeyFrame {
err := file.Sync()
if err != nil {
log.Log.Error(err.Error())
} else {
log.Log.Info("HandleRecordStream: Synced file: " + name)
}
}
}
pkt = nextPkt
}
@@ -357,63 +240,22 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
if cursorError != nil {
if recordingStatus == "started" {
log.Log.Info("capture.main.HandleRecordStream(continuous): Recording finished: file save: " + name)
// Cleanup muxer
start = false
// Update the name of the recording with the duration.
// We will update the name of the recording with the duration in milliseconds.
if mp4Video.VideoTotalDuration > 0 {
duration := mp4Video.VideoTotalDuration
// Update the name with the duration in milliseconds.
startRecordingSeconds := startRecording / 1000 // convert to seconds
startRecordingMilliseconds := startRecording % 1000 // convert to milliseconds
s := strconv.FormatInt(startRecordingSeconds, 10) + "_" +
strconv.Itoa(len(strconv.FormatInt(startRecordingMilliseconds, 10))) + "-" +
strconv.FormatInt(startRecordingMilliseconds, 10) + "_" +
config.Name + "_" +
"0-0-0-0" + "_" + // region coordinates, we
"-1" + "_" + // token
strconv.FormatInt(int64(duration), 10) // + "_" + // duration of recording
//utils.VERSION // version of the agent
oldName := name
name = s + ".mp4"
fullName = configDirectory + "/data/recordings/" + name
log.Log.Info("capture.main.HandleRecordStream(motiondetection): renamed file from: " + oldName + " to: " + name)
// Rename the file to the new name.
err := os.Rename(
configDirectory+"/data/recordings/"+oldName,
configDirectory+"/data/recordings/"+s+".mp4")
if err != nil {
log.Log.Error("capture.main.HandleRecordStream(motiondetection): error renaming file: " + err.Error())
}
} else {
log.Log.Info("capture.main.HandleRecordStream(continuous): no video data recorded, not renaming file.")
// This will write the trailer a well.
if err := myMuxer.WriteTrailer(); err != nil {
log.Log.Error(err.Error())
}
// 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("capture.main.HandleRecordStream(motiondetection): error writing file: " + err.Error())
}
} else {
log.Log.Error("capture.main.HandleRecordStream(motiondetection): error encrypting file: " + err.Error())
}
} else {
log.Log.Error("capture.main.HandleRecordStream(motiondetection): error reading file: " + err.Error())
}
log.Log.Info("HandleRecordStream: Recording finished: file save: " + name)
// Cleanup muxer
start = false
myMuxer.Close()
myMuxer = nil
file.Close()
file = nil
// Check if need to convert to fragmented using bento
if config.Capture.Fragmented == "true" && config.Capture.FragmentedDuration > 0 {
utils.CreateFragmentedMP4(fullName, config.Capture.FragmentedDuration)
}
// Create a symbol link.
@@ -421,53 +263,38 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
fc.Close()
recordingStatus = "idle"
// Clean up the recording directory if necessary.
CleanupRecordingDirectory(configDirectory, configuration)
}
}
} else {
log.Log.Info("capture.main.HandleRecordStream(motiondetection): Start motion based recording ")
log.Log.Info("HandleRecordStream: Start motion based recording ")
var lastRecordingTime int64 = 0 // last recording timestamp in milliseconds
var displayTime int64 = 0 // display time in milliseconds
var myMuxer *mp4.Muxer
var file *os.File
var err error
var videoTrack uint32
var audioTrack uint32
var lastDuration time.Duration
var lastRecordingTime int64
for motion := range communication.HandleMotion {
// Get as much packets we need.
var cursorError error
var pkt packets.Packet
var nextPkt packets.Packet
recordingCursor := queue.Oldest() // Start from the latest packet in the queue)
timestamp = time.Now().Unix()
startRecording = time.Now().Unix() // we mark the current time when the record started.
numberOfChanges := motion.NumberOfChanges
now := time.Now().UnixMilli()
motionTimestamp := now
// If we have prerecording we will substract the number of seconds.
// Taking into account FPS = GOP size (Keyfram interval)
if config.Capture.PreRecording > 0 {
start := false
// Might be that recordings are coming short after each other.
// Therefore we do some math with the current time and the last recording time.
if cursorError == nil {
pkt, cursorError = recordingCursor.ReadPacket()
}
displayTime = pkt.CurrentTime
startRecording := pkt.CurrentTime
// We have more packets in the queue (which might still be older than where we close the previous recording).
// In that case we will use the last recording time to determine the start time of the recording, otherwise
// we will have duplicate frames in the recording.
if startRecording < lastRecordingTime {
displayTime = lastRecordingTime
startRecording = lastRecordingTime
}
// If startRecording is 0, we will continue as it might be we are in a state of restarting the agent.
if startRecording == 0 {
log.Log.Info("capture.main.HandleRecordStream(motiondetection): startRecording is 0, we will continue as it might be we are in a state of restarting the agent.")
continue
timeBetweenNowAndLastRecording := startRecording - lastRecordingTime
if timeBetweenNowAndLastRecording > int64(config.Capture.PreRecording) {
startRecording = startRecording - int64(config.Capture.PreRecording) + 1
} else {
startRecording = startRecording - timeBetweenNowAndLastRecording
}
}
// timestamp_microseconds_instanceName_regionCoordinates_numberOfChanges_token
@@ -479,108 +306,80 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
// - Number of changes
// - Token
displayTimeSeconds := displayTime / 1000 // convert to seconds
displayTimeMilliseconds := displayTime % 1000 // convert to milliseconds
motionRectangleString := "0-0-0-0"
if motion.Rectangle.X != 0 || motion.Rectangle.Y != 0 ||
motion.Rectangle.Width != 0 || motion.Rectangle.Height != 0 {
motionRectangleString = strconv.Itoa(motion.Rectangle.X) + "-" + strconv.Itoa(motion.Rectangle.Y) + "-" +
strconv.Itoa(motion.Rectangle.Width) + "-" + strconv.Itoa(motion.Rectangle.Height)
}
// Get the number of changes from the motion detection.
numberOfChanges := motion.NumberOfChanges
s := strconv.FormatInt(displayTimeSeconds, 10) + "_" + // start timestamp in seconds
strconv.Itoa(len(strconv.FormatInt(displayTimeMilliseconds, 10))) + "-" + // length of milliseconds
strconv.FormatInt(displayTimeMilliseconds, 10) + "_" + // milliseconds
config.Name + "_" + // device name
motionRectangleString + "_" + // region coordinates, we will not use this for continuous recording
strconv.Itoa(numberOfChanges) + "_" + // number of changes
"0" // + "_" + // duration of recording in milliseconds
//utils.VERSION // version of the agent
s := strconv.FormatInt(startRecording, 10) + "_" +
"6" + "-" +
"967003" + "_" +
config.Name + "_" +
"200-200-400-400" + "_" +
strconv.Itoa(numberOfChanges) + "_" +
"769"
name := s + ".mp4"
fullName := configDirectory + "/data/recordings/" + name
// Running...
log.Log.Info("capture.main.HandleRecordStream(motiondetection): recording started (" + name + ")" + " at " + strconv.FormatInt(displayTimeSeconds, 10) + " unix")
// Get width and height from the camera.
width := configuration.Config.Capture.IPCamera.Width
height := configuration.Config.Capture.IPCamera.Height
// Get SPS and PPS NALUs from the camera.
spsNALUS := configuration.Config.Capture.IPCamera.SPSNALUs
ppsNALUS := configuration.Config.Capture.IPCamera.PPSNALUs
vpsNALUS := configuration.Config.Capture.IPCamera.VPSNALUs
if len(spsNALUS) == 0 || len(ppsNALUS) == 0 {
log.Log.Warning("capture.main.HandleRecordStream(motiondetection): missing SPS/PPS at recording start")
log.Log.Info("HandleRecordStream: Recording started")
file, err = os.Create(fullName)
if err == nil {
myMuxer = mp4.NewMuxer(file)
}
// Create a video file, and set the dimensions.
mp4Video := video.NewMP4(fullName, spsNALUS, ppsNALUS, vpsNALUS, configuration.Config.Capture.MaxLengthRecording)
mp4Video.SetWidth(width)
mp4Video.SetHeight(height)
if videoCodec == "H264" {
videoTrack = mp4Video.AddVideoTrack("H264")
} else if videoCodec == "H265" {
videoTrack = mp4Video.AddVideoTrack("H265")
start := false
log.Log.Info("HandleRecordStream: composing recording")
log.Log.Info("HandleRecordStream: write header")
// Creating the file, might block sometimes.
if err := myMuxer.WriteHeader(streams); err != nil {
log.Log.Error(err.Error())
}
if audioCodec == "AAC" {
audioTrack = mp4Video.AddAudioTrack("AAC")
} else if audioCodec == "PCM_MULAW" {
log.Log.Debug("capture.main.HandleRecordStream(continuous): no AAC audio codec detected, skipping audio track.")
// Get as much packets we need.
var cursorError error
var pkt av.Packet
var nextPkt av.Packet
recordingCursor := queue.DelayedGopCount(int(config.Capture.PreRecording))
if cursorError == nil {
pkt, cursorError = recordingCursor.ReadPacket()
}
for cursorError == nil {
nextPkt, cursorError = recordingCursor.ReadPacket()
if cursorError != nil {
log.Log.Error("capture.main.HandleRecordStream(motiondetection): " + cursorError.Error())
log.Log.Error("HandleRecordStream: " + cursorError.Error())
}
now = time.Now().UnixMilli()
now := time.Now().Unix()
select {
case motion := <-communication.HandleMotion:
motionTimestamp = now
log.Log.Info("capture.main.HandleRecordStream(motiondetection): motion detected while recording. Expanding recording.")
numberOfChanges := motion.NumberOfChanges
log.Log.Info("capture.main.HandleRecordStream(motiondetection): Received message with recording data, detected changes to save: " + strconv.Itoa(numberOfChanges))
timestamp = now
log.Log.Info("HandleRecordStream: motion detected while recording. Expanding recording.")
numberOfChanges = motion.NumberOfChanges
log.Log.Info("Received message with recording data, detected changes to save: " + strconv.Itoa(numberOfChanges))
default:
}
if (motionTimestamp+postRecording-now < 0 || now-startRecording > maxRecordingPeriod-500) && nextPkt.IsKeyFrame {
log.Log.Info("capture.main.HandleRecordStream(motiondetection): timestamp+postRecording-now < 0 - " + strconv.FormatInt(motionTimestamp+postRecording-now, 10) + " < 0")
log.Log.Info("capture.main.HandleRecordStream(motiondetection): now-startRecording > maxRecordingPeriod-500 - " + strconv.FormatInt(now-startRecording, 10) + " > " + strconv.FormatInt(maxRecordingPeriod-500, 10))
log.Log.Info("capture.main.HandleRecordStream(motiondetection): closing recording (timestamp: " + strconv.FormatInt(motionTimestamp, 10) + ", postRecording: " + strconv.FormatInt(postRecording, 10) + ", now: " + strconv.FormatInt(now, 10) + ", startRecording: " + strconv.FormatInt(startRecording, 10) + ", maxRecordingPeriod: " + strconv.FormatInt(maxRecordingPeriod, 10))
if (timestamp+recordingPeriod-now < 0 || now-startRecording > maxRecordingPeriod) && nextPkt.IsKeyFrame {
log.Log.Info("HandleRecordStream: closing recording (timestamp: " + strconv.FormatInt(timestamp, 10) + ", recordingPeriod: " + strconv.FormatInt(recordingPeriod, 10) + ", now: " + strconv.FormatInt(now, 10) + ", startRecording: " + strconv.FormatInt(startRecording, 10) + ", maxRecordingPeriod: " + strconv.FormatInt(maxRecordingPeriod, 10))
break
}
if pkt.IsKeyFrame && !start && pkt.CurrentTime >= startRecording {
// We start the recording if we have a keyframe and the last duration is 0 or less than the current packet time.
// It could be start we start from the beginning of the recording.
log.Log.Debug("capture.main.HandleRecordStream(motiondetection): write frames")
if pkt.IsKeyFrame && !start && pkt.Time >= lastDuration {
log.Log.Info("HandleRecordStream: write frames")
start = true
}
if start {
pts := convertPTS(pkt.TimeLegacy)
if pkt.IsVideo {
log.Log.Debug("capture.main.HandleRecordStream(motiondetection): add video sample")
if err := mp4Video.AddSampleToTrack(videoTrack, pkt.IsKeyFrame, pkt.Data, pts); err != nil {
log.Log.Error("capture.main.HandleRecordStream(motiondetection): " + err.Error())
}
} else if pkt.IsAudio {
log.Log.Debug("capture.main.HandleRecordStream(motiondetection): add audio sample")
if pkt.Codec == "AAC" {
if err := mp4Video.AddSampleToTrack(audioTrack, pkt.IsKeyFrame, pkt.Data, pts); err != nil {
log.Log.Error("capture.main.HandleRecordStream(motiondetection): " + err.Error())
}
} else if pkt.Codec == "PCM_MULAW" {
// TODO: transcode to AAC, some work to do..
// We might need to use ffmpeg to transcode the audio to AAC.
// For now we will skip the audio track.
log.Log.Debug("capture.main.HandleRecordStream(motiondetection): no AAC audio codec detected, skipping audio track.")
if err := myMuxer.WritePacket(pkt); err != nil {
log.Log.Error(err.Error())
}
// We will sync to file every keyframe.
if pkt.IsKeyFrame {
err := file.Sync()
if err != nil {
log.Log.Error(err.Error())
} else {
log.Log.Info("HandleRecordStream: Synced file: " + name)
}
}
}
@@ -588,78 +387,22 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
pkt = nextPkt
}
// Update the last duration and last recording time.
// This is used to determine if we need to start a new recording.
lastRecordingTime = pkt.CurrentTime
// This will write the trailer as well.
myMuxer.WriteTrailerWithPacket(nextPkt)
log.Log.Info("HandleRecordStream: file save: " + name)
// This will close the recording and write the last packet.
if len(mp4Video.SPSNALUs) == 0 && len(configuration.Config.Capture.IPCamera.SPSNALUs) > 0 {
mp4Video.SPSNALUs = configuration.Config.Capture.IPCamera.SPSNALUs
}
if len(mp4Video.PPSNALUs) == 0 && len(configuration.Config.Capture.IPCamera.PPSNALUs) > 0 {
mp4Video.PPSNALUs = configuration.Config.Capture.IPCamera.PPSNALUs
}
if len(mp4Video.VPSNALUs) == 0 && len(configuration.Config.Capture.IPCamera.VPSNALUs) > 0 {
mp4Video.VPSNALUs = configuration.Config.Capture.IPCamera.VPSNALUs
}
if (videoCodec == "H264" && (len(mp4Video.SPSNALUs) == 0 || len(mp4Video.PPSNALUs) == 0)) ||
(videoCodec == "H265" && (len(mp4Video.VPSNALUs) == 0 || len(mp4Video.SPSNALUs) == 0 || len(mp4Video.PPSNALUs) == 0)) {
log.Log.Warning("capture.main.HandleRecordStream(motiondetection): closing MP4 without full parameter sets, moov may be incomplete")
}
mp4Video.Close(&config)
log.Log.Info("capture.main.HandleRecordStream(motiondetection): file save: " + name)
lastDuration = pkt.Time
lastRecordingTime = time.Now().Unix()
// Update the name of the recording with the duration.
// We will update the name of the recording with the duration in milliseconds.
if mp4Video.VideoTotalDuration > 0 {
duration := mp4Video.VideoTotalDuration
// Cleanup muxer
myMuxer.Close()
myMuxer = nil
file.Close()
file = nil
// Update the name with the duration in milliseconds.
s := strconv.FormatInt(displayTimeSeconds, 10) + "_" +
strconv.Itoa(len(strconv.FormatInt(displayTimeMilliseconds, 10))) + "-" +
strconv.FormatInt(displayTimeMilliseconds, 10) + "_" +
config.Name + "_" +
motionRectangleString + "_" +
strconv.Itoa(numberOfChanges) + "_" + // number of changes
strconv.FormatInt(int64(duration), 10) // + "_" + // duration of recording in milliseconds
//utils.VERSION // version of the agent
oldName := name
name = s + ".mp4"
fullName = configDirectory + "/data/recordings/" + name
log.Log.Info("capture.main.HandleRecordStream(motiondetection): renamed file from: " + oldName + " to: " + name)
// Rename the file to the new name.
err := os.Rename(
configDirectory+"/data/recordings/"+oldName,
configDirectory+"/data/recordings/"+s+".mp4")
if err != nil {
log.Log.Error("capture.main.HandleRecordStream(motiondetection): error renaming file: " + err.Error())
}
} else {
log.Log.Info("capture.main.HandleRecordStream(motiondetection): no video data recorded, not renaming file.")
}
// 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("capture.main.HandleRecordStream(motiondetection): error writing file: " + err.Error())
}
} else {
log.Log.Error("capture.main.HandleRecordStream(motiondetection): error encrypting file: " + err.Error())
}
} else {
log.Log.Error("capture.main.HandleRecordStream(motiondetection): error reading file: " + err.Error())
}
// Check if need to convert to fragmented using bento
if config.Capture.Fragmented == "true" && config.Capture.FragmentedDuration > 0 {
utils.CreateFragmentedMP4(fullName, config.Capture.FragmentedDuration)
}
// Create a symbol linc.
@@ -671,7 +414,7 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
}
}
log.Log.Debug("capture.main.HandleRecordStream(): finished")
log.Log.Debug("HandleRecordStream: finished")
}
}
@@ -686,10 +429,6 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
// @Success 200 {object} models.APIResponse
func VerifyCamera(c *gin.Context) {
// Start OpenTelemetry tracing
ctxVerifyCamera, span := tracer.Start(context.Background(), "VerifyCamera", trace.WithSpanKind(trace.SpanKindServer))
defer span.End()
var cameraStreams models.CameraStreams
err := c.BindJSON(&cameraStreams)
@@ -708,45 +447,30 @@ func VerifyCamera(c *gin.Context) {
if streamType == "secondary" {
rtspUrl = cameraStreams.SubRTSP
}
// Currently only support H264 encoded cameras, this will change.
// Establishing the camera connection without backchannel if no substream
rtspClient := &Golibrtsp{
Url: rtspUrl,
}
err := rtspClient.Connect(ctx, ctxVerifyCamera)
_, codecs, err := OpenRTSP(ctx, rtspUrl)
if err == nil {
// Get the streams from the rtsp client.
streams, _ := rtspClient.GetStreams()
videoIdx := -1
audioIdx := -1
for i, stream := range streams {
if (stream.Name == "H264" || stream.Name == "H265") && videoIdx < 0 {
for i, codec := range codecs {
if codec.Type().String() == "H264" && videoIdx < 0 {
videoIdx = i
} else if stream.Name == "PCM_MULAW" && audioIdx < 0 {
} else if codec.Type().String() == "PCM_MULAW" && audioIdx < 0 {
audioIdx = i
}
}
err := rtspClient.Close(ctxVerifyCamera)
if err == nil {
if videoIdx > -1 {
c.JSON(200, models.APIResponse{
Message: "All good, detected a H264 codec.",
Data: streams,
})
} else {
c.JSON(400, models.APIResponse{
Message: "Stream doesn't have a H264 codec, we only support H264 so far.",
})
}
if videoIdx > -1 {
c.JSON(200, models.APIResponse{
Message: "All good, detected a H264 codec.",
Data: codecs,
})
} else {
c.JSON(400, models.APIResponse{
Message: "Something went wrong while closing the connection " + err.Error(),
Message: "Stream doesn't have a H264 codec, we only support H264 so far.",
})
}
} else {
c.JSON(400, models.APIResponse{
Message: err.Error(),
@@ -758,99 +482,3 @@ func VerifyCamera(c *gin.Context) {
})
}
}
func Base64Image(captureDevice *Capture, communication *models.Communication, configuration *models.Configuration) string {
// We'll try to get a snapshot from the camera.
var queue *packets.Queue
var cursor *packets.QueueCursor
// We'll pick the right client and decoder.
rtspClient := captureDevice.RTSPSubClient
if rtspClient != nil {
queue = communication.SubQueue
cursor = queue.Latest()
} else {
rtspClient = captureDevice.RTSPClient
queue = communication.Queue
cursor = queue.Latest()
}
// We'll try to have a keyframe, if not we'll return an empty string.
var encodedImage string
// Try for 3 times in a row.
count := 0
for count < 3 {
if queue != nil && cursor != nil && rtspClient != nil {
pkt, err := cursor.ReadPacket()
if err == nil {
if !pkt.IsKeyFrame {
continue
}
var img image.YCbCr
img, err = (*rtspClient).DecodePacket(pkt)
if err == nil {
imageResized, _ := utils.ResizeImage(&img, uint(configuration.Config.Capture.IPCamera.BaseWidth), uint(configuration.Config.Capture.IPCamera.BaseHeight))
bytes, _ := utils.ImageToBytes(imageResized)
encodedImage = base64.StdEncoding.EncodeToString(bytes)
break
} else {
count++
continue
}
}
} else {
break
}
}
return encodedImage
}
func JpegImage(captureDevice *Capture, communication *models.Communication) image.YCbCr {
// We'll try to get a snapshot from the camera.
var queue *packets.Queue
var cursor *packets.QueueCursor
// We'll pick the right client and decoder.
rtspClient := captureDevice.RTSPSubClient
if rtspClient != nil {
queue = communication.SubQueue
cursor = queue.Latest()
} else {
rtspClient = captureDevice.RTSPClient
queue = communication.Queue
cursor = queue.Latest()
}
// We'll try to have a keyframe, if not we'll return an empty string.
var image image.YCbCr
// Try for 3 times in a row.
count := 0
for count < 3 {
if queue != nil && cursor != nil && rtspClient != nil {
pkt, err := cursor.ReadPacket()
if err == nil {
if !pkt.IsKeyFrame {
continue
}
image, err = (*rtspClient).DecodePacket(pkt)
if err != nil {
count++
continue
} else {
break
}
}
} else {
break
}
}
return image
}
func convertPTS(v time.Duration) uint64 {
return uint64(v.Milliseconds())
}
/*func convertPTS2(v int64) uint64 {
return uint64(v) / 100
}*/

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,105 @@
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 UploadKerberosVault(configuration *models.Configuration, fileName string) (bool, bool, error) {
config := configuration.Config
if config.KStorage.AccessKey == "" ||
config.KStorage.SecretAccessKey == "" ||
config.KStorage.Directory == "" ||
config.KStorage.URI == "" {
err := "UploadKerberosVault: Kerberos Vault 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
// 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 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)
}
publicKey := config.KStorage.CloudKey
// This is the new way ;)
if config.HubKey != "" {
publicKey = config.HubKey
}
req, err := http.NewRequest("POST", config.KStorage.URI+"/storage", file)
if err != nil {
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)
req.Header.Set("X-Kerberos-Storage-AccessKey", config.KStorage.AccessKey)
req.Header.Set("X-Kerberos-Storage-SecretAccessKey", config.KStorage.SecretAccessKey)
req.Header.Set("X-Kerberos-Storage-Provider", config.KStorage.Provider)
req.Header.Set("X-Kerberos-Storage-FileName", fileName)
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)
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 {
body, err := ioutil.ReadAll(resp.Body)
if err == nil {
if resp.StatusCode == 200 {
log.Log.Info("UploadKerberosVault: Upload Finished, " + resp.Status + ", " + string(body))
return true, true, nil
} else {
log.Log.Info("UploadKerberosVault: Upload Failed, " + resp.Status + ", " + string(body))
return false, true, nil
}
}
}
}
errorMessage := "UploadKerberosVault: Upload Failed, " + err.Error()
log.Log.Info(errorMessage)
return false, true, errors.New(errorMessage)
}

View File

@@ -1,194 +0,0 @@
package cloud
import (
"crypto/tls"
"errors"
"io"
"net/http"
"os"
"time"
"github.com/kerberos-io/agent/machinery/src/log"
"github.com/kerberos-io/agent/machinery/src/models"
)
// We will count the number of retries we have done.
// If we have done more than "kstorageRetryPolicy" retries, we will stop, and start sending to the secondary storage.
var kstorageRetryCount = 0
var kstorageRetryTimeout = time.Now().Unix()
func UploadKerberosVault(configuration *models.Configuration, fileName string) (bool, bool, error) {
config := configuration.Config
if config.KStorage.AccessKey == "" ||
config.KStorage.SecretAccessKey == "" ||
config.KStorage.Directory == "" ||
config.KStorage.URI == "" {
err := "UploadKerberosVault: Kerberos Vault 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
// 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 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)
}
publicKey := config.KStorage.CloudKey
if config.HubKey != "" {
publicKey = config.HubKey
}
// We need to check if we are in a retry timeout.
if kstorageRetryTimeout <= time.Now().Unix() {
req, err := http.NewRequest("POST", config.KStorage.URI+"/storage", file)
if err != nil {
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)
req.Header.Set("X-Kerberos-Storage-AccessKey", config.KStorage.AccessKey)
req.Header.Set("X-Kerberos-Storage-SecretAccessKey", config.KStorage.SecretAccessKey)
req.Header.Set("X-Kerberos-Storage-Provider", config.KStorage.Provider)
req.Header.Set("X-Kerberos-Storage-FileName", fileName)
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)
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 {
body, err := io.ReadAll(resp.Body)
if err == nil {
if resp.StatusCode == 200 {
kstorageRetryCount = 0
log.Log.Info("UploadKerberosVault: Upload Finished, " + resp.Status + ", " + string(body))
return true, true, nil
} else {
// We increase the retry count, and set the timeout.
// If we have reached the retry policy, we set the timeout.
// This means we will not retry for the next 5 minutes.
if kstorageRetryCount < config.KStorage.MaxRetries {
kstorageRetryCount = (kstorageRetryCount + 1)
}
if kstorageRetryCount == config.KStorage.MaxRetries {
kstorageRetryTimeout = time.Now().Add(time.Duration(config.KStorage.Timeout) * time.Second).Unix()
}
log.Log.Info("UploadKerberosVault: Upload Failed, " + resp.Status + ", " + string(body))
}
}
}
} else {
log.Log.Info("UploadKerberosVault: Upload Failed, " + err.Error())
}
}
// We might need to check if we can upload to our secondary storage.
if config.KStorageSecondary.AccessKey == "" ||
config.KStorageSecondary.SecretAccessKey == "" ||
config.KStorageSecondary.Directory == "" ||
config.KStorageSecondary.URI == "" {
log.Log.Info("UploadKerberosVault (Secondary): Secondary Kerberos Vault not properly configured.")
} else {
if kstorageRetryCount < config.KStorage.MaxRetries {
log.Log.Info("UploadKerberosVault (Secondary): Do not upload to secondary storage, we are still in retry policy.")
return false, true, nil
}
log.Log.Info("UploadKerberosVault (Secondary): Uploading to Secondary Kerberos Vault (" + config.KStorageSecondary.URI + ")")
file, err = os.OpenFile(fullname, os.O_RDWR, 0755)
if file != nil {
defer file.Close()
}
if err != nil {
err := "UploadKerberosVault (Secondary): Upload Failed, file doesn't exists anymore"
log.Log.Info(err)
return false, false, errors.New(err)
}
req, err := http.NewRequest("POST", config.KStorageSecondary.URI+"/storage", file)
if err != nil {
errorMessage := "UploadKerberosVault (Secondary): error reading request, " + config.KStorageSecondary.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)
req.Header.Set("X-Kerberos-Storage-AccessKey", config.KStorageSecondary.AccessKey)
req.Header.Set("X-Kerberos-Storage-SecretAccessKey", config.KStorageSecondary.SecretAccessKey)
req.Header.Set("X-Kerberos-Storage-Provider", config.KStorageSecondary.Provider)
req.Header.Set("X-Kerberos-Storage-FileName", fileName)
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.KStorageSecondary.Directory)
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 {
body, err := io.ReadAll(resp.Body)
if err == nil {
if resp.StatusCode == 200 {
log.Log.Info("UploadKerberosVault (Secondary): Upload Finished to secondary, " + resp.Status + ", " + string(body))
return true, true, nil
} else {
log.Log.Info("UploadKerberosVault (Secondary): Upload Failed to secondary, " + resp.Status + ", " + string(body))
}
}
}
}
}
return false, true, nil
}

View File

@@ -2,14 +2,14 @@ package components
import (
"context"
"os"
"runtime"
"strconv"
"sync"
"sync/atomic"
"time"
mqtt "github.com/eclipse/paho.mqtt.golang"
"github.com/gin-gonic/gin"
"go.opentelemetry.io/otel"
"github.com/kerberos-io/joy4/cgo/ffmpeg"
"github.com/kerberos-io/agent/machinery/src/capture"
"github.com/kerberos-io/agent/machinery/src/cloud"
@@ -18,20 +18,14 @@ import (
"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/packets"
routers "github.com/kerberos-io/agent/machinery/src/routers/mqtt"
"github.com/kerberos-io/agent/machinery/src/utils"
"github.com/kerberos-io/joy4/av"
"github.com/kerberos-io/joy4/av/pubsub"
"github.com/tevino/abool"
)
var tracer = otel.Tracer("github.com/kerberos-io/agent/machinery/src/components")
func Bootstrap(ctx context.Context, configDirectory string, configuration *models.Configuration, communication *models.Communication, captureDevice *capture.Capture) {
log.Log.Debug("components.Kerberos.Bootstrap(): bootstrapping the kerberos agent.")
bootstrapContext := context.Background()
_, span := tracer.Start(bootstrapContext, "Bootstrap")
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
// This is send to Kerberos Hub in a heartbeat.
@@ -43,20 +37,12 @@ func Bootstrap(ctx context.Context, configDirectory string, configuration *model
packageCounter.Store(int64(0))
communication.PackageCounter = &packageCounter
var packageCounterSub atomic.Value
packageCounterSub.Store(int64(0))
communication.PackageCounterSub = &packageCounterSub
// This is used when the last packet was received (timestamp),
// this metric is used to determine if the camera is still online/connected.
var lastPacketTimer atomic.Value
packageCounter.Store(int64(0))
communication.LastPacketTimer = &lastPacketTimer
var lastPacketTimerSub atomic.Value
packageCounterSub.Store(int64(0))
communication.LastPacketTimerSub = &lastPacketTimerSub
// This is used to understand if we have a working Kerberos Hub connection
// cloudTimestamp will be updated when successfully sending heartbeats.
var cloudTimestamp atomic.Value
@@ -70,14 +56,18 @@ func Bootstrap(ctx context.Context, configDirectory string, configuration *model
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()
cameraSettings := &models.Camera{}
// Before starting the agent, we have a control goroutine, that might
// do several checks to see if the agent is still operational.
go ControlAgent(communication)
// Create some global variables
decoder := &ffmpeg.VideoDecoder{}
subDecoder := &ffmpeg.VideoDecoder{}
cameraSettings := &models.Camera{}
// Handle heartbeats
go cloud.HandleHeartBeat(configuration, communication, uptimeStart)
@@ -85,19 +75,15 @@ func Bootstrap(ctx context.Context, configDirectory string, configuration *model
// Configure a MQTT client which helps for a bi-directional communication
mqttClient := routers.ConfigureMQTT(configDirectory, configuration, communication)
span.End()
// 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(configDirectory, configuration, communication, mqttClient, uptimeStart, cameraSettings, captureDevice)
status := RunAgent(configDirectory, configuration, communication, mqttClient, uptimeStart, cameraSettings, decoder, subDecoder)
if status == "stop" {
log.Log.Info("components.Kerberos.Bootstrap(): shutting down the agent in 3 seconds.")
time.Sleep(time.Second * 3)
os.Exit(0)
break
}
if status == "not started" {
@@ -119,351 +105,258 @@ func Bootstrap(ctx context.Context, configDirectory string, configuration *model
communication.Context = &ctx
communication.CancelContext = &cancel
}
log.Log.Debug("Bootstrap: finished")
}
func RunAgent(configDirectory string, configuration *models.Configuration, communication *models.Communication, mqttClient mqtt.Client, uptimeStart time.Time, cameraSettings *models.Camera, captureDevice *capture.Capture) string {
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 {
ctx := context.Background()
ctxRunAgent, span := tracer.Start(ctx, "RunAgent")
log.Log.Info("components.Kerberos.RunAgent(): Creating camera and processing threads.")
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 without backchannel if no substream
// Establishing the camera connection
rtspUrl := config.Capture.IPCamera.RTSP
rtspClient := captureDevice.SetMainClient(rtspUrl)
if rtspUrl != "" {
err := rtspClient.Connect(ctx, ctxRunAgent)
if err != nil {
log.Log.Error("components.Kerberos.RunAgent(): error connecting to RTSP stream: " + err.Error())
rtspClient.Close(ctxRunAgent)
rtspClient = nil
time.Sleep(time.Second * 3)
return status
}
} else {
log.Log.Error("components.Kerberos.RunAgent(): no rtsp url found in config, please provide one.")
rtspClient = nil
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
}
log.Log.Info("components.Kerberos.RunAgent(): opened RTSP stream: " + rtspUrl)
// Get the video streams from the RTSP server.
videoStreams, err := rtspClient.GetVideoStreams()
if err != nil || len(videoStreams) == 0 {
log.Log.Error("components.Kerberos.RunAgent(): no video stream found, might be the wrong codec (we only support H264 for the moment)")
rtspClient.Close(ctxRunAgent)
time.Sleep(time.Second * 3)
return status
}
// Get the video stream from the RTSP server.
videoStream := videoStreams[0]
// Get some information from the video stream.
width := videoStream.Width
height := videoStream.Height
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
// Set the liveview width and height, this is used for the liveview and motion regions (drawing on the hub).
baseWidth := config.Capture.IPCamera.BaseWidth
baseHeight := config.Capture.IPCamera.BaseHeight
// If the liveview height is not set, we will calculate it based on the width and aspect ratio of the camera.
if baseWidth > 0 && baseHeight == 0 {
widthAspectRatio := float64(baseWidth) / float64(width)
configuration.Config.Capture.IPCamera.BaseHeight = int(float64(height) * widthAspectRatio)
} else if baseHeight > 0 && baseWidth > 0 {
configuration.Config.Capture.IPCamera.BaseHeight = baseHeight
configuration.Config.Capture.IPCamera.BaseWidth = baseWidth
} else {
configuration.Config.Capture.IPCamera.BaseHeight = height
configuration.Config.Capture.IPCamera.BaseWidth = width
}
var queue *pubsub.Queue
var subQueue *pubsub.Queue
// Set the SPS and PPS values in the configuration.
configuration.Config.Capture.IPCamera.SPSNALUs = [][]byte{videoStream.SPS}
configuration.Config.Capture.IPCamera.PPSNALUs = [][]byte{videoStream.PPS}
configuration.Config.Capture.IPCamera.VPSNALUs = [][]byte{videoStream.VPS}
var decoderMutex sync.Mutex
var subDecoderMutex sync.Mutex
// Define queues for the main and sub stream.
var queue *packets.Queue
var subQueue *packets.Queue
// Create a packet queue, which is filled by the HandleStream routing
// and consumed by all other routines: motion, livestream, etc.
if config.Capture.PreRecording <= 0 {
config.Capture.PreRecording = 1
log.Log.Warning("components.Kerberos.RunAgent(): Prerecording value not found in config or invalid value! Found: " + strconv.FormatInt(config.Capture.PreRecording, 10))
}
// We might have a secondary rtsp url, so we might need to use that for livestreaming let us check first!
subStreamEnabled := false
subRtspUrl := config.Capture.IPCamera.SubRTSP
var videoSubStreams []packets.Stream
if subRtspUrl != "" && subRtspUrl != rtspUrl {
// For the sub stream we will not enable backchannel.
subStreamEnabled = true
rtspSubClient := captureDevice.SetSubClient(subRtspUrl)
captureDevice.RTSPSubClient = rtspSubClient
err := rtspSubClient.Connect(ctx, ctxRunAgent)
if err != nil {
log.Log.Error("components.Kerberos.RunAgent(): error connecting to RTSP sub stream: " + err.Error())
time.Sleep(time.Second * 3)
return status
}
log.Log.Info("components.Kerberos.RunAgent(): opened RTSP sub stream: " + subRtspUrl)
// Get the video streams from the RTSP server.
videoSubStreams, err = rtspSubClient.GetVideoStreams()
if err != nil || len(videoSubStreams) == 0 {
log.Log.Error("components.Kerberos.RunAgent(): no video sub stream found, might be the wrong codec (we only support H264 for the moment)")
rtspSubClient.Close(ctxRunAgent)
time.Sleep(time.Second * 3)
return status
}
// Get the video stream from the RTSP server.
videoSubStream := videoSubStreams[0]
width := videoSubStream.Width
height := videoSubStream.Height
// Set config values as well
configuration.Config.Capture.IPCamera.SubWidth = width
configuration.Config.Capture.IPCamera.SubHeight = height
// If we have a substream, we need to set the width and height of the substream. (so we will override above information)
// Set the liveview width and height, this is used for the liveview and motion regions (drawing on the hub).
baseWidth := config.Capture.IPCamera.BaseWidth
baseHeight := config.Capture.IPCamera.BaseHeight
// If the liveview height is not set, we will calculate it based on the width and aspect ratio of the camera.
if baseWidth > 0 && baseHeight == 0 {
widthAspectRatio := float64(baseWidth) / float64(width)
configuration.Config.Capture.IPCamera.BaseHeight = int(float64(height) * widthAspectRatio)
} else if baseHeight > 0 && baseWidth > 0 {
configuration.Config.Capture.IPCamera.BaseHeight = baseHeight
configuration.Config.Capture.IPCamera.BaseWidth = baseWidth
} else {
configuration.Config.Capture.IPCamera.BaseHeight = height
configuration.Config.Capture.IPCamera.BaseWidth = width
}
}
// We are creating a queue to store the RTSP frames in, these frames will be
// processed by the different consumers: motion detection, recording, etc.
queue = packets.NewQueue()
communication.Queue = queue
// Set the maximum GOP count, this is used to determine the pre-recording time.
log.Log.Info("components.Kerberos.RunAgent(): SetMaxGopCount was set with: " + strconv.Itoa(int(config.Capture.PreRecording)+1))
queue.SetMaxGopCount(1) // We will adjust this later on, when we have the GOP size.
queue.WriteHeader(videoStreams)
go rtspClient.Start(ctx, "main", queue, configuration, communication)
// Main stream is connected and ready to go.
communication.MainStreamConnected = true
// Try to create backchannel
rtspBackChannelClient := captureDevice.SetBackChannelClient(rtspUrl)
err = rtspBackChannelClient.ConnectBackChannel(ctx, ctxRunAgent)
if err == nil {
log.Log.Info("components.Kerberos.RunAgent(): opened RTSP backchannel stream: " + rtspUrl)
go rtspBackChannelClient.StartBackChannel(ctx, ctxRunAgent)
}
rtspSubClient := captureDevice.RTSPSubClient
if subStreamEnabled && rtspSubClient != nil {
subQueue = packets.NewQueue()
communication.SubQueue = subQueue
subQueue.SetMaxGopCount(1) // GOP time frame is set to 1 for motion detection and livestreaming.
subQueue.WriteHeader(videoSubStreams)
go rtspSubClient.Start(ctx, "sub", subQueue, configuration, communication)
log.Log.Info("RunAgent: opened RTSP stream: " + rtspUrl)
// Sub stream is connected and ready to go.
communication.SubStreamConnected = true
}
// We might have a secondary rtsp url, so we might need to use that.
var subInfile av.DemuxCloser
var subStreams []av.CodecData
subStreamEnabled := false
subRtspUrl := config.Capture.IPCamera.SubRTSP
if subRtspUrl != "" && subRtspUrl != rtspUrl {
subInfile, subStreams, err = capture.OpenRTSP(context.Background(), subRtspUrl)
if err == nil {
log.Log.Info("RunAgent: opened RTSP sub stream " + subRtspUrl)
subStreamEnabled = true
}
// Handle livestream SD (low resolution over MQTT)
if subStreamEnabled {
livestreamCursor := subQueue.Latest()
go cloud.HandleLiveStreamSD(livestreamCursor, configuration, communication, mqttClient, rtspSubClient)
} else {
livestreamCursor := queue.Latest()
go cloud.HandleLiveStreamSD(livestreamCursor, configuration, communication, mqttClient, rtspClient)
}
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
}
// Handle livestream HD (high resolution over WEBRTC)
communication.HandleLiveHDHandshake = make(chan models.RequestHDStreamPayload, 10)
if subStreamEnabled {
livestreamHDCursor := subQueue.Latest()
go cloud.HandleLiveStreamHD(livestreamHDCursor, configuration, communication, mqttClient, rtspSubClient)
} else {
livestreamHDCursor := queue.Latest()
go cloud.HandleLiveStreamHD(livestreamHDCursor, configuration, communication, mqttClient, rtspClient)
}
width := videoStream.(av.VideoCodecData).Width()
height := videoStream.(av.VideoCodecData).Height()
// Handle recording, will write an mp4 to disk.
go capture.HandleRecordStream(queue, configDirectory, configuration, communication, rtspClient)
// Handle processing of motion
communication.HandleMotion = make(chan models.MotionDataPartial, 10)
if subStreamEnabled {
motionCursor := subQueue.Latest()
go computervision.ProcessMotion(motionCursor, configuration, communication, mqttClient, rtspSubClient)
} else {
motionCursor := queue.Latest()
go computervision.ProcessMotion(motionCursor, configuration, communication, mqttClient, rtspClient)
}
// Handle realtime processing if enabled.
if subStreamEnabled {
realtimeProcessingCursor := subQueue.Latest()
go cloud.HandleRealtimeProcessing(realtimeProcessingCursor, configuration, communication, mqttClient, rtspClient)
} else {
realtimeProcessingCursor := queue.Latest()
go cloud.HandleRealtimeProcessing(realtimeProcessingCursor, configuration, communication, mqttClient, rtspClient)
}
// Handle Upload to cloud provider (Kerberos Hub, Kerberos Vault and others)
go cloud.HandleUpload(configDirectory, configuration, communication)
// Handle ONVIF actions
communication.HandleONVIF = make(chan models.OnvifAction, 10)
go onvif.HandleONVIFActions(configuration, communication)
communication.HandleAudio = make(chan models.AudioDataPartial, 10)
if rtspBackChannelClient.HasBackChannel {
communication.HasBackChannel = true
go WriteAudioToBackchannel(communication, rtspBackChannelClient)
}
// If we reach this point, we have a working RTSP connection.
communication.CameraConnected = true
// Otel end span
span.End()
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// 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
communication.MainStreamConnected = false
communication.SubStreamConnected = 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" {
select {
case communication.HandleUpload <- "stop":
log.Log.Info("components.Kerberos.RunAgent(): stopping upload")
case <-time.After(1 * time.Second):
log.Log.Info("components.Kerberos.RunAgent(): stopping upload timed out")
// Set config values as well
configuration.Config.Capture.IPCamera.Width = width
configuration.Config.Capture.IPCamera.Height = height
}
}
select {
case communication.HandleStream <- "stop":
log.Log.Info("components.Kerberos.RunAgent(): stopping stream")
case <-time.After(1 * time.Second):
log.Log.Info("components.Kerberos.RunAgent(): stopping stream timed out")
}
// We use the steam channel to stop both main and sub stream.
//if subStreamEnabled {
// communication.HandleSubStream <- "stop"
//}
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() {
time.Sleep(time.Second * 3)
if cameraSettings.RTSP != "" && cameraSettings.SubRTSP != "" && cameraSettings.Initialized {
decoder.Close()
if subStreamEnabled {
subDecoder.Close()
}
}
// At some routines we will need to decode the image.
// Make sure its properly locked as we only have a single decoder.
log.Log.Info("RunAgent: camera settings changed, reloading decoder")
capture.GetVideoDecoder(decoder, streams)
if subStreamEnabled {
capture.GetVideoDecoder(subDecoder, subStreams)
}
cameraSettings.RTSP = rtspUrl
cameraSettings.SubRTSP = subRtspUrl
cameraSettings.Width = width
cameraSettings.Height = height
cameraSettings.Framerate = float64(num) / float64(denum)
cameraSettings.Num = num
cameraSettings.Denum = denum
cameraSettings.Codec = videoStream.(av.VideoCodecData).Type()
cameraSettings.Initialized = true
} else {
log.Log.Info("RunAgent: camera settings did not change, keeping decoder")
}
communication.Decoder = decoder
communication.SubDecoder = subDecoder
communication.DecoderMutex = &decoderMutex
communication.SubDecoderMutex = &subDecoderMutex
// Create a packet queue, which is filled by the HandleStream routing
// and consumed by all other routines: motion, livestream, etc.
if config.Capture.PreRecording <= 0 {
config.Capture.PreRecording = 1
log.Log.Warning("RunAgent: Prerecording value not found in config or invalid value! Found: " + strconv.FormatInt(config.Capture.PreRecording, 10))
}
// We are creating a queue to store the RTSP frames in, these frames will be
// processed by the different consumers: motion detection, recording, etc.
queue = pubsub.NewQueue()
communication.Queue = queue
queue.SetMaxGopCount(int(config.Capture.PreRecording) + 1) // GOP time frame is set to prerecording (we'll add 2 gops to leave some room).
log.Log.Info("RunAgent: SetMaxGopCount was set with: " + strconv.Itoa(int(config.Capture.PreRecording)+1))
queue.WriteHeader(streams)
// We might have a substream, if so we'll create a seperate queue.
if subStreamEnabled {
log.Log.Info("RunAgent: Creating sub stream queue with SetMaxGopCount set to " + strconv.Itoa(int(1)))
subQueue = pubsub.NewQueue()
communication.SubQueue = subQueue
subQueue.SetMaxGopCount(1)
subQueue.WriteHeader(subStreams)
}
// Handle the camera stream
go capture.HandleStream(infile, queue, communication)
// Handle the substream if enabled
if subStreamEnabled {
go capture.HandleSubStream(subInfile, subQueue, communication)
}
// Handle processing of motion
communication.HandleMotion = make(chan models.MotionDataPartial, 1)
if subStreamEnabled {
motionCursor := subQueue.Latest()
go computervision.ProcessMotion(motionCursor, configuration, communication, mqttClient, subDecoder, &subDecoderMutex)
} else {
motionCursor := queue.Latest()
go computervision.ProcessMotion(motionCursor, configuration, communication, mqttClient, decoder, &decoderMutex)
}
// Handle livestream SD (low resolution over MQTT)
if subStreamEnabled {
livestreamCursor := subQueue.Latest()
go cloud.HandleLiveStreamSD(livestreamCursor, configuration, communication, mqttClient, subDecoder, &subDecoderMutex)
} else {
livestreamCursor := queue.Latest()
go cloud.HandleLiveStreamSD(livestreamCursor, configuration, communication, mqttClient, decoder, &decoderMutex)
}
// Handle livestream HD (high resolution over WEBRTC)
communication.HandleLiveHDHandshake = make(chan models.SDPPayload, 1)
if subStreamEnabled {
livestreamHDCursor := subQueue.Latest()
go cloud.HandleLiveStreamHD(livestreamHDCursor, configuration, communication, mqttClient, subStreams, subDecoder, &decoderMutex)
} else {
livestreamHDCursor := queue.Latest()
go cloud.HandleLiveStreamHD(livestreamHDCursor, configuration, communication, mqttClient, streams, decoder, &decoderMutex)
}
// Handle recording, will write an mp4 to disk.
go capture.HandleRecordStream(queue, configDirectory, configuration, communication, streams)
// Handle Upload to cloud provider (Kerberos Hub, Kerberos Vault and others)
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
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// 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.HandleUpload <- "stop"
}
communication.HandleStream <- "stop"
if subStreamEnabled {
communication.HandleSubStream <- "stop"
}
err = rtspClient.Close(ctxRunAgent)
if err != nil {
log.Log.Error("components.Kerberos.RunAgent(): error closing RTSP stream: " + err.Error())
time.Sleep(time.Second * 3)
return status
}
queue.Close()
queue = nil
communication.Queue = nil
if subStreamEnabled {
err = rtspSubClient.Close(ctxRunAgent)
if err != nil {
log.Log.Error("components.Kerberos.RunAgent(): error closing RTSP sub stream: " + err.Error())
time.Sleep(time.Second * 3)
return status
infile.Close()
infile = nil
queue.Close()
queue = nil
communication.Queue = nil
if subStreamEnabled {
subInfile.Close()
subInfile = nil
subQueue.Close()
subQueue = nil
communication.SubQueue = nil
}
subQueue.Close()
subQueue = nil
communication.SubQueue = nil
close(communication.HandleMotion)
communication.HandleMotion = 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)
}
err = rtspBackChannelClient.Close(ctxRunAgent)
if err != nil {
log.Log.Error("components.Kerberos.RunAgent(): error closing RTSP backchannel stream: " + err.Error())
}
log.Log.Debug("RunAgent: finished")
time.Sleep(time.Second * 3)
close(communication.HandleLiveHDHandshake)
communication.HandleLiveHDHandshake = nil
close(communication.HandleMotion)
communication.HandleMotion = nil
close(communication.HandleAudio)
communication.HandleAudio = nil
close(communication.HandleONVIF)
communication.HandleONVIF = nil
// Waiting for some seconds to make sure everything is properly closed.
log.Log.Info("components.Kerberos.RunAgent(): waiting 3 seconds to make sure everything is properly closed.")
time.Sleep(time.Second * 3)
// Clean up, force garbage collection
runtime.GC()
return status
}
// ControlAgent will check if the camera is still connected, if not it will restart the agent.
// In the other thread we are keeping track of the number of packets received, and particular the keyframe packets.
// Once we are not receiving any packets anymore, we will restart the agent.
func ControlAgent(communication *models.Communication) {
log.Log.Debug("components.Kerberos.ControlAgent(): started")
log.Log.Debug("ControlAgent: started")
packageCounter := communication.PackageCounter
packageSubCounter := communication.PackageCounterSub
go func() {
// A channel to check the camera activity
var previousPacket int64 = 0
var previousPacketSub int64 = 0
var occurence = 0
var occurenceSub = 0
for {
// If camera is connected, we'll check if we are still receiving packets.
if communication.CameraConnected {
// First we'll check the main stream.
packetsR := packageCounter.Load().(int64)
if packetsR == previousPacket {
// If we are already reconfiguring,
@@ -475,314 +368,20 @@ func ControlAgent(communication *models.Communication) {
occurence = 0
}
log.Log.Info("components.Kerberos.ControlAgent(): Number of packets read from mainstream: " + strconv.FormatInt(packetsR, 10))
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("components.Kerberos.ControlAgent(): Restarting machinery because of blocking mainstream.")
select {
case communication.HandleBootstrap <- "restart":
log.Log.Info("components.Kerberos.ControlAgent(): Restarting machinery because of blocking substream.")
case <-time.After(1 * time.Second):
log.Log.Info("components.Kerberos.ControlAgent(): Restarting machinery because of blocking substream timed out")
}
log.Log.Info("Main: Restarting machinery.")
communication.HandleBootstrap <- "restart"
time.Sleep(2 * time.Second)
occurence = 0
}
// Now we'll check the sub stream.
packetsSubR := packageSubCounter.Load().(int64)
if communication.SubStreamConnected {
if packetsSubR == previousPacketSub {
// If we are already reconfiguring,
// we dont need to check if the stream is blocking.
if !communication.IsConfiguring.IsSet() {
occurenceSub = occurenceSub + 1
}
} else {
occurenceSub = 0
}
log.Log.Info("components.Kerberos.ControlAgent(): Number of packets read from substream: " + strconv.FormatInt(packetsSubR, 10))
// After 15 seconds without activity this is thrown..
if occurenceSub == 3 {
select {
case communication.HandleBootstrap <- "restart":
log.Log.Info("components.Kerberos.ControlAgent(): Restarting machinery because of blocking substream.")
case <-time.After(1 * time.Second):
log.Log.Info("components.Kerberos.ControlAgent(): Restarting machinery because of blocking substream timed out")
}
occurenceSub = 0
}
}
previousPacket = packageCounter.Load().(int64)
previousPacketSub = packageSubCounter.Load().(int64)
}
time.Sleep(5 * time.Second)
}
}()
log.Log.Debug("components.Kerberos.ControlAgent(): finished")
}
// GetDashboard godoc
// @Router /api/dashboard [get]
// @ID dashboard
// @Tags general
// @Summary Get all information showed on the dashboard.
// @Description Get all information showed on the dashboard.
// @Success 200
func GetDashboard(c *gin.Context, configDirectory string, configuration *models.Configuration, communication *models.Communication) {
// 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.
cloudIsOnline := false
if communication.CloudTimestamp != nil && communication.CloudTimestamp.Load() != nil {
timestamp := communication.CloudTimestamp.Load().(int64)
if timestamp > 0 {
cloudIsOnline = true
}
}
// The total number of recordings stored in the directory.
recordingDirectory := configDirectory + "/data/recordings"
numberOfRecordings := utils.NumberOfMP4sInDirectory(recordingDirectory)
// All days stored in this agent.
days := []string{}
latestEvents := []models.Media{}
files, err := utils.ReadDirectory(recordingDirectory)
if err == nil {
events := utils.GetSortedDirectory(files)
// Get All days
days = utils.GetDays(events, recordingDirectory, configuration)
// Get all latest events
var eventFilter models.EventFilter
eventFilter.NumberOfElements = 5
latestEvents = utils.GetMediaFormatted(events, recordingDirectory, configuration, eventFilter) // will get 5 latest recordings.
}
c.JSON(200, gin.H{
"offlineMode": configuration.Config.Offline,
"cameraOnline": cameraIsOnline,
"cloudOnline": cloudIsOnline,
"numberOfRecordings": numberOfRecordings,
"days": days,
"latestEvents": latestEvents,
})
}
// GetLatestEvents godoc
// @Router /api/latest-events [post]
// @ID latest-events
// @Tags general
// @Param eventFilter body models.EventFilter true "Event filter"
// @Summary Get the latest recordings (events) from the recordings directory.
// @Description Get the latest recordings (events) from the recordings directory.
// @Success 200
func GetLatestEvents(c *gin.Context, configDirectory string, configuration *models.Configuration, communication *models.Communication) {
var eventFilter models.EventFilter
err := c.BindJSON(&eventFilter)
if err == nil {
// Default to 10 if no limit is set.
if eventFilter.NumberOfElements == 0 {
eventFilter.NumberOfElements = 10
}
recordingDirectory := configDirectory + "/data/recordings"
files, err := utils.ReadDirectory(recordingDirectory)
if err == nil {
events := utils.GetSortedDirectory(files)
// We will get all recordings from the directory (as defined by the filter).
fileObjects := utils.GetMediaFormatted(events, recordingDirectory, configuration, eventFilter)
c.JSON(200, gin.H{
"events": fileObjects,
})
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
}
// GetDays godoc
// @Router /api/days [get]
// @ID days
// @Tags general
// @Summary Get all days stored in the recordings directory.
// @Description Get all days stored in the recordings directory.
// @Success 200
func GetDays(c *gin.Context, configDirectory string, configuration *models.Configuration, communication *models.Communication) {
recordingDirectory := configDirectory + "/data/recordings"
files, err := utils.ReadDirectory(recordingDirectory)
if err == nil {
events := utils.GetSortedDirectory(files)
days := utils.GetDays(events, recordingDirectory, configuration)
c.JSON(200, gin.H{
"events": days,
})
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
}
// StopAgent godoc
// @Router /api/camera/stop [post]
// @ID camera-stop
// @Tags camera
// @Summary Stop the agent.
// @Description Stop the agent.
// @Success 200 {object} models.APIResponse
func StopAgent(c *gin.Context, communication *models.Communication) {
log.Log.Info("components.Kerberos.StopAgent(): sending signal to stop agent, this will os.Exit(0).")
select {
case communication.HandleBootstrap <- "stop":
log.Log.Info("components.Kerberos.StopAgent(): Stopping machinery.")
case <-time.After(1 * time.Second):
log.Log.Info("components.Kerberos.StopAgent(): Stopping machinery timed out")
}
c.JSON(200, gin.H{
"stopped": true,
})
}
// RestartAgent godoc
// @Router /api/camera/restart [post]
// @ID camera-restart
// @Tags camera
// @Summary Restart the agent.
// @Description Restart the agent.
// @Success 200 {object} models.APIResponse
func RestartAgent(c *gin.Context, communication *models.Communication) {
log.Log.Info("components.Kerberos.RestartAgent(): sending signal to restart agent.")
select {
case communication.HandleBootstrap <- "restart":
log.Log.Info("components.Kerberos.RestartAgent(): Restarting machinery.")
case <-time.After(1 * time.Second):
log.Log.Info("components.Kerberos.RestartAgent(): Restarting machinery timed out")
}
c.JSON(200, gin.H{
"restarted": true,
})
}
// MakeRecording godoc
// @Router /api/camera/record [post]
// @ID camera-record
// @Tags camera
// @Summary Make a recording.
// @Description Make a recording.
// @Success 200 {object} models.APIResponse
func MakeRecording(c *gin.Context, communication *models.Communication) {
log.Log.Info("components.Kerberos.MakeRecording(): sending signal to start recording.")
dataToPass := models.MotionDataPartial{
Timestamp: time.Now().Unix(),
NumberOfChanges: 100000000, // hack set the number of changes to a high number to force recording
}
communication.HandleMotion <- dataToPass //Save data to the channel
c.JSON(200, gin.H{
"recording": true,
})
}
// GetSnapshotBase64 godoc
// @Router /api/camera/snapshot/base64 [get]
// @ID snapshot-base64
// @Tags camera
// @Summary Get a snapshot from the camera in base64.
// @Description Get a snapshot from the camera in base64.
// @Success 200
func GetSnapshotBase64(c *gin.Context, captureDevice *capture.Capture, configuration *models.Configuration, communication *models.Communication) {
// We'll try to get a snapshot from the camera.
base64Image := capture.Base64Image(captureDevice, communication, configuration)
if base64Image != "" {
communication.Image = base64Image
}
c.JSON(200, gin.H{
"base64": communication.Image,
})
}
// GetSnapshotJpeg godoc
// @Router /api/camera/snapshot/jpeg [get]
// @ID snapshot-jpeg
// @Tags camera
// @Summary Get a snapshot from the camera in jpeg format.
// @Description Get a snapshot from the camera in jpeg format.
// @Success 200
func GetSnapshotRaw(c *gin.Context, captureDevice *capture.Capture, configuration *models.Configuration, communication *models.Communication) {
// We'll try to get a snapshot from the camera.
image := capture.JpegImage(captureDevice, communication)
// encode image to jpeg
imageResized, _ := utils.ResizeImage(&image, uint(configuration.Config.Capture.IPCamera.BaseWidth), uint(configuration.Config.Capture.IPCamera.BaseHeight))
bytes, _ := utils.ImageToBytes(imageResized)
// Return image/jpeg
c.Data(200, "image/jpeg", bytes)
}
// GetConfig godoc
// @Router /api/config [get]
// @ID config
// @Tags config
// @Summary Get the current configuration.
// @Description Get the current configuration.
// @Success 200
func GetConfig(c *gin.Context, captureDevice *capture.Capture, configuration *models.Configuration, communication *models.Communication) {
// We'll try to get a snapshot from the camera.
base64Image := capture.Base64Image(captureDevice, communication, configuration)
if base64Image != "" {
communication.Image = base64Image
}
c.JSON(200, gin.H{
"config": configuration.Config,
"custom": configuration.CustomConfig,
"global": configuration.GlobalConfig,
"snapshot": communication.Image,
})
}
// UpdateConfig godoc
// @Router /api/config [post]
// @ID config
// @Tags config
// @Param config body models.Config true "Configuration"
// @Summary Update the current configuration.
// @Description Update the current configuration.
// @Success 200
func UpdateConfig(c *gin.Context, configDirectory string, configuration *models.Configuration, communication *models.Communication) {
var config models.Config
err := c.BindJSON(&config)
if err == nil {
err := configService.SaveConfig(configDirectory, config, configuration, communication)
if err == nil {
c.JSON(200, gin.H{
"data": "☄ Reconfiguring",
})
} else {
c.JSON(200, gin.H{
"data": "☄ Reconfiguring",
})
}
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
log.Log.Debug("ControlAgent: finished")
}

View File

@@ -0,0 +1,25 @@
package components
import (
"time"
"github.com/cedricve/go-onvif"
"github.com/kerberos-io/agent/machinery/src/log"
)
func Discover(timeout time.Duration) {
log.Log.Info("Discovering devices")
log.Log.Info("Waiting for " + (timeout * time.Second).String())
devices, err := onvif.StartDiscovery(timeout * time.Second)
if err != nil {
log.Log.Error(err.Error())
} else {
for _, device := range devices {
hostname, _ := device.GetHostname()
log.Log.Info(hostname.Name)
}
if len(devices) == 0 {
log.Log.Info("No devices descovered\n")
}
}
}

View File

@@ -0,0 +1,93 @@
package components
import (
"fmt"
"image"
"image/jpeg"
"log"
"time"
"github.com/deepch/vdk/av"
"github.com/deepch/vdk/codec/h264parser"
"github.com/deepch/vdk/format/rtsp"
"github.com/nsmith5/mjpeg"
)
type Stream struct {
Name string
Url string
Debug bool
Codecs string
}
func CreateStream(name string, url string) *Stream {
return &Stream{
Name: name,
Url: url,
}
}
func (s Stream) Open() *rtsp.Client {
// Enable debugging
if s.Debug {
rtsp.DebugRtsp = true
}
fmt.Println("Dialing in to " + s.Url)
session, err := rtsp.Dial(s.Url)
if err != nil {
log.Println("Something went wrong dialing into stream: ", err)
time.Sleep(5 * time.Second)
}
session.RtpKeepAliveTimeout = 10 * time.Second
return session
}
func (s Stream) Close(session *rtsp.Client) {
fmt.Println("Closing RTSP session.")
err := session.Close()
if err != nil {
log.Println("Something went wrong while closing your RTSP session: ", err)
}
}
func (s Stream) GetCodecs() []av.CodecData {
session := s.Open()
codec, err := session.Streams()
log.Println("Reading codecs from stream: ", codec)
if err != nil {
log.Println("Something went wrong while reading codecs from stream: ", err)
time.Sleep(5 * time.Second)
}
s.Close(session)
return codec
}
func (s Stream) ReadPackets(packetChannel chan av.Packet) {
session := s.Open()
for {
packet, err := session.ReadPacket()
if err != nil {
break
}
if len(packetChannel) < cap(packetChannel) {
packetChannel <- packet
}
}
s.Close(session)
}
func GetSPSFromCodec(codecs []av.CodecData) ([]byte, []byte) {
sps := codecs[0].(h264parser.CodecData).SPS()
pps := codecs[0].(h264parser.CodecData).PPS()
return sps, pps
}
func StartMotionJPEG(imageFunction func() (image.Image, error), quality int) mjpeg.Handler {
stream := mjpeg.Handler{
Next: imageFunction,
Options: &jpeg.Options{Quality: quality},
}
return stream
}

View File

@@ -1,95 +0,0 @@
package components
import (
"bufio"
"fmt"
"os"
"time"
"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/agent/machinery/src/packets"
"github.com/kerberos-io/joy4/av"
"github.com/pion/rtp"
"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(communication *models.Communication, rtspClient capture.RTSPClient) {
log.Log.Info("Audio.WriteAudioToBackchannel(): writing to backchannel audio codec")
length := uint32(0)
sequenceNumber := uint16(0)
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)
}
pkt := packets.Packet{
Packet: &rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: true, // should be true
PayloadType: 0, //packet.PayloadType, // will be owerwriten
SequenceNumber: sequenceNumber,
Timestamp: uint32(length),
SSRC: 1293847657,
},
Payload: bufferUlaw,
},
}
err := rtspClient.WritePacket(pkt)
if err != nil {
log.Log.Error("Audio.WriteAudioToBackchannel(): error writing packet to backchannel")
}
length = (length + uint32(len(bufferUlaw))) % 65536
sequenceNumber = (sequenceNumber + 1) % 65535
time.Sleep(128 * time.Millisecond)
}
log.Log.Info("Audio.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
infile.Write(buffer, 2, uint32(count))
count = count + 1024
time.Sleep(128 * time.Millisecond)
}
}

View File

@@ -1,27 +1,31 @@
package computervision
import (
"bufio"
"bytes"
"encoding/base64"
"image"
"image/jpeg"
"sync"
"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/conditions"
"github.com/kerberos-io/agent/machinery/src/log"
"github.com/kerberos-io/agent/machinery/src/models"
"github.com/kerberos-io/agent/machinery/src/packets"
"github.com/kerberos-io/joy4/av"
"github.com/kerberos-io/joy4/av/pubsub"
"github.com/kerberos-io/joy4/cgo/ffmpeg"
)
func ProcessMotion(motionCursor *packets.QueueCursor, configuration *models.Configuration, communication *models.Communication, mqttClient mqtt.Client, rtspClient capture.RTSPClient) {
func ProcessMotion(motionCursor *pubsub.QueueCursor, configuration *models.Configuration, communication *models.Communication, mqttClient mqtt.Client, decoder *ffmpeg.VideoDecoder, decoderMutex *sync.Mutex) { //, wg *sync.WaitGroup) {
log.Log.Debug("computervision.main.ProcessMotion(): start motion detection")
log.Log.Debug("ProcessMotion: started")
config := configuration.Config
loc, _ := time.LoadLocation(config.Timezone)
var isPixelChangeThresholdReached = false
var changesToReturn = 0
var motionRectangle models.MotionRectangle
pixelThreshold := config.Capture.PixelChangeThreshold
// Might not be set in the config file, so set it to 150
@@ -31,30 +35,33 @@ func ProcessMotion(motionCursor *packets.QueueCursor, configuration *models.Conf
if config.Capture.Continuous == "true" {
log.Log.Info("computervision.main.ProcessMotion(): you've enabled continuous recording, so no motion detection required.")
log.Log.Info("ProcessMotion: Continuous recording, so no motion detection.")
} else {
log.Log.Info("computervision.main.ProcessMotion(): motion detected is enabled, so starting the motion detection.")
log.Log.Info("ProcessMotion: Motion detection enabled.")
hubKey := config.HubKey
deviceKey := config.Key
// Allocate a VideoFrame
frame := ffmpeg.AllocVideoFrame()
// Initialise first 2 elements
var imageArray [3]*image.Gray
j := 0
var cursorError error
var pkt packets.Packet
var pkt av.Packet
for cursorError == nil {
pkt, cursorError = motionCursor.ReadPacket()
// Check If valid package.
if len(pkt.Data) > 0 && pkt.IsKeyFrame {
grayImage, err := rtspClient.DecodePacketRaw(pkt)
grayImage, err := GetGrayImage(frame, pkt, decoder, decoderMutex)
if err == nil {
imageArray[j] = &grayImage
imageArray[j] = grayImage
j++
}
}
@@ -63,51 +70,34 @@ func ProcessMotion(motionCursor *packets.QueueCursor, configuration *models.Conf
}
}
// A user might have set the base width and height for the IPCamera.
// This means also the polygon coordinates are set to a specific width and height (which might be different than the actual packets
// received from the IPCamera). So we will resize the polygon coordinates to the base width and height.
baseWidthRatio := 1.0
baseHeightRatio := 1.0
baseWidth := config.Capture.IPCamera.BaseWidth
baseHeight := config.Capture.IPCamera.BaseHeight
if baseWidth > 0 && baseHeight > 0 {
// We'll get the first image to calculate the ratio
img := imageArray[0]
if img != nil {
bounds := img.Bounds()
rows := bounds.Dy()
cols := bounds.Dx()
baseWidthRatio = float64(cols) / float64(baseWidth)
baseHeightRatio = float64(rows) / float64(baseHeight)
}
}
// Calculate mask
var polyObjects []geo.Polygon
if config.Region != nil {
for _, polygon := range config.Region.Polygon {
coords := polygon.Coordinates
poly := geo.Polygon{}
for _, c := range coords {
x := c.X * baseWidthRatio
y := c.Y * baseHeightRatio
p := geo.NewPoint(x, y)
if !poly.Contains(p) {
poly.Add(p)
}
}
polyObjects = append(polyObjects, poly)
}
}
img := imageArray[0]
var coordinatesToCheck []int
if img != nil {
// Calculate mask
var polyObjects []geo.Polygon
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)
}
}
bounds := img.Bounds()
rows := bounds.Dy()
cols := bounds.Dx()
// Make fixed size array of uinty8
var coordinatesToCheck []int
for y := 0; y < rows; y++ {
for x := 0; x < cols; x++ {
for _, poly := range polyObjects {
@@ -118,13 +108,10 @@ func ProcessMotion(motionCursor *packets.QueueCursor, configuration *models.Conf
}
}
}
}
// If no region is set, we'll skip the motion detection
if len(coordinatesToCheck) > 0 {
// Start the motion detection
i := 0
loc, _ := time.LoadLocation(config.Timezone)
for cursorError == nil {
pkt, cursorError = motionCursor.ReadPacket()
@@ -134,59 +121,67 @@ func ProcessMotion(motionCursor *packets.QueueCursor, configuration *models.Conf
continue
}
grayImage, err := rtspClient.DecodePacketRaw(pkt)
grayImage, err := GetGrayImage(frame, pkt, decoder, decoderMutex)
if err == nil {
imageArray[2] = &grayImage
imageArray[2] = grayImage
}
// We might have different conditions enabled such as time window or uri response.
// We'll validate those conditions and if not valid we'll not do anything.
detectMotion, err := conditions.Validate(loc, configuration)
if !detectMotion && err != nil {
log.Log.Debug("computervision.main.ProcessMotion(): " + err.Error() + ".")
// Store snapshots (jpg) for hull.
if config.Capture.Snapshots != "false" {
StoreSnapshot(communication, frame, pkt, decoder, decoderMutex)
}
// Check if within time interval
detectMotion := true
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.Info("ProcessMotion: Time interval not valid, disabling motion detection.")
}
}
}
}
if config.Capture.Motion != "false" {
if detectMotion {
// Remember additional information about the result of findmotion
isPixelChangeThresholdReached, changesToReturn = FindMotion(imageArray, coordinatesToCheck, pixelThreshold)
if detectMotion && isPixelChangeThresholdReached {
// Remember additional information about the result of findmotion
isPixelChangeThresholdReached, changesToReturn, motionRectangle = FindMotion(imageArray, coordinatesToCheck, pixelThreshold)
if isPixelChangeThresholdReached {
// 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, 2, false, payload)
} else {
log.Log.Info("computervision.main.ProcessMotion(): failed to package MQTT message: " + err.Error())
}
} else {
mqttClient.Publish("kerberos/agent/"+deviceKey, 2, false, "motion")
}
// If offline mode is disabled, send a message to the hub
if config.Offline != "true" {
if mqttClient != nil {
if hubKey != "" {
mqttClient.Publish("kerberos/"+hubKey+"/device/"+deviceKey+"/motion", 2, false, "motion")
} else {
mqttClient.Publish("kerberos/device/"+deviceKey+"/motion", 2, false, "motion")
}
}
}
if config.Capture.Recording != "false" {
dataToPass := models.MotionDataPartial{
Timestamp: time.Now().Unix(),
NumberOfChanges: changesToReturn,
Rectangle: motionRectangle,
}
communication.HandleMotion <- dataToPass //Save data to the channel
if config.Capture.Recording != "false" {
dataToPass := models.MotionDataPartial{
Timestamp: time.Now().Unix(),
NumberOfChanges: changesToReturn,
}
communication.HandleMotion <- dataToPass //Save data to the channel
}
}
@@ -200,63 +195,67 @@ func ProcessMotion(motionCursor *packets.QueueCursor, configuration *models.Conf
img = nil
}
}
frame.Free()
}
log.Log.Debug("computervision.main.ProcessMotion(): stop the motion detection.")
log.Log.Debug("ProcessMotion: finished")
}
func FindMotion(imageArray [3]*image.Gray, coordinatesToCheck []int, pixelChangeThreshold int) (thresholdReached bool, changesDetected int, motionRectangle models.MotionRectangle) {
func FindMotion(imageArray [3]*image.Gray, coordinatesToCheck []int, pixelChangeThreshold int) (thresholdReached bool, changesDetected int) {
image1 := imageArray[0]
image2 := imageArray[1]
image3 := imageArray[2]
threshold := 60
changes, motionRectangle := AbsDiffBitwiseAndThreshold(image1, image2, image3, threshold, coordinatesToCheck)
return changes > pixelChangeThreshold, changes, motionRectangle
changes := AbsDiffBitwiseAndThreshold(image1, image2, image3, threshold, coordinatesToCheck)
return changes > pixelChangeThreshold, changes
}
func AbsDiffBitwiseAndThreshold(img1 *image.Gray, img2 *image.Gray, img3 *image.Gray, threshold int, coordinatesToCheck []int) (int, models.MotionRectangle) {
func GetGrayImage(frame *ffmpeg.VideoFrame, pkt av.Packet, dec *ffmpeg.VideoDecoder, decoderMutex *sync.Mutex) (*image.Gray, error) {
_, err := capture.DecodeImage(frame, pkt, dec, decoderMutex)
// Do a deep copy of the image
imgDeepCopy := image.NewGray(frame.ImageGray.Bounds())
imgDeepCopy.Stride = frame.ImageGray.Stride
copy(imgDeepCopy.Pix, frame.ImageGray.Pix)
return imgDeepCopy, err
}
func GetRawImage(frame *ffmpeg.VideoFrame, pkt av.Packet, dec *ffmpeg.VideoDecoder, decoderMutex *sync.Mutex) (*ffmpeg.VideoFrame, error) {
_, err := capture.DecodeImage(frame, pkt, dec, decoderMutex)
return frame, err
}
func ImageToBytes(img image.Image) ([]byte, error) {
buffer := new(bytes.Buffer)
w := bufio.NewWriter(buffer)
err := jpeg.Encode(w, img, &jpeg.Options{Quality: 15})
return buffer.Bytes(), err
}
func AbsDiffBitwiseAndThreshold(img1 *image.Gray, img2 *image.Gray, img3 *image.Gray, threshold int, coordinatesToCheck []int) int {
changes := 0
var pixelList [][]int
for i := 0; i < len(coordinatesToCheck); i++ {
pixel := coordinatesToCheck[i]
diff := int(img3.Pix[pixel]) - int(img1.Pix[pixel])
diff2 := int(img3.Pix[pixel]) - int(img2.Pix[pixel])
if (diff > threshold || diff < -threshold) && (diff2 > threshold || diff2 < -threshold) {
changes++
// Store the pixel coordinates where the change is detected
pixelList = append(pixelList, []int{pixel % img1.Bounds().Dx(), pixel / img1.Bounds().Dx()})
}
}
// Calculate rectangle of pixelList (startX, startY, endX, endY)
var motionRectangle models.MotionRectangle
if len(pixelList) > 0 {
startX := pixelList[0][0]
startY := pixelList[0][1]
endX := startX
endY := startY
for _, pixel := range pixelList {
if pixel[0] < startX {
startX = pixel[0]
}
if pixel[1] < startY {
startY = pixel[1]
}
if pixel[0] > endX {
endX = pixel[0]
}
if pixel[1] > endY {
endY = pixel[1]
}
}
log.Log.Debugf("Rectangle of changes detected: startX: %d, startY: %d, endX: %d, endY: %d", startX, startY, endX, endY)
motionRectangle = models.MotionRectangle{
X: startX,
Y: startY,
Width: endX - startX,
Height: endY - startY,
}
log.Log.Debugf("Motion rectangle: %+v", motionRectangle)
}
return changes, motionRectangle
return changes
}
func StoreSnapshot(communication *models.Communication, frame *ffmpeg.VideoFrame, pkt av.Packet, decoder *ffmpeg.VideoDecoder, decoderMutex *sync.Mutex) {
rgbImage, err := GetRawImage(frame, pkt, decoder, decoderMutex)
if err == nil {
buffer := new(bytes.Buffer)
w := bufio.NewWriter(buffer)
err := jpeg.Encode(w, &rgbImage.Image, &jpeg.Options{Quality: 15})
if err == nil {
snapshot := base64.StdEncoding.EncodeToString(buffer.Bytes())
communication.Image = snapshot
}
}
}

View File

@@ -1,28 +0,0 @@
package conditions
import (
"errors"
"time"
"github.com/kerberos-io/agent/machinery/src/models"
)
func Validate(loc *time.Location, configuration *models.Configuration) (valid bool, err error) {
valid = true
err = nil
withinTimeInterval := IsWithinTimeInterval(loc, configuration)
if !withinTimeInterval {
valid = false
err = errors.New("time interval not valid")
return
}
validUriResponse := IsValidUriResponse(configuration)
if !validUriResponse {
valid = false
err = errors.New("uri response not valid")
return
}
return
}

View File

@@ -1,39 +0,0 @@
package conditions
import (
"time"
"github.com/kerberos-io/agent/machinery/src/log"
"github.com/kerberos-io/agent/machinery/src/models"
)
func IsWithinTimeInterval(loc *time.Location, configuration *models.Configuration) (enabled bool) {
config := configuration.Config
timeEnabled := config.Time
enabled = true
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) {
log.Log.Debug("conditions.timewindow.IsWithinTimeInterval(): time interval valid, enabling recording.")
} else {
log.Log.Info("conditions.timewindow.IsWithinTimeInterval(): time interval not valid, disabling recording.")
enabled = false
}
}
}
}
return
}

View File

@@ -1,59 +0,0 @@
package conditions
import (
"bytes"
"crypto/tls"
"fmt"
"net/http"
"os"
"time"
"github.com/kerberos-io/agent/machinery/src/log"
"github.com/kerberos-io/agent/machinery/src/models"
)
func IsValidUriResponse(configuration *models.Configuration) (enabled bool) {
config := configuration.Config
conditionURI := config.ConditionURI
enabled = true
if conditionURI != "" {
// We will send a POST request to the conditionURI, and expect a 200 response.
// In the payload we will send some information, so the other end can decide
// if it should enable or disable recording.
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{}
}
var object = fmt.Sprintf(`{
"camera_id" : "%s",
"camera_name" : "%s",
"site_id" : "%s",
"hub_key" : "%s",
"timestamp" : "%s",
}`, config.Key, config.FriendlyName, config.HubSite, config.HubKey, time.Now().Format("2006-01-02 15:04:05"))
var jsonStr = []byte(object)
buffy := bytes.NewBuffer(jsonStr)
req, _ := http.NewRequest("POST", conditionURI, buffy)
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if resp != nil {
resp.Body.Close()
}
if err == nil && resp.StatusCode == 200 {
log.Log.Info("conditions.uri.IsValidUriResponse(): response 200, enabling recording.")
} else {
log.Log.Info("conditions.uri.IsValidUriResponse(): response not 200, disabling recording.")
enabled = false
}
}
return
}

View File

@@ -4,9 +4,11 @@ import (
"context"
"encoding/json"
"errors"
"image"
"io/ioutil"
"os"
"reflect"
"sort"
"strconv"
"strings"
"time"
@@ -18,6 +20,25 @@ import (
"go.mongodb.org/mongo-driver/bson"
)
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 := configDirectory + "/data/snapshots/" + files[1].Name()
f, err := os.Open(filePath)
if err != nil {
return nil, err
}
defer f.Close()
image, _, err := image.Decode(f)
return image, err
}
return nil, errors.New("Could not find a snapshot in " + snapshotDirectory)
}
// 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.
@@ -59,7 +80,7 @@ func OpenConfig(configDirectory string, configuration *models.Configuration) {
// Write to mongodb
client := database.New()
db := client.Client.Database(database.DatabaseName)
db := client.Database(database.DatabaseName)
collection := db.Collection("configuration")
var globalConfig models.Config
@@ -120,13 +141,8 @@ func OpenConfig(configDirectory string, configuration *models.Configuration) {
},
)
// Reset main configuration Config.
configuration.Config = models.Config{}
// Merge the global settings in the main config
// Merge Config toplevel
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
@@ -135,27 +151,12 @@ func OpenConfig(configDirectory string, configuration *models.Configuration) {
conjungo.Merge(&kerberosvault, configuration.CustomConfig.KStorage, opts)
configuration.Config.KStorage = &kerberosvault
// Merge Secondary Kerberos Vault settings
var kerberosvaultSecondary models.KStorage
conjungo.Merge(&kerberosvaultSecondary, configuration.GlobalConfig.KStorageSecondary, opts)
conjungo.Merge(&kerberosvaultSecondary, configuration.CustomConfig.KStorageSecondary, opts)
configuration.Config.KStorageSecondary = &kerberosvaultSecondary
// Merge Kerberos S3 settings
var s3 models.S3
conjungo.Merge(&s3, configuration.GlobalConfig.S3, opts)
conjungo.Merge(&s3, configuration.CustomConfig.S3, opts)
configuration.Config.S3 = &s3
// Merge Encryption settings
var encryption models.Encryption
conjungo.Merge(&encryption, configuration.GlobalConfig.Encryption, opts)
conjungo.Merge(&encryption, configuration.CustomConfig.Encryption, opts)
configuration.Config.Encryption = &encryption
// Merge timetable manually because it's an array
configuration.Config.Timetable = configuration.CustomConfig.Timetable
// Cleanup
opts = nil
@@ -189,19 +190,15 @@ func OpenConfig(configDirectory string, configuration *models.Configuration) {
}
jsonFile.Close()
}
}
return
}
// This function will override the configuration with environment variables.
func OverrideWithEnvironmentVariables(configuration *models.Configuration) {
environmentVariables := os.Environ()
// Initialize the configuration for some new fields.
if configuration.Config.KStorageSecondary == nil {
configuration.Config.KStorageSecondary = &models.KStorage{}
}
for _, env := range environmentVariables {
if strings.Contains(env, "AGENT_") {
key := strings.Split(env, "=")[0]
@@ -239,15 +236,7 @@ func OverrideWithEnvironmentVariables(configuration *models.Configuration) {
configuration.Config.Capture.IPCamera.SubRTSP = value
break
/* Base width and height for the liveview and motion regions */
case "AGENT_CAPTURE_IPCAMERA_BASE_WIDTH":
configuration.Config.Capture.IPCamera.BaseWidth, _ = strconv.Atoi(value)
break
case "AGENT_CAPTURE_IPCAMERA_BASE_HEIGHT":
configuration.Config.Capture.IPCamera.BaseHeight, _ = strconv.Atoi(value)
break
/* ONVIF connnection settings */
/* ONVIF connnection settings */
case "AGENT_CAPTURE_IPCAMERA_ONVIF":
configuration.Config.Capture.IPCamera.ONVIF = value
break
@@ -400,26 +389,10 @@ func OverrideWithEnvironmentVariables(configuration *models.Configuration) {
configuration.Config.MQTTPassword = value
break
/* MQTT chunking of low-resolution images into multiple messages */
case "AGENT_CAPTURE_LIVEVIEW_CHUNKING":
configuration.Config.Capture.LiveviewChunking = value
break
/* Real-time streaming of keyframes to a MQTT topic */
case "AGENT_REALTIME_PROCESSING":
configuration.Config.RealtimeProcessing = value
break
case "AGENT_REALTIME_PROCESSING_TOPIC":
configuration.Config.RealtimeProcessingTopic = value
break
/* WebRTC settings for live-streaming (remote) */
case "AGENT_STUN_URI":
configuration.Config.STUNURI = value
break
case "AGENT_FORCE_TURN":
configuration.Config.ForceTurn = value
break
case "AGENT_TURN_URI":
configuration.Config.TURNURI = value
break
@@ -440,9 +413,6 @@ func OverrideWithEnvironmentVariables(configuration *models.Configuration) {
break
/* When connected and storing in Kerberos Hub (SAAS) */
case "AGENT_HUB_ENCRYPTION":
configuration.Config.HubEncryption = value
break
case "AGENT_HUB_URI":
configuration.Config.HubURI = value
break
@@ -459,7 +429,7 @@ func OverrideWithEnvironmentVariables(configuration *models.Configuration) {
configuration.Config.S3.Region = value
break
/* When storing in a Vault */
/* When storing in a Kerberos Vault */
case "AGENT_KERBEROSVAULT_URI":
configuration.Config.KStorage.URI = value
break
@@ -476,37 +446,6 @@ func OverrideWithEnvironmentVariables(configuration *models.Configuration) {
configuration.Config.KStorage.Directory = value
break
/* Retry policy and timeout */
case "AGENT_KERBEROSVAULT_MAX_RETRIES":
maxRetries, err := strconv.Atoi(value)
if err == nil {
configuration.Config.KStorage.MaxRetries = maxRetries
}
break
case "AGENT_KERBEROSVAULT_TIMEOUT":
timeout, err := strconv.Atoi(value)
if err == nil {
configuration.Config.KStorage.Timeout = timeout
}
break
/* When storing in a secondary Vault */
case "AGENT_KERBEROSVAULT_SECONDARY_URI":
configuration.Config.KStorageSecondary.URI = value
break
case "AGENT_KERBEROSVAULT_SECONDARY_ACCESS_KEY":
configuration.Config.KStorageSecondary.AccessKey = value
break
case "AGENT_KERBEROSVAULT_SECONDARY_SECRET_KEY":
configuration.Config.KStorageSecondary.SecretAccessKey = value
break
case "AGENT_KERBEROSVAULT_SECONDARY_PROVIDER":
configuration.Config.KStorageSecondary.Provider = value
break
case "AGENT_KERBEROSVAULT_SECONDARY_DIRECTORY":
configuration.Config.KStorageSecondary.Directory = value
break
/* When storing in dropbox */
case "AGENT_DROPBOX_ACCESS_TOKEN":
configuration.Config.Dropbox.AccessToken = value
@@ -514,44 +453,9 @@ func OverrideWithEnvironmentVariables(configuration *models.Configuration) {
case "AGENT_DROPBOX_DIRECTORY":
configuration.Config.Dropbox.Directory = value
break
/* When encryption is enabled */
case "AGENT_ENCRYPTION":
configuration.Config.Encryption.Enabled = value
break
case "AGENT_ENCRYPTION_RECORDINGS":
configuration.Config.Encryption.Recordings = value
break
case "AGENT_ENCRYPTION_FINGERPRINT":
configuration.Config.Encryption.Fingerprint = value
break
case "AGENT_ENCRYPTION_PRIVATE_KEY":
encryptionPrivateKey := strings.ReplaceAll(value, "\\n", "\n")
configuration.Config.Encryption.PrivateKey = encryptionPrivateKey
break
case "AGENT_ENCRYPTION_SYMMETRIC_KEY":
configuration.Config.Encryption.SymmetricKey = value
break
/* When signing is enabled */
case "AGENT_SIGNING":
configuration.Config.Signing.Enabled = value
break
case "AGENT_SIGNING_PRIVATE_KEY":
signingPrivateKey := strings.ReplaceAll(value, "\\n", "\n")
configuration.Config.Signing.PrivateKey = signingPrivateKey
break
}
}
}
// Signing is a new feature, so if empty we set default values.
if configuration.Config.Signing == nil || configuration.Config.Signing.PrivateKey == "" {
configuration.Config.Signing = &models.Signing{
Enabled: "true",
PrivateKey: "-----BEGIN PRIVATE KEY-----\nMIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQDoSxjyw08lRxF4Yoqmcaewjq3XjB55dMy4tlN5MGLdr8aAPuNR9Mwh3jlh1bDpwQXNgZkHDV/q9bpdPGGi7SQo2xw+rDuo5Y1f3wdzz+iuCTPbzoGFalE+1PZlU5TEtUtlbt7MRc4pxTaLP3u0P3EtW3KnzcUarcJWZJYxzv7gqVNCA/47BN+1ptqjwz3LAlah5yaftEvVjkaANOsafUswbS4VT44XfSlbKgebORCKDuNgQiyhuV5gU+J0TOaqRWwwMAWV0UoScyJLfhHRBCrUwrCUTwqH9jfkB7pgRFsYoZJd4MKMeHJjFSum+QXCBqInSnwu8c2kJChiLMWqJ+mhpTdfUAmSkeUSStfbbcavIPbDABvMgzOcmYMIVXXe57twU0xdu3AqWLtc9kw1BkUgZblM9pSSpYrIDheEyMs2/hiLgXsIaM0nVQtqwrA7rbeEGuPblzA6hvHgwN9K6HaBqdlGSlpYZ0v3SWIMwmxRB+kIojlyuggm8Qa4mqL97GFDGl6gOBGlNUFTBUVEa3EaJ7NJpGobRGsh/9dXzcW4aYmT9WxlzTlIKksI1ro6KdRfuVWfEs4AnG8bVEJmofK8EUrueB9IdXlcJZB49xolnOZPFohtMe/0U7evQOQP3sZnX+KotCsE7OXJvL09oF58JKoqmK9lPp0+pFBU4g6NjQIDAQABAoICAA+RSWph1t+q5R3nxUxFTYMrhv5IjQe2mDxJpF3B409zolC9OHxgGUisobTY3pBqs0DtKbxUeH2A0ehUH/axEosWHcz3cmIbgxHE9kdlJ9B3Lmss6j/uw+PWutu1sgm5phaIFIvuNNRWhPB6yXUwU4sLRat1+Z9vTmIQiKdtLIrtJz/n2VDvrJxn1N+yAsE20fnrksFKyZuxVsJaZPiX/t5Yv1/z0LjFjVoL7GUA5/Si7csN4ftqEhUrkNr2BvcZlTyffrF4lZCXrtl76RNUaxhqIu3H0gFbV2UfBpuckkfAhNRpXJ4iFSxm4nQbk4ojV8+l21RFOBeDN2Z7Ocu6auP5MnzpopR66vmDCmPoid498VGgDzFQEVkOar8WAa4v9h85QgLKrth6FunmaWJUT6OggQD3yY58GSwp5+ARMETMBP2x6Eld+PGgqoJvPT1+l/e9gOw7/SJ+Wz6hRXZAm/eiXMppHtB7sfea5rscNanPjJkK9NvPM0MX9cq/iA6QjXuETkMbubjo+Cxk3ydZiIQmWQDAx/OgxTyHbeRCVhLPcAphX0clykCuHZpI9Mvvj643/LoE0mjTByWJXf/WuGJA8ElHkjSdokVJ7jumz8OZZHfq0+V7+la2opsObeQANHW5MLWrnHlRVzTGV0IRZDXh7h1ptUJ4ubdvw/GJ2NeTAoIBAQD0lXXdjYKWC4uZ4YlgydP8b1CGda9cBV5RcPt7q9Ya1R2E4ieYyohmzltopvdaOXdsTZzhtdzOzKF+2qNcbBKhBTleYZ8GN5RKbo7HwXWpzfCTjseKHOD/QPwvBKXzLVWNtXn1NrLR79Rv0wbkYF6DtoqpEPf5kMs4bx79yW+mz8FUgdEeMjKphx6Jd5RYlTUxS64K6bnK7gjHNCF2cwdxsh4B6EB649GKeNz4JXi+oQBmOcX5ncXnkJrbju+IjtCkQ40HINVNdX7XeEaaw6KGaImVjw61toPUuDaioYUojufayoyXaUJnDbHQ2tNekEpq5iwnenZCbUKWmSeRe7dLAoIBAQDzIscYujsrmPxiTj2prhG0v36NRNP99mShnnJGowiIs+UBS0EMdOmBFa2sC9uFs/VnreQNYPDJdfr7O5VK9kfbH/PSiiKJ+wVebfdAlWkJYH27JN2Kl2l/OsvRVelNvF3BWIYF46qzGxIM0axaz3T2ZAJ9SrUgeAYhak6uyM4fbexEWXxDgPGu6C0jB6IAzmHJnnh+j5+4ZXqjVyUxBYtUsWXF/TXomVcT9jxj7aUmS2/Us0XTVOVNpALqqYcekrzsX/wX0OEi5HkivYXHcNaDHx3NuUf6KdYof5DwPUM76qe+5/kWlSIHP3M6rIFK3pYFUnkHn2E8jNWcO97Aio+HAoIBAA+bcff/TbPxbKkXIUMR3fsfx02tONFwbkJYKVQM9Q6lRsrx+4Dee7HDvUWCUgpp3FsG4NnuVvbDTBLiNMZzBwVLZgvFwvYMmePeBjJs/+sj/xQLamQ/z4O6S91cOJK589mlGPEy2lpXKYExQCFWnPFetp5vPMOqH62sOZgMQJmubDHOTt/UaDM1Mhenj8nPS6OnpqV/oKF4awr7Ip+CW5k/unZ4sZSl8PsbF06mZXwUngfn6+Av1y8dpSQZjONz6ZBx1w/7YmEc/EkXnbnGfhqBlTX7+P5TdTofvyzFjc+2vsjRYANRbjFRSGWBcTd5kaYcpfim8eDvQ+6EO2gnMt0CggEAH2ln1Y8B5AEQ4lZ/avOdP//ZhsDUrqPtnl/NHckkahzrwj4JumVEYbP+SxMBGoYEd4+kvgG/OhfvBBRPlm65G9tF8fZ8vdzbdba5UfO7rUV1GP+LS8OCErjy6imySaPDbR5Vul8Oh7NAor1YCidxUf/bvnovanF3QUvtvHEfCDp4YuA4yLPZBaLjaforePUw9w5tPNSravRZYs74dBvmQ1vj7S9ojpN5B5AxfyuNwaPPX+iFZec69MvywISEe3Ozysof1Kfc3lgsOkvIA9tVK32SqSh93xkWnQbWH+OaUxxe7bAko0FDMzKEXZk53wVg1nEwR8bUljEPy+6EOdXs8wKCAQEAsEOWYMY5m7HkeG2XTTvX7ECmmdGl/c4ZDVwzB4IPxqUG7XfLmtsON8YoKOEUpJoc4ANafLXzmU+esUGbH4Ph22IWgP9jzws7jxaN/Zoku64qrSjgEZFTRIpKyhFk/ImWbS9laBW4l+m0tqTTRqoE0QEJf/2uv/04q65zrA70X9z2+KTrAtqOiRQPWl/IxRe9U4OEeGL+oD+YlXKCDsnJ3rwUIOZgJx0HWZg7K35DKwqs1nVi56FBdljiTRKAjVLRedjgDCSfGS1yUZ3krHzpaPt1qgnT3rdtYcIdbYDr66V2/gEEaz6XMGHuTk/ewjzUJxq9UTVeXOCbkRPXgVJg1w==\n-----END PRIVATE KEY-----",
}
}
}
func SaveConfig(configDirectory string, config models.Config, configuration *models.Configuration, communication *models.Communication) error {
@@ -567,9 +471,7 @@ func SaveConfig(configDirectory string, config models.Config, configuration *mod
if communication.CameraConnected {
select {
case communication.HandleBootstrap <- "restart":
log.Log.Info("config.main.SaveConfig(): update config, restart agent.")
case <-time.After(1 * time.Second):
log.Log.Info("config.main.SaveConfig(): update config, restart agent.")
default:
}
}
@@ -582,25 +484,12 @@ func SaveConfig(configDirectory string, config models.Config, configuration *mod
}
func StoreConfig(configDirectory string, config models.Config) error {
// Encryption key can be set wrong.
if config.Encryption != nil {
encryptionPrivateKey := config.Encryption.PrivateKey
// Replace \\n by \n
encryptionPrivateKey = strings.ReplaceAll(encryptionPrivateKey, "\\n", "\n")
config.Encryption.PrivateKey = encryptionPrivateKey
}
// Reset the basewidth and baseheight
config.Capture.IPCamera.BaseWidth = 0
config.Capture.IPCamera.BaseHeight = 0
// Save into database
if os.Getenv("DEPLOYMENT") == "factory" || os.Getenv("MACHINERY_ENVIRONMENT") == "kubernetes" {
// Write to mongodb
client := database.New()
db := client.Client.Database(database.DatabaseName)
db := client.Database(database.DatabaseName)
collection := db.Collection("configuration")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)

View File

@@ -15,19 +15,12 @@ type DB struct {
Client *mongo.Client
}
var TIMEOUT = 10 * time.Second
var _init_ctx sync.Once
var _instance *DB
var DatabaseName = "KerberosFactory"
var DatabaseName = os.Getenv("MONGODB_DATABASE_FACTORY")
func New() *mongo.Client {
func New() *DB {
if DatabaseName == "" {
DatabaseName = "KerberosFactory"
}
mongodbURI := os.Getenv("MONGODB_URI")
host := os.Getenv("MONGODB_HOST")
databaseCredentials := os.Getenv("MONGODB_DATABASE_CREDENTIALS")
replicaset := os.Getenv("MONGODB_REPLICASET")
@@ -35,46 +28,28 @@ func New() *DB {
password := os.Getenv("MONGODB_PASSWORD")
authentication := "SCRAM-SHA-256"
ctx, cancel := context.WithTimeout(context.Background(), TIMEOUT)
defer cancel()
_init_ctx.Do(func() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_instance = new(DB)
// We can also apply the complete URI
// e.g. "mongodb+srv://<username>:<password>@kerberos-hub.shhng.mongodb.net/?retryWrites=true&w=majority&appName=kerberos-hub"
if mongodbURI != "" {
serverAPI := options.ServerAPI(options.ServerAPIVersion1)
opts := options.Client().ApplyURI(mongodbURI).SetServerAPIOptions(serverAPI)
// Create a new client and connect to the server
client, err := mongo.Connect(ctx, opts)
if err != nil {
fmt.Printf("Error setting up mongodb connection: %+v\n", err)
os.Exit(1)
}
_instance.Client = client
} else {
// New MongoDB driver
mongodbURI := fmt.Sprintf("mongodb://%s:%s@%s", username, password, host)
if replicaset != "" {
mongodbURI = fmt.Sprintf("%s/?replicaSet=%s", mongodbURI, replicaset)
}
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 setting up mongodb connection: %+v\n", err)
os.Exit(1)
}
_instance.Client = client
mongodbURI := fmt.Sprintf("mongodb://%s:%s@%s", username, password, host)
if replicaset != "" {
mongodbURI = fmt.Sprintf("%s/?replicaSet=%s", mongodbURI, replicaset)
}
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 setting up mongodb connection: %+v\n", err)
os.Exit(1)
}
_instance.Client = client
})
return _instance
return _instance.Client
}

View File

@@ -1,126 +0,0 @@
package encryption
import (
"bytes"
"crypto"
"crypto/aes"
"crypto/cipher"
"crypto/md5"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"encoding/base64"
"errors"
"hash"
)
func DecryptWithPrivateKey(ciphertext string, privateKey *rsa.PrivateKey) ([]byte, error) {
cipheredValue, _ := base64.StdEncoding.DecodeString(ciphertext)
out, err := rsa.DecryptPKCS1v15(nil, privateKey, cipheredValue)
return out, err
}
func SignWithPrivateKey(data []byte, privateKey *rsa.PrivateKey) ([]byte, error) {
hashed := sha256.Sum256(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)
return cipherText, nil
}
func AesDecrypt(cipherText []byte, password string) ([]byte, error) {
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
}
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) {
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
}
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...)
}

View File

@@ -12,6 +12,7 @@ import (
// The logging library being used everywhere.
var Log = Logging{
Logger: "logrus",
Level: "debug",
}
// -----------------
@@ -44,44 +45,19 @@ func ConfigureGoLogging(configDirectory string, timezone *time.Location) {
// This a logrus
// -> github.com/sirupsen/logrus
func ConfigureLogrus(level string, output string, timezone *time.Location) {
if output == "json" {
// Log as JSON instead of the default ASCII formatter.
logrus.SetFormatter(LocalTimeZoneFormatter{
Timezone: timezone,
Formatter: &logrus.JSONFormatter{},
})
} else if output == "text" {
// Log as text with colors.
formatter := logrus.TextFormatter{
ForceColors: true,
FullTimestamp: true,
}
logrus.SetFormatter(LocalTimeZoneFormatter{
Timezone: timezone,
Formatter: &formatter,
})
}
// Use local timezone for providing datetime in logs!
func ConfigureLogrus(timezone *time.Location) {
// Log as JSON instead of the default ASCII formatter.
logrus.SetFormatter(LocalTimeZoneFormatter{
Timezone: timezone,
Formatter: &logrus.JSONFormatter{},
}) // Use local timezone for providing datetime in logs!
// Output to stdout instead of the default stderr
// Can be any io.Writer, see below for File example
logrus.SetOutput(os.Stdout)
// Only log the warning severity or above.
logLevel := logrus.InfoLevel
if level == "error" {
logLevel = logrus.ErrorLevel
} else if level == "debug" {
logLevel = logrus.DebugLevel
} else if level == "fatal" {
logLevel = logrus.FatalLevel
} else if level == "warning" {
logLevel = logrus.WarnLevel
} // Add this line for logging filename and line number!
logrus.SetLevel(logLevel)
logrus.SetLevel(logrus.InfoLevel)
}
type LocalTimeZoneFormatter struct {
@@ -96,14 +72,15 @@ func (u LocalTimeZoneFormatter) Format(e *logrus.Entry) ([]byte, error) {
type Logging struct {
Logger string
Level string
}
func (self *Logging) Init(level string, logoutput string, configDirectory string, timezone *time.Location) {
func (self *Logging) Init(configDirectory string, timezone *time.Location) {
switch self.Logger {
case "go-logging":
ConfigureGoLogging(configDirectory, timezone)
case "logrus":
ConfigureLogrus(level, logoutput, timezone)
ConfigureLogrus(timezone)
default:
}
}
@@ -118,16 +95,6 @@ func (self *Logging) Info(sentence string) {
}
}
func (self *Logging) Infof(format string, args ...interface{}) {
switch self.Logger {
case "go-logging":
gologging.Infof(format, args...)
case "logrus":
logrus.Infof(format, args...)
default:
}
}
func (self *Logging) Warning(sentence string) {
switch self.Logger {
case "go-logging":
@@ -148,16 +115,6 @@ func (self *Logging) Debug(sentence string) {
}
}
func (self *Logging) Debugf(format string, args ...interface{}) {
switch self.Logger {
case "go-logging":
gologging.Debugf(format, args...)
case "logrus":
logrus.Debugf(format, args...)
default:
}
}
func (self *Logging) Error(sentence string) {
switch self.Logger {
case "go-logging":

View File

@@ -1,6 +0,0 @@
package models
type AudioDataPartial struct {
Timestamp int64 `json:"timestamp" bson:"timestamp"`
Data []int16 `json:"data" bson:"data"`
}

View File

@@ -2,9 +2,11 @@ package models
import (
"context"
"sync"
"sync/atomic"
"github.com/kerberos-io/agent/machinery/src/packets"
"github.com/kerberos-io/joy4/av/pubsub"
"github.com/kerberos-io/joy4/cgo/ffmpeg"
"github.com/tevino/abool"
)
@@ -15,27 +17,25 @@ type Communication struct {
CancelContext *context.CancelFunc
PackageCounter *atomic.Value
LastPacketTimer *atomic.Value
PackageCounterSub *atomic.Value
LastPacketTimerSub *atomic.Value
CloudTimestamp *atomic.Value
HandleBootstrap chan string
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 RequestHDStreamPayload
HandleLiveHDHandshake chan SDPPayload
HandleLiveHDPeers chan string
HandleONVIF chan OnvifAction
IsConfiguring *abool.AtomicBool
Queue *packets.Queue
SubQueue *packets.Queue
Queue *pubsub.Queue
SubQueue *pubsub.Queue
DecoderMutex *sync.Mutex
SubDecoderMutex *sync.Mutex
Decoder *ffmpeg.VideoDecoder
SubDecoder *ffmpeg.VideoDecoder
Image string
CameraConnected bool
MainStreamConnected bool
SubStreamConnected bool
HasBackChannel bool
}

View File

@@ -12,43 +12,36 @@ 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"`
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"`
KStorageSecondary *KStorage `json:"kstorage_secondary,omitempty" bson:"kstorage_secondary,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"`
ForceTurn string `json:"turn_force" bson:"turn_force"`
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*/
HubEncryption string `json:"hub_encryption" bson:"hub_encryption"`
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"`
Signing *Signing `json:"signing,omitempty" bson:"signing,omitempty"`
RealtimeProcessing string `json:"realtimeprocessing,omitempty" bson:"realtimeprocessing,omitempty"`
RealtimeProcessingTopic string `json:"realtimeprocessing_topic" bson:"realtimeprocessing_topic"`
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,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"`
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"`
}
// Capture defines which camera type (Id) you are using (IP, USB or Raspberry Pi camera),
@@ -62,11 +55,9 @@ type Capture struct {
Snapshots string `json:"snapshots,omitempty"`
Motion string `json:"motion,omitempty"`
Liveview string `json:"liveview,omitempty"`
LiveviewChunking string `json:"liveview_chunking,omitempty" bson:"liveview_chunking,omitempty"`
Continuous string `json:"continuous,omitempty"`
PostRecording int64 `json:"postrecording"`
PreRecording int64 `json:"prerecording"`
GopSize int `json:"gopsize,omitempty" bson:"gopsize,omitempty"` // GOP size in seconds, used for pre-recording
MaxLengthRecording int64 `json:"maxlengthrecording"`
TranscodingWebRTC string `json:"transcodingwebrtc"`
TranscodingResolution int64 `json:"transcodingresolution"`
@@ -79,28 +70,15 @@ type Capture struct {
// IPCamera configuration, such as the RTSP url of the IPCamera and the FPS.
// Also includes ONVIF integration
type IPCamera struct {
RTSP string `json:"rtsp"`
Width int `json:"width"`
Height int `json:"height"`
FPS string `json:"fps"`
SubRTSP string `json:"sub_rtsp"`
SubWidth int `json:"sub_width"`
SubHeight int `json:"sub_height"`
BaseWidth int `json:"base_width"`
BaseHeight int `json:"base_height"`
SubFPS string `json:"sub_fps"`
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"`
SPSNALUs [][]byte `json:"sps_nalus,omitempty" bson:"sps_nalus,omitempty"`
PPSNALUs [][]byte `json:"pps_nalus,omitempty" bson:"pps_nalus,omitempty"`
VPSNALUs [][]byte `json:"vps_nalus,omitempty" bson:"vps_nalus,omitempty"`
SampleRate int `json:"sample_rate,omitempty" bson:"sample_rate,omitempty"`
Channels int `json:"channels,omitempty" bson:"channels,omitempty"`
Width int `json:"width"`
Height int `json:"height"`
FPS string `json:"fps"`
RTSP string `json:"rtsp"`
SubRTSP string `json:"sub_rtsp"`
ONVIF string `json:"onvif,omitempty" bson:"onvif"`
ONVIFXAddr string `json:"onvif_xaddr,omitempty" bson:"onvif_xaddr"`
ONVIFUsername string `json:"onvif_username,omitempty" bson:"onvif_username"`
ONVIFPassword string `json:"onvif_password,omitempty" bson:"onvif_password"`
}
// USBCamera configuration, such as the device path (/dev/video*)
@@ -172,8 +150,6 @@ type KStorage struct {
SecretAccessKey string `json:"secret_access_key,omitempty" bson:"secret_access_key,omitempty"`
Provider string `json:"provider,omitempty" bson:"provider,omitempty"`
Directory string `json:"directory,omitempty" bson:"directory,omitempty"`
MaxRetries int `json:"max_retries,omitempty" bson:"max_retries,omitempty"`
Timeout int `json:"timeout,omitempty" bson:"timeout,omitempty"`
}
// Dropbox integration
@@ -181,18 +157,3 @@ 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"`
}
// Signing
type Signing struct {
Enabled string `json:"enabled" bson:"enabled"`
PrivateKey string `json:"private_key" bson:"private_key"`
}

View File

@@ -1,201 +0,0 @@
package models
import (
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"io"
"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()
// Configuration
config := configuration.Config
// Next to hiding the message, we can also encrypt it using your own private key.
// Which is not stored in a remote environment (hence you are the only one owning it).
msg.Encrypted = false
if config.Encryption != nil && 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("models.mqtt.PackageMQTTMessage(): failed to marshal payload: " + err.Error())
}
// Encrypt the value
privateKey := configuration.Config.Encryption.PrivateKey
r := strings.NewReader(privateKey)
pemBytes, _ := io.ReadAll(r)
block, _ := pem.Decode(pemBytes)
if block == nil {
log.Log.Error("models.mqtt.PackageMQTTMessage(): 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("models.mqtt.PackageMQTTMessage(): error parsing private key: " + err.Error())
}
// Conver key to *rsa.PrivateKey
rsaKey, _ := key.(*rsa.PrivateKey)
// Create a 16bit key random
if config.Encryption != nil && config.Encryption.SymmetricKey != "" {
k := 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{})
}
}
}
}
}
// We'll hide the message (by default in latest version)
// We will encrypt using the Kerberos Hub private key if set.
msg.Hidden = false
if config.HubEncryption == "true" && config.HubPrivateKey != "" {
msg.Hidden = true
}
if msg.Hidden {
pload := msg.Payload
// Pload to base64
data, err := json.Marshal(pload)
if err != nil {
msg.Hidden = false
} else {
k := config.HubPrivateKey
encryptedValue, err := encryption.AesEncrypt(data, k)
if err == nil {
data := base64.StdEncoding.EncodeToString(encryptedValue)
msg.Payload.HiddenValue = data
msg.Payload.EncryptedValue = ""
msg.Payload.Signature = ""
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"`
Hidden bool `json:"hidden"`
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 {
Version string `json:"version"` // Version of the message, e.g. "1.0"
Action string `json:"action"`
DeviceId string `json:"device_id"`
Signature string `json:"signature"`
EncryptedValue string `json:"encrypted_value"`
HiddenValue string `json:"hidden_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
}
type TriggerRelay struct {
Timestamp int64 `json:"timestamp"` // timestamp
DeviceId string `json:"device_id"` // device id
Token string `json:"token"` // token
}

View File

@@ -1,9 +1,8 @@
package models
type MotionDataPartial struct {
Timestamp int64 `json:"timestamp" bson:"timestamp"`
NumberOfChanges int `json:"numberOfChanges" bson:"numberOfChanges"`
Rectangle MotionRectangle `json:"rectangle" bson:"rectangle"`
Timestamp int64 `json:"timestamp" bson:"timestamp"`
NumberOfChanges int `json:"numberOfChanges" bson:"numberOfChanges"`
}
type MotionDataFull struct {
@@ -15,10 +14,3 @@ type MotionDataFull struct {
NumberOfChanges int `json:"numberOfChanges" bson:"numberOfChanges"`
Token int `json:"token" bson:"token"`
}
type MotionRectangle struct {
X int `json:"x" bson:"x"`
Y int `json:"y" bson:"y"`
Width int `json:"width" bson:"width"`
Height int `json:"height" bson:"height"`
}

View File

@@ -1,15 +0,0 @@
package models
import "time"
// The OutputMessage contains the relevant information
// to specify the type of triggers we want to execute.
type OutputMessage struct {
Name string
Outputs []string
Trigger string
Timestamp time.Time
File string
CameraId string
SiteId string
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,59 +0,0 @@
package outputs
import (
"github.com/kerberos-io/agent/machinery/src/log"
"github.com/kerberos-io/agent/machinery/src/models"
)
type Output interface {
// Triggers the integration
Trigger(message models.OutputMessage) error
}
func Execute(message *models.OutputMessage) (err error) {
err = nil
outputs := message.Outputs
for _, output := range outputs {
switch output {
case "slack":
slack := &SlackOutput{}
err := slack.Trigger(message)
if err == nil {
log.Log.Debug("outputs.main.Execute(slack): message was processed by output.")
} else {
log.Log.Error("outputs.main.Execute(slack): " + err.Error())
}
break
case "webhook":
webhook := &WebhookOutput{}
err := webhook.Trigger(message)
if err == nil {
log.Log.Debug("outputs.main.Execute(webhook): message was processed by output.")
} else {
log.Log.Error("outputs.main.Execute(webhook): " + err.Error())
}
break
case "onvif_relay":
onvif := &OnvifRelayOutput{}
err := onvif.Trigger(message)
if err == nil {
log.Log.Debug("outputs.main.Execute(onvif): message was processed by output.")
} else {
log.Log.Error("outputs.main.Execute(onvif): " + err.Error())
}
break
case "script":
script := &ScriptOutput{}
err := script.Trigger(message)
if err == nil {
log.Log.Debug("outputs.main.Execute(script): message was processed by output.")
} else {
log.Log.Error("outputs.main.Execute(script): " + err.Error())
}
break
}
}
return err
}

View File

@@ -1,12 +0,0 @@
package outputs
import "github.com/kerberos-io/agent/machinery/src/models"
type OnvifRelayOutput struct {
Output
}
func (o *OnvifRelayOutput) Trigger(message *models.OutputMessage) (err error) {
err = nil
return err
}

View File

@@ -1,12 +0,0 @@
package outputs
import "github.com/kerberos-io/agent/machinery/src/models"
type ScriptOutput struct {
Output
}
func (scr *ScriptOutput) Trigger(message *models.OutputMessage) (err error) {
err = nil
return err
}

View File

@@ -1,12 +0,0 @@
package outputs
import "github.com/kerberos-io/agent/machinery/src/models"
type SlackOutput struct {
Output
}
func (s *SlackOutput) Trigger(message *models.OutputMessage) (err error) {
err = nil
return err
}

View File

@@ -1,12 +0,0 @@
package outputs
import "github.com/kerberos-io/agent/machinery/src/models"
type WebhookOutput struct {
Output
}
func (w *WebhookOutput) Trigger(message *models.OutputMessage) (err error) {
err = nil
return err
}

View File

@@ -1,69 +0,0 @@
package packets
type Buf struct {
Head, Tail BufPos
pkts []Packet
Size int
Count int
}
func NewBuf() *Buf {
return &Buf{
pkts: make([]Packet, 64),
}
}
func (self *Buf) Pop() Packet {
if self.Count == 0 {
panic("pktque.Buf: Pop() when count == 0")
}
i := int(self.Head) & (len(self.pkts) - 1)
pkt := self.pkts[i]
self.pkts[i] = Packet{}
self.Size -= len(pkt.Data)
self.Head++
self.Count--
return pkt
}
func (self *Buf) grow() {
newpkts := make([]Packet, len(self.pkts)*2)
for i := self.Head; i.LT(self.Tail); i++ {
newpkts[int(i)&(len(newpkts)-1)] = self.pkts[int(i)&(len(self.pkts)-1)]
}
self.pkts = newpkts
}
func (self *Buf) Push(pkt Packet) {
if self.Count == len(self.pkts) {
self.grow()
}
self.pkts[int(self.Tail)&(len(self.pkts)-1)] = pkt
self.Tail++
self.Count++
self.Size += len(pkt.Data)
}
func (self *Buf) Get(pos BufPos) Packet {
return self.pkts[int(pos)&(len(self.pkts)-1)]
}
func (self *Buf) IsValidPos(pos BufPos) bool {
return pos.GE(self.Head) && pos.LT(self.Tail)
}
type BufPos int
func (self BufPos) LT(pos BufPos) bool {
return self-pos < 0
}
func (self BufPos) GE(pos BufPos) bool {
return self-pos >= 0
}
func (self BufPos) GT(pos BufPos) bool {
return self-pos > 0
}

View File

@@ -1,23 +0,0 @@
package packets
import (
"time"
"github.com/pion/rtp"
)
// Packet represents an RTP Packet
type Packet struct {
Packet *rtp.Packet
IsAudio bool // packet is audio
IsVideo bool // packet is video
IsKeyFrame bool // video packet is key frame
Idx int8 // stream index in container format
Codec string // codec name
CompositionTime int64 // packet presentation time minus decode time for H264 B-Frame
Time int64 // packet decode time
TimeLegacy time.Duration
CurrentTime int64 // current time in milliseconds (UNIX timestamp)
Data []byte // packet data
Gopsize int // size of the GOP
}

View File

@@ -1,229 +0,0 @@
// Packege pubsub implements publisher-subscribers model used in multi-channel streaming.
package packets
import (
"io"
"sync"
)
// time
// ----------------->
//
// V-A-V-V-A-V-V-A-V-V
// | |
// 0 5 10
// head tail
// oldest latest
//
// One publisher and multiple subscribers thread-safe packet buffer queue.
type Queue struct {
buf *Buf
head, tail int
lock *sync.RWMutex
cond *sync.Cond
curgopcount, maxgopcount int
streams []Stream
videoidx int
closed bool
}
func NewQueue() *Queue {
q := &Queue{}
q.buf = NewBuf()
q.maxgopcount = 2
q.lock = &sync.RWMutex{}
q.cond = sync.NewCond(q.lock.RLocker())
q.videoidx = -1
return q
}
func (self *Queue) SetMaxGopCount(n int) {
self.lock.Lock()
self.maxgopcount = n
self.lock.Unlock()
return
}
func (self *Queue) GetMaxGopCount() int {
n := self.maxgopcount
return n
}
func (self *Queue) WriteHeader(streams []Stream) error {
self.lock.Lock()
self.streams = streams
for i, stream := range streams {
if stream.IsVideo {
self.videoidx = i
}
}
self.cond.Broadcast()
self.lock.Unlock()
return nil
}
func (self *Queue) WriteTrailer() error {
return nil
}
// After Close() called, all QueueCursor's ReadPacket will return io.EOF.
func (self *Queue) Close() (err error) {
self.lock.Lock()
self.closed = true
self.cond.Broadcast()
// Close all QueueCursor's ReadPacket
for i := 0; i < self.buf.Size; i++ {
pkt := self.buf.Pop()
pkt.Data = nil
}
self.lock.Unlock()
return
}
func (self *Queue) GetSize() int {
return self.buf.Count
}
// Put packet into buffer, old packets will be discared.
func (self *Queue) WritePacket(pkt Packet) (err error) {
self.lock.Lock()
self.buf.Push(pkt)
if pkt.Idx == int8(self.videoidx) && pkt.IsKeyFrame {
self.curgopcount++
}
for self.curgopcount >= self.maxgopcount && self.buf.Count > 1 {
pkt := self.buf.Pop()
if pkt.Idx == int8(self.videoidx) && pkt.IsKeyFrame {
self.curgopcount--
}
if self.curgopcount < self.maxgopcount {
break
}
}
//println("shrink", self.curgopcount, self.maxgopcount, self.buf.Head, self.buf.Tail, "count", self.buf.Count, "size", self.buf.Size)
self.cond.Broadcast()
self.lock.Unlock()
return
}
type QueueCursor struct {
que *Queue
pos BufPos
gotpos bool
init func(buf *Buf, videoidx int) BufPos
}
func (self *Queue) newCursor() *QueueCursor {
return &QueueCursor{
que: self,
}
}
// Create cursor position at latest packet.
func (self *Queue) Latest() *QueueCursor {
cursor := self.newCursor()
cursor.init = func(buf *Buf, videoidx int) BufPos {
return buf.Tail
}
return cursor
}
// Create cursor position at oldest buffered packet.
func (self *Queue) Oldest() *QueueCursor {
cursor := self.newCursor()
cursor.init = func(buf *Buf, videoidx int) BufPos {
return buf.Head
}
return cursor
}
// Create cursor position at specific time in buffered packets.
func (self *Queue) DelayedTime(dur int64) *QueueCursor {
cursor := self.newCursor()
cursor.init = func(buf *Buf, videoidx int) BufPos {
i := buf.Tail - 1
if buf.IsValidPos(i) {
end := buf.Get(i)
for buf.IsValidPos(i) {
if end.Time-buf.Get(i).Time > dur {
break
}
i--
}
}
return i
}
return cursor
}
// Create cursor position at specific delayed GOP count in buffered packets.
func (self *Queue) DelayedGopCount(n int) *QueueCursor {
cursor := self.newCursor()
cursor.init = func(buf *Buf, videoidx int) BufPos {
i := buf.Tail - 1
if videoidx != -1 {
for gop := 0; buf.IsValidPos(i) && gop < n; i-- {
pkt := buf.Get(i)
if pkt.Idx == int8(self.videoidx) && pkt.IsKeyFrame {
gop++
}
}
}
return i
}
return cursor
}
func (self *QueueCursor) Streams() (streams []Stream, err error) {
self.que.cond.L.Lock()
for self.que.streams == nil && !self.que.closed {
self.que.cond.Wait()
}
if self.que.streams != nil {
streams = self.que.streams
} else {
err = io.EOF
}
self.que.cond.L.Unlock()
return
}
// ReadPacket will not consume packets in Queue, it's just a cursor.
func (self *QueueCursor) ReadPacket() (pkt Packet, err error) {
self.que.cond.L.Lock()
buf := self.que.buf
if !self.gotpos {
self.pos = self.init(buf, self.que.videoidx)
self.gotpos = true
}
for {
if self.pos.LT(buf.Head) {
self.pos = buf.Head
} else if self.pos.GT(buf.Tail) {
self.pos = buf.Tail
}
if buf.IsValidPos(self.pos) {
pkt = buf.Get(self.pos)
self.pos++
break
}
if self.que.closed {
err = io.EOF
break
}
self.que.cond.Wait()
}
self.que.cond.L.Unlock()
return
}

View File

@@ -1,54 +0,0 @@
package packets
type Stream struct {
// The ID of the stream.
Index int `json:"index" bson:"index"`
// The name of the stream.
Name string
// The URL of the stream.
URL string
// Is the stream a video stream.
IsVideo bool
// Is the stream a audio stream.
IsAudio bool
// The width of the stream.
Width int
// The height of the stream.
Height int
// Num is the numerator of the framerate.
Num int
// Denum is the denominator of the framerate.
Denum int
// FPS is the framerate of the stream.
FPS float64
// For H264, this is the sps.
SPS []byte
// For H264, this is the pps.
PPS []byte
// For H265, this is the vps.
VPS []byte
// IsBackChannel is true if this stream is a back channel.
IsBackChannel bool
// SampleRate is the sample rate of the audio stream.
SampleRate int
// Channels is the number of audio channels.
Channels int
// GopSize is the size of the GOP (Group of Pictures).
GopSize int
}

View File

@@ -1,60 +0,0 @@
package packets
import (
"time"
)
/*
pop push
seg seg seg
|--------| |---------| |---|
20ms 40ms 5ms
----------------- time -------------------->
headtm tailtm
*/
type tlSeg struct {
tm, dur time.Duration
}
type Timeline struct {
segs []tlSeg
headtm time.Duration
}
func (self *Timeline) Push(tm time.Duration, dur time.Duration) {
if len(self.segs) > 0 {
tail := self.segs[len(self.segs)-1]
diff := tm - (tail.tm + tail.dur)
if diff < 0 {
tm -= diff
}
}
self.segs = append(self.segs, tlSeg{tm, dur})
}
func (self *Timeline) Pop(dur time.Duration) (tm time.Duration) {
if len(self.segs) == 0 {
return self.headtm
}
tm = self.segs[0].tm
for dur > 0 && len(self.segs) > 0 {
seg := &self.segs[0]
sub := dur
if seg.dur < sub {
sub = seg.dur
}
seg.dur -= sub
dur -= sub
seg.tm += sub
self.headtm += sub
if seg.dur == 0 {
copy(self.segs[0:], self.segs[1:])
self.segs = self.segs[:len(self.segs)-1]
}
}
return
}

View File

@@ -2,7 +2,6 @@ package http
import (
"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/agent/machinery/src/onvif"
)
@@ -20,7 +19,7 @@ func Login() {}
// LoginToOnvif godoc
// @Router /api/camera/onvif/login [post]
// @ID camera-onvif-login
// @Tags onvif
// @Tags camera
// @Param config body models.OnvifCredentials true "OnvifCredentials"
// @Summary Try to login into ONVIF supported camera.
// @Description Try to login into ONVIF supported camera.
@@ -44,21 +43,11 @@ func LoginToOnvif(c *gin.Context) {
}
cameraConfiguration := configuration.Config.Capture.IPCamera
device, capabilities, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
device, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
if err == nil {
// Get token from the first profile
token, err := onvif.GetTokenFromProfile(device, 0)
if err == nil {
c.JSON(200, gin.H{
"device": device,
"capabilities": capabilities,
"token": token,
})
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
c.JSON(200, gin.H{
"device": device,
})
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
@@ -74,7 +63,7 @@ func LoginToOnvif(c *gin.Context) {
// GetOnvifCapabilities godoc
// @Router /api/camera/onvif/capabilities [post]
// @ID camera-onvif-capabilities
// @Tags onvif
// @Tags camera
// @Param config body models.OnvifCredentials true "OnvifCredentials"
// @Summary Will return the ONVIF capabilities for the specific camera.
// @Description Will return the ONVIF capabilities for the specific camera.
@@ -98,10 +87,10 @@ func GetOnvifCapabilities(c *gin.Context) {
}
cameraConfiguration := configuration.Config.Capture.IPCamera
_, capabilities, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
device, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
if err == nil {
c.JSON(200, gin.H{
"capabilities": capabilities,
"capabilities": onvif.GetCapabilitiesFromDevice(device),
})
} else {
c.JSON(400, gin.H{
@@ -118,7 +107,7 @@ func GetOnvifCapabilities(c *gin.Context) {
// DoOnvifPanTilt godoc
// @Router /api/camera/onvif/pantilt [post]
// @ID camera-onvif-pantilt
// @Tags onvif
// @Tags camera
// @Param panTilt body models.OnvifPanTilt true "OnvifPanTilt"
// @Summary Panning or/and tilting the camera.
// @Description Panning or/and tilting the camera using a direction (x,y).
@@ -142,7 +131,7 @@ func DoOnvifPanTilt(c *gin.Context) {
}
cameraConfiguration := configuration.Config.Capture.IPCamera
device, _, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
device, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
if err == nil {
// Get token from the first profile
@@ -192,7 +181,7 @@ func DoOnvifPanTilt(c *gin.Context) {
// DoOnvifZoom godoc
// @Router /api/camera/onvif/zoom [post]
// @ID camera-onvif-zoom
// @Tags onvif
// @Tags camera
// @Param zoom body models.OnvifZoom true "OnvifZoom"
// @Summary Zooming in or out the camera.
// @Description Zooming in or out the camera.
@@ -216,7 +205,7 @@ func DoOnvifZoom(c *gin.Context) {
}
cameraConfiguration := configuration.Config.Capture.IPCamera
device, _, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
device, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
if err == nil {
// Get token from the first profile
@@ -265,7 +254,7 @@ func DoOnvifZoom(c *gin.Context) {
// GetOnvifPresets godoc
// @Router /api/camera/onvif/presets [post]
// @ID camera-onvif-presets
// @Tags onvif
// @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.
@@ -289,7 +278,7 @@ func GetOnvifPresets(c *gin.Context) {
}
cameraConfiguration := configuration.Config.Capture.IPCamera
device, _, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
device, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
if err == nil {
presets, err := onvif.GetPresetsFromDevice(device)
if err == nil {
@@ -316,7 +305,7 @@ func GetOnvifPresets(c *gin.Context) {
// GoToOnvifPReset godoc
// @Router /api/camera/onvif/gotopreset [post]
// @ID camera-onvif-gotopreset
// @Tags onvif
// @Tags camera
// @Param config body models.OnvifPreset true "OnvifPreset"
// @Summary Will activate the desired ONVIF preset.
// @Description Will activate the desired ONVIF preset.
@@ -340,7 +329,7 @@ func GoToOnvifPreset(c *gin.Context) {
}
cameraConfiguration := configuration.Config.Capture.IPCamera
device, _, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
device, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
if err == nil {
err := onvif.GoToPresetFromDevice(device, onvifPreset.Preset)
if err == nil {
@@ -363,228 +352,3 @@ func GoToOnvifPreset(c *gin.Context) {
})
}
}
// DoGetDigitalInputs godoc
// @Router /api/camera/onvif/inputs [post]
// @ID get-digital-inputs
// @Security Bearer
// @securityDefinitions.apikey Bearer
// @in header
// @name Authorization
// @Tags onvif
// @Param config body models.OnvifCredentials true "OnvifCredentials"
// @Summary Will get the digital inputs from the ONVIF device.
// @Description Will get the digital inputs from the ONVIF device.
// @Success 200 {object} models.APIResponse
func DoGetDigitalInputs(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)
onvifInputs, _ := onvif.GetDigitalInputs(device)
if err == nil {
// Get the digital inputs and outputs from the device
inputOutputs, err := onvif.GetInputOutputs()
if err == nil {
if err == nil {
// Get the digital outputs from the device
var inputs []onvif.ONVIFEvents
for _, event := range inputOutputs {
if event.Type == "input" {
inputs = append(inputs, event)
}
}
// Iterate over inputs from onvif and compare
for _, input := range onvifInputs.DigitalInputs {
find := false
for _, event := range inputs {
key := string(input.Token)
if key == event.Key {
find = true
}
}
if !find {
key := string(input.Token)
inputs = append(inputs, onvif.ONVIFEvents{
Key: key,
Type: "input",
})
}
}
c.JSON(200, gin.H{
"data": inputs,
})
} 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(),
})
}
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
}
// DoGetRelayOutputs godoc
// @Router /api/camera/onvif/outputs [post]
// @ID get-relay-outputs
// @Security Bearer
// @securityDefinitions.apikey Bearer
// @in header
// @name Authorization
// @Tags onvif
// @Param config body models.OnvifCredentials true "OnvifCredentials"
// @Summary Will get the relay outputs from the ONVIF device.
// @Description Will get the relay outputs from the ONVIF device.
// @Success 200 {object} models.APIResponse
func DoGetRelayOutputs(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
_, _, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
if err == nil {
// Get the digital inputs and outputs from the device
inputOutputs, err := onvif.GetInputOutputs()
if err == nil {
if err == nil {
// Get the digital outputs from the device
var outputs []onvif.ONVIFEvents
for _, event := range inputOutputs {
if event.Type == "output" {
outputs = append(outputs, event)
}
}
c.JSON(200, gin.H{
"data": outputs,
})
} 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(),
})
}
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
}
// DoTriggerRelayOutput godoc
// @Router /api/camera/onvif/outputs/{output} [post]
// @ID trigger-relay-output
// @Security Bearer
// @securityDefinitions.apikey Bearer
// @in header
// @name Authorization
// @Tags onvif
// @Param config body models.OnvifCredentials true "OnvifCredentials"
// @Param output path string true "Output"
// @Summary Will trigger the relay output from the ONVIF device.
// @Description Will trigger the relay output from the ONVIF device.
// @Success 200 {object} models.APIResponse
func DoTriggerRelayOutput(c *gin.Context) {
var onvifCredentials models.OnvifCredentials
err := c.BindJSON(&onvifCredentials)
// Get the output from the url
output := c.Param("output")
if err == nil && onvifCredentials.ONVIFXAddr != "" && output != "" {
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 {
err := onvif.TriggerRelayOutput(device, output)
if err == nil {
msg := "relay output triggered: " + output
log.Log.Info("routers.http.methods.DoTriggerRelayOutput(): " + msg)
c.JSON(200, gin.H{
"data": msg,
})
} else {
msg := "something went wrong: " + err.Error()
log.Log.Error("routers.http.methods.DoTriggerRelayOutput(): " + msg)
c.JSON(400, gin.H{
"data": msg,
})
}
} else {
msg := "something went wrong: " + err.Error()
log.Log.Error("routers.http.methods.DoTriggerRelayOutput(): " + msg)
c.JSON(400, gin.H{
"data": msg,
})
}
} else {
msg := "something went wrong: " + err.Error()
log.Log.Error("routers.http.methods.DoTriggerRelayOutput(): " + msg)
c.JSON(400, gin.H{
"data": msg,
})
}
}

View File

@@ -0,0 +1,243 @@
package http
import (
"image"
"time"
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, configDirectory string, configuration *models.Configuration, communication *models.Communication) *gin.RouterGroup {
r.GET("/ws", func(c *gin.Context) {
websocket.WebsocketHandler(c, communication)
})
// This is legacy should be removed in future! Now everything
// lives under the /api prefix.
r.GET("/config", func(c *gin.Context) {
c.JSON(200, gin.H{
"config": configuration.Config,
"custom": configuration.CustomConfig,
"global": configuration.GlobalConfig,
"snapshot": communication.Image,
})
})
// This is legacy should be removed in future! Now everything
// lives under the /api prefix.
r.POST("/config", func(c *gin.Context) {
var config models.Config
err := c.BindJSON(&config)
if err == nil {
err := configService.SaveConfig(configDirectory, config, configuration, communication)
if err == nil {
c.JSON(200, gin.H{
"data": "☄ Reconfiguring",
})
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
})
api := r.Group("/api")
{
api.POST("/login", authMiddleware.LoginHandler)
api.GET("/dashboard", func(c *gin.Context) {
// 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.
cloudIsOnline := false
if communication.CloudTimestamp != nil && communication.CloudTimestamp.Load() != nil {
timestamp := communication.CloudTimestamp.Load().(int64)
if timestamp > 0 {
cloudIsOnline = true
}
}
// The total number of recordings stored in the directory.
recordingDirectory := configDirectory + "/data/recordings"
numberOfRecordings := utils.NumberOfMP4sInDirectory(recordingDirectory)
// All days stored in this agent.
days := []string{}
latestEvents := []models.Media{}
files, err := utils.ReadDirectory(recordingDirectory)
if err == nil {
events := utils.GetSortedDirectory(files)
// Get All days
days = utils.GetDays(events, recordingDirectory, configuration)
// Get all latest events
var eventFilter models.EventFilter
eventFilter.NumberOfElements = 5
latestEvents = utils.GetMediaFormatted(events, recordingDirectory, configuration, eventFilter) // will get 5 latest recordings.
}
c.JSON(200, gin.H{
"offlineMode": configuration.Config.Offline,
"cameraOnline": cameraIsOnline,
"cloudOnline": cloudIsOnline,
"numberOfRecordings": numberOfRecordings,
"days": days,
"latestEvents": latestEvents,
})
})
api.POST("/latest-events", func(c *gin.Context) {
var eventFilter models.EventFilter
err := c.BindJSON(&eventFilter)
if err == nil {
// Default to 10 if no limit is set.
if eventFilter.NumberOfElements == 0 {
eventFilter.NumberOfElements = 10
}
recordingDirectory := configDirectory + "/data/recordings"
files, err := utils.ReadDirectory(recordingDirectory)
if err == nil {
events := utils.GetSortedDirectory(files)
// We will get all recordings from the directory (as defined by the filter).
fileObjects := utils.GetMediaFormatted(events, recordingDirectory, configuration, eventFilter)
c.JSON(200, gin.H{
"events": fileObjects,
})
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
})
api.GET("/days", func(c *gin.Context) {
recordingDirectory := configDirectory + "/data/recordings"
files, err := utils.ReadDirectory(recordingDirectory)
if err == nil {
events := utils.GetSortedDirectory(files)
days := utils.GetDays(events, recordingDirectory, configuration)
c.JSON(200, gin.H{
"events": days,
})
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
})
api.GET("/config", func(c *gin.Context) {
c.JSON(200, gin.H{
"config": configuration.Config,
"custom": configuration.CustomConfig,
"global": configuration.GlobalConfig,
"snapshot": communication.Image,
})
})
api.POST("/config", func(c *gin.Context) {
var config models.Config
err := c.BindJSON(&config)
if err == nil {
err := configService.SaveConfig(configDirectory, config, configuration, communication)
if err == nil {
c.JSON(200, gin.H{
"data": "☄ Reconfiguring",
})
} else {
c.JSON(200, gin.H{
"data": "☄ Reconfiguring",
})
}
} else {
c.JSON(400, gin.H{
"data": "Something went wrong: " + err.Error(),
})
}
})
api.GET("/restart", func(c *gin.Context) {
communication.HandleBootstrap <- "restart"
c.JSON(200, gin.H{
"restarted": true,
})
})
api.GET("/stop", func(c *gin.Context) {
communication.HandleBootstrap <- "stop"
c.JSON(200, gin.H{
"stopped": true,
})
})
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, configDirectory)
})
// Streaming handler
api.GET("/stream", func(c *gin.Context) {
// TODO add a token validation!
imageFunction := func() (image.Image, error) {
// 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 := configService.GetImageFromFilePath(configDirectory)
return img, err
}
h := components.StartMotionJPEG(imageFunction, 80)
h.ServeHTTP(c.Writer, c.Request)
})
// Camera specific methods. Doesn't require any authorization.
// These are available for anyone, but require the agent, to reach
// 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)
// Secured endpoints..
api.Use(authMiddleware.MiddlewareFunc())
{
}
}
return api
}

View File

@@ -1,9 +1,7 @@
package http
import (
"io"
"os"
"strconv"
jwt "github.com/appleboy/gin-jwt/v2"
"github.com/gin-contrib/pprof"
@@ -14,8 +12,6 @@ import (
"log"
_ "github.com/kerberos-io/agent/machinery/docs"
"github.com/kerberos-io/agent/machinery/src/capture"
"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"
@@ -39,15 +35,12 @@ import (
// @in header
// @name Authorization
func StartServer(configDirectory string, configuration *models.Configuration, communication *models.Communication, captureDevice *capture.Capture) {
// Set release mode
gin.SetMode(gin.ReleaseMode)
func StartServer(configDirectory string, configuration *models.Configuration, communication *models.Communication) {
// Initialize REST API
r := gin.Default()
// Profiler
// Profileerggerg
pprof.Register(r)
// Setup CORS
@@ -64,7 +57,7 @@ func StartServer(configDirectory string, configuration *models.Configuration, co
}
// Add all routes
AddRoutes(r, authMiddleware, configDirectory, configuration, communication, captureDevice)
AddRoutes(r, authMiddleware, configDirectory, configuration, communication)
// Update environment variables
environmentVariables := configDirectory + "/www/env.js"
@@ -84,7 +77,7 @@ func StartServer(configDirectory string, configuration *models.Configuration, co
r.Use(static.Serve("/settings", static.LocalFile(configDirectory+"/www", true)))
r.Use(static.Serve("/login", static.LocalFile(configDirectory+"/www", true)))
r.Handle("GET", "/file/*filepath", func(c *gin.Context) {
Files(c, configDirectory, configuration)
Files(c, configDirectory)
})
// Run the api on port
@@ -94,51 +87,8 @@ func StartServer(configDirectory string, configuration *models.Configuration, co
}
}
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
encryptedRecordings := configuration.Config.Encryption.Recordings
// Decrypt file
if encryptedRecordings == "true" && 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
}
func Files(c *gin.Context, configDirectory string) {
c.Header("Access-Control-Allow-Origin", "*")
c.Header("Content-Type", "video/mp4")
c.File(configDirectory + "/data/recordings" + c.Param("filepath"))
}

View File

@@ -1,116 +0,0 @@
package http
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/components"
"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/models"
)
func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware, configDirectory string, configuration *models.Configuration, communication *models.Communication, captureDevice *capture.Capture) *gin.RouterGroup {
r.GET("/ws", func(c *gin.Context) {
websocket.WebsocketHandler(c, configuration, communication, captureDevice)
})
// This is legacy should be removed in future! Now everything
// lives under the /api prefix.
r.GET("/config", func(c *gin.Context) {
components.GetConfig(c, captureDevice, configuration, communication)
})
// This is legacy should be removed in future! Now everything
// lives under the /api prefix.
r.POST("/config", func(c *gin.Context) {
components.UpdateConfig(c, configDirectory, configuration, communication)
})
api := r.Group("/api")
{
api.POST("/login", authMiddleware.LoginHandler)
api.GET("/dashboard", func(c *gin.Context) {
components.GetDashboard(c, configDirectory, configuration, communication)
})
api.POST("/latest-events", func(c *gin.Context) {
components.GetLatestEvents(c, configDirectory, configuration, communication)
})
api.GET("/days", func(c *gin.Context) {
components.GetDays(c, configDirectory, configuration, communication)
})
api.GET("/config", func(c *gin.Context) {
components.GetConfig(c, captureDevice, configuration, communication)
})
api.POST("/config", func(c *gin.Context) {
components.UpdateConfig(c, configDirectory, configuration, communication)
})
// Will verify the hub settings.
api.POST("/hub/verify", func(c *gin.Context) {
cloud.VerifyHub(c)
})
// Will verify the persistence settings.
api.POST("/persistence/verify", func(c *gin.Context) {
cloud.VerifyPersistence(c, configDirectory)
})
// Will verify the secondary persistence settings.
api.POST("/persistence/secondary/verify", func(c *gin.Context) {
cloud.VerifySecondaryPersistence(c, configDirectory)
})
// Camera specific methods. Doesn't require any authorization.
// These are available for anyone, but require the agent, to reach
// the camera.
api.POST("/camera/restart", func(c *gin.Context) {
components.RestartAgent(c, communication)
})
api.POST("/camera/stop", func(c *gin.Context) {
components.StopAgent(c, communication)
})
api.POST("/camera/record", func(c *gin.Context) {
components.MakeRecording(c, communication)
})
api.GET("/camera/snapshot/jpeg", func(c *gin.Context) {
components.GetSnapshotRaw(c, captureDevice, configuration, communication)
})
api.GET("/camera/snapshot/base64", func(c *gin.Context) {
components.GetSnapshotBase64(c, captureDevice, configuration, communication)
})
// Onvif specific methods. Doesn't require any authorization.
// Will verify the current onvif settings.
api.POST("/camera/onvif/verify", onvif.VerifyOnvifConnection)
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/onvif/inputs", DoGetDigitalInputs)
api.POST("/camera/onvif/outputs", DoGetRelayOutputs)
api.POST("/camera/onvif/outputs/:output", DoTriggerRelayOutput)
api.POST("/camera/verify/:streamType", capture.VerifyCamera)
// Secured endpoints..
api.Use(authMiddleware.MiddlewareFunc())
{
}
}
return api
}

View File

@@ -1,11 +1,10 @@
package routers
import (
"github.com/kerberos-io/agent/machinery/src/capture"
"github.com/kerberos-io/agent/machinery/src/models"
"github.com/kerberos-io/agent/machinery/src/routers/http"
)
func StartWebserver(configDirectory string, configuration *models.Configuration, communication *models.Communication, captureDevice *capture.Capture) {
http.StartServer(configDirectory, configuration, communication, captureDevice)
func StartWebserver(configDirectory string, configuration *models.Configuration, communication *models.Communication) {
http.StartServer(configDirectory, configuration, communication)
}

View File

@@ -1,27 +1,40 @@
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"
"github.com/gofrs/uuid"
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"
)
// The message structure which is used to send over
// and receive messages from the MQTT broker
type Message struct {
Mid string `json:"mid"`
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"`
Value map[string]interface{} `json:"value"`
}
// 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
@@ -43,15 +56,58 @@ func HasMQTTClientModified(configuration *models.Configuration) bool {
return false
}
func PackageMQTTMessage(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.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
msg.PublicKey = ""
msg.Fingerprint = ""
payload, err := json.Marshal(msg)
return payload, err
}
// Configuring MQTT to subscribe for various bi-directional messaging
// Listen and reply (a generic method to share and retrieve information)
//
// !!! NEW METHOD TO COMMUNICATE: only create a single subscription for all communication.
// and an additional publish messages back
//
// - [SUBSCRIPTION] kerberos/agent/{hubkey} (hub -> agent)
// - [PUBLISH] kerberos/hub/{hubkey} (agent -> hub)
//
// !!! LEGACY METHODS BELOW, WE SHOULD LEVERAGE THE ABOVE METHOD!
//
// [SUBSCRIPTIONS]
//
// SD Streaming (Base64 JPEGs)
// - kerberos/{hubkey}/device/{devicekey}/request-live: use for polling of SD live streaming (as long the user requests stream, we'll send JPEGs over).
//
// HD Streaming (WebRTC)
// - kerberos/register: use for receiving HD live streaming requests.
// - candidate/cloud: remote ICE candidates are shared over this line.
// - kerberos/webrtc/keepalivehub/{devicekey}: use for polling of HD streaming (as long the user requests stream, we'll send it over).
// - kerberos/webrtc/peers/{devicekey}: we'll keep track of the number of peers (we can have more than 1 concurrent listeners).
//
// ONVIF capabilities
// - kerberos/onvif/{devicekey}: endpoint to execute ONVIF commands such as (PTZ, Zoom, IO, etc)
//
// [PUBlISH]
// Next to subscribing to various topics, we'll also publish messages to various topics, find a list of available Publish methods.
//
// - kerberos/webrtc/packets/{devicekey}: use for forwarding WebRTC (RTP Packets) over MQTT -> Complex firewall.
// - kerberos/webrtc/keepalive/{devicekey}: use for keeping alive forwarded WebRTC stream
// - {devicekey}/{sessionid}/answer: once a WebRTC request is received through (kerberos/register), we'll draft an answer and send it back to the remote WebRTC client.
// - kerberos/{hubkey}/device/{devicekey}/motion: a motion signal
func ConfigureMQTT(configDirectory string, configuration *models.Configuration, communication *models.Communication) mqtt.Client {
@@ -66,7 +122,7 @@ func ConfigureMQTT(configDirectory string, configuration *models.Configuration,
PREV_AgentKey = configuration.Config.Key
if config.Offline == "true" {
log.Log.Info("routers.mqtt.main.ConfigureMQTT(): not starting as running in Offline mode.")
log.Log.Info("ConfigureMQTT: not starting as running in Offline mode.")
} else {
opts := mqtt.NewClientOptions()
@@ -75,7 +131,7 @@ func ConfigureMQTT(configDirectory string, configuration *models.Configuration,
// and share and receive messages to/from.
mqttURL := config.MQTTURI
opts.AddBroker(mqttURL)
log.Log.Debug("routers.mqtt.main.ConfigureMQTT(): Set broker uri " + mqttURL)
log.Log.Info("ConfigureMQTT: Set broker uri " + mqttURL)
// Our MQTT broker can have username/password credentials
// to protect it from the outside.
@@ -84,8 +140,8 @@ func ConfigureMQTT(configDirectory string, configuration *models.Configuration,
if mqtt_username != "" || mqtt_password != "" {
opts.SetUsername(mqtt_username)
opts.SetPassword(mqtt_password)
log.Log.Debug("routers.mqtt.main.ConfigureMQTT(): Set username " + mqtt_username)
log.Log.Debug("routers.mqtt.main.ConfigureMQTT(): Set password " + mqtt_password)
log.Log.Info("ConfigureMQTT: Set username " + mqtt_username)
log.Log.Info("ConfigureMQTT: Set password " + mqtt_password)
}
// Some extra options to make sure the connection behaves
@@ -121,21 +177,41 @@ func ConfigureMQTT(configDirectory string, configuration *models.Configuration,
}
opts.SetClientID(mqttClientID)
log.Log.Info("routers.mqtt.main.ConfigureMQTT(): Set ClientID " + mqttClientID)
log.Log.Info("ConfigureMQTT: Set ClientID " + mqttClientID)
rand.Seed(time.Now().UnixNano())
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("routers.mqtt.main.ConfigureMQTT(): " + mqttClientID + " connected to " + mqttURL)
log.Log.Info("ConfigureMQTT: " + mqttClientID + " connected to " + mqttURL)
// Create a susbcription for listen and reply
MQTTListenerHandler(c, hubKey, configDirectory, configuration, communication)
// Legacy methods below -> should be converted to the above method.
// 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)
}
}
mqc := mqtt.NewClient(opts)
if token := mqc.Connect(); token.WaitTimeout(3 * time.Second) {
if token.Error() != nil {
log.Log.Error("routers.mqtt.main.ConfigureMQTT(): unable to establish mqtt broker connection, error was: " + token.Error().Error())
log.Log.Error("ConfigureMQTT: unable to establish mqtt broker connection, error was: " + token.Error().Error())
}
}
return mqc
@@ -146,10 +222,10 @@ func ConfigureMQTT(configDirectory string, configuration *models.Configuration,
func MQTTListenerHandler(mqttClient mqtt.Client, hubKey string, configDirectory string, configuration *models.Configuration, communication *models.Communication) {
if hubKey == "" {
log.Log.Info("routers.mqtt.main.MQTTListenerHandler(): no hub key provided, not subscribing to kerberos/hub/{hubkey}")
log.Log.Info("MQTTListenerHandler: no hub key provided, not subscribing to kerberos/hub/{hubkey}")
} else {
agentListener := fmt.Sprintf("kerberos/agent/%s", hubKey)
mqttClient.Subscribe(agentListener, 1, func(c mqtt.Client, msg mqtt.Message) {
topicOnvif := fmt.Sprintf("kerberos/agent/%s", hubKey)
mqttClient.Subscribe(topicOnvif, 1, func(c mqtt.Client, msg mqtt.Message) {
// Decode the message, we are expecting following format.
// {
@@ -160,131 +236,57 @@ func MQTTListenerHandler(mqttClient mqtt.Client, hubKey string, configDirectory
// payload: Payload, "a json object which might be encrypted"
// }
var message models.Message
var message Message
json.Unmarshal(msg.Payload(), &message)
// 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 {
var payload models.Payload
// Messages might be hidden, if so we'll need to decrypt them using the Kerberos Hub private key.
if message.Hidden && configuration.Config.HubEncryption == "true" {
hiddenValue := message.Payload.HiddenValue
if len(hiddenValue) > 0 {
privateKey := configuration.Config.HubPrivateKey
if privateKey != "" {
data, err := base64.StdEncoding.DecodeString(hiddenValue)
if err != nil {
return
}
visibleValue, err := encryption.AesDecrypt(data, privateKey)
if err != nil {
log.Log.Error("routers.mqtt.main.MQTTListenerHandler(): error decrypting message: " + err.Error())
return
}
json.Unmarshal(visibleValue, &payload)
message.Payload = payload
} else {
log.Log.Error("routers.mqtt.main.MQTTListenerHandler(): error decrypting message, no private key provided.")
}
}
}
// Messages might be end-to-end encrypted, if so we'll need to decrypt them,
// using our own keys.
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("routers.mqtt.main.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("routers.mqtt.main.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("routers.mqtt.main.MQTTListenerHandler(): error decrypting message: " + err.Error())
return
}
json.Unmarshal(decryptedValue, &payload)
} else {
log.Log.Error("routers.mqtt.main.MQTTListenerHandler(): error decrypting message, assymetric keys do not match.")
return
}
} else if err != nil {
log.Log.Error("routers.mqtt.main.MQTTListenerHandler(): error decrypting message: " + err.Error())
return
}
}
}
}
if message.Mid != "" && message.Timestamp != 0 {
// Messages might be encrypted, if so we'll
// need to decrypt them.
var payload Payload
if message.Encrypted {
// We'll find out the key we use to decrypt the message.
// TODO -> still needs to be implemented.
// Use to fingerprint to act accordingly.
} else {
payload = message.Payload
}
// We'll find out which message we received, and act accordingly.
log.Log.Info("routers.mqtt.main.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)
case "trigger-relay":
go HandleTriggerRelay(mqttClient, hubKey, payload, configuration, communication)
}
// We will receive all messages from our hub, so we'll need to filter to the relevant device.
if payload.DeviceId != configuration.Config.Key {
// Not relevant for this device, so we'll ignore it.
} else {
// 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":
HandleRecording(mqttClient, hubKey, payload, configuration, communication)
case "get-ptz-position":
HandleGetPTZPosition(mqttClient, hubKey, payload, configuration, communication)
case "update-ptz-position":
HandleUpdatePTZPosition(mqttClient, hubKey, payload, configuration, communication)
case "request-config":
HandleRequestConfig(mqttClient, hubKey, payload, configuration, communication)
case "update-config":
HandleUpdateConfig(mqttClient, hubKey, payload, configDirectory, configuration, communication)
}
}
}
})
}
}
func HandleRecording(mqttClient mqtt.Client, hubKey string, payload models.Payload, configuration *models.Configuration, communication *models.Communication) {
// 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.
}
func HandleRecording(mqttClient mqtt.Client, hubKey string, payload Payload, configuration *models.Configuration, communication *models.Communication) {
value := payload.Value
// Convert map[string]interface{} to RecordPayload
jsonData, _ := json.Marshal(value)
var recordPayload models.RecordPayload
var recordPayload RecordPayload
json.Unmarshal(jsonData, &recordPayload)
if recordPayload.Timestamp != 0 {
@@ -295,41 +297,29 @@ func HandleRecording(mqttClient mqtt.Client, hubKey string, payload models.Paylo
}
}
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
}
// 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.
}
func HandleGetPTZPosition(mqttClient mqtt.Client, hubKey string, payload models.Payload, configuration *models.Configuration, communication *models.Communication) {
func HandleGetPTZPosition(mqttClient mqtt.Client, hubKey string, payload Payload, configuration *models.Configuration, communication *models.Communication) {
value := payload.Value
// Convert map[string]interface{} to PTZPositionPayload
jsonData, _ := json.Marshal(value)
var positionPayload models.PTZPositionPayload
var positionPayload PTZPositionPayload
json.Unmarshal(jsonData, &positionPayload)
if positionPayload.Timestamp != 0 {
// Get Position from device
pos, err := onvif.GetPositionFromDevice(*configuration)
if err != nil {
log.Log.Error("routers.mqtt.main.HandlePTZPosition(): error getting position from device: " + err.Error())
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{
message := Message{
Payload: Payload{
Action: "ptz-position",
DeviceId: configuration.Config.Key,
Value: map[string]interface{}{
@@ -338,17 +328,17 @@ func HandleGetPTZPosition(mqttClient mqtt.Client, hubKey string, payload models.
},
},
}
payload, err := models.PackageMQTTMessage(configuration, message)
payload, err := PackageMQTTMessage(message)
if err == nil {
mqttClient.Publish("kerberos/hub/"+hubKey, 2, false, payload)
mqttClient.Publish("kerberos/hub/"+hubKey, 0, false, payload)
} else {
log.Log.Info("routers.mqtt.main.HandlePTZPosition(): something went wrong while sending position to hub: " + string(payload))
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) {
func HandleUpdatePTZPosition(mqttClient mqtt.Client, hubKey string, payload Payload, configuration *models.Configuration, communication *models.Communication) {
value := payload.Value
// Convert map[string]interface{} to PTZPositionPayload
@@ -359,206 +349,95 @@ func HandleUpdatePTZPosition(mqttClient mqtt.Client, hubKey string, payload mode
if onvifAction.Action != "" {
if communication.CameraConnected {
communication.HandleONVIF <- onvifAction
log.Log.Info("routers.mqtt.main.MQTTListenerHandleONVIF(): Received an action - " + onvifAction.Action)
log.Log.Info("MQTTListenerHandleONVIF: Received an action - " + onvifAction.Action)
} else {
log.Log.Info("routers.mqtt.main.MQTTListenerHandleONVIF(): received action, but camera is not connected.")
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) {
// 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.
}
func HandleRequestConfig(mqttClient mqtt.Client, hubKey string, payload Payload, configuration *models.Configuration, communication *models.Communication) {
value := payload.Value
// Convert map[string]interface{} to RequestConfigPayload
jsonData, _ := json.Marshal(value)
var configPayload models.RequestConfigPayload
var configPayload RequestConfigPayload
json.Unmarshal(jsonData, &configPayload)
if configPayload.Timestamp != 0 {
// Get Config from the device
key := configuration.Config.Key
name := configuration.Config.Name
if configuration.Config.FriendlyName != "" {
name = configuration.Config.FriendlyName
}
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)
inrec, _ := json.Marshal(configuration.Config)
json.Unmarshal(inrec, &configMap)
// Unset encryption part.
delete(configMap, "encryption")
message := models.Message{
Payload: models.Payload{
message := Message{
Payload: Payload{
Action: "receive-config",
DeviceId: configuration.Config.Key,
Value: configMap,
},
}
payload, err := models.PackageMQTTMessage(configuration, message)
payload, err := PackageMQTTMessage(message)
if err == nil {
mqttClient.Publish("kerberos/hub/"+hubKey, 2, false, payload)
mqttClient.Publish("kerberos/hub/"+hubKey, 0, false, payload)
} else {
log.Log.Info("routers.mqtt.main.HandleRequestConfig(): something went wrong while sending config to hub: " + string(payload))
log.Log.Info("HandleRequestConfig: something went wrong while sending config to hub: " + string(payload))
}
} else {
log.Log.Info("routers.mqtt.main.HandleRequestConfig(): no config available")
log.Log.Info("HandleRequestConfig: no config available")
}
log.Log.Info("routers.mqtt.main.HandleRequestConfig(): Received a request for the config")
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) {
// 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 models.Config `json:"config"`
}
func HandleUpdateConfig(mqttClient mqtt.Client, hubKey string, payload 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
var configPayload 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("routers.mqtt.main.HandleUpdateConfig(): Config updated")
message := models.Message{
Payload: models.Payload{
log.Log.Info("HandleUpdateConfig: Config updated")
message := Message{
Payload: Payload{
Action: "acknowledge-update-config",
DeviceId: configuration.Config.Key,
},
}
payload, err := models.PackageMQTTMessage(configuration, message)
payload, err := PackageMQTTMessage(message)
if err == nil {
mqttClient.Publish("kerberos/hub/"+hubKey, 2, false, payload)
mqttClient.Publish("kerberos/hub/"+hubKey, 0, false, payload)
} else {
log.Log.Info("routers.mqtt.main.HandleUpdateConfig(): something went wrong while sending acknowledge config to hub: " + string(payload))
log.Log.Info("HandleRequestConfig: something went wrong while sending acknowledge config to hub: " + string(payload))
}
} else {
log.Log.Info("routers.mqtt.main.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("routers.mqtt.main.HandleRequestSDStream(): received request to livestream.")
} else {
log.Log.Info("routers.mqtt.main.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("routers.mqtt.main.HandleRequestHDStream(): received request to setup webrtc.")
} else {
log.Log.Info("routers.mqtt.main.HandleRequestHDStream(): received request to setup webrtc, but camera is not connected.")
}
}
}
func HandleReceiveHDCandidates(mqttClient mqtt.Client, hubKey string, payload models.Payload, configuration *models.Configuration, communication *models.Communication) {
value := payload.Value
// Convert map[string]interface{} to ReceiveHDCandidatesPayload
jsonData, _ := json.Marshal(value)
var receiveHDCandidatesPayload models.ReceiveHDCandidatesPayload
json.Unmarshal(jsonData, &receiveHDCandidatesPayload)
if receiveHDCandidatesPayload.Timestamp != 0 {
if communication.CameraConnected {
// Register candidate channel
key := configuration.Config.Key + "/" + receiveHDCandidatesPayload.SessionID
go webrtc.RegisterCandidates(key, receiveHDCandidatesPayload)
} else {
log.Log.Info("routers.mqtt.main.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("routers.mqtt.main.HandleNavigatePTZ(): Received an action - " + onvifAction.Action)
} else {
log.Log.Info("routers.mqtt.main.HandleNavigatePTZ(): received action, but camera is not connected.")
}
}
}
func HandleTriggerRelay(mqttClient mqtt.Client, hubKey string, payload models.Payload, configuration *models.Configuration, communication *models.Communication) {
value := payload.Value
jsonData, _ := json.Marshal(value)
var triggerRelayPayload models.TriggerRelay
json.Unmarshal(jsonData, &triggerRelayPayload)
if triggerRelayPayload.Timestamp != 0 {
if communication.CameraConnected {
// Get token (name of relay)
token := triggerRelayPayload.Token
// Connect to Onvif device
cameraConfiguration := configuration.Config.Capture.IPCamera
device, _, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
if err == nil {
// Trigger relay output
err := onvif.TriggerRelayOutput(device, token)
if err != nil {
log.Log.Error("routers.mqtt.main.HandleTriggerRelay(): error triggering relay: " + err.Error())
} else {
log.Log.Info("routers.mqtt.main.HandleTriggerRelay(): trigger (" + token + ") relay output.")
}
} else {
log.Log.Error("routers.mqtt.main.HandleTriggerRelay(): error connecting to device: " + err.Error())
}
} else {
log.Log.Info("routers.mqtt.main.HandleTriggerRelay(): received trigger, but camera is not connected.")
log.Log.Info("HandleUpdateConfig: Config update failed")
}
}
}
@@ -568,8 +447,124 @@ func DisconnectMQTT(mqttClient mqtt.Client, config *models.Config) {
// Cleanup all subscriptions
// New methods
mqttClient.Unsubscribe("kerberos/agent/" + PREV_HubKey)
// Legacy methods
mqttClient.Unsubscribe("kerberos/" + PREV_HubKey + "/device/" + PREV_AgentKey + "/request-live")
mqttClient.Unsubscribe(PREV_AgentKey + "/register")
mqttClient.Unsubscribe("kerberos/webrtc/keepalivehub/" + PREV_AgentKey)
mqttClient.Unsubscribe("kerberos/webrtc/peers/" + PREV_AgentKey)
mqttClient.Unsubscribe("candidate/cloud")
mqttClient.Unsubscribe("kerberos/onvif/" + PREV_AgentKey)
mqttClient.Disconnect(1000)
mqttClient = nil
log.Log.Info("routers.mqtt.main.DisconnectMQTT(): MQTT client disconnected.")
log.Log.Info("DisconnectMQTT: MQTT client disconnected.")
}
}
// #################################################################################################
// Below you'll find legacy methods, as of now we'll have a single subscription, which scales better
func MQTTListenerHandleLiveSD(mqttClient mqtt.Client, hubKey string, configuration *models.Configuration, communication *models.Communication) {
config := configuration.Config
topicRequest := "kerberos/" + hubKey + "/device/" + config.Key + "/request-live"
mqttClient.Subscribe(topicRequest, 0, func(c mqtt.Client, msg mqtt.Message) {
if communication.CameraConnected {
select {
case communication.HandleLiveSD <- time.Now().Unix():
default:
}
log.Log.Info("MQTTListenerHandleLiveSD: received request to livestream.")
} else {
log.Log.Info("MQTTListenerHandleLiveSD: received request to livestream, but camera is not connected.")
}
msg.Ack()
})
}
func MQTTListenerHandleLiveHDHandshake(mqttClient mqtt.Client, hubKey string, configuration *models.Configuration, communication *models.Communication) {
config := configuration.Config
topicRequestWebRtc := config.Key + "/register"
mqttClient.Subscribe(topicRequestWebRtc, 0, func(c mqtt.Client, msg mqtt.Message) {
if communication.CameraConnected {
var sdp models.SDPPayload
json.Unmarshal(msg.Payload(), &sdp)
select {
case communication.HandleLiveHDHandshake <- sdp:
default:
}
log.Log.Info("MQTTListenerHandleLiveHDHandshake: received request to setup webrtc.")
} else {
log.Log.Info("MQTTListenerHandleLiveHDHandshake: received request to setup webrtc, but camera is not connected.")
}
msg.Ack()
})
}
func MQTTListenerHandleLiveHDKeepalive(mqttClient mqtt.Client, hubKey string, configuration *models.Configuration, communication *models.Communication) {
config := configuration.Config
topicKeepAlive := fmt.Sprintf("kerberos/webrtc/keepalivehub/%s", config.Key)
mqttClient.Subscribe(topicKeepAlive, 0, func(c mqtt.Client, msg mqtt.Message) {
if communication.CameraConnected {
alive := string(msg.Payload())
communication.HandleLiveHDKeepalive <- alive
log.Log.Info("MQTTListenerHandleLiveHDKeepalive: Received keepalive: " + alive)
} else {
log.Log.Info("MQTTListenerHandleLiveHDKeepalive: received keepalive, but camera is not connected.")
}
})
}
func MQTTListenerHandleLiveHDPeers(mqttClient mqtt.Client, hubKey string, configuration *models.Configuration, communication *models.Communication) {
config := configuration.Config
topicPeers := fmt.Sprintf("kerberos/webrtc/peers/%s", config.Key)
mqttClient.Subscribe(topicPeers, 0, func(c mqtt.Client, msg mqtt.Message) {
if communication.CameraConnected {
peerCount := string(msg.Payload())
communication.HandleLiveHDPeers <- peerCount
log.Log.Info("MQTTListenerHandleLiveHDPeers: Number of peers listening: " + peerCount)
} else {
log.Log.Info("MQTTListenerHandleLiveHDPeers: received peer count, but camera is not connected.")
}
})
}
func MQTTListenerHandleLiveHDCandidates(mqttClient mqtt.Client, hubKey string, configuration *models.Configuration, communication *models.Communication) {
config := configuration.Config
topicCandidates := "candidate/cloud"
mqttClient.Subscribe(topicCandidates, 0, func(c mqtt.Client, msg mqtt.Message) {
if communication.CameraConnected {
var candidate models.Candidate
json.Unmarshal(msg.Payload(), &candidate)
if candidate.CloudKey == config.Key {
key := candidate.CloudKey + "/" + candidate.Cuuid
candidatesExists := false
var channel chan string
for !candidatesExists {
webrtc.CandidatesMutex.Lock()
channel, candidatesExists = webrtc.CandidateArrays[key]
webrtc.CandidatesMutex.Unlock()
}
log.Log.Info("MQTTListenerHandleLiveHDCandidates: " + string(msg.Payload()))
channel <- string(msg.Payload())
}
} else {
log.Log.Info("MQTTListenerHandleLiveHDCandidates: received candidate, but camera is not connected.")
}
})
}
func MQTTListenerHandleONVIF(mqttClient mqtt.Client, hubKey string, configuration *models.Configuration, communication *models.Communication) {
config := configuration.Config
topicOnvif := fmt.Sprintf("kerberos/onvif/%s", config.Key)
mqttClient.Subscribe(topicOnvif, 0, func(c mqtt.Client, msg mqtt.Message) {
if communication.CameraConnected {
var onvifAction models.OnvifAction
json.Unmarshal(msg.Payload(), &onvifAction)
communication.HandleONVIF <- onvifAction
log.Log.Info("MQTTListenerHandleONVIF: Received an action - " + onvifAction.Action)
} else {
log.Log.Info("MQTTListenerHandleONVIF: received action, but camera is not connected.")
}
})
}

View File

@@ -3,17 +3,15 @@ package websocket
import (
"context"
"encoding/base64"
"image"
"net/http"
"sync"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"github.com/kerberos-io/agent/machinery/src/capture"
"github.com/kerberos-io/agent/machinery/src/computervision"
"github.com/kerberos-io/agent/machinery/src/log"
"github.com/kerberos-io/agent/machinery/src/models"
"github.com/kerberos-io/agent/machinery/src/packets"
"github.com/kerberos-io/agent/machinery/src/utils"
"github.com/kerberos-io/joy4/cgo/ffmpeg"
)
type Message struct {
@@ -49,7 +47,7 @@ var upgrader = websocket.Upgrader{
},
}
func WebsocketHandler(c *gin.Context, configuration *models.Configuration, communication *models.Communication, captureDevice *capture.Capture) {
func WebsocketHandler(c *gin.Context, communication *models.Communication) {
w := c.Writer
r := c.Request
conn, err := upgrader.Upgrade(w, r, nil)
@@ -60,17 +58,12 @@ func WebsocketHandler(c *gin.Context, configuration *models.Configuration, commu
var message Message
err = conn.ReadJSON(&message)
if err != nil {
log.Log.Error("routers.websocket.main.WebsocketHandler(): " + err.Error())
return
}
clientID := message.ClientID
if sockets[clientID] == nil {
connection := new(Connection)
connection.Socket = conn
sockets[clientID] = connection
sockets[clientID].Cancels = make(map[string]context.CancelFunc)
log.Log.Info("routers.websocket.main.WebsocketHandler(): " + clientID + ": connected.")
}
// Continuously read messages
@@ -92,14 +85,14 @@ func WebsocketHandler(c *gin.Context, configuration *models.Configuration, commu
if exists {
sockets[clientID].Cancels["stream-sd"]()
} else {
log.Log.Error("routers.websocket.main.WebsocketHandler(): streaming sd does not exists for " + clientID)
log.Log.Error("Streaming sd does not exists for " + clientID)
}
case "stream-sd":
if communication.CameraConnected {
_, exists := sockets[clientID].Cancels["stream-sd"]
if exists {
log.Log.Debug("routers.websocket.main.WebsocketHandler(): already streaming sd for " + clientID)
log.Log.Info("Already streaming sd for " + clientID)
} else {
startStream := Message{
ClientID: clientID,
@@ -112,7 +105,7 @@ func WebsocketHandler(c *gin.Context, configuration *models.Configuration, commu
ctx, cancel := context.WithCancel(context.Background())
sockets[clientID].Cancels["stream-sd"] = cancel
go ForwardSDStream(ctx, clientID, sockets[clientID], configuration, communication, captureDevice)
go ForwardSDStream(ctx, clientID, sockets[clientID], communication)
}
}
}
@@ -126,49 +119,37 @@ func WebsocketHandler(c *gin.Context, configuration *models.Configuration, commu
_, exists := sockets[clientID]
if exists {
delete(sockets, clientID)
log.Log.Info("routers.websocket.main.WebsocketHandler(): " + clientID + ": terminated and disconnected websocket connection.")
log.Log.Info("WebsocketHandler: " + clientID + ": terminated and disconnected websocket connection.")
}
}
}
func ForwardSDStream(ctx context.Context, clientID string, connection *Connection, configuration *models.Configuration, communication *models.Communication, captureDevice *capture.Capture) {
func ForwardSDStream(ctx context.Context, clientID string, connection *Connection, communication *models.Communication) {
var queue *packets.Queue
var cursor *packets.QueueCursor
queue := communication.Queue
cursor := queue.Latest()
decoder := communication.Decoder
decoderMutex := communication.DecoderMutex
// We'll pick the right client and decoder.
rtspClient := captureDevice.RTSPSubClient
if rtspClient != nil {
queue = communication.SubQueue
cursor = queue.Latest()
} else {
rtspClient = captureDevice.RTSPClient
queue = communication.Queue
cursor = queue.Latest()
}
// Allocate ffmpeg.VideoFrame
frame := ffmpeg.AllocVideoFrame()
logreader:
for {
var encodedImage string
if queue != nil && cursor != nil && rtspClient != nil {
if queue != nil && cursor != nil && decoder != nil {
pkt, err := cursor.ReadPacket()
if err == nil {
if !pkt.IsKeyFrame {
continue
}
var img image.YCbCr
img, err = (*rtspClient).DecodePacket(pkt)
img, err := computervision.GetRawImage(frame, pkt, decoder, decoderMutex)
if err == nil {
config := configuration.Config
// Resize the image to the base width and height
imageResized, _ := utils.ResizeImage(&img, uint(config.Capture.IPCamera.BaseWidth), uint(config.Capture.IPCamera.BaseHeight))
bytes, _ := utils.ImageToBytes(imageResized)
bytes, _ := computervision.ImageToBytes(&img.Image)
encodedImage = base64.StdEncoding.EncodeToString(bytes)
} else {
continue
}
} else {
log.Log.Error("routers.websocket.main.ForwardSDStream():" + err.Error())
log.Log.Error("ForwardSDStream:" + err.Error())
break logreader
}
}
@@ -182,7 +163,7 @@ logreader:
}
err := connection.WriteJson(startStrean)
if err != nil {
log.Log.Error("routers.websocket.main.ForwardSDStream():" + err.Error())
log.Log.Error("ForwardSDStream:" + err.Error())
break logreader
}
select {
@@ -192,14 +173,16 @@ logreader:
}
}
frame.Free()
// Close socket for streaming
_, exists := connection.Cancels["stream-sd"]
if exists {
delete(connection.Cancels, "stream-sd")
} else {
log.Log.Error("routers.websocket.main.ForwardSDStream(): streaming sd does not exists for " + clientID)
log.Log.Error("Streaming sd does not exists for " + clientID)
}
// Send stop streaming message
log.Log.Info("routers.websocket.main.ForwardSDStream(): stop sending streaming over websocket")
log.Log.Info("ForwardSDStream: stop sending streaming over websocket")
}

View 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
}
}

View 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
}

View 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
}

View 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
}

View File

@@ -1,12 +1,9 @@
package utils
import (
"bufio"
"bytes"
"errors"
"fmt"
"image"
"image/jpeg"
"io/ioutil"
"math/rand"
"os"
@@ -18,18 +15,10 @@ import (
"strings"
"time"
"github.com/kerberos-io/agent/machinery/src/encryption"
"github.com/kerberos-io/agent/machinery/src/log"
"github.com/kerberos-io/agent/machinery/src/models"
"github.com/nfnt/resize"
)
// VERSION is the agent version. It defaults to "0.0.0" for local dev builds
// and is overridden at build time via:
// go build -ldflags "-X github.com/kerberos-io/agent/machinery/src/utils.VERSION=v1.2.3"
var VERSION = "0.0.0"
const letterBytes = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
// MaxUint8 - maximum value which can be held in an uint8
@@ -341,96 +330,3 @@ func PrintConfiguration(configuration *models.Configuration) {
}
log.Log.Info("Printing our configuration (config.json): " + configurationVariables)
}
func Decrypt(directoryOrFile string, symmetricKey []byte) {
// Check if file or directory
fileInfo, err := os.Stat(directoryOrFile)
if err != nil {
log.Log.Fatal(err.Error())
return
}
var files []string
if fileInfo.IsDir() {
// Create decrypted directory
err = os.MkdirAll(directoryOrFile+"/decrypted", 0755)
if err != nil {
log.Log.Fatal(err.Error())
return
}
dir, err := os.ReadDir(directoryOrFile)
if err != nil {
log.Log.Fatal(err.Error())
return
}
for _, file := range dir {
// Check if file is not a directory
if !file.IsDir() {
// Check if an mp4 file
if strings.HasSuffix(file.Name(), ".mp4") {
files = append(files, directoryOrFile+"/"+file.Name())
}
}
}
} else {
files = append(files, directoryOrFile)
}
// We'll loop over all files and decrypt them one by one.
for _, file := range files {
// Read file
content, err := os.ReadFile(file)
if err != nil {
log.Log.Fatal(err.Error())
return
}
// Decrypt using AES key
decrypted, err := encryption.AesDecrypt(content, string(symmetricKey))
if err != nil {
log.Log.Fatal("Something went wrong while decrypting: " + err.Error())
return
}
// Write decrypted content to file with appended .decrypted
// Get filename split by / and get last element.
fileParts := strings.Split(file, "/")
fileName := fileParts[len(fileParts)-1]
pathToFile := strings.Join(fileParts[:len(fileParts)-1], "/")
err = os.WriteFile(pathToFile+"/decrypted/"+fileName, []byte(decrypted), 0644)
if err != nil {
log.Log.Fatal(err.Error())
return
}
}
}
func ImageToBytes(img *image.Image) ([]byte, error) {
buffer := new(bytes.Buffer)
w := bufio.NewWriter(buffer)
err := jpeg.Encode(w, *img, &jpeg.Options{Quality: 35})
log.Log.Debug("ImageToBytes() - buffer size: " + strconv.Itoa(buffer.Len()))
return buffer.Bytes(), err
}
func ResizeImage(img image.Image, newWidth uint, newHeight uint) (*image.Image, error) {
if img == nil {
return nil, errors.New("image is nil")
}
// resize to width 640 using Lanczos resampling
// and preserve aspect ratio
m := resize.Resize(newWidth, newHeight, img, resize.Lanczos3)
return &m, nil
}
func ResizeHeightWithAspectRatio(newWidth int, width int, height int) (int, int) {
if newWidth <= 0 || width <= 0 || height <= 0 {
return width, height
}
// Calculate the new height based on the aspect ratio
newHeight := (newWidth * height) / width
// Return the new dimensions
return newWidth, newHeight
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,176 +0,0 @@
package video
import (
"fmt"
"os"
"testing"
mp4ff "github.com/Eyevinn/mp4ff/mp4"
"github.com/kerberos-io/agent/machinery/src/models"
)
// TestMP4Duration creates an MP4 file simulating a 5-second video recording
// and verifies that the durations in all boxes match the sum of sample durations.
func TestMP4Duration(t *testing.T) {
tmpFile := "/tmp/test_duration.mp4"
defer os.Remove(tmpFile)
// Minimal SPS for H.264 (baseline, 640x480) - proper Annex B format with start code
sps := []byte{0x67, 0x42, 0xc0, 0x1e, 0xd9, 0x00, 0xa0, 0x47, 0xfe, 0xc8}
pps := []byte{0x68, 0xce, 0x38, 0x80}
mp4Video := NewMP4(tmpFile, [][]byte{sps}, [][]byte{pps}, nil, 10)
mp4Video.SetWidth(640)
mp4Video.SetHeight(480)
videoTrack := mp4Video.AddVideoTrack("H264")
// Simulate 5 seconds at 25fps (200 frames, keyframe every 50 frames = 2s)
// PTS in milliseconds (timescale=1000)
frameDuration := uint64(40) // 40ms per frame = 25fps
numFrames := 150
gopSize := 50
// Create a fake Annex B NAL unit (keyframe IDR = type 5, non-keyframe = type 1)
makeFrame := func(isKey bool) []byte {
nalType := byte(0x01) // non-IDR slice
if isKey {
nalType = 0x65 // IDR slice
}
// Start code (4 bytes) + NAL header + some data
frame := []byte{0x00, 0x00, 0x00, 0x01, nalType}
// Add some padding data
for i := 0; i < 100; i++ {
frame = append(frame, byte(i))
}
return frame
}
var expectedDuration uint64
for i := 0; i < numFrames; i++ {
pts := uint64(i) * frameDuration
isKeyframe := i%gopSize == 0
err := mp4Video.AddSampleToTrack(videoTrack, isKeyframe, makeFrame(isKeyframe), pts)
if err != nil {
t.Fatalf("AddSampleToTrack failed at frame %d: %v", i, err)
}
}
expectedDuration = uint64(numFrames) * frameDuration // Should be 6000ms (150 * 40)
// Close with config that has signing key to avoid nil panics
config := &models.Config{
Signing: &models.Signing{
PrivateKey: "",
},
}
mp4Video.Close(config)
// Log what the code computed
t.Logf("VideoTotalDuration: %d ms", mp4Video.VideoTotalDuration)
t.Logf("Expected duration: %d ms", expectedDuration)
t.Logf("Segments: %d", len(mp4Video.SegmentDurations))
var sumSegDur uint64
for i, d := range mp4Video.SegmentDurations {
t.Logf(" Segment %d: duration=%d ms", i, d)
sumSegDur += d
}
t.Logf("Sum of segment durations: %d ms", sumSegDur)
// Now read back the file and inspect the boxes
f, err := os.Open(tmpFile)
if err != nil {
t.Fatalf("Failed to open output file: %v", err)
}
defer f.Close()
fi, err := f.Stat()
if err != nil {
t.Fatalf("Failed to stat output file: %v", err)
}
parsedFile, err := mp4ff.DecodeFile(f)
if err != nil {
t.Fatalf("Failed to decode MP4: %v", err)
}
t.Logf("File size: %d bytes", fi.Size())
// Check moov box
if parsedFile.Moov == nil {
t.Fatal("No moov box found")
}
// Check mvhd duration
mvhd := parsedFile.Moov.Mvhd
t.Logf("mvhd.Duration: %d (timescale=%d) = %.2f seconds", mvhd.Duration, mvhd.Timescale, float64(mvhd.Duration)/float64(mvhd.Timescale))
t.Logf("mvhd.Rate: 0x%08x", mvhd.Rate)
t.Logf("mvhd.Volume: 0x%04x", mvhd.Volume)
// Check each trak
for i, trak := range parsedFile.Moov.Traks {
t.Logf("Track %d:", i)
t.Logf(" tkhd.Duration: %d", trak.Tkhd.Duration)
t.Logf(" mdhd.Duration: %d (timescale=%d) = %.2f seconds", trak.Mdia.Mdhd.Duration, trak.Mdia.Mdhd.Timescale, float64(trak.Mdia.Mdhd.Duration)/float64(trak.Mdia.Mdhd.Timescale))
}
// Check mvex/mehd
if parsedFile.Moov.Mvex != nil && parsedFile.Moov.Mvex.Mehd != nil {
t.Logf("mehd.FragmentDuration: %d", parsedFile.Moov.Mvex.Mehd.FragmentDuration)
}
// Sum up actual sample durations from trun boxes in all segments
var actualTrunDuration uint64
var sampleCount int
for _, seg := range parsedFile.Segments {
for _, frag := range seg.Fragments {
for _, traf := range frag.Moof.Trafs {
// Only count video track (track 1)
if traf.Tfhd.TrackID == 1 {
for _, trun := range traf.Truns {
for _, s := range trun.Samples {
actualTrunDuration += uint64(s.Dur)
sampleCount++
}
}
}
}
}
}
t.Logf("Actual trun sample count: %d", sampleCount)
t.Logf("Actual trun total duration: %d ms", actualTrunDuration)
// Check sidx
if parsedFile.Sidx != nil {
var sidxDuration uint64
for _, ref := range parsedFile.Sidx.SidxRefs {
sidxDuration += uint64(ref.SubSegmentDuration)
}
t.Logf("sidx total duration: %d ms", sidxDuration)
}
// VERIFY: All duration values should be consistent
// The expected duration for 150 frames at 40ms each:
// - The sample-buffering pattern means the LAST sample uses LastVideoSampleDTS as duration
// - So all 150 samples should produce 150 * 40ms = 6000ms total
// But due to the pending sample pattern, the actual trun durations might differ
fmt.Println()
fmt.Println("=== DURATION CONSISTENCY CHECK ===")
fmt.Printf("Expected (150 * 40ms): %d ms\n", expectedDuration)
fmt.Printf("mvhd.Duration: %d ms\n", mvhd.Duration)
fmt.Printf("tkhd.Duration: %d ms\n", parsedFile.Moov.Traks[0].Tkhd.Duration)
fmt.Printf("mdhd.Duration: %d ms\n", parsedFile.Moov.Traks[0].Mdia.Mdhd.Duration)
fmt.Printf("Actual trun durations sum: %d ms\n", actualTrunDuration)
fmt.Printf("VideoTotalDuration: %d ms\n", mp4Video.VideoTotalDuration)
fmt.Printf("Sum of SegmentDurations: %d ms\n", sumSegDur)
fmt.Println()
// The key assertion: header duration must equal trun sum
if mvhd.Duration != actualTrunDuration {
t.Errorf("MISMATCH: mvhd.Duration (%d) != actual trun sum (%d), diff = %d ms",
mvhd.Duration, actualTrunDuration, int64(mvhd.Duration)-int64(actualTrunDuration))
}
if parsedFile.Moov.Traks[0].Mdia.Mdhd.Duration != 0 {
t.Errorf("MISMATCH: mdhd.Duration should be 0 for fragmented MP4, got %d",
parsedFile.Moov.Traks[0].Mdia.Mdhd.Duration)
}
}

View File

@@ -1,126 +1,34 @@
package webrtc
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"strconv"
"sync"
"sync/atomic"
"time"
//"github.com/izern/go-fdkaac/fdkaac"
"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/agent/machinery/src/packets"
"github.com/kerberos-io/joy4/av/pubsub"
mqtt "github.com/eclipse/paho.mqtt.golang"
"github.com/pion/interceptor"
"github.com/pion/interceptor/pkg/intervalpli"
pionWebRTC "github.com/pion/webrtc/v4"
pionMedia "github.com/pion/webrtc/v4/pkg/media"
av "github.com/kerberos-io/joy4/av"
"github.com/kerberos-io/joy4/cgo/ffmpeg"
h264parser "github.com/kerberos-io/joy4/codec/h264parser"
pionWebRTC "github.com/pion/webrtc/v3"
pionMedia "github.com/pion/webrtc/v3/pkg/media"
)
const (
// Channel buffer sizes
candidateChannelBuffer = 100
rtcpBufferSize = 1500
// Timeouts and intervals
keepAliveTimeout = 15 * time.Second
defaultTimeout = 10 * time.Second
// Track identifiers
trackStreamID = "kerberos-stream"
)
// ConnectionManager manages WebRTC peer connections in a thread-safe manner
type ConnectionManager struct {
mu sync.RWMutex
candidateChannels map[string]chan string
peerConnections map[string]*peerConnectionWrapper
var (
CandidatesMutex sync.Mutex
CandidateArrays map[string](chan string)
peerConnectionCount int64
}
// peerConnectionWrapper wraps a peer connection with additional metadata
type peerConnectionWrapper struct {
conn *pionWebRTC.PeerConnection
cancelCtx context.CancelFunc
done chan struct{}
closeOnce sync.Once
}
var globalConnectionManager = NewConnectionManager()
// NewConnectionManager creates a new connection manager
func NewConnectionManager() *ConnectionManager {
return &ConnectionManager{
candidateChannels: make(map[string]chan string),
peerConnections: make(map[string]*peerConnectionWrapper),
}
}
// GetOrCreateCandidateChannel gets or creates a candidate channel for a session
func (cm *ConnectionManager) GetOrCreateCandidateChannel(sessionKey string) chan string {
cm.mu.Lock()
defer cm.mu.Unlock()
if ch, exists := cm.candidateChannels[sessionKey]; exists {
return ch
}
ch := make(chan string, candidateChannelBuffer)
cm.candidateChannels[sessionKey] = ch
return ch
}
// CloseCandidateChannel safely closes and removes a candidate channel
func (cm *ConnectionManager) CloseCandidateChannel(sessionKey string) {
cm.mu.Lock()
defer cm.mu.Unlock()
if ch, exists := cm.candidateChannels[sessionKey]; exists {
close(ch)
delete(cm.candidateChannels, sessionKey)
}
}
// AddPeerConnection adds a peer connection to the manager
func (cm *ConnectionManager) AddPeerConnection(sessionID string, wrapper *peerConnectionWrapper) {
cm.mu.Lock()
defer cm.mu.Unlock()
cm.peerConnections[sessionID] = wrapper
}
// RemovePeerConnection removes a peer connection from the manager
func (cm *ConnectionManager) RemovePeerConnection(sessionID string) {
cm.mu.Lock()
defer cm.mu.Unlock()
if wrapper, exists := cm.peerConnections[sessionID]; exists {
if wrapper.cancelCtx != nil {
wrapper.cancelCtx()
}
delete(cm.peerConnections, sessionID)
}
}
// GetPeerConnectionCount returns the current count of active peer connections
func (cm *ConnectionManager) GetPeerConnectionCount() int64 {
return atomic.LoadInt64(&cm.peerConnectionCount)
}
// IncrementPeerCount atomically increments the peer connection count
func (cm *ConnectionManager) IncrementPeerCount() int64 {
return atomic.AddInt64(&cm.peerConnectionCount, 1)
}
// DecrementPeerCount atomically decrements the peer connection count
func (cm *ConnectionManager) DecrementPeerCount() int64 {
return atomic.AddInt64(&cm.peerConnectionCount, -1)
}
peerConnections map[string]*pionWebRTC.PeerConnection
//encoder *ffmpeg.VideoEncoder
)
type WebRTC struct {
Name string
@@ -132,6 +40,24 @@ type WebRTC struct {
PacketsCount chan int
}
// 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)
if err != nil {
return
}
if encoder == nil {
err = fmt.Errorf("Video encoder not found")
return
}
encoder.SetFramerate(30, 1)
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{
Name: name,
@@ -139,14 +65,15 @@ func CreateWebRTC(name string, stunServers []string, turnServers []string, turnS
TurnServers: turnServers,
TurnServersUsername: turnServersUsername,
TurnServersCredential: turnServersCredential,
Timer: time.NewTimer(defaultTimeout),
Timer: time.NewTimer(time.Second * 10),
PacketsCount: make(chan int),
}
}
func (w WebRTC) DecodeSessionDescription(data string) ([]byte, error) {
sd, err := base64.StdEncoding.DecodeString(data)
if err != nil {
log.Log.Error("webrtc.main.DecodeSessionDescription(): " + err.Error())
log.Log.Error("DecodeString error: " + err.Error())
return []byte{}, err
}
return sd, nil
@@ -160,88 +87,28 @@ func (w WebRTC) CreateOffer(sd []byte) pionWebRTC.SessionDescription {
return offer
}
func RegisterCandidates(key string, candidate models.ReceiveHDCandidatesPayload) {
ch := globalConnectionManager.GetOrCreateCandidateChannel(key)
log.Log.Info("webrtc.main.RegisterCandidates(): " + candidate.Candidate)
select {
case ch <- candidate.Candidate:
default:
log.Log.Info("webrtc.main.RegisterCandidates(): channel is full, dropping candidate")
}
}
func RegisterDefaultInterceptors(mediaEngine *pionWebRTC.MediaEngine, interceptorRegistry *interceptor.Registry) error {
if err := pionWebRTC.ConfigureNack(mediaEngine, interceptorRegistry); err != nil {
return err
}
if err := pionWebRTC.ConfigureRTCPReports(interceptorRegistry); err != nil {
return err
}
if err := pionWebRTC.ConfigureSimulcastExtensionHeaders(mediaEngine); err != nil {
return err
}
return nil
}
func InitializeWebRTCConnection(configuration *models.Configuration, communication *models.Communication, mqttClient mqtt.Client, videoTrack *pionWebRTC.TrackLocalStaticSample, audioTrack *pionWebRTC.TrackLocalStaticSample, handshake models.RequestHDStreamPayload) {
func InitializeWebRTCConnection(configuration *models.Configuration, communication *models.Communication, mqttClient mqtt.Client, videoTrack *pionWebRTC.TrackLocalStaticSample, audioTrack *pionWebRTC.TrackLocalStaticSample, handshake models.SDPPayload, candidates chan string) {
config := configuration.Config
deviceKey := config.Key
stunServers := []string{config.STUNURI}
turnServers := []string{config.TURNURI}
turnServersUsername := config.TURNUsername
turnServersCredential := config.TURNPassword
// We create a channel which will hold the candidates for this session.
sessionKey := config.Key + "/" + handshake.SessionID
candidateChannel := globalConnectionManager.GetOrCreateCandidateChannel(sessionKey)
// Set variables
hubKey := handshake.HubKey
sessionDescription := handshake.SessionDescription
// Create WebRTC object
w := CreateWebRTC(deviceKey, stunServers, turnServers, turnServersUsername, turnServersCredential)
sd, err := w.DecodeSessionDescription(sessionDescription)
sd, err := w.DecodeSessionDescription(handshake.Sdp)
if err == nil {
mediaEngine := &pionWebRTC.MediaEngine{}
if err := mediaEngine.RegisterDefaultCodecs(); err != nil {
log.Log.Error("webrtc.main.InitializeWebRTCConnection(): something went wrong registering codecs for media engine: " + err.Error())
log.Log.Error("InitializeWebRTCConnection: something went wrong registering codecs.")
}
// Create a InterceptorRegistry. This is the user configurable RTP/RTCP Pipeline.
// This provides NACKs, RTCP Reports and other features. If you use `webrtc.NewPeerConnection`
// this is enabled by default. If you are manually managing You MUST create a InterceptorRegistry
// for each PeerConnection.
interceptorRegistry := &interceptor.Registry{}
// Use the default set of Interceptors
if err := pionWebRTC.RegisterDefaultInterceptors(mediaEngine, interceptorRegistry); err != nil {
panic(err)
}
// Register a intervalpli factory
// This interceptor sends a PLI every 3 seconds. A PLI causes a video keyframe to be generated by the sender.
// This makes our video seekable and more error resilent, but at a cost of lower picture quality and higher bitrates
// A real world application should process incoming RTCP packets from viewers and forward them to senders
intervalPliFactory, err := intervalpli.NewReceiverInterceptor()
if err != nil {
panic(err)
}
interceptorRegistry.Add(intervalPliFactory)
api := pionWebRTC.NewAPI(
pionWebRTC.WithMediaEngine(mediaEngine),
pionWebRTC.WithInterceptorRegistry(interceptorRegistry),
)
policy := pionWebRTC.ICETransportPolicyAll
if config.ForceTurn == "true" {
policy = pionWebRTC.ICETransportPolicyRelay
}
api := pionWebRTC.NewAPI(pionWebRTC.WithMediaEngine(mediaEngine))
peerConnection, err := api.NewPeerConnection(
pionWebRTC.Configuration{
@@ -255,512 +122,283 @@ func InitializeWebRTCConnection(configuration *models.Configuration, communicati
Credential: w.TurnServersCredential,
},
},
ICETransportPolicy: policy,
//ICETransportPolicy: pionWebRTC.ICETransportPolicyRelay,
},
)
if err == nil && peerConnection != nil {
// Create context for this connection
ctx, cancel := context.WithCancel(context.Background())
wrapper := &peerConnectionWrapper{
conn: peerConnection,
cancelCtx: cancel,
done: make(chan struct{}),
if _, err = peerConnection.AddTrack(videoTrack); err != nil {
panic(err)
}
var videoSender *pionWebRTC.RTPSender = nil
if videoTrack != nil {
if videoSender, err = peerConnection.AddTrack(videoTrack); err != nil {
log.Log.Error("webrtc.main.InitializeWebRTCConnection(): error adding video track: " + err.Error())
cancel()
return
}
} else {
log.Log.Info("webrtc.main.InitializeWebRTCConnection(): video track is nil, skipping video")
if _, err = peerConnection.AddTrack(audioTrack); err != nil {
panic(err)
}
// Read incoming RTCP packets
// Before these packets are returned they are processed by interceptors. For things
// like NACK this needs to be called.
if videoSender != nil {
go func() {
defer func() {
log.Log.Info("webrtc.main.InitializeWebRTCConnection(): video RTCP reader stopped")
}()
rtcpBuf := make([]byte, rtcpBufferSize)
for {
select {
case <-ctx.Done():
return
default:
if _, _, rtcpErr := videoSender.Read(rtcpBuf); rtcpErr != nil {
return
}
if err != nil {
panic(err)
}
peerConnection.OnICEConnectionStateChange(func(connectionState pionWebRTC.ICEConnectionState) {
if connectionState == pionWebRTC.ICEConnectionStateDisconnected {
atomic.AddInt64(&peerConnectionCount, -1)
peerConnections[handshake.Cuuid] = nil
close(candidates)
close(w.PacketsCount)
if err := peerConnection.Close(); err != nil {
panic(err)
}
} else if connectionState == pionWebRTC.ICEConnectionStateConnected {
atomic.AddInt64(&peerConnectionCount, 1)
} else if connectionState == pionWebRTC.ICEConnectionStateChecking {
for candidate := range candidates {
log.Log.Info("InitializeWebRTCConnection: Received candidate.")
if candidateErr := peerConnection.AddICECandidate(pionWebRTC.ICECandidateInit{Candidate: string(candidate)}); candidateErr != nil {
}
}
}()
}
var audioSender *pionWebRTC.RTPSender = nil
if audioTrack != nil {
if audioSender, err = peerConnection.AddTrack(audioTrack); err != nil {
log.Log.Error("webrtc.main.InitializeWebRTCConnection(): error adding audio track: " + err.Error())
cancel()
return
}
} else {
log.Log.Info("webrtc.main.InitializeWebRTCConnection(): audio track is nil, skipping audio")
}
// Read incoming RTCP packets
// Before these packets are returned they are processed by interceptors. For things
// like NACK this needs to be called.
if audioSender != nil {
go func() {
defer func() {
log.Log.Info("webrtc.main.InitializeWebRTCConnection(): audio RTCP reader stopped")
}()
rtcpBuf := make([]byte, rtcpBufferSize)
for {
select {
case <-ctx.Done():
return
default:
if _, _, rtcpErr := audioSender.Read(rtcpBuf); rtcpErr != nil {
return
}
}
}
}()
}
peerConnection.OnConnectionStateChange(func(connectionState pionWebRTC.PeerConnectionState) {
log.Log.Info("webrtc.main.InitializeWebRTCConnection(): connection state changed to: " + connectionState.String())
switch connectionState {
case pionWebRTC.PeerConnectionStateDisconnected, pionWebRTC.PeerConnectionStateClosed:
wrapper.closeOnce.Do(func() {
count := globalConnectionManager.DecrementPeerCount()
log.Log.Info("webrtc.main.InitializeWebRTCConnection(): Peer disconnected. Active peers: " + string(rune(count)))
// Clean up resources
globalConnectionManager.CloseCandidateChannel(sessionKey)
if err := peerConnection.Close(); err != nil {
log.Log.Error("webrtc.main.InitializeWebRTCConnection(): error closing peer connection: " + err.Error())
}
globalConnectionManager.RemovePeerConnection(handshake.SessionID)
close(wrapper.done)
})
case pionWebRTC.PeerConnectionStateConnected:
count := globalConnectionManager.IncrementPeerCount()
log.Log.Info("webrtc.main.InitializeWebRTCConnection(): Peer connected. Active peers: " + string(rune(count)))
case pionWebRTC.PeerConnectionStateFailed:
log.Log.Info("webrtc.main.InitializeWebRTCConnection(): ICE connection failed")
}
log.Log.Info("InitializeWebRTCConnection: connection state changed to: " + connectionState.String())
log.Log.Info("InitializeWebRTCConnection: Number of peers connected (" + strconv.FormatInt(peerConnectionCount, 10) + ")")
})
go func() {
defer func() {
log.Log.Info("webrtc.main.InitializeWebRTCConnection(): candidate processor stopped for session: " + handshake.SessionID)
}()
// Iterate over the candidates and send them to the remote client
for {
select {
case <-ctx.Done():
return
case candidate, ok := <-candidateChannel:
if !ok {
return
}
log.Log.Info("webrtc.main.InitializeWebRTCConnection(): Received candidate from channel: " + candidate)
if candidateErr := peerConnection.AddICECandidate(pionWebRTC.ICECandidateInit{Candidate: candidate}); candidateErr != nil {
log.Log.Error("webrtc.main.InitializeWebRTCConnection(): error adding candidate: " + candidateErr.Error())
}
}
}
}()
offer := w.CreateOffer(sd)
if err = peerConnection.SetRemoteDescription(offer); err != nil {
log.Log.Error("webrtc.main.InitializeWebRTCConnection(): something went wrong while setting remote description: " + err.Error())
panic(err)
}
//gatherCompletePromise := pionWebRTC.GatheringCompletePromise(peerConnection)
answer, err := peerConnection.CreateAnswer(nil)
if err != nil {
log.Log.Error("webrtc.main.InitializeWebRTCConnection(): something went wrong while creating answer: " + err.Error())
panic(err)
} else if err = peerConnection.SetLocalDescription(answer); err != nil {
log.Log.Error("webrtc.main.InitializeWebRTCConnection(): something went wrong while setting local description: " + err.Error())
panic(err)
}
// 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
var hasRelayCandidates bool
// 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
peerConnection.OnICECandidate(func(candidate *pionWebRTC.ICECandidate) {
if candidate == nil {
log.Log.Info("webrtc.main.InitializeWebRTCConnection(): ICE gathering complete (candidate is nil)")
if !hasRelayCandidates {
log.Log.Error("webrtc.main.InitializeWebRTCConnection(): WARNING - No TURN (relay) candidates were gathered! TURN servers: " +
config.TURNURI + ", Username: " + config.TURNUsername + ", ForceTurn: " + config.ForceTurn)
}
return
}
// Log candidate details for debugging
candidateJSON := candidate.ToJSON()
candidateStr := candidateJSON.Candidate
candidatesMux.Lock()
defer candidatesMux.Unlock()
// Determine candidate type from the candidate string
candidateType := "unknown"
if candidateJSON.Candidate != "" {
switch candidate.Typ {
case pionWebRTC.ICECandidateTypeRelay:
candidateType = "relay"
case pionWebRTC.ICECandidateTypeSrflx:
candidateType = "srflx"
case pionWebRTC.ICECandidateTypeHost:
candidateType = "host"
case pionWebRTC.ICECandidateTypePrflx:
candidateType = "prflx"
}
}
// Track if we received any relay (TURN) candidates
if candidateType == "relay" {
hasRelayCandidates = true
}
log.Log.Info("webrtc.main.InitializeWebRTCConnection(): ICE candidate received - Type: " + candidateType +
", Candidate: " + candidateStr)
// Create a config map
valueMap := make(map[string]interface{})
candateBinary, err := json.Marshal(candidateJSON)
topic := fmt.Sprintf("%s/%s/candidate/edge", deviceKey, handshake.Cuuid)
log.Log.Info("InitializeWebRTCConnection: Send candidate to " + topic)
candiInit := candidate.ToJSON()
sdpmid := "0"
candiInit.SDPMid = &sdpmid
candi, err := json.Marshal(candiInit)
if err == nil {
valueMap["candidate"] = string(candateBinary)
// SDP is not needed to be send..
//valueMap["sdp"] = []byte(base64.StdEncoding.EncodeToString([]byte(answer.SDP)))
valueMap["session_id"] = handshake.SessionID
log.Log.Info("webrtc.main.InitializeWebRTCConnection(): sending " + candidateType + " candidate to hub")
} else {
log.Log.Error("webrtc.main.InitializeWebRTCConnection(): failed to marshal 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 {
token := mqttClient.Publish("kerberos/hub/"+hubKey, 2, false, payload)
log.Log.Info("InitializeWebRTCConnection:" + string(candi))
token := mqttClient.Publish(topic, 2, false, candi)
token.Wait()
} else {
log.Log.Info("webrtc.main.InitializeWebRTCConnection(): while packaging mqtt message: " + err.Error())
}
})
// Store peer connection in manager
globalConnectionManager.AddPeerConnection(handshake.SessionID, wrapper)
peerConnections[handshake.Cuuid] = peerConnection
if err == nil {
// Create a config map
valueMap := make(map[string]interface{})
valueMap["sdp"] = []byte(base64.StdEncoding.EncodeToString([]byte(answer.SDP)))
valueMap["session_id"] = handshake.SessionID
log.Log.Info("webrtc.main.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("webrtc.main.InitializeWebRTCConnection(): while packaging mqtt message: " + err.Error())
}
topic := fmt.Sprintf("%s/%s/answer", deviceKey, handshake.Cuuid)
log.Log.Info("InitializeWebRTCConnection: Send SDP answer to " + topic)
mqttClient.Publish(topic, 2, false, []byte(base64.StdEncoding.EncodeToString([]byte(answer.SDP))))
}
}
} else {
log.Log.Error("Initializwebrtc.main.InitializeWebRTCConnection()eWebRTCConnection: NewPeerConnection failed: " + err.Error())
log.Log.Error("InitializeWebRTCConnection: NewPeerConnection failed: " + err.Error())
}
}
func NewVideoTrack(streams []packets.Stream) *pionWebRTC.TrackLocalStaticSample {
mimeType := pionWebRTC.MimeTypeH264
outboundVideoTrack, err := pionWebRTC.NewTrackLocalStaticSample(pionWebRTC.RTPCodecCapability{MimeType: mimeType}, "video", trackStreamID)
if err != nil {
log.Log.Error("webrtc.main.NewVideoTrack(): error creating video track: " + err.Error())
return nil
}
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 NewAudioTrack(streams []packets.Stream) *pionWebRTC.TrackLocalStaticSample {
func NewAudioTrack(codecs []av.CodecData) *pionWebRTC.TrackLocalStaticSample {
var mimeType string
for _, stream := range streams {
if stream.Name == "OPUS" {
for _, codec := range codecs {
if codec.Type().String() == "OPUS" {
mimeType = pionWebRTC.MimeTypeOpus
} else if stream.Name == "PCM_MULAW" {
} else if codec.Type().String() == "PCM_MULAW" {
mimeType = pionWebRTC.MimeTypePCMU
} else if stream.Name == "PCM_ALAW" {
} else if codec.Type().String() == "PCM_ALAW" {
mimeType = pionWebRTC.MimeTypePCMA
}
}
if mimeType == "" {
log.Log.Error("webrtc.main.NewAudioTrack(): no supported audio codec found")
return nil
}
outboundAudioTrack, err := pionWebRTC.NewTrackLocalStaticSample(pionWebRTC.RTPCodecCapability{MimeType: mimeType}, "audio", trackStreamID)
if err != nil {
log.Log.Error("webrtc.main.NewAudioTrack(): error creating audio track: " + err.Error())
return nil
}
outboundAudioTrack, _ := pionWebRTC.NewTrackLocalStaticSample(pionWebRTC.RTPCodecCapability{MimeType: mimeType}, "audio", "pion124")
return outboundAudioTrack
}
// streamState holds state information for the streaming process
type streamState struct {
lastKeepAlive int64
peerCount int64
start bool
receivedKeyFrame bool
lastAudioSample *pionMedia.Sample
lastVideoSample *pionMedia.Sample
}
// codecSupport tracks which codecs are available in the stream
type codecSupport struct {
hasH264 bool
hasPCM_MULAW bool
hasAAC bool
hasOpus bool
}
// detectCodecs examines the stream to determine which codecs are available
func detectCodecs(rtspClient capture.RTSPClient) codecSupport {
support := codecSupport{}
streams, _ := rtspClient.GetStreams()
for _, stream := range streams {
switch stream.Name {
case "H264":
support.hasH264 = true
case "PCM_MULAW":
support.hasPCM_MULAW = true
case "AAC":
support.hasAAC = true
case "OPUS":
support.hasOpus = true
}
}
return support
}
// hasValidCodecs checks if at least one valid video or audio codec is present
func (cs codecSupport) hasValidCodecs() bool {
hasVideo := cs.hasH264
hasAudio := cs.hasPCM_MULAW || cs.hasAAC || cs.hasOpus
return hasVideo || hasAudio
}
// shouldContinueStreaming determines if streaming should continue based on keepalive and peer count
func shouldContinueStreaming(config models.Config, state *streamState) bool {
if config.Capture.ForwardWebRTC != "true" {
return true
}
now := time.Now().Unix()
hasTimedOut := (now - state.lastKeepAlive) > int64(keepAliveTimeout.Seconds())
hasNoPeers := state.peerCount == 0
return !hasTimedOut && !hasNoPeers
}
// updateStreamState updates keepalive and peer count from communication channels
func updateStreamState(communication *models.Communication, state *streamState) {
select {
case keepAliveStr := <-communication.HandleLiveHDKeepalive:
if val, err := strconv.ParseInt(keepAliveStr, 10, 64); err == nil {
state.lastKeepAlive = val
}
default:
}
select {
case peerCountStr := <-communication.HandleLiveHDPeers:
if val, err := strconv.ParseInt(peerCountStr, 10, 64); err == nil {
state.peerCount = val
}
default:
}
}
// writeFinalSamples writes any remaining buffered samples
func writeFinalSamples(state *streamState, videoTrack, audioTrack *pionWebRTC.TrackLocalStaticSample) {
if state.lastVideoSample != nil && videoTrack != nil {
if err := videoTrack.WriteSample(*state.lastVideoSample); err != nil && err != io.ErrClosedPipe {
log.Log.Error("webrtc.main.writeFinalSamples(): error writing final video sample: " + err.Error())
}
}
if state.lastAudioSample != nil && audioTrack != nil {
if err := audioTrack.WriteSample(*state.lastAudioSample); err != nil && err != io.ErrClosedPipe {
log.Log.Error("webrtc.main.writeFinalSamples(): error writing final audio sample: " + err.Error())
}
}
}
// processVideoPacket processes a video packet and writes samples to the track
func processVideoPacket(pkt packets.Packet, state *streamState, videoTrack *pionWebRTC.TrackLocalStaticSample, config models.Config) {
if videoTrack == nil {
return
}
// Start at the first keyframe
if pkt.IsKeyFrame {
state.start = true
}
if !state.start {
return
}
sample := pionMedia.Sample{Data: pkt.Data, PacketTimestamp: uint32(pkt.Time)}
if config.Capture.ForwardWebRTC == "true" {
// Remote forwarding not yet implemented
log.Log.Debug("webrtc.main.processVideoPacket(): remote forwarding not implemented")
return
}
if state.lastVideoSample != nil {
duration := sample.PacketTimestamp - state.lastVideoSample.PacketTimestamp
state.lastVideoSample.Duration = time.Duration(duration) * time.Millisecond
if err := videoTrack.WriteSample(*state.lastVideoSample); err != nil && err != io.ErrClosedPipe {
log.Log.Error("webrtc.main.processVideoPacket(): error writing video sample: " + err.Error())
}
}
state.lastVideoSample = &sample
}
// processAudioPacket processes an audio packet and writes samples to the track
func processAudioPacket(pkt packets.Packet, state *streamState, audioTrack *pionWebRTC.TrackLocalStaticSample, hasAAC bool) {
if audioTrack == nil {
return
}
if hasAAC {
// AAC transcoding not yet implemented
// TODO: Implement AAC to PCM_MULAW transcoding
return
}
sample := pionMedia.Sample{Data: pkt.Data, PacketTimestamp: uint32(pkt.Time)}
if state.lastAudioSample != nil {
duration := sample.PacketTimestamp - state.lastAudioSample.PacketTimestamp
state.lastAudioSample.Duration = time.Duration(duration) * time.Millisecond
if err := audioTrack.WriteSample(*state.lastAudioSample); err != nil && err != io.ErrClosedPipe {
log.Log.Error("webrtc.main.processAudioPacket(): error writing audio sample: " + err.Error())
}
}
state.lastAudioSample = &sample
}
func WriteToTrack(livestreamCursor *packets.QueueCursor, configuration *models.Configuration, communication *models.Communication, mqttClient mqtt.Client, videoTrack *pionWebRTC.TrackLocalStaticSample, audioTrack *pionWebRTC.TrackLocalStaticSample, rtspClient capture.RTSPClient) {
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
// Check if at least one track is available
if videoTrack == nil && audioTrack == nil {
log.Log.Error("webrtc.main.WriteToTrack(): both video and audio tracks are nil, cannot proceed")
return
}
// Make peerconnection map
peerConnections = make(map[string]*pionWebRTC.PeerConnection)
// Detect available codecs
codecs := detectCodecs(rtspClient)
if !codecs.hasValidCodecs() {
log.Log.Error("webrtc.main.WriteToTrack(): no valid video or audio codec found")
return
}
if config.Capture.TranscodingWebRTC == "true" {
log.Log.Info("webrtc.main.WriteToTrack(): transcoding enabled but not yet implemented")
}
// Initialize streaming state
state := &streamState{
lastKeepAlive: time.Now().Unix(),
peerCount: 0,
}
defer func() {
writeFinalSamples(state, videoTrack, audioTrack)
log.Log.Info("webrtc.main.WriteToTrack(): stopped writing to track")
}()
var pkt packets.Packet
var cursorError error
for cursorError == nil {
pkt, cursorError = livestreamCursor.ReadPacket()
if cursorError != nil {
break
// Set the indexes for the video & audio streams
// Later when we read a packet we need to figure out which track to send it to.
videoIdx := -1
audioIdx := -1
for i, codec := range codecs {
if codec.Type().String() == "H264" && videoIdx < 0 {
videoIdx = i
} else if (codec.Type().String() == "OPUS" || codec.Type().String() == "PCM_MULAW" || codec.Type().String() == "PCM_ALAW") && audioIdx < 0 {
audioIdx = i
}
}
// Update state from communication channels
updateStreamState(communication, state)
if videoIdx == -1 {
log.Log.Error("WriteToTrack: no video codec found.")
} else {
annexbNALUStartCode := func() []byte { return []byte{0x00, 0x00, 0x00, 0x01} }
// Check if we should continue streaming
if !shouldContinueStreaming(config, state) {
state.start = false
state.receivedKeyFrame = false
continue
}
// Skip empty packets
if len(pkt.Data) == 0 || pkt.Data == nil {
state.receivedKeyFrame = false
continue
}
// Wait for first keyframe before processing
if !state.receivedKeyFrame {
if pkt.IsKeyFrame {
state.receivedKeyFrame = true
if config.Capture.TranscodingWebRTC == "true" {
if videoIdx > -1 {
log.Log.Info("WriteToTrack: successfully using a transcoder.")
} else {
}
} else {
log.Log.Info("WriteToTrack: not using a transcoder.")
}
var cursorError error
var pkt av.Packet
var previousTime time.Duration
start := false
receivedKeyFrame := false
codecData := codecs[videoIdx]
lastKeepAlive := "0"
peerCount := "0"
for cursorError == nil {
pkt, cursorError = livestreamCursor.ReadPacket()
bufferDuration := pkt.Time - previousTime
previousTime = pkt.Time
if config.Capture.ForwardWebRTC != "true" && peerConnectionCount == 0 {
start = false
receivedKeyFrame = false
continue
}
}
// Process video or audio packets
if pkt.IsVideo {
processVideoPacket(pkt, state, videoTrack, config)
} else if pkt.IsAudio {
processAudioPacket(pkt, state, audioTrack, codecs.hasAAC)
select {
case lastKeepAlive = <-communication.HandleLiveHDKeepalive:
default:
}
select {
case peerCount = <-communication.HandleLiveHDPeers:
default:
}
now := time.Now().Unix()
lastKeepAliveN, _ := strconv.ParseInt(lastKeepAlive, 10, 64)
hasTimedOut := (now - lastKeepAliveN) > 15 // if longer then no response in 15 sec.
hasNoPeers := peerCount == "0"
if config.Capture.ForwardWebRTC == "true" && (hasTimedOut || hasNoPeers) {
start = false
receivedKeyFrame = false
continue
}
if len(pkt.Data) == 0 || pkt.Data == nil {
receivedKeyFrame = false
continue
}
if !receivedKeyFrame {
if pkt.IsKeyFrame {
receivedKeyFrame = true
} else {
continue
}
}
if config.Capture.TranscodingWebRTC == "true" {
/*decoderMutex.Lock()
decoder.SetFramerate(30, 1)
frame, err := decoder.Decode(pkt.Data)
decoderMutex.Unlock()
if err == nil && frame != nil && frame.Width() > 0 && frame.Height() > 0 {
var _outpkts []av.Packet
transcodingResolution := config.Capture.TranscodingResolution
newWidth := frame.Width() * int(transcodingResolution) / 100
newHeight := frame.Height() * int(transcodingResolution) / 100
encoder.SetResolution(newWidth, newHeight)
if _outpkts, err = encoder.Encode(frame); err != nil {
}
if len(_outpkts) > 0 {
pkt = _outpkts[0]
codecData, _ = encoder.CodecData()
}
}*/
}
switch int(pkt.Idx) {
case videoIdx:
// For every key-frame pre-pend the SPS and PPS
pkt.Data = pkt.Data[4:]
if pkt.IsKeyFrame {
start = true
pkt.Data = append(annexbNALUStartCode(), pkt.Data...)
pkt.Data = append(codecData.(h264parser.CodecData).PPS(), pkt.Data...)
pkt.Data = append(annexbNALUStartCode(), pkt.Data...)
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)
if err == nil {
// Write packets
topic := fmt.Sprintf("kerberos/webrtc/packets/%s", config.Key)
mqttClient.Publish(topic, 0, false, samplePacket)
} else {
log.Log.Info("WriteToTrack: Error marshalling frame, " + err.Error())
}
} else {
if err := videoTrack.WriteSample(sample); err != nil && err != io.ErrClosedPipe {
log.Log.Error("WriteToTrack: something went wrong while writing sample: " + err.Error())
}
}
}
case audioIdx:
// 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())
}
}
}
}
for _, p := range peerConnections {
if p != nil {
p.Close()
}
}
peerConnectionCount = 0
log.Log.Info("WriteToTrack: stop writing to track.")
}

View File

@@ -1,4 +0,0 @@
export GOSUMDB=off
rm -rf go.*
go mod init github.com/kerberos-io/agent/machinery
go mod tidy

6
snap/hooks/configure vendored Normal file
View 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
View 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

View File

@@ -25,7 +25,6 @@
"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": [

View File

@@ -1,6 +1,7 @@
{
"name": "agent-ui",
"version": "0.1.0",
"private": false,
"dependencies": {
"@giantmachines/redux-websocket": "^1.5.1",
"@kerberos-io/ui": "^1.76.0",

View File

@@ -80,29 +80,19 @@
"description_general": "Allgemeine Einstellungen für den Kerberos Agent",
"key": "Schlüssel",
"camera_name": "Kamera Name",
"camera_friendly_name": "Kamera Anzeigename",
"timezone": "Zeitzone",
"select_timezone": "Zeitzone auswählen",
"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",
"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"
"description_offline_mode": "Ausgehende Verbindungen deaktivieren"
},
"camera": {
"camera": "Kamera",
"description_camera": "Diese Einstellungen sind notwendig um eine Verbindung mit der Kamera herzustellen",
"only_h264": "Aktuell werden nur H264/H265 RTSP kompatible Kameras unterstützt",
"only_h264": "Aktuell werden nur H264 RTSP kompatible Kameras unterstützt",
"rtsp_url": "RTSP URL",
"rtsp_h264": "H264/H265 RTSP URL der Kamera",
"rtsp_h264": "H264 RTSP URL der Kamera",
"sub_rtsp_url": "RTSP url für die Live Übertragung.",
"sub_rtsp_h264": "Ergänzende URL der Kamera mit geringerer Auflösung für die Live Übertragung.",
"onvif": "ONVIF",
@@ -146,8 +136,6 @@
"turn_server": "TURN Server",
"turn_username": "Benutzername",
"turn_password": "Passwort",
"force_turn": "Erzwinge TURN",
"force_turn_description": "Erzwinge die Verwendung von TURN",
"stun_turn_forward": "Weiterleiten und transkodieren",
"stun_turn_description_forward": "Optiemierungen und Verbesserungen der TURN/STUN Kommunikation.",
"stun_turn_webrtc": "Weiterleiten an WebRTC Schnittstelle",
@@ -188,8 +176,6 @@
"description_persistence": "Die möglichkeit zur Speicherung der Daten an einem Zentralen Ort ist der Beginn einer effektiven Videoüberwachung. Es kann zwischen",
"description2_persistence": ", oder einem Drittanbieter gewählt werden.",
"select_persistence": "Speicherort auswählen",
"kerberoshub_encryption": "Encryption",
"kerberoshub_encryption_description": "All traffic from/to Kerberos Hub will encrypted using AES-256.",
"kerberoshub_proxyurl": "Kerberos Hub Proxy URL",
"kerberoshub_description_proxyurl": "Der Proxy Endpunkt zum hochladen der Aufnahmen.",
"kerberoshub_apiurl": "Kerberos Hub API URL",

View File

@@ -9,7 +9,7 @@
},
"navigation": {
"profile": "Profile",
"admin": "Admin",
"admin": "admin",
"management": "Management",
"dashboard": "Dashboard",
"recordings": "Recordings",
@@ -23,7 +23,7 @@
},
"dashboard": {
"title": "Dashboard",
"heading": "Overview of your video surveillance",
"heading": "Overview of your video surveilance",
"number_of_days": "Number of days",
"total_recordings": "Total recordings",
"connected": "Connected",
@@ -32,11 +32,11 @@
"latest_events": "Latest events",
"configure_connection": "Configure connection",
"no_events": "No events",
"no_events_description": "No recordings were found, make sure your Agent is properly configured.",
"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.",
"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.",
"time": "Time",
"description": "Description",
"name": "Name"
@@ -59,51 +59,41 @@
"persistence": "Persistence"
},
"info": {
"kerberos_hub_demo": "Have a look at our Hub demo environment, to see Hub in action!",
"configuration_updated_success": "Your configuration has been updated successfully.",
"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 Hub settings.",
"verify_hub_success": "Hub settings are successfully verified.",
"verify_hub_error": "Something went wrong while verifying Hub.",
"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_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."
"verify_onvif_error": "Something went wrong while verifying the ONVIF settings"
},
"overview": {
"general": "General",
"description_general": "General settings for your Agent",
"description_general": "General settings for your Kerberos Agent",
"key": "Key",
"camera_name": "Camera name",
"camera_friendly_name": "Friendly 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 Agent",
"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",
"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"
"description_offline_mode": "Disable all outgoing traffic"
},
"camera": {
"camera": "Camera",
"description_camera": "Camera settings are required to make a connection to your camera of choice.",
"only_h264": "Currently only H264/H265 RTSP streams are supported.",
"rtsp_url": "RTSP URL",
"rtsp_h264": "A H264/H265 RTSP connection to your camera.",
"sub_rtsp_url": "Sub RTSP URL (used for livestreaming)",
"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.",
"onvif": "ONVIF",
"description_onvif": "Credentials to communicate with ONVIF capabilities. These are used for PTZ or other capabilities provided by the camera.",
@@ -115,28 +105,28 @@
},
"recording": {
"recording": "Recording",
"description_recording": "Specify how you would like to make recordings. Having a continuous 24/7 setup or a motion-based 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_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)",
"pre_recording": "pre recording (key frames buffered)",
"description_pre_recording": "Seconds before an event occurred.",
"post_recording": "Post recording (seconds)",
"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.",
"description_threshold": "The number of pixels changed to record",
"autoclean": "Auto clean",
"description_autoclean": "Specify if the Agent can clean up recordings when a specific storage capacity (MB) is reached. This will remove the oldest recordings when the capacity is reached.",
"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 MBs of recordings stored.",
"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.",
"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_duration": "fragment duration",
"fragmentedrecordings_description_duration": "Duration of a single fragment."
},
"streaming": {
@@ -146,26 +136,19 @@
"turn_server": "TURN server",
"turn_username": "Username",
"turn_password": "Password",
"force_turn": "Force TURN",
"force_turn_description": "Force TURN usage, even when STUN is available.",
"stun_turn_forward": "Forwarding and transcoding",
"stun_turn_description_forward": "Optimizations and enhancements for TURN/STUN communication.",
"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_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 % of original resolution)",
"stun_turn_downscale": "Downscale resolution (in % or original resolution)",
"mqtt": "MQTT",
"description_mqtt": "An MQTT broker is used to communicate from",
"description2_mqtt": "to the Agent, to achieve for example livestreaming or ONVIF (PTZ) capabilities.",
"mqtt_brokeruri": "Broker URI",
"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",
"realtimeprocessing": "Realtime Processing",
"description_realtimeprocessing": "By enabling realtime processing, you will receive realtime video keyframes through the MQTT connection specified above.",
"realtimeprocessing_topic": "Topic to publish",
"realtimeprocessing_enabled": "Enable realtime processing",
"description_realtimeprocessing_enabled": "Send realtime video keyframes through MQTT."
"mqtt_password": "Password"
},
"conditions": {
"timeofinterest": "Time Of Interest",
@@ -180,61 +163,53 @@
"friday": "Friday",
"saturday": "Saturday",
"externalcondition": "External Condition",
"description_externalcondition": "Depending on an external web service, recording can be enabled or disabled.",
"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."
},
"persistence": {
"kerberoshub": "Hub",
"description_kerberoshub": "Agents can send heartbeats to a central",
"description2_kerberoshub": "installation. Heartbeats and other relevant information are synced to Hub to show realtime information about your video landscape.",
"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",
"secondary_persistence": "Secondary Persistence",
"description_secondary_persistence": "Recordings will be sent to secondary persistence if the primary persistence is unavailable or fails. This can be useful for failover purposes.",
"saasoffering": "Hub (SaaS offering)",
"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_encryption": "Encryption",
"kerberoshub_encryption_description": "All traffic from/to Hub will be encrypted using AES-256.",
"kerberoshub_proxyurl": "Hub Proxy URL",
"kerberoshub_proxyurl": "Kerberos Hub Proxy URL",
"kerberoshub_description_proxyurl": "The Proxy endpoint for uploading your recordings.",
"kerberoshub_apiurl": "Hub API URL",
"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 Hub account.",
"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 Hub account.",
"kerberoshub_description_privatekey": "The private key granted to your Kerberos Hub account.",
"kerberoshub_site": "Site",
"kerberoshub_description_site": "The site ID the Agents belong to in Hub.",
"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.",
"kerberoshub_bucket": "Bucket",
"kerberoshub_description_bucket": "The bucket we are storing our recordings in.",
"kerberoshub_username": "Username/Directory (should match Hub username)",
"kerberoshub_description_username": "The username of your Hub account.",
"kerberosvault_apiurl": "Vault API URL",
"kerberosvault_description_apiurl": "The Vault API",
"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 sent.",
"kerberosvault_directory": "Directory (should match Hub username)",
"kerberosvault_description_directory": "Subdirectory the recordings will be stored in your provider.",
"kerberosvault_description_provider": "The provider to which your recordings will be send.",
"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 Vault account.",
"kerberosvault_description_accesskey": "The access key of your Kerberos Vault account.",
"kerberosvault_secretkey": "Secret key",
"kerberosvault_description_secretkey": "The secret key of your Vault account.",
"kerberosvault_maxretries": "Max retries",
"kerberosvault_description_maxretries": "The maximum number of retries to upload a recording.",
"kerberosvault_timeout": "Timeout",
"kerberosvault_description_timeout": "If a timeout occurs, recordings will be sent directly to the secondary Vault.",
"kerberosvault_description_secretkey": "The secret key of your Kerberos Vault account.",
"dropbox_directory": "Directory",
"dropbox_description_directory": "The subdirectory where the recordings will be stored in your Dropbox account.",
"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 Agent.",
"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": "Enable delete on upload"
"remove_after_upload_enabled": "Enabled delete on upload"
}
}
}
}

View File

@@ -80,29 +80,19 @@
"description_general": "General settings for your Kerberos Agent",
"key": "Key",
"camera_name": "Camera name",
"camera_friendly_name": "Camera friendly 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",
"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"
"description_offline_mode": "Disable all outgoing traffic"
},
"camera": {
"camera": "Camera",
"description_camera": "Camera settings are required to make a connection to your camera of choice.",
"only_h264": "Currently only H264/H265 RTSP streams are supported.",
"only_h264": "Currently only H264 RTSP streams are supported.",
"rtsp_url": "RTSP url",
"rtsp_h264": "A H264/H265 RTSP connection to your camera.",
"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.",
"onvif": "ONVIF",
@@ -146,8 +136,6 @@
"turn_server": "TURN server",
"turn_username": "Username",
"turn_password": "Password",
"force_turn": "Force TURN",
"force_turn_description": "Force TURN usage, even when STUN is available.",
"stun_turn_forward": "Forwarding and transcoding",
"stun_turn_description_forward": "Optimisations and enhancements for TURN/STUN communication.",
"stun_turn_webrtc": "Forwarding to WebRTC broker",
@@ -188,8 +176,6 @@
"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_encryption": "Encryption",
"kerberoshub_encryption_description": "All traffic from/to Kerberos Hub will encrypted using AES-256.",
"kerberoshub_proxyurl": "Kerberos Hub Proxy URL",
"kerberoshub_description_proxyurl": "The Proxy endpoint for uploading your recordings.",
"kerberoshub_apiurl": "Kerberos Hub API URL",

View File

@@ -79,29 +79,19 @@
"description_general": "Paramètres généraux pour votre Agent Kerberos",
"key": "Clé",
"camera_name": "Nom de la caméra",
"camera_friendly_name": "Nom convivial de la caméra",
"timezone": "Fuseau horaire",
"select_timezone": "Sélectionner un fuseau horaire",
"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",
"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"
"description_offline_mode": "Désactiver tout le trafic sortant"
},
"camera": {
"camera": "Caméra",
"description_camera": "Les paramètres de la caméra sont requis pour établir une connexion à la caméra de votre choix.",
"only_h264": "Actuellement, seuls les flux RTSP H264/H265 sont pris en charge.",
"only_h264": "Actuellement, seuls les flux RTSP H264 sont pris en charge.",
"rtsp_url": "URL RTSP",
"rtsp_h264": "Une connexion RTSP H264/H265 à votre caméra.",
"rtsp_h264": "Une connexion RTSP H264 à votre caméra.",
"sub_rtsp_url": "URL RTSP secondaire (utilisé pour le direct)",
"sub_rtsp_h264": "Une connexion RTSP secondaire vers le flux basse résolution de votre caméra.",
"onvif": "ONVIF",
@@ -145,8 +135,6 @@
"turn_server": "Serveur TURN",
"turn_username": "Nom d'utilisateur",
"turn_password": "Mot de passe",
"force_turn": "Forcer l'utilisation de TURN",
"force_turn_description": "Forcer l'utilisation de TURN au lieu de STUN",
"stun_turn_forward": "Redirection et transcodage",
"stun_turn_description_forward": "Optimisations et améliorations pour la communication TURN/STUN.",
"stun_turn_webrtc": "Redirection pour l'agent WebRTC",
@@ -187,8 +175,6 @@
"description_persistence": "Avoir la possibilité de stocker vos enregistrements est le commencement de tout. Vous pouvez choisir entre notre",
"description2_persistence": " ou auprès d'un fournisseur tiers",
"select_persistence": "Sélectionner une persistance",
"kerberoshub_encryption": "Encryption",
"kerberoshub_encryption_description": "All traffic from/to Kerberos Hub will encrypted using AES-256.",
"kerberoshub_proxyurl": "URL du proxy Kerberos Hub",
"kerberoshub_description_proxyurl": "Le point de terminaison du proxy pour téléverser vos enregistrements.",
"kerberoshub_apiurl": "URL de l'API Kerberos Hub",

View File

@@ -1,229 +0,0 @@
{
"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": "कैमरे का नाम",
"camera_friendly_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/H265 RTSP स्ट्रीम समर्थित हैं।",
"rtsp_url": "RTSP URL",
"rtsp_h264": "आपके कैमरे से H264/H265 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": "पासवर्ड",
"force_turn": "Force TURN",
"force_turn_description": "Force TURN usage, even when STUN is available.",
"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_encryption": "Encryption",
"kerberoshub_encryption_description": "All traffic from/to Kerberos Hub will encrypted using AES-256.",
"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": "अपलोड पर डिलीट सक्षम"
}
}
}

View File

@@ -1,229 +0,0 @@
{
"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",
"camera_friendly_name": "Nome amichevole 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/H265.",
"rtsp_url": "Url RTSP",
"rtsp_h264": "Connessione RTSP H264/H265 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",
"force_turn": "Forza TURN",
"force_turn_description": "Forza l'uso di TURN per lo streaming in diretta.",
"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_encryption": "Encryption",
"kerberoshub_encryption_description": "All traffic from/to Kerberos Hub will encrypted using AES-256.",
"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"
}
}
}

Some files were not shown because too many files have changed in this diff Show More