mirror of
https://github.com/kerberos-io/agent.git
synced 2026-09-08 17:48:35 +00:00
Compare commits
70 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a67c5a1f39 | ||
|
|
b7a87f95e5 | ||
|
|
0aa0b8ad8f | ||
|
|
2bff868de6 | ||
|
|
8b59828126 | ||
|
|
f55e25db07 | ||
|
|
243c969666 | ||
|
|
ec7f2e0303 | ||
|
|
a4a032d994 | ||
|
|
0a84744e49 | ||
|
|
1425430376 | ||
|
|
ca8d88ffce | ||
|
|
af3f8bb639 | ||
|
|
1f9772d472 | ||
|
|
94cf361b55 | ||
|
|
6acdf258e7 | ||
|
|
cc0a810ab3 | ||
|
|
c19bfbe552 | ||
|
|
39aaf5ad6c | ||
|
|
6fba2ff05d | ||
|
|
d78e682759 | ||
|
|
ed582a9d57 | ||
|
|
aa925d5c9b | ||
|
|
08d191e542 | ||
|
|
cc075d7237 | ||
|
|
1974bddfbe | ||
|
|
12cb88e1c1 | ||
|
|
c054526998 | ||
|
|
ffa97598b8 | ||
|
|
f5afbf3a63 | ||
|
|
e666695c96 | ||
|
|
55816e4b7b | ||
|
|
016fb51951 | ||
|
|
550a444650 | ||
|
|
4332e43f27 | ||
|
|
fdc3bfb4a4 | ||
|
|
c17d6b7117 | ||
|
|
5d7a8103c0 | ||
|
|
5d7cb98b8f | ||
|
|
f6046c6a6c | ||
|
|
f59f9d71a9 | ||
|
|
ff72f9647d | ||
|
|
fa604b16cf | ||
|
|
0342869733 | ||
|
|
8685ce31a2 | ||
|
|
0e259f0e7a | ||
|
|
5823abed95 | ||
|
|
86acff58f0 | ||
|
|
d3fc5d4c29 | ||
|
|
50bb40938c | ||
|
|
1977d98ad9 | ||
|
|
448d4a946d | ||
|
|
61ac314bb7 | ||
|
|
c1b144ca28 | ||
|
|
e16987bf9d | ||
|
|
9991597984 | ||
|
|
2c0314cea4 | ||
|
|
0584e52b98 | ||
|
|
1fc90eaee2 | ||
|
|
aef3eacbc9 | ||
|
|
2843568473 | ||
|
|
53ffc8cae0 | ||
|
|
86e654fe19 | ||
|
|
46d57f7664 | ||
|
|
963d8672eb | ||
|
|
9b7a62816a | ||
|
|
237134fe0e | ||
|
|
c8730e8f26 | ||
|
|
acbbe8b444 | ||
|
|
f690016aa5 |
133
.github/workflows/docker.yml
vendored
133
.github/workflows/docker.yml
vendored
@@ -14,7 +14,7 @@ env:
|
||||
|
||||
jobs:
|
||||
build-amd64:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: write
|
||||
strategy:
|
||||
@@ -32,53 +32,36 @@ jobs:
|
||||
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}}-${{github.event.inputs.tag || github.ref_name}} --push .
|
||||
- name: Create new and append to manifest
|
||||
run: docker buildx imagetools create -t $REPO:${{ github.event.inputs.tag || github.ref_name }} $REPO-arch:arch-${{matrix.architecture}}-${{github.event.inputs.tag || github.ref_name}}
|
||||
- name: Create new and append to manifest latest
|
||||
run: docker buildx imagetools create -t $REPO:latest $REPO-arch:arch-${{matrix.architecture}}-${{github.event.inputs.tag || github.ref_name}}
|
||||
if: github.event.inputs.tag == 'test'
|
||||
- name: Run Buildx with output
|
||||
run: docker buildx build --platform linux/$(echo ${{matrix.architecture}} | tr - /) -t $REPO-arch:arch-$(echo ${{matrix.architecture}} | tr / -)-${{github.event.inputs.tag || github.ref_name}} --output type=tar,dest=output-${{matrix.architecture}}.tar .
|
||||
- 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: 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
|
||||
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: Create new manifest
|
||||
run: docker manifest create $REPO:${{ github.event.inputs.tag || github.ref_name }} $REPO-arch:arch-${{matrix.architecture}}-${{github.event.inputs.tag || github.ref_name}}
|
||||
- name: Create latest manifest
|
||||
run: docker manifest create $REPO:latest $REPO-arch:arch-${{matrix.architecture}}-${{github.event.inputs.tag || github.ref_name}}
|
||||
if: github.event.inputs.tag == 'test'
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
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-${{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
|
||||
name: agent-${{matrix.architecture}}.tar
|
||||
path: agent-${{matrix.architecture}}.tar
|
||||
|
||||
build-arm64:
|
||||
runs-on: ubuntu-24.04-arm
|
||||
permissions:
|
||||
contents: write
|
||||
needs: build-amd64
|
||||
strategy:
|
||||
matrix:
|
||||
architecture: [arm64, arm-v7, arm-v6]
|
||||
#architecture: [arm64, arm-v7]
|
||||
architecture: [arm64]
|
||||
steps:
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
@@ -91,23 +74,55 @@ jobs:
|
||||
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}}-${{github.event.inputs.tag || github.ref_name}} --push .
|
||||
- name: Create new and append to manifest
|
||||
run: docker buildx imagetools create --append -t $REPO:${{ github.event.inputs.tag || github.ref_name }} $REPO-arch:arch-${{matrix.architecture}}-${{github.event.inputs.tag || github.ref_name}}
|
||||
- name: Create new and append to manifest latest
|
||||
run: docker buildx imagetools create --append -t $REPO:latest $REPO-arch:arch-${{matrix.architecture}}-${{github.event.inputs.tag || github.ref_name}}
|
||||
if: github.event.inputs.tag == 'test'
|
||||
- name: Run Buildx with output
|
||||
run: docker buildx build --platform linux/$(echo ${{matrix.architecture}} | tr - /) -t $REPO-arch:arch-$(echo ${{matrix.architecture}} | tr / -)-${{github.event.inputs.tag || github.ref_name}} --output type=tar,dest=output-${{matrix.architecture}}.tar .
|
||||
- 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: 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
|
||||
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:
|
||||
@@ -117,4 +132,4 @@ jobs:
|
||||
tag: ${{ github.event.inputs.tag || github.ref_name }}
|
||||
generateReleaseNotes: false
|
||||
omitBodyDuringUpdate: true
|
||||
artifacts: "agent-${{matrix.architecture}}.tar"
|
||||
artifacts: "agent-*.tar/agent-*.tar"
|
||||
|
||||
75
.github/workflows/pr-build.yml
vendored
Normal file
75
.github/workflows/pr-build.yml
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
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
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,6 +1,8 @@
|
||||
ui/node_modules
|
||||
ui/build
|
||||
ui/public/assets/env.js
|
||||
.DS_Store
|
||||
__debug*
|
||||
.idea
|
||||
machinery/www
|
||||
yarn.lock
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
ARG BASE_IMAGE_VERSION=70ec57e
|
||||
ARG BASE_IMAGE_VERSION=amd64-ddbe40e
|
||||
FROM kerberos/base:${BASE_IMAGE_VERSION} AS build-machinery
|
||||
LABEL AUTHOR=Kerberos.io
|
||||
LABEL AUTHOR=uug.ai
|
||||
|
||||
ENV GOROOT=/usr/local/go
|
||||
ENV GOPATH=/go
|
||||
|
||||
138
Dockerfile.arm64
Normal file
138
Dockerfile.arm64
Normal file
@@ -0,0 +1,138 @@
|
||||
|
||||
ARG BASE_IMAGE_VERSION=arm64-ddbe40e
|
||||
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 && \
|
||||
go build -tags timetzdata,netgo,osusergo --ldflags '-s -w -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"]
|
||||
@@ -208,6 +208,8 @@ Next to attaching the configuration file, it is also possible to override the co
|
||||
| `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. | "" |
|
||||
|
||||
@@ -14,7 +14,9 @@
|
||||
"ipcamera": {
|
||||
"rtsp": "",
|
||||
"sub_rtsp": "",
|
||||
"fps": ""
|
||||
"fps": "",
|
||||
"base_width": 640,
|
||||
"base_height": 0
|
||||
},
|
||||
"usbcamera": {
|
||||
"device": ""
|
||||
@@ -26,6 +28,7 @@
|
||||
"recording": "true",
|
||||
"snapshots": "true",
|
||||
"liveview": "true",
|
||||
"liveview_chunking": "false",
|
||||
"motion": "true",
|
||||
"postrecording": 20,
|
||||
"prerecording": 10,
|
||||
@@ -119,4 +122,4 @@
|
||||
"signing": {},
|
||||
"realtimeprocessing": "false",
|
||||
"realtimeprocessing_topic": ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ require (
|
||||
github.com/kerberos-io/joy4 v1.0.64
|
||||
github.com/kerberos-io/onvif v1.0.0
|
||||
github.com/minio/minio-go/v6 v6.0.57
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||
github.com/pion/interceptor v0.1.40
|
||||
github.com/pion/rtp v1.8.19
|
||||
@@ -41,6 +42,7 @@ require (
|
||||
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
|
||||
)
|
||||
|
||||
@@ -118,7 +120,6 @@ require (
|
||||
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/otel/trace 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
|
||||
|
||||
@@ -847,6 +847,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
|
||||
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY=
|
||||
github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc=
|
||||
|
||||
@@ -36,7 +36,7 @@ func startTracing(agentKey string, otelEndpoint string) (*trace.TracerProvider,
|
||||
exporter, err := otlptrace.New(
|
||||
context.Background(),
|
||||
otlptracehttp.NewClient(
|
||||
otlptracehttp.WithEndpoint("74.241.203.114:4318"),
|
||||
otlptracehttp.WithEndpoint(otelEndpoint),
|
||||
otlptracehttp.WithHeaders(headers),
|
||||
otlptracehttp.WithInsecure(),
|
||||
),
|
||||
|
||||
@@ -84,6 +84,21 @@ type Golibrtsp struct {
|
||||
AudioMPEG4Decoder *rtpmpeg4audio.Decoder
|
||||
|
||||
Streams []packets.Stream
|
||||
|
||||
// FPS calculation fields
|
||||
lastFrameTime time.Time
|
||||
frameTimeBuffer []time.Duration
|
||||
frameBufferSize int
|
||||
frameBufferIndex int
|
||||
fpsMutex sync.Mutex
|
||||
|
||||
// I-frame interval tracking fields
|
||||
packetsSinceLastKeyframe int
|
||||
lastKeyframePacketCount int
|
||||
keyframeIntervals []int
|
||||
keyframeBufferSize int
|
||||
keyframeBufferIndex int
|
||||
keyframeMutex sync.Mutex
|
||||
}
|
||||
|
||||
// Init function
|
||||
@@ -137,8 +152,9 @@ func (g *Golibrtsp) Connect(ctx context.Context, ctxOtel context.Context) (err e
|
||||
return
|
||||
}
|
||||
|
||||
// Iniatlise the mutex.
|
||||
// Initialize the mutex and FPS calculation.
|
||||
g.VideoDecoderMutex = &sync.Mutex{}
|
||||
g.initFPSCalculation()
|
||||
|
||||
// find the H264 media and format
|
||||
var formaH264 *format.H264
|
||||
@@ -462,6 +478,7 @@ func (g *Golibrtsp) Start(ctx context.Context, streamType string, queue *packets
|
||||
Time: pts2,
|
||||
TimeLegacy: pts,
|
||||
CompositionTime: pts2,
|
||||
CurrentTime: time.Now().UnixMilli(),
|
||||
Idx: g.AudioG711Index,
|
||||
IsVideo: false,
|
||||
IsAudio: true,
|
||||
@@ -503,6 +520,7 @@ func (g *Golibrtsp) Start(ctx context.Context, streamType string, queue *packets
|
||||
Time: pts2,
|
||||
TimeLegacy: pts,
|
||||
CompositionTime: pts2,
|
||||
CurrentTime: time.Now().UnixMilli(),
|
||||
Idx: g.AudioG711Index,
|
||||
IsVideo: false,
|
||||
IsAudio: true,
|
||||
@@ -574,6 +592,9 @@ func (g *Golibrtsp) Start(ctx context.Context, streamType string, queue *packets
|
||||
var sps h264.SPS
|
||||
errSPS := sps.Unmarshal(nalu)
|
||||
if errSPS == nil {
|
||||
// Debug SPS information
|
||||
g.debugSPSInfo(&sps, streamType)
|
||||
|
||||
// Get width
|
||||
g.Streams[g.VideoH264Index].Width = sps.Width()
|
||||
if streamType == "main" {
|
||||
@@ -588,12 +609,14 @@ func (g *Golibrtsp) Start(ctx context.Context, streamType string, queue *packets
|
||||
} else if streamType == "sub" {
|
||||
configuration.Config.Capture.IPCamera.SubHeight = sps.Height()
|
||||
}
|
||||
// Get FPS
|
||||
g.Streams[g.VideoH264Index].FPS = sps.FPS()
|
||||
// Get FPS using enhanced method
|
||||
fps := g.getEnhancedFPS(&sps, g.VideoH264Index)
|
||||
g.Streams[g.VideoH264Index].FPS = fps
|
||||
log.Log.Debug(fmt.Sprintf("capture.golibrtsp.Start(%s): Final FPS=%.2f", streamType, fps))
|
||||
g.VideoH264Forma.SPS = nalu
|
||||
|
||||
}
|
||||
case h264.NALUTypePPS:
|
||||
// Read out pps
|
||||
g.VideoH264Forma.PPS = nalu
|
||||
}
|
||||
filteredAU = append(filteredAU, nalu)
|
||||
@@ -610,19 +633,13 @@ func (g *Golibrtsp) Start(ctx context.Context, streamType string, queue *packets
|
||||
return
|
||||
}
|
||||
|
||||
// Extract DTS from RTP packets
|
||||
//dts2, err := dtsExtractor.Extract(filteredAU, pts2)
|
||||
//if err != nil {
|
||||
// log.Log.Error("capture.golibrtsp.Start(): " + err.Error())
|
||||
// return
|
||||
//}
|
||||
|
||||
pkt := packets.Packet{
|
||||
IsKeyFrame: idrPresent,
|
||||
Packet: rtppkt,
|
||||
Data: enc,
|
||||
Time: pts2,
|
||||
TimeLegacy: pts,
|
||||
CurrentTime: time.Now().UnixMilli(),
|
||||
CompositionTime: pts2,
|
||||
Idx: g.VideoH264Index,
|
||||
IsVideo: true,
|
||||
@@ -630,6 +647,21 @@ func (g *Golibrtsp) Start(ctx context.Context, streamType string, queue *packets
|
||||
Codec: "H264",
|
||||
}
|
||||
|
||||
// Track keyframe intervals
|
||||
keyframeInterval := g.trackKeyframeInterval(idrPresent)
|
||||
if idrPresent && keyframeInterval > 0 {
|
||||
avgInterval := g.getAverageKeyframeInterval()
|
||||
gopDuration := float64(keyframeInterval) / g.Streams[g.VideoH265Index].FPS
|
||||
gopSize := int(avgInterval) // Store GOP size in a separate variable
|
||||
g.Streams[g.VideoH264Index].GopSize = gopSize
|
||||
log.Log.Debug(fmt.Sprintf("capture.golibrtsp.Start(%s): Keyframe interval=%d packets, Avg=%.1f, GOP=%.1fs, GOPSize=%d",
|
||||
streamType, keyframeInterval, avgInterval, gopDuration, gopSize))
|
||||
preRecording := configuration.Config.Capture.PreRecording
|
||||
if preRecording > 0 && int(gopDuration) > 0 {
|
||||
queue.SetMaxGopCount(int(preRecording)/int(gopDuration) + 1)
|
||||
}
|
||||
}
|
||||
|
||||
pkt.Data = pkt.Data[4:]
|
||||
if pkt.IsKeyFrame {
|
||||
annexbNALUStartCode := func() []byte { return []byte{0x00, 0x00, 0x00, 0x01} }
|
||||
@@ -752,6 +784,7 @@ func (g *Golibrtsp) Start(ctx context.Context, streamType string, queue *packets
|
||||
Data: enc,
|
||||
Time: pts2,
|
||||
TimeLegacy: pts,
|
||||
CurrentTime: time.Now().UnixMilli(),
|
||||
CompositionTime: pts2,
|
||||
Idx: g.VideoH265Index,
|
||||
IsVideo: true,
|
||||
@@ -759,6 +792,21 @@ func (g *Golibrtsp) Start(ctx context.Context, streamType string, queue *packets
|
||||
Codec: "H265",
|
||||
}
|
||||
|
||||
// Track keyframe intervals for H265
|
||||
keyframeInterval := g.trackKeyframeInterval(isRandomAccess)
|
||||
if isRandomAccess && keyframeInterval > 0 {
|
||||
avgInterval := g.getAverageKeyframeInterval()
|
||||
gopDuration := float64(keyframeInterval) / g.Streams[g.VideoH265Index].FPS
|
||||
gopSize := int(avgInterval) // Store GOP size in a separate variable
|
||||
g.Streams[g.VideoH265Index].GopSize = gopSize
|
||||
log.Log.Debug(fmt.Sprintf("capture.golibrtsp.Start(%s): Keyframe interval=%d packets, Avg=%.1f, GOP=%.1fs, GOPSize=%d",
|
||||
streamType, keyframeInterval, avgInterval, gopDuration, gopSize))
|
||||
preRecording := configuration.Config.Capture.PreRecording
|
||||
if preRecording > 0 && int(gopDuration) > 0 {
|
||||
queue.SetMaxGopCount(int(preRecording)/int(gopDuration) + 1)
|
||||
}
|
||||
}
|
||||
|
||||
queue.WritePacket(pkt)
|
||||
|
||||
// This will check if we need to stop the thread,
|
||||
@@ -1128,3 +1176,185 @@ func WriteMPEG4Audio(forma *format.MPEG4Audio, aus [][]byte) ([]byte, error) {
|
||||
}
|
||||
return enc, nil
|
||||
}
|
||||
|
||||
// Initialize FPS calculation buffers
|
||||
func (g *Golibrtsp) initFPSCalculation() {
|
||||
g.frameBufferSize = 30 // Store last 30 frame intervals
|
||||
g.frameTimeBuffer = make([]time.Duration, g.frameBufferSize)
|
||||
g.frameBufferIndex = 0
|
||||
g.lastFrameTime = time.Time{}
|
||||
|
||||
// Initialize I-frame interval tracking
|
||||
g.keyframeBufferSize = 10 // Store last 10 keyframe intervals
|
||||
g.keyframeIntervals = make([]int, g.keyframeBufferSize)
|
||||
g.keyframeBufferIndex = 0
|
||||
g.packetsSinceLastKeyframe = 0
|
||||
g.lastKeyframePacketCount = 0
|
||||
}
|
||||
|
||||
// Calculate FPS from frame timestamps
|
||||
func (g *Golibrtsp) calculateFPSFromTimestamps() float64 {
|
||||
g.fpsMutex.Lock()
|
||||
defer g.fpsMutex.Unlock()
|
||||
|
||||
if g.lastFrameTime.IsZero() {
|
||||
g.lastFrameTime = time.Now()
|
||||
return 0
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
interval := now.Sub(g.lastFrameTime)
|
||||
g.lastFrameTime = now
|
||||
|
||||
// Store the interval
|
||||
g.frameTimeBuffer[g.frameBufferIndex] = interval
|
||||
g.frameBufferIndex = (g.frameBufferIndex + 1) % g.frameBufferSize
|
||||
|
||||
// Calculate average FPS from stored intervals
|
||||
var totalInterval time.Duration
|
||||
validSamples := 0
|
||||
|
||||
for _, interval := range g.frameTimeBuffer {
|
||||
if interval > 0 {
|
||||
totalInterval += interval
|
||||
validSamples++
|
||||
}
|
||||
}
|
||||
|
||||
if validSamples == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
avgInterval := totalInterval / time.Duration(validSamples)
|
||||
if avgInterval == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
return float64(time.Second) / float64(avgInterval)
|
||||
}
|
||||
|
||||
// Get enhanced FPS information from SPS with fallback
|
||||
func (g *Golibrtsp) getEnhancedFPS(sps *h264.SPS, streamIndex int8) float64 {
|
||||
// First try to get FPS from SPS
|
||||
spsFPS := sps.FPS()
|
||||
|
||||
// Check if SPS FPS is reasonable (between 1 and 120 fps)
|
||||
if spsFPS > 0 && spsFPS <= 120 {
|
||||
log.Log.Debug(fmt.Sprintf("capture.golibrtsp.getEnhancedFPS(): SPS FPS: %.2f", spsFPS))
|
||||
return spsFPS
|
||||
}
|
||||
|
||||
// Fallback to timestamp-based calculation
|
||||
timestampFPS := g.calculateFPSFromTimestamps()
|
||||
if timestampFPS > 0 && timestampFPS <= 120 {
|
||||
log.Log.Debug(fmt.Sprintf("capture.golibrtsp.getEnhancedFPS(): Timestamp FPS: %.2f", timestampFPS))
|
||||
return timestampFPS
|
||||
}
|
||||
|
||||
// Return SPS FPS even if it seems unreasonable, or default
|
||||
if spsFPS > 0 {
|
||||
return spsFPS
|
||||
}
|
||||
|
||||
return 25.0 // Default fallback FPS
|
||||
}
|
||||
|
||||
// Track I-frame intervals by counting packets between keyframes
|
||||
func (g *Golibrtsp) trackKeyframeInterval(isKeyframe bool) int {
|
||||
g.keyframeMutex.Lock()
|
||||
defer g.keyframeMutex.Unlock()
|
||||
|
||||
g.packetsSinceLastKeyframe++
|
||||
|
||||
if isKeyframe {
|
||||
// Store the interval since the last keyframe
|
||||
if g.lastKeyframePacketCount > 0 {
|
||||
interval := g.packetsSinceLastKeyframe
|
||||
g.keyframeIntervals[g.keyframeBufferIndex] = interval
|
||||
g.keyframeBufferIndex = (g.keyframeBufferIndex + 1) % g.keyframeBufferSize
|
||||
}
|
||||
|
||||
// Reset counter for next interval
|
||||
g.lastKeyframePacketCount = g.packetsSinceLastKeyframe
|
||||
g.packetsSinceLastKeyframe = 0
|
||||
|
||||
return g.lastKeyframePacketCount
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// Get average keyframe interval (GOP size)
|
||||
func (g *Golibrtsp) getAverageKeyframeInterval() float64 {
|
||||
g.keyframeMutex.Lock()
|
||||
defer g.keyframeMutex.Unlock()
|
||||
|
||||
var totalInterval int
|
||||
validSamples := 0
|
||||
|
||||
for _, interval := range g.keyframeIntervals {
|
||||
if interval > 0 {
|
||||
totalInterval += interval
|
||||
validSamples++
|
||||
}
|
||||
}
|
||||
|
||||
if validSamples == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
return float64(totalInterval) / float64(validSamples)
|
||||
}
|
||||
|
||||
// Calculate GOP size in seconds based on FPS and keyframe interval
|
||||
func (g *Golibrtsp) getGOPDuration(fps float64) float64 {
|
||||
avgInterval := g.getAverageKeyframeInterval()
|
||||
if avgInterval > 0 && fps > 0 {
|
||||
return avgInterval / fps
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Get detailed SPS timing information
|
||||
func (g *Golibrtsp) getSPSTimingInfo(sps *h264.SPS) (hasVUI bool, timeScale uint32, numUnitsInTick uint32, fps float64) {
|
||||
// Try to get FPS from SPS
|
||||
fps = sps.FPS()
|
||||
|
||||
// Note: The gortsplib SPS struct may not expose VUI parameters directly
|
||||
// but we can still work with the calculated FPS
|
||||
if fps > 0 {
|
||||
hasVUI = true
|
||||
// These are estimated values based on common patterns
|
||||
if fps == 25.0 {
|
||||
timeScale = 50
|
||||
numUnitsInTick = 1
|
||||
} else if fps == 30.0 {
|
||||
timeScale = 60
|
||||
numUnitsInTick = 1
|
||||
} else if fps == 24.0 {
|
||||
timeScale = 48
|
||||
numUnitsInTick = 1
|
||||
} else {
|
||||
// Generic calculation
|
||||
timeScale = uint32(fps * 2)
|
||||
numUnitsInTick = 1
|
||||
}
|
||||
}
|
||||
|
||||
return hasVUI, timeScale, numUnitsInTick, fps
|
||||
}
|
||||
|
||||
// Debug SPS information
|
||||
func (g *Golibrtsp) debugSPSInfo(sps *h264.SPS, streamType string) {
|
||||
hasVUI, timeScale, numUnitsInTick, fps := g.getSPSTimingInfo(sps)
|
||||
|
||||
log.Log.Debug(fmt.Sprintf("capture.golibrtsp.debugSPSInfo(%s): Width=%d, Height=%d",
|
||||
streamType, sps.Width(), sps.Height()))
|
||||
log.Log.Debug(fmt.Sprintf("capture.golibrtsp.debugSPSInfo(%s): HasVUI=%t, FPS=%.2f",
|
||||
streamType, hasVUI, fps))
|
||||
|
||||
if hasVUI {
|
||||
log.Log.Debug(fmt.Sprintf("capture.golibrtsp.debugSPSInfo(%s): TimeScale=%d, NumUnitsInTick=%d",
|
||||
streamType, timeScale, numUnitsInTick))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,10 +68,16 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
|
||||
postRecording := config.Capture.PostRecording * 1000 // number of seconds to record.
|
||||
maxRecordingPeriod := config.Capture.MaxLengthRecording * 1000 // maximum number of seconds to record.
|
||||
|
||||
// Synchronise the last synced time
|
||||
now := time.Now().UnixMilli()
|
||||
startRecording := now
|
||||
timestamp := now
|
||||
// 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
|
||||
@@ -105,8 +111,6 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
|
||||
// Do not do anything!
|
||||
log.Log.Info("capture.main.HandleRecordStream(continuous): start recording")
|
||||
|
||||
now = time.Now().Unix()
|
||||
timestamp = now
|
||||
start := false
|
||||
|
||||
// If continuous record the full length
|
||||
@@ -114,6 +118,8 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
|
||||
// Recording file name
|
||||
fullName := ""
|
||||
|
||||
var startRecording int64 = 0 // start recording timestamp in milliseconds
|
||||
|
||||
// Get as much packets we need.
|
||||
var cursorError error
|
||||
var pkt packets.Packet
|
||||
@@ -132,7 +138,7 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
|
||||
now := time.Now().UnixMilli()
|
||||
|
||||
if start && // If already recording and current frame is a keyframe and we should stop recording
|
||||
nextPkt.IsKeyFrame && (timestamp+postRecording-now <= 0 || now-startRecording >= maxRecordingPeriod) {
|
||||
nextPkt.IsKeyFrame && (startRecording+postRecording-now <= 0 || now-startRecording > maxRecordingPeriod-500) {
|
||||
|
||||
pts := convertPTS(pkt.TimeLegacy)
|
||||
if pkt.IsVideo {
|
||||
@@ -159,6 +165,39 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
|
||||
// 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.")
|
||||
}
|
||||
|
||||
// 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'
|
||||
@@ -203,7 +242,6 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
|
||||
}
|
||||
|
||||
start = true
|
||||
timestamp = now
|
||||
|
||||
// timestamp_microseconds_instanceName_regionCoordinates_numberOfChanges_token
|
||||
// 1564859471_6-474162_oprit_577-283-727-375_1153_27.mp4
|
||||
@@ -214,14 +252,17 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
|
||||
// - Number of changes
|
||||
// - Token
|
||||
|
||||
startRecording = time.Now().UnixMilli()
|
||||
startRecordingSeconds := startRecording / 1000 // convert to seconds
|
||||
s := strconv.FormatInt(startRecordingSeconds, 10) + "_" +
|
||||
"6" + "-" +
|
||||
"967003" + "_" +
|
||||
config.Name + "_" +
|
||||
"200-200-400-400" + "_0_" +
|
||||
"769"
|
||||
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
|
||||
|
||||
name = s + ".mp4"
|
||||
fullName = configDirectory + "/data/recordings/" + name
|
||||
@@ -305,6 +346,39 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
|
||||
// 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.")
|
||||
}
|
||||
|
||||
// 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'
|
||||
@@ -340,31 +414,44 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
|
||||
|
||||
log.Log.Info("capture.main.HandleRecordStream(motiondetection): Start motion based recording ")
|
||||
|
||||
var lastDuration int64 = 0 // last duration in milliseconds
|
||||
var lastRecordingTime int64 = 0 // last recording time in milliseconds
|
||||
var lastRecordingTime int64 = 0 // last recording timestamp in milliseconds
|
||||
var displayTime int64 = 0 // display time in milliseconds
|
||||
|
||||
var videoTrack uint32
|
||||
var audioTrack uint32
|
||||
|
||||
for motion := range communication.HandleMotion {
|
||||
|
||||
timestamp = time.Now().UnixMilli()
|
||||
startRecording = time.Now().UnixMilli() // we mark the current time when the record started.
|
||||
numberOfChanges := motion.NumberOfChanges
|
||||
// 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)
|
||||
|
||||
// If we have prerecording we will substract the number of seconds.
|
||||
// Taking into account FPS = GOP size (Keyfram interval)
|
||||
if preRecording > 0 && lastRecordingTime > 0 {
|
||||
now := time.Now().UnixMilli()
|
||||
motionTimestamp := now
|
||||
|
||||
// Might be that recordings are coming short after each other.
|
||||
// Therefore we do some math with the current time and the last recording time.
|
||||
start := false
|
||||
|
||||
timeBetweenNowAndLastRecording := startRecording - lastRecordingTime
|
||||
if timeBetweenNowAndLastRecording > preRecording {
|
||||
startRecording = startRecording - preRecording + 1000 // we add 1000 milliseconds to make sure we have a full second of pre-recording.
|
||||
} else {
|
||||
startRecording = startRecording - timeBetweenNowAndLastRecording
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// timestamp_microseconds_instanceName_regionCoordinates_numberOfChanges_token
|
||||
@@ -375,20 +462,33 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
|
||||
// - Region
|
||||
// - Number of changes
|
||||
// - Token
|
||||
startRecordingSeconds := startRecording / 1000 // convert to seconds
|
||||
s := strconv.FormatInt(startRecordingSeconds, 10) + "_" +
|
||||
"6" + "-" +
|
||||
"967003" + "_" +
|
||||
config.Name + "_" +
|
||||
"200-200-400-400" + "_" +
|
||||
strconv.Itoa(numberOfChanges) + "_" +
|
||||
"769"
|
||||
|
||||
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
|
||||
|
||||
name := s + ".mp4"
|
||||
fullName := configDirectory + "/data/recordings/" + name
|
||||
|
||||
// Running...
|
||||
log.Log.Info("capture.main.HandleRecordStream(motiondetection): recording started")
|
||||
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
|
||||
@@ -415,18 +515,6 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
|
||||
log.Log.Debug("capture.main.HandleRecordStream(continuous): no AAC audio codec detected, skipping audio track.")
|
||||
}
|
||||
|
||||
start := false
|
||||
|
||||
// Get as much packets we need.
|
||||
var cursorError error
|
||||
var pkt packets.Packet
|
||||
var nextPkt packets.Packet
|
||||
recordingCursor := queue.DelayedGopCount(int(config.Capture.PreRecording + 1))
|
||||
|
||||
if cursorError == nil {
|
||||
pkt, cursorError = recordingCursor.ReadPacket()
|
||||
}
|
||||
|
||||
for cursorError == nil {
|
||||
|
||||
nextPkt, cursorError = recordingCursor.ReadPacket()
|
||||
@@ -434,21 +522,23 @@ func HandleRecordStream(queue *packets.Queue, configDirectory string, configurat
|
||||
log.Log.Error("capture.main.HandleRecordStream(motiondetection): " + cursorError.Error())
|
||||
}
|
||||
|
||||
now := time.Now().UnixMilli()
|
||||
now = time.Now().UnixMilli()
|
||||
select {
|
||||
case motion := <-communication.HandleMotion:
|
||||
timestamp = now
|
||||
motionTimestamp = now
|
||||
log.Log.Info("capture.main.HandleRecordStream(motiondetection): motion detected while recording. Expanding recording.")
|
||||
numberOfChanges = motion.NumberOfChanges
|
||||
numberOfChanges := motion.NumberOfChanges
|
||||
log.Log.Info("capture.main.HandleRecordStream(motiondetection): Received message with recording data, detected changes to save: " + strconv.Itoa(numberOfChanges))
|
||||
default:
|
||||
}
|
||||
|
||||
if (timestamp+postRecording-now < 0 || now-startRecording > maxRecordingPeriod-1000) && nextPkt.IsKeyFrame {
|
||||
log.Log.Info("capture.main.HandleRecordStream(motiondetection): closing recording (timestamp: " + strconv.FormatInt(timestamp, 10) + ", postRecording: " + strconv.FormatInt(postRecording, 10) + ", now: " + strconv.FormatInt(now, 10) + ", startRecording: " + strconv.FormatInt(startRecording, 10) + ", maxRecordingPeriod: " + strconv.FormatInt(maxRecordingPeriod, 10))
|
||||
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))
|
||||
break
|
||||
}
|
||||
if pkt.IsKeyFrame && !start && (pkt.Time >= lastDuration || pkt.Time == 0) {
|
||||
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")
|
||||
@@ -479,12 +569,45 @@ 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 close the recording and write the last packet.
|
||||
mp4Video.Close(&config)
|
||||
log.Log.Info("capture.main.HandleRecordStream(motiondetection): file save: " + name)
|
||||
|
||||
lastDuration = pkt.Time
|
||||
lastRecordingTime = time.Now().UnixMilli()
|
||||
// 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.
|
||||
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 != "" {
|
||||
@@ -604,7 +727,7 @@ func VerifyCamera(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func Base64Image(captureDevice *Capture, communication *models.Communication) string {
|
||||
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
|
||||
@@ -634,7 +757,8 @@ func Base64Image(captureDevice *Capture, communication *models.Communication) st
|
||||
var img image.YCbCr
|
||||
img, err = (*rtspClient).DecodePacket(pkt)
|
||||
if err == nil {
|
||||
bytes, _ := utils.ImageToBytes(&img)
|
||||
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 {
|
||||
|
||||
@@ -672,6 +672,7 @@ func HandleLiveStreamSD(livestreamCursor *packets.QueueCursor, configuration *mo
|
||||
// Check if we need to enable the live stream
|
||||
if config.Capture.Liveview != "false" {
|
||||
|
||||
deviceId := config.Key
|
||||
hubKey := ""
|
||||
if config.Cloud == "s3" && config.S3 != nil && config.S3.Publickey != "" {
|
||||
hubKey = config.S3.Publickey
|
||||
@@ -705,25 +706,79 @@ func HandleLiveStreamSD(livestreamCursor *packets.QueueCursor, configuration *mo
|
||||
log.Log.Info("cloud.HandleLiveStreamSD(): Sending base64 encoded images to MQTT.")
|
||||
img, err := rtspClient.DecodePacket(pkt)
|
||||
if err == nil {
|
||||
bytes, _ := utils.ImageToBytes(&img)
|
||||
encoded := base64.StdEncoding.EncodeToString(bytes)
|
||||
imageResized, _ := utils.ResizeImage(&img, uint(config.Capture.IPCamera.BaseWidth), uint(config.Capture.IPCamera.BaseHeight))
|
||||
bytes, _ := utils.ImageToBytes(imageResized)
|
||||
|
||||
valueMap := make(map[string]interface{})
|
||||
valueMap["image"] = encoded
|
||||
message := models.Message{
|
||||
Payload: models.Payload{
|
||||
Action: "receive-sd-stream",
|
||||
DeviceId: configuration.Config.Key,
|
||||
Value: valueMap,
|
||||
},
|
||||
}
|
||||
payload, err := models.PackageMQTTMessage(configuration, message)
|
||||
if err == nil {
|
||||
mqttClient.Publish("kerberos/hub/"+hubKey, 0, false, payload)
|
||||
chunking := config.Capture.LiveviewChunking
|
||||
|
||||
if chunking == "true" {
|
||||
|
||||
// Split encoded image into chunks of 2kb
|
||||
// This is to prevent the MQTT message to be too large.
|
||||
// By default, bytes are not encoded to base64 here; you are splitting the raw JPEG/PNG bytes.
|
||||
// However, in MQTT and web contexts, binary data may not be handled well, so base64 is often used.
|
||||
// To avoid base64 encoding, just send the raw []byte chunks as you do here.
|
||||
// If you want to avoid base64, make sure the receiver can handle binary payloads.
|
||||
|
||||
chunkSize := 25 * 1024 // 25KB chunks
|
||||
var chunks [][]byte
|
||||
for i := 0; i < len(bytes); i += chunkSize {
|
||||
end := i + chunkSize
|
||||
if end > len(bytes) {
|
||||
end = len(bytes)
|
||||
}
|
||||
chunk := bytes[i:end]
|
||||
chunks = append(chunks, chunk)
|
||||
}
|
||||
|
||||
log.Log.Infof("cloud.HandleLiveStreamSD(): Sending %d chunks of size %d bytes.", len(chunks), chunkSize)
|
||||
|
||||
timestamp := time.Now().Unix()
|
||||
for i, chunk := range chunks {
|
||||
valueMap := make(map[string]interface{})
|
||||
valueMap["id"] = timestamp
|
||||
valueMap["chunk"] = chunk
|
||||
valueMap["chunkIndex"] = i
|
||||
valueMap["chunkSize"] = chunkSize
|
||||
valueMap["chunkCount"] = len(chunks)
|
||||
message := models.Message{
|
||||
Payload: models.Payload{
|
||||
Version: "v1.0.0",
|
||||
Action: "receive-sd-stream",
|
||||
DeviceId: deviceId,
|
||||
Value: valueMap,
|
||||
},
|
||||
}
|
||||
payload, err := models.PackageMQTTMessage(configuration, message)
|
||||
if err == nil {
|
||||
mqttClient.Publish("kerberos/hub/"+hubKey+"/"+deviceId, 1, false, payload)
|
||||
log.Log.Infof("cloud.HandleLiveStreamSD(): sent chunk %d/%d to MQTT topic kerberos/hub/%s/%s", i+1, len(chunks), hubKey, deviceId)
|
||||
time.Sleep(33 * time.Millisecond) // Sleep to avoid flooding the MQTT broker with messages
|
||||
} else {
|
||||
log.Log.Info("cloud.HandleLiveStreamSD(): something went wrong while sending acknowledge config to hub: " + string(payload))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Log.Info("cloud.HandleLiveStreamSD(): something went wrong while sending acknowledge config to hub: " + string(payload))
|
||||
|
||||
valueMap := make(map[string]interface{})
|
||||
valueMap["image"] = bytes
|
||||
message := models.Message{
|
||||
Payload: models.Payload{
|
||||
Action: "receive-sd-stream",
|
||||
DeviceId: configuration.Config.Key,
|
||||
Value: valueMap,
|
||||
},
|
||||
}
|
||||
payload, err := models.PackageMQTTMessage(configuration, message)
|
||||
if err == nil {
|
||||
mqttClient.Publish("kerberos/hub/"+hubKey, 0, false, payload)
|
||||
} else {
|
||||
log.Log.Info("cloud.HandleLiveStreamSD(): something went wrong while sending acknowledge config to hub: " + string(payload))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
time.Sleep(1000 * time.Millisecond) // Sleep to avoid flooding the MQTT broker with messages
|
||||
}
|
||||
|
||||
} else {
|
||||
@@ -810,7 +865,8 @@ func HandleRealtimeProcessing(processingCursor *packets.QueueCursor, configurati
|
||||
log.Log.Info("cloud.RealtimeProcessing(): Sending base64 encoded images to MQTT.")
|
||||
img, err := rtspClient.DecodePacket(pkt)
|
||||
if err == nil {
|
||||
bytes, _ := utils.ImageToBytes(&img)
|
||||
imageResized, _ := utils.ResizeImage(&img, uint(config.Capture.IPCamera.BaseWidth), uint(config.Capture.IPCamera.BaseHeight))
|
||||
bytes, _ := utils.ImageToBytes(imageResized)
|
||||
encoded := base64.StdEncoding.EncodeToString(bytes)
|
||||
|
||||
valueMap := make(map[string]interface{})
|
||||
|
||||
@@ -173,6 +173,21 @@ func RunAgent(configDirectory string, configuration *models.Configuration, commu
|
||||
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
|
||||
}
|
||||
|
||||
// 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}
|
||||
@@ -226,6 +241,22 @@ func RunAgent(configDirectory string, configuration *models.Configuration, commu
|
||||
// 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
|
||||
@@ -235,7 +266,7 @@ func RunAgent(configDirectory string, configuration *models.Configuration, commu
|
||||
|
||||
// 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(int(config.Capture.PreRecording) + 1) // GOP time frame is set to prerecording (we'll add 2 gops to leave some room).
|
||||
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)
|
||||
|
||||
@@ -254,7 +285,7 @@ func RunAgent(configDirectory string, configuration *models.Configuration, commu
|
||||
if subStreamEnabled && rtspSubClient != nil {
|
||||
subQueue = packets.NewQueue()
|
||||
communication.SubQueue = subQueue
|
||||
subQueue.SetMaxGopCount(3) // GOP time frame is set to prerecording (we'll add 2 gops to leave some room).
|
||||
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)
|
||||
|
||||
@@ -676,7 +707,7 @@ func MakeRecording(c *gin.Context, communication *models.Communication) {
|
||||
// @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)
|
||||
base64Image := capture.Base64Image(captureDevice, communication, configuration)
|
||||
if base64Image != "" {
|
||||
communication.Image = base64Image
|
||||
}
|
||||
@@ -698,7 +729,8 @@ func GetSnapshotRaw(c *gin.Context, captureDevice *capture.Capture, configuratio
|
||||
image := capture.JpegImage(captureDevice, communication)
|
||||
|
||||
// encode image to jpeg
|
||||
bytes, _ := utils.ImageToBytes(&image)
|
||||
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)
|
||||
@@ -713,7 +745,7 @@ func GetSnapshotRaw(c *gin.Context, captureDevice *capture.Capture, configuratio
|
||||
// @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)
|
||||
base64Image := capture.Base64Image(captureDevice, communication, configuration)
|
||||
if base64Image != "" {
|
||||
communication.Image = base64Image
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ func ProcessMotion(motionCursor *packets.QueueCursor, configuration *models.Conf
|
||||
|
||||
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
|
||||
@@ -62,16 +63,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
|
||||
y := c.Y
|
||||
x := c.X * baseWidthRatio
|
||||
y := c.Y * baseHeightRatio
|
||||
p := geo.NewPoint(x, y)
|
||||
if !poly.Contains(p) {
|
||||
poly.Add(p)
|
||||
@@ -132,7 +151,7 @@ func ProcessMotion(motionCursor *packets.QueueCursor, configuration *models.Conf
|
||||
if detectMotion {
|
||||
|
||||
// Remember additional information about the result of findmotion
|
||||
isPixelChangeThresholdReached, changesToReturn = FindMotion(imageArray, coordinatesToCheck, pixelThreshold)
|
||||
isPixelChangeThresholdReached, changesToReturn, motionRectangle = FindMotion(imageArray, coordinatesToCheck, pixelThreshold)
|
||||
if isPixelChangeThresholdReached {
|
||||
|
||||
// If offline mode is disabled, send a message to the hub
|
||||
@@ -164,6 +183,7 @@ func ProcessMotion(motionCursor *packets.QueueCursor, configuration *models.Conf
|
||||
dataToPass := models.MotionDataPartial{
|
||||
Timestamp: time.Now().Unix(),
|
||||
NumberOfChanges: changesToReturn,
|
||||
Rectangle: motionRectangle,
|
||||
}
|
||||
communication.HandleMotion <- dataToPass //Save data to the channel
|
||||
}
|
||||
@@ -185,24 +205,58 @@ func ProcessMotion(motionCursor *packets.QueueCursor, configuration *models.Conf
|
||||
log.Log.Debug("computervision.main.ProcessMotion(): stop the motion detection.")
|
||||
}
|
||||
|
||||
func FindMotion(imageArray [3]*image.Gray, coordinatesToCheck []int, pixelChangeThreshold int) (thresholdReached bool, changesDetected int) {
|
||||
func FindMotion(imageArray [3]*image.Gray, coordinatesToCheck []int, pixelChangeThreshold int) (thresholdReached bool, changesDetected int, motionRectangle models.MotionRectangle) {
|
||||
image1 := imageArray[0]
|
||||
image2 := imageArray[1]
|
||||
image3 := imageArray[2]
|
||||
threshold := 60
|
||||
changes := AbsDiffBitwiseAndThreshold(image1, image2, image3, threshold, coordinatesToCheck)
|
||||
return changes > pixelChangeThreshold, changes
|
||||
changes, motionRectangle := AbsDiffBitwiseAndThreshold(image1, image2, image3, threshold, coordinatesToCheck)
|
||||
return changes > pixelChangeThreshold, changes, motionRectangle
|
||||
}
|
||||
|
||||
func AbsDiffBitwiseAndThreshold(img1 *image.Gray, img2 *image.Gray, img3 *image.Gray, threshold int, coordinatesToCheck []int) int {
|
||||
func AbsDiffBitwiseAndThreshold(img1 *image.Gray, img2 *image.Gray, img3 *image.Gray, threshold int, coordinatesToCheck []int) (int, models.MotionRectangle) {
|
||||
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()})
|
||||
}
|
||||
}
|
||||
return changes
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -239,7 +239,15 @@ func OverrideWithEnvironmentVariables(configuration *models.Configuration) {
|
||||
configuration.Config.Capture.IPCamera.SubRTSP = value
|
||||
break
|
||||
|
||||
/* ONVIF connnection settings */
|
||||
/* 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 */
|
||||
case "AGENT_CAPTURE_IPCAMERA_ONVIF":
|
||||
configuration.Config.Capture.IPCamera.ONVIF = value
|
||||
break
|
||||
@@ -392,6 +400,11 @@ 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
|
||||
@@ -578,6 +591,10 @@ func StoreConfig(configDirectory string, config models.Config) error {
|
||||
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
|
||||
|
||||
@@ -118,6 +118,16 @@ 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":
|
||||
@@ -138,6 +148,16 @@ 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":
|
||||
|
||||
@@ -62,9 +62,11 @@ 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"`
|
||||
@@ -77,13 +79,18 @@ 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"`
|
||||
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"`
|
||||
|
||||
@@ -132,6 +132,7 @@ type Message struct {
|
||||
// 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"`
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package models
|
||||
|
||||
type MotionDataPartial struct {
|
||||
Timestamp int64 `json:"timestamp" bson:"timestamp"`
|
||||
NumberOfChanges int `json:"numberOfChanges" bson:"numberOfChanges"`
|
||||
Timestamp int64 `json:"timestamp" bson:"timestamp"`
|
||||
NumberOfChanges int `json:"numberOfChanges" bson:"numberOfChanges"`
|
||||
Rectangle MotionRectangle `json:"rectangle" bson:"rectangle"`
|
||||
}
|
||||
|
||||
type MotionDataFull struct {
|
||||
@@ -14,3 +15,10 @@ 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"`
|
||||
}
|
||||
|
||||
@@ -17,5 +17,7 @@ type Packet struct {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -45,6 +45,11 @@ func (self *Queue) SetMaxGopCount(n int) {
|
||||
return
|
||||
}
|
||||
|
||||
func (self *Queue) GetMaxGopCount() int {
|
||||
n := self.maxgopcount
|
||||
return n
|
||||
}
|
||||
|
||||
func (self *Queue) WriteHeader(streams []Stream) error {
|
||||
self.lock.Lock()
|
||||
|
||||
|
||||
@@ -48,4 +48,7 @@ type Stream struct {
|
||||
|
||||
// Channels is the number of audio channels.
|
||||
Channels int
|
||||
|
||||
// GopSize is the size of the GOP (Group of Pictures).
|
||||
GopSize int
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
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, communication, captureDevice)
|
||||
websocket.WebsocketHandler(c, configuration, communication, captureDevice)
|
||||
})
|
||||
|
||||
// This is legacy should be removed in future! Now everything
|
||||
|
||||
@@ -389,14 +389,6 @@ func HandleRequestConfig(mqttClient mqtt.Client, hubKey string, payload models.P
|
||||
// Copy the config, as we don't want to share the encryption part.
|
||||
deepCopy := configuration.Config
|
||||
|
||||
// We need a fix for the width and height if a substream.
|
||||
// The ROI requires the width and height of the sub stream.
|
||||
if configuration.Config.Capture.IPCamera.SubRTSP != "" &&
|
||||
configuration.Config.Capture.IPCamera.SubRTSP != configuration.Config.Capture.IPCamera.RTSP {
|
||||
deepCopy.Capture.IPCamera.Width = configuration.Config.Capture.IPCamera.SubWidth
|
||||
deepCopy.Capture.IPCamera.Height = configuration.Config.Capture.IPCamera.SubHeight
|
||||
}
|
||||
|
||||
var configMap map[string]interface{}
|
||||
inrec, _ := json.Marshal(deepCopy)
|
||||
json.Unmarshal(inrec, &configMap)
|
||||
|
||||
@@ -49,7 +49,7 @@ var upgrader = websocket.Upgrader{
|
||||
},
|
||||
}
|
||||
|
||||
func WebsocketHandler(c *gin.Context, communication *models.Communication, captureDevice *capture.Capture) {
|
||||
func WebsocketHandler(c *gin.Context, configuration *models.Configuration, communication *models.Communication, captureDevice *capture.Capture) {
|
||||
w := c.Writer
|
||||
r := c.Request
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
@@ -112,7 +112,7 @@ func WebsocketHandler(c *gin.Context, communication *models.Communication, captu
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
sockets[clientID].Cancels["stream-sd"] = cancel
|
||||
go ForwardSDStream(ctx, clientID, sockets[clientID], communication, captureDevice)
|
||||
go ForwardSDStream(ctx, clientID, sockets[clientID], configuration, communication, captureDevice)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -131,7 +131,7 @@ func WebsocketHandler(c *gin.Context, communication *models.Communication, captu
|
||||
}
|
||||
}
|
||||
|
||||
func ForwardSDStream(ctx context.Context, clientID string, connection *Connection, communication *models.Communication, captureDevice *capture.Capture) {
|
||||
func ForwardSDStream(ctx context.Context, clientID string, connection *Connection, configuration *models.Configuration, communication *models.Communication, captureDevice *capture.Capture) {
|
||||
|
||||
var queue *packets.Queue
|
||||
var cursor *packets.QueueCursor
|
||||
@@ -159,7 +159,10 @@ logreader:
|
||||
var img image.YCbCr
|
||||
img, err = (*rtspClient).DecodePacket(pkt)
|
||||
if err == nil {
|
||||
bytes, _ := utils.ImageToBytes(&img)
|
||||
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)
|
||||
encodedImage = base64.StdEncoding.EncodeToString(bytes)
|
||||
} else {
|
||||
continue
|
||||
|
||||
@@ -21,9 +21,11 @@ import (
|
||||
"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"
|
||||
)
|
||||
|
||||
const VERSION = "3.3.5"
|
||||
const VERSION = "3.5.0"
|
||||
|
||||
const letterBytes = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
|
||||
@@ -401,9 +403,31 @@ func Decrypt(directoryOrFile string, symmetricKey []byte) {
|
||||
}
|
||||
}
|
||||
|
||||
func ImageToBytes(img image.Image) ([]byte, error) {
|
||||
func ImageToBytes(img *image.Image) ([]byte, error) {
|
||||
buffer := new(bytes.Buffer)
|
||||
w := bufio.NewWriter(buffer)
|
||||
err := jpeg.Encode(w, img, &jpeg.Options{Quality: 15})
|
||||
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
|
||||
}
|
||||
|
||||
@@ -64,15 +64,14 @@ func NewMP4(fileName string, spsNALUs [][]byte, ppsNALUs [][]byte, vpsNALUs [][]
|
||||
init := mp4ff.NewMP4Init()
|
||||
|
||||
// Add a free box to the init segment
|
||||
// Prepend a free box to the init segment with a size of 1000
|
||||
freeBoxSize := 2048
|
||||
// Prepend a free box to the init segment with a size of 4096 bytes, so we can overwrite it later with the actual init segment.
|
||||
freeBoxSize := 4096
|
||||
free := mp4ff.NewFreeBox(make([]byte, freeBoxSize))
|
||||
init.AddChild(free)
|
||||
|
||||
// Create a writer
|
||||
ofd, err := os.Create(fileName)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Create a buffered writer
|
||||
@@ -82,7 +81,6 @@ func NewMP4(fileName string, spsNALUs [][]byte, ppsNALUs [][]byte, vpsNALUs [][]
|
||||
// so we can overwrite it later with the actual init segment.
|
||||
err = init.Encode(bufferedWriter)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &MP4{
|
||||
@@ -142,7 +140,7 @@ func (mp4 *MP4) AddSampleToTrack(trackID uint32, isKeyframe bool, data []byte, p
|
||||
mp4.MoofBoxSizes = append(mp4.MoofBoxSizes, int64(mp4.Segment.Size()))
|
||||
err := mp4.Segment.Encode(mp4.Writer)
|
||||
if err != nil {
|
||||
return err
|
||||
log.Log.Error("mp4.AddSampleToTrack(): error encoding segment: " + err.Error())
|
||||
}
|
||||
mp4.Segments = append(mp4.Segments, mp4.Segment)
|
||||
}
|
||||
@@ -158,6 +156,7 @@ func (mp4 *MP4) AddSampleToTrack(trackID uint32, isKeyframe bool, data []byte, p
|
||||
// Create a video fragment
|
||||
multiTrackFragment, err := mp4ff.CreateMultiTrackFragment(uint32(mp4.SegmentCount), mp4.TrackIDs) // Assuming 1 for video track and 2 for audio track
|
||||
if err != nil {
|
||||
log.Log.Error("mp4.AddSampleToTrack(): error creating multi track fragment: " + err.Error())
|
||||
}
|
||||
mp4.MultiTrackFragment = multiTrackFragment
|
||||
seg.AddFragment(multiTrackFragment)
|
||||
@@ -175,26 +174,25 @@ func (mp4 *MP4) AddSampleToTrack(trackID uint32, isKeyframe bool, data []byte, p
|
||||
|
||||
var lengthPrefixed []byte
|
||||
var err error
|
||||
if mp4.VideoTrackName == "H264" || mp4.VideoTrackName == "AVC1" { // Convert Annex B to length-prefixed NAL units if H264
|
||||
switch mp4.VideoTrackName {
|
||||
case "H264", "AVC1": // Convert Annex B to length-prefixed NAL units if H264
|
||||
lengthPrefixed, err = annexBToLengthPrefixed(data)
|
||||
} else if mp4.VideoTrackName == "H265" || mp4.VideoTrackName == "HVC1" { // Convert H265 Annex B to length-prefixed NAL units
|
||||
case "H265", "HVC1": // Convert H265 Annex B to length-prefixed NAL units
|
||||
lengthPrefixed, err = annexBToLengthPrefixed(data)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
if mp4.VideoFullSample != nil {
|
||||
duration := pts - mp4.VideoFullSample.DecodeTime
|
||||
log.Log.Info("Adding sample to track " + fmt.Sprintf("%d, PTS: %d, Duration: %d, size: %d, Keyframe: %t", trackID, pts, duration, len(lengthPrefixed), isKeyframe))
|
||||
log.Log.Debug("Adding sample to track " + fmt.Sprintf("%d, PTS: %d, Duration: %d, size: %d, Keyframe: %t", trackID, pts, duration, len(lengthPrefixed), isKeyframe))
|
||||
|
||||
mp4.LastVideoSampleDTS = duration
|
||||
//fmt.Printf("Adding sample to track %d, PTS: %d, Duration: %d, size: %d, Keyframe: %t\n", trackID, pts, duration, len(mp4.VideoFullSample.Data), isKeyframe)
|
||||
mp4.VideoTotalDuration += duration
|
||||
mp4.VideoFullSample.DecodeTime = mp4.VideoTotalDuration - duration
|
||||
mp4.VideoFullSample.Sample.Dur = uint32(duration)
|
||||
err := mp4.MultiTrackFragment.AddFullSampleToTrack(*mp4.VideoFullSample, trackID)
|
||||
if err != nil {
|
||||
//log.Printf("Error adding sample to track %d: %v", trackID, err)
|
||||
return err
|
||||
log.Log.Error("mp4.AddSampleToTrack(): error adding sample to track " + fmt.Sprintf("%d: %v", trackID, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,44 +259,23 @@ func (mp4 *MP4) AddSampleToTrack(trackID uint32, isKeyframe bool, data []byte, p
|
||||
|
||||
func (mp4 *MP4) Close(config *models.Config) {
|
||||
|
||||
// Add the last sample to the track, we will predict the duration based on the last sample
|
||||
// We are not insert the last sample as we might corrupt playback (as we do not know accurately the next PTS).
|
||||
// In theory it means we will lose the last sample, so there is millisecond dataloss, but it is better than corrupting playback.
|
||||
// We could this by using a delayed packet reader, and look for the next PTS (closest one), but that would require a lot of memory and CPU.
|
||||
|
||||
/*duration := uint64(0)
|
||||
trackID := uint32(1)
|
||||
if mp4.SampleType == "video" {
|
||||
duration = mp4.LastVideoSampleDTS
|
||||
trackID = uint32(mp4.VideoTrack)
|
||||
} else if mp4.SampleType == "audio" {
|
||||
duration = 21 //mp4.LastAudioSampleDTS
|
||||
|
||||
} else {
|
||||
log.Println("mp4.Close(): unknown sample type, cannot calculate duration")
|
||||
if mp4.VideoTotalDuration == 0 && mp4.AudioTotalDuration == 0 {
|
||||
log.Log.Error("mp4.Close(): no video or audio samples added, cannot create MP4 file")
|
||||
}
|
||||
|
||||
if duration > 0 {
|
||||
mp4.VideoTotalDuration += duration
|
||||
mp4.VideoFullSample.DecodeTime = mp4.VideoTotalDuration - duration
|
||||
mp4.VideoFullSample.Sample.Dur = uint32(duration)
|
||||
err := mp4.MultiTrackFragment.AddFullSampleToTrack(*mp4.VideoFullSample, trackID)
|
||||
if err != nil {
|
||||
}
|
||||
mp4.Segments = append(mp4.Segments, mp4.Segment)
|
||||
}*/
|
||||
|
||||
// Encode the last segment
|
||||
err := mp4.Segment.Encode(mp4.Writer)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
if mp4.Segment != nil {
|
||||
err := mp4.Segment.Encode(mp4.Writer)
|
||||
if err != nil {
|
||||
log.Log.Error("mp4.Close(): error encoding last segment: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
mp4.Writer.Flush()
|
||||
defer mp4.FileWriter.Close()
|
||||
|
||||
// Now we have all the moof and mdat boxes written to the file.
|
||||
// We can now generate the ftyp and moov boxes, and replace it with the free box we added earlier (size of 10008 bytes).
|
||||
// We can now generate the ftyp and moov boxes, and replace it with the free box we added earlier (size of 2048 bytes).
|
||||
init := mp4ff.NewMP4Init()
|
||||
|
||||
// Create a new ftyp box
|
||||
@@ -331,22 +308,21 @@ func (mp4 *MP4) Close(config *models.Config) {
|
||||
init.Moov.AddChild(mvex)
|
||||
|
||||
// Add a track for the video
|
||||
if mp4.VideoTrackName == "H264" || mp4.VideoTrackName == "AVC1" {
|
||||
switch mp4.VideoTrackName {
|
||||
case "H264", "AVC1":
|
||||
init.AddEmptyTrack(videoTimescale, "video", "und")
|
||||
includePS := true
|
||||
err := init.Moov.Traks[0].SetAVCDescriptor("avc1", mp4.SPSNALUs, mp4.PPSNALUs, includePS)
|
||||
if err != nil {
|
||||
//panic(err)
|
||||
}
|
||||
init.Moov.Traks[0].Tkhd.Duration = mp4.VideoTotalDuration
|
||||
init.Moov.Traks[0].Mdia.Hdlr.Name = "agent " + utils.VERSION
|
||||
//init.Moov.Traks[0].Mdia.Mdhd.Duration = mp4.VideoTotalDuration
|
||||
} else if mp4.VideoTrackName == "H265" || mp4.VideoTrackName == "HVC1" {
|
||||
case "H265", "HVC1":
|
||||
init.AddEmptyTrack(videoTimescale, "video", "und")
|
||||
includePS := true
|
||||
err := init.Moov.Traks[0].SetHEVCDescriptor("hvc1", mp4.VPSNALUs, mp4.SPSNALUs, mp4.PPSNALUs, [][]byte{}, includePS)
|
||||
if err != nil {
|
||||
//panic(err)
|
||||
}
|
||||
init.Moov.Traks[0].Tkhd.Duration = mp4.VideoTotalDuration
|
||||
init.Moov.Traks[0].Mdia.Hdlr.Name = "agent " + utils.VERSION
|
||||
@@ -366,7 +342,6 @@ func (mp4 *MP4) Close(config *models.Config) {
|
||||
// Set the audio descriptor
|
||||
err := init.Moov.Traks[1].SetAACDescriptor(29, audioSampleRate)
|
||||
if err != nil {
|
||||
//panic(err)
|
||||
}
|
||||
init.Moov.Traks[1].Tkhd.Duration = mp4.AudioTotalDuration
|
||||
init.Moov.Traks[1].Mdia.Hdlr.Name = "agent " + utils.VERSION
|
||||
@@ -475,16 +450,15 @@ func (mp4 *MP4) Close(config *models.Config) {
|
||||
init.AddChild(sidx)*/
|
||||
|
||||
// Get a bit slice writer for the init segment
|
||||
// Get a byte buffer of 10008 bytes to write the init segment
|
||||
// Get a byte buffer of FreeBoxSize bytes to write the init segment
|
||||
buffer := bytes.NewBuffer(make([]byte, 0))
|
||||
init.Encode(buffer)
|
||||
|
||||
// The first 10008 bytes of the file is a free box, so we can read it and replace it with the moov box.
|
||||
// The init box might not be 10008 bytes, so we need to read the first 10008 bytes and then replace it with the moov box.
|
||||
// The first FreeBoxSize bytes of the file is a free box, so we can read it and replace it with the moov box.
|
||||
// The init box might not be FreeBoxSize bytes, so we need to read the first FreeBoxSize bytes and then replace it with the moov box.
|
||||
// while the remaining bytes are for a new free box.
|
||||
// Write the init segment at the beginning of the file, replacing the free box
|
||||
if _, err := mp4.FileWriter.WriteAt(buffer.Bytes(), 0); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Calculate the remaining size for the free box
|
||||
@@ -493,10 +467,10 @@ func (mp4 *MP4) Close(config *models.Config) {
|
||||
newFreeBox := mp4ff.NewFreeBox(make([]byte, remainingSize))
|
||||
var freeBuf bytes.Buffer
|
||||
if err := newFreeBox.Encode(&freeBuf); err != nil {
|
||||
panic(err)
|
||||
log.Log.Error("mp4.Close(): error encoding free box: " + err.Error())
|
||||
}
|
||||
if _, err := mp4.FileWriter.WriteAt(freeBuf.Bytes(), int64(buffer.Len())); err != nil {
|
||||
panic(err)
|
||||
log.Log.Error("mp4.Close(): error writing free box: " + err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -903,7 +877,7 @@ func SampleToAACSampleIndex(sampling int) int {
|
||||
return i
|
||||
}
|
||||
}
|
||||
panic("not Found AAC Sample Index")
|
||||
return -1
|
||||
}
|
||||
|
||||
func AACSampleIdxToSample(idx int) int {
|
||||
|
||||
@@ -281,7 +281,8 @@ func InitializeWebRTCConnection(configuration *models.Configuration, communicati
|
||||
candateBinary, err := json.Marshal(candateJSON)
|
||||
if err == nil {
|
||||
valueMap["candidate"] = string(candateBinary)
|
||||
valueMap["sdp"] = []byte(base64.StdEncoding.EncodeToString([]byte(answer.SDP)))
|
||||
// SDP is not needed to be send..
|
||||
//valueMap["sdp"] = []byte(base64.StdEncoding.EncodeToString([]byte(answer.SDP)))
|
||||
valueMap["session_id"] = handshake.SessionID
|
||||
} else {
|
||||
log.Log.Info("webrtc.main.InitializeWebRTCConnection(): something went wrong while marshalling candidate: " + err.Error())
|
||||
|
||||
@@ -1715,10 +1715,10 @@
|
||||
"@jridgewell/resolve-uri" "^3.0.3"
|
||||
"@jridgewell/sourcemap-codec" "^1.4.10"
|
||||
|
||||
"@kerberos-io/ui@^1.71.0":
|
||||
version "1.71.0"
|
||||
resolved "https://registry.yarnpkg.com/@kerberos-io/ui/-/ui-1.71.0.tgz#06914c94e8b0982068d2099acf8158917a511bfc"
|
||||
integrity sha512-pHCTn/iQTcQEPoCK82eJHGRn6BgzW3wgV4C+mNqdKOtLTquxL+vh7molEgC66tl3DGf7HyjSNa8LuoxYbt9TEg==
|
||||
"@kerberos-io/ui@^1.76.0":
|
||||
version "1.77.0"
|
||||
resolved "https://registry.yarnpkg.com/@kerberos-io/ui/-/ui-1.77.0.tgz#b748b2a9abf793ff2a9ba64ee41f84debc0ca9dc"
|
||||
integrity sha512-CHh4jeLKwrYvJRL5PM3UEN4p2k1fqwMKgSF2U6IR4v0fE2FwPc/2Ry4zGk6pvLDFHbDpR9jUkHX+iNphvStoyQ==
|
||||
dependencies:
|
||||
"@emotion/react" "^11.10.4"
|
||||
"@emotion/styled" "^11.10.4"
|
||||
|
||||
Reference in New Issue
Block a user