mirror of
https://github.com/kerberos-io/agent.git
synced 2026-11-05 20:20:23 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
553fd8e065 | ||
|
|
e1b886edd8 | ||
|
|
1445789694 |
@@ -1,30 +0,0 @@
|
||||
**
|
||||
!Dockerfile
|
||||
!machinery/
|
||||
!machinery/go.mod
|
||||
!machinery/go.sum
|
||||
!machinery/*.go
|
||||
!machinery/docs/
|
||||
!machinery/docs/**
|
||||
!machinery/src/
|
||||
!machinery/src/**
|
||||
!machinery/data/
|
||||
!machinery/data/config/
|
||||
!machinery/data/config/config.json
|
||||
!machinery/data/config/user.json
|
||||
!machinery/data/test-480p.mp4
|
||||
!ui/
|
||||
!ui/package.json
|
||||
!ui/yarn.lock
|
||||
!ui/.eslintrc.json
|
||||
!ui/.prettierrc.json
|
||||
!ui/public/
|
||||
!ui/public/**
|
||||
!ui/src/
|
||||
!ui/src/**
|
||||
**/.env
|
||||
**/.env.*
|
||||
**/*.pem
|
||||
**/*.key
|
||||
**/node_modules
|
||||
**/.git
|
||||
@@ -1,52 +0,0 @@
|
||||
name: Verify Agent production image
|
||||
description: Native media smoke, vulnerability and SBOM gates for the exact image to publish
|
||||
inputs:
|
||||
image:
|
||||
required: true
|
||||
description: Locally loaded image tag
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Functional least-privilege smoke
|
||||
shell: bash
|
||||
env:
|
||||
IMAGE: ${{ inputs.image }}
|
||||
run: bash image/smoke.sh "$IMAGE"
|
||||
- name: Complete vulnerability inventory
|
||||
uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # v0.33.1
|
||||
with:
|
||||
image-ref: ${{ inputs.image }}
|
||||
format: json
|
||||
output: image-vulnerabilities.json
|
||||
exit-code: '0'
|
||||
ignore-unfixed: false
|
||||
severity: UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL
|
||||
version: v0.69.3
|
||||
- name: Gate high and critical vulnerabilities including unfixed
|
||||
uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # v0.33.1
|
||||
with:
|
||||
image-ref: ${{ inputs.image }}
|
||||
format: table
|
||||
exit-code: '1'
|
||||
ignore-unfixed: false
|
||||
severity: HIGH,CRITICAL
|
||||
version: v0.69.3
|
||||
- name: Generate SPDX SBOM
|
||||
if: ${{ !cancelled() }}
|
||||
uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # v0.33.1
|
||||
with:
|
||||
image-ref: ${{ inputs.image }}
|
||||
format: spdx-json
|
||||
output: image.spdx.json
|
||||
version: v0.69.3
|
||||
- name: Upload image evidence
|
||||
if: ${{ !cancelled() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: agent-image-evidence-${{ runner.arch }}-${{ strategy.job-index }}
|
||||
path: |
|
||||
image-vulnerabilities.json
|
||||
image.spdx.json
|
||||
image-env-*.js
|
||||
image-expected-*.js
|
||||
if-no-files-found: error
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.26.10"
|
||||
go-version: "1.25.x"
|
||||
check-latest: true
|
||||
cache: true
|
||||
cache-dependency-path: machinery/go.sum
|
||||
@@ -34,8 +34,8 @@ jobs:
|
||||
- name: Get dependencies
|
||||
run: cd machinery && go mod download
|
||||
- name: Build
|
||||
run: cd machinery && go build -p=1 -v ./...
|
||||
run: cd machinery && go build -v ./...
|
||||
- name: Vet
|
||||
run: cd machinery && go vet -p=1 -v ./...
|
||||
run: cd machinery && go vet -v ./...
|
||||
- name: Test
|
||||
run: cd machinery && go test -p=1 -v ./...
|
||||
run: cd machinery && go test -v ./...
|
||||
|
||||
@@ -1,68 +1,62 @@
|
||||
name: Nightly build
|
||||
|
||||
on:
|
||||
# Triggers the workflow every day at 9PM (CET).
|
||||
schedule:
|
||||
- cron: "0 22 * * *"
|
||||
# Allows manual triggering from the Actions tab.
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ${{ matrix.runner }}
|
||||
nightly-build-amd64:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- architecture: amd64
|
||||
runner: ubuntu-24.04
|
||||
- architecture: arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
- architecture: arm/v7
|
||||
runner: ubuntu-24.04
|
||||
- architecture: arm/v6
|
||||
runner: ubuntu-24.04
|
||||
architecture: [amd64]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: docker/setup-qemu-action@v3
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- name: Build native media tests and image
|
||||
env:
|
||||
ARCHITECTURE: ${{ matrix.architecture }}
|
||||
run: |
|
||||
docker buildx build --platform "linux/$ARCHITECTURE" --target test .
|
||||
docker buildx build --platform "linux/$ARCHITECTURE" \
|
||||
--build-arg VERSION="${GITHUB_SHA:0:7}" --build-arg REVISION="$GITHUB_SHA" \
|
||||
--load -t agent-nightly:scan .
|
||||
- name: Verify exact image before publication
|
||||
uses: ./.github/actions/verify-image
|
||||
with:
|
||||
image: agent-nightly:scan
|
||||
- uses: docker/login-action@v3
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Push verified architecture
|
||||
env:
|
||||
ARCHITECTURE: ${{ matrix.architecture }}
|
||||
run: |
|
||||
target="kerberos/agent-nightly:arch-${ARCHITECTURE//\//-}-${GITHUB_SHA:0:7}"
|
||||
docker tag agent-nightly:scan "$target"
|
||||
docker push "$target"
|
||||
|
||||
manifest:
|
||||
needs: build
|
||||
runs-on: ubuntu-24.04
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: master
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Available platforms
|
||||
run: echo ${{ steps.buildx.outputs.platforms }}
|
||||
- name: Run Buildx
|
||||
run: docker buildx build --build-arg VERSION=$(echo $GITHUB_SHA | cut -c1-7) --platform linux/${{matrix.architecture}} -t kerberos/agent-nightly:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7) --push .
|
||||
- name: Create new and append to manifest
|
||||
run: docker buildx imagetools create -t kerberos/agent-nightly:$(echo $GITHUB_SHA | cut -c1-7) kerberos/agent-nightly:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7)
|
||||
nightly-build-other:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
architecture: [arm64, arm/v7, arm/v6]
|
||||
steps:
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- uses: docker/login-action@v3
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Promote only the complete verified architecture set
|
||||
run: |
|
||||
sources=()
|
||||
for architecture in amd64 arm64 arm-v7 arm-v6; do
|
||||
sources+=("kerberos/agent-nightly:arch-$architecture-${GITHUB_SHA:0:7}")
|
||||
done
|
||||
docker buildx imagetools create -t "kerberos/agent-nightly:${GITHUB_SHA:0:7}" "${sources[@]}"
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: master
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Available platforms
|
||||
run: echo ${{ steps.buildx.outputs.platforms }}
|
||||
- name: Run Buildx
|
||||
run: docker buildx build --build-arg VERSION=$(echo $GITHUB_SHA | cut -c1-7) --platform linux/${{matrix.architecture}} -t kerberos/agent-nightly:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7) --push .
|
||||
- name: Create new and append to manifest
|
||||
run: docker buildx imagetools create --append -t kerberos/agent-nightly:$(echo $GITHUB_SHA | cut -c1-7) kerberos/agent-nightly:arch-$(echo ${{matrix.architecture}} | tr / -)-$(echo $GITHUB_SHA | cut -c1-7)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: Build pull request
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
types: [opened, synchronize]
|
||||
|
||||
env:
|
||||
REPO: kerberos/agent
|
||||
@@ -12,7 +12,6 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- architecture: amd64
|
||||
@@ -23,21 +22,12 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Run Build
|
||||
env:
|
||||
REVISION: ${{ github.event.pull_request.head.sha }}
|
||||
run: |
|
||||
docker build --target test .
|
||||
docker build --build-arg VERSION="${REVISION:0:7}" --build-arg REVISION="$REVISION" -t ${{ matrix.architecture }} .
|
||||
- name: Verify exact image
|
||||
uses: ./.github/actions/verify-image
|
||||
with:
|
||||
image: ${{ matrix.architecture }}
|
||||
- name: Extract verified release bundle
|
||||
run: |
|
||||
docker build --build-arg VERSION=$(echo "${{ github.event.pull_request.head.sha }}" | cut -c1-7) -t ${{ matrix.architecture }} .
|
||||
CID=$(docker create ${{matrix.architecture}})
|
||||
docker cp ${CID}:/home/agent ./output-${{matrix.architecture}}
|
||||
docker rm ${CID}
|
||||
- name: Package binary and runtime assets
|
||||
- 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
|
||||
|
||||
@@ -29,21 +29,12 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Run Build
|
||||
env:
|
||||
VERSION: ${{ github.event.inputs.tag || github.ref_name }}
|
||||
run: |
|
||||
docker build --target test .
|
||||
docker build --build-arg VERSION="$VERSION" --build-arg REVISION="$GITHUB_SHA" -t ${{matrix.architecture}} .
|
||||
- name: Verify exact image before publication
|
||||
uses: ./.github/actions/verify-image
|
||||
with:
|
||||
image: ${{ matrix.architecture }}
|
||||
- name: Extract verified release bundle
|
||||
run: |
|
||||
docker build --provenance=false --build-arg VERSION=${{github.event.inputs.tag || github.ref_name}} -t ${{matrix.architecture}} .
|
||||
CID=$(docker create ${{matrix.architecture}})
|
||||
docker cp ${CID}:/home/agent ./output-${{matrix.architecture}}
|
||||
docker rm ${CID}
|
||||
- name: Package binary and runtime assets
|
||||
- name: Strip binary
|
||||
run: tar -cf agent-${{matrix.architecture}}.tar -C output-${{matrix.architecture}} . && rm -rf output-${{matrix.architecture}}
|
||||
- name: Build and push Docker image
|
||||
run: |
|
||||
@@ -71,21 +62,12 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Run Build
|
||||
env:
|
||||
VERSION: ${{ github.event.inputs.tag || github.ref_name }}
|
||||
run: |
|
||||
docker build --target test .
|
||||
docker build --build-arg VERSION="$VERSION" --build-arg REVISION="$GITHUB_SHA" -t ${{matrix.architecture}} .
|
||||
- name: Verify exact image before publication
|
||||
uses: ./.github/actions/verify-image
|
||||
with:
|
||||
image: ${{ matrix.architecture }}
|
||||
- name: Extract verified release bundle
|
||||
run: |
|
||||
docker build --provenance=false --build-arg VERSION=${{github.event.inputs.tag || github.ref_name}} -t ${{matrix.architecture}} .
|
||||
CID=$(docker create ${{matrix.architecture}})
|
||||
docker cp ${CID}:/home/agent ./output-${{matrix.architecture}}
|
||||
docker rm ${CID}
|
||||
- name: Package binary and runtime assets
|
||||
- name: Strip binary
|
||||
run: tar -cf agent-${{matrix.architecture}}.tar -C output-${{matrix.architecture}} . && rm -rf output-${{matrix.architecture}}
|
||||
- name: Build and push Docker image
|
||||
run: |
|
||||
@@ -131,7 +113,7 @@ jobs:
|
||||
- name: Create a release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
makeLatest: true
|
||||
latest: true
|
||||
allowUpdates: true
|
||||
name: ${{ github.event.inputs.tag || github.ref_name }}
|
||||
tag: ${{ github.event.inputs.tag || github.ref_name }}
|
||||
|
||||
+1
-2
@@ -17,5 +17,4 @@ machinery/vendor
|
||||
machinery/go.work
|
||||
machinery/go.work.sum
|
||||
deployments/docker/private-docker-compose.yaml
|
||||
video.mp4
|
||||
.checks/
|
||||
video.mp4
|
||||
+145
-76
@@ -1,86 +1,155 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
FROM golang:1.26.10-trixie@sha256:909771fef53e3dcb0950255f0c9a8b4728c3b841e61509c7b29b098de7ee02d4 AS build-machinery
|
||||
|
||||
ARG GO_IMAGE=golang:1.25-trixie
|
||||
ARG RUNTIME_IMAGE=debian:trixie-slim
|
||||
ARG VERSION=0.0.0
|
||||
FROM ${GO_IMAGE} AS build-machinery
|
||||
LABEL AUTHOR=uug.ai
|
||||
|
||||
# Re-declare VERSION inside this stage so the value passed via
|
||||
# `--build-arg VERSION=...` (e.g. the release tag) is available below.
|
||||
# ARGs declared before the first FROM are not visible inside build stages.
|
||||
ARG VERSION
|
||||
ARG TARGETARCH
|
||||
ENV GOTOOLCHAIN=local CGO_ENABLED=1
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
pkg-config libavcodec-dev libavutil-dev libswscale-dev && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
WORKDIR /src/machinery
|
||||
|
||||
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 update && apt-get install -y --fix-missing --no-install-recommends \
|
||||
git build-essential cmake pkg-config unzip libgtk2.0-dev \
|
||||
curl ca-certificates libavcodec-dev libavutil-dev libcurl4-openssl-dev \
|
||||
libssl-dev libjpeg62-turbo-dev libswscale-dev && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
##############################################################################
|
||||
# Copy dependency metadata first so module downloads can be cached separately.
|
||||
|
||||
RUN mkdir -p /go/src/github.com/kerberos-io/agent
|
||||
WORKDIR /go/src/github.com/kerberos-io/agent/machinery
|
||||
COPY machinery/go.mod machinery/go.sum ./
|
||||
RUN --mount=type=cache,target=/go/pkg/mod,sharing=locked \
|
||||
go mod download && go mod verify
|
||||
COPY machinery/*.go ./
|
||||
COPY machinery/docs ./docs
|
||||
COPY machinery/src ./src
|
||||
RUN --mount=type=cache,target=/go/pkg/mod,sharing=locked \
|
||||
--mount=type=cache,target=/root/.cache/go-build,sharing=locked \
|
||||
BUILD_TAGS=timetzdata,netgo,osusergo && \
|
||||
case "${TARGETARCH:-$(go env GOARCH)}" in amd64|arm64) BUILD_TAGS="moq,${BUILD_TAGS}" ;; esac && \
|
||||
go build -mod=readonly -trimpath -buildvcs=false -tags "${BUILD_TAGS}" \
|
||||
-ldflags="-w -buildid= -X github.com/kerberos-io/agent/machinery/src/utils.VERSION=${VERSION}" \
|
||||
-o /out/main . && \
|
||||
printf '%s' "${VERSION}" > /out/version && /out/main -action version
|
||||
RUN go mod download
|
||||
|
||||
FROM build-machinery AS test
|
||||
RUN --mount=type=cache,target=/go/pkg/mod,sharing=locked \
|
||||
--mount=type=cache,target=/root/.cache/go-build,sharing=locked \
|
||||
BUILD_TAGS=timetzdata,netgo,osusergo && \
|
||||
case "${TARGETARCH:-$(go env GOARCH)}" in amd64|arm64) BUILD_TAGS="moq,${BUILD_TAGS}" ;; esac && \
|
||||
go vet -tags "${BUILD_TAGS}" ./... && go test -p=1 -tags "${BUILD_TAGS}" ./... && \
|
||||
go run golang.org/x/vuln/cmd/govulncheck@v1.9.0 -tags "${BUILD_TAGS}" ./... && \
|
||||
go run golang.org/x/vuln/cmd/govulncheck@v1.9.0 -mode=binary /out/main
|
||||
##############################################################################
|
||||
# Copy the rest of the source after dependencies are primed.
|
||||
|
||||
FROM node:22-bookworm-slim@sha256:de170b39439eb0e5a3444ad7d60b015614a381e10fecae2e30ec9e735122f38d AS build-ui
|
||||
WORKDIR /src/ui
|
||||
COPY machinery ./
|
||||
RUN rm -rf .env
|
||||
|
||||
##################
|
||||
# Build Machinery
|
||||
|
||||
RUN RESOLVED_VERSION="${VERSION:-0.0.0}" && \
|
||||
printf '%s' "${RESOLVED_VERSION}" > version && \
|
||||
BUILD_TAGS=timetzdata,netgo,osusergo && \
|
||||
case "${TARGETARCH:-$(go env GOARCH)}" in amd64|arm64) BUILD_TAGS="moq,${BUILD_TAGS}" ;; esac && \
|
||||
go build -o main -tags "${BUILD_TAGS}" --ldflags "-s -w -X github.com/kerberos-io/agent/machinery/src/utils.VERSION=${RESOLVED_VERSION}" . && \
|
||||
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:22-alpine 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
|
||||
WORKDIR /go/src/github.com/kerberos-io/agent/ui
|
||||
COPY ui/package.json ui/yarn.lock ./
|
||||
RUN yarn install --frozen-lockfile --non-interactive
|
||||
COPY ui/public ./public
|
||||
COPY ui/src ./src
|
||||
COPY ui/.eslintrc.json ui/.prettierrc.json ./
|
||||
RUN mkdir -p /src/machinery && yarn build && test -s /src/machinery/www/index.html
|
||||
RUN yarn config set network-timeout 300000 && yarn --frozen-lockfile
|
||||
COPY ui ./
|
||||
RUN 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 ${RUNTIME_IMAGE}
|
||||
|
||||
############################
|
||||
# Protect by non-root user.
|
||||
|
||||
# Native FFmpeg capture/transcoding and the opt-in remote terminal require
|
||||
# glibc, codecs and /bin/sh. Keep this media exception, not a scratch image.
|
||||
FROM debian:trixie-slim@sha256:62bd423aeee912639ca8ee681c2b762cb3e25137e0a92b20be455898a9f034ad AS runtime-files
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates curl ffmpeg libatomic1 libstdc++6 && \
|
||||
rm -rf /var/lib/apt/lists/* /var/cache/apt/* && \
|
||||
groupadd --gid 65532 kerberosio && \
|
||||
useradd --uid 65532 --gid 65532 --groups video --home-dir /home/agent \
|
||||
--no-create-home --shell /bin/sh agent && \
|
||||
find / -xdev -type f -perm /6000 -exec chmod a-s {} + && \
|
||||
rm -f /usr/bin/apt /usr/bin/apt-* /usr/bin/dpkg /usr/bin/dpkg-* \
|
||||
/usr/sbin/dpkg-* /usr/sbin/useradd /usr/sbin/groupadd \
|
||||
/usr/sbin/usermod /usr/sbin/userdel /usr/sbin/groupmod /usr/sbin/groupdel && \
|
||||
rm -rf /usr/lib/apt /usr/lib/dpkg && \
|
||||
install -d -o 0 -g 0 -m 0755 /home/agent && \
|
||||
install -d -o 65532 -g 65532 -m 0750 \
|
||||
/home/agent/data /home/agent/data/cloud /home/agent/data/snapshots \
|
||||
/home/agent/data/log /home/agent/data/recordings /home/agent/data/capture-test \
|
||||
/home/agent/data/config /home/agent/data/tus
|
||||
COPY --chown=65532:65532 --chmod=0640 machinery/data/config/config.json machinery/data/config/user.json /home/agent/data/config/
|
||||
COPY --chown=0:0 --chmod=0444 machinery/data/config/config.json /home/agent/data/config.template.json
|
||||
COPY --chown=0:0 --chmod=0444 machinery/data/test-480p.mp4 /home/agent/data/test-480p.mp4
|
||||
ca-certificates curl ffmpeg libatomic1 libcap2-bin libstdc++6 && \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
groupadd --system kerberosio && \
|
||||
useradd --system --gid kerberosio --groups video --create-home agent && \
|
||||
chmod 0755 /home/agent
|
||||
|
||||
FROM runtime-files
|
||||
ARG VERSION=0.0.0
|
||||
ARG REVISION=unknown
|
||||
ARG CREATED
|
||||
LABEL org.opencontainers.image.title="agent" \
|
||||
org.opencontainers.image.description="Kerberos edge camera and media agent" \
|
||||
org.opencontainers.image.source="https://github.com/kerberos-io/agent" \
|
||||
org.opencontainers.image.version="${VERSION}" \
|
||||
org.opencontainers.image.revision="${REVISION}" \
|
||||
org.opencontainers.image.created="${CREATED}" \
|
||||
org.opencontainers.image.vendor="UUG.AI"
|
||||
COPY --from=build-machinery --chown=0:0 --chmod=0555 /out/main /home/agent/main
|
||||
COPY --from=build-machinery --chown=0:0 --chmod=0444 /out/version /home/agent/version
|
||||
COPY --from=build-ui --chown=0:0 /src/machinery/www /home/agent/www
|
||||
COPY --chown=0:0 --chmod=0444 ui/package.json ui/yarn.lock /usr/share/agent-ui/
|
||||
USER 65532:65532
|
||||
#################################
|
||||
# Copy files from previous images
|
||||
|
||||
COPY --chown=0:0 --from=build-machinery /dist /
|
||||
COPY --chown=0:0 --from=build-ui /dist /
|
||||
|
||||
##################
|
||||
# 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 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:${AGENT_PORT:-80}/health" || exit 1
|
||||
|
||||
###################################################
|
||||
# Leeeeettttt'ssss goooooo!!!
|
||||
# Run the shizzle from the right working directory.
|
||||
WORKDIR /home/agent
|
||||
EXPOSE 80/tcp
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s \
|
||||
CMD curl --fail --silent "http://localhost:${AGENT_PORT:-80}/health" || exit 1
|
||||
CMD ["./main", "-action", "run"]
|
||||
CMD ["./main", "-action", "run"]
|
||||
@@ -384,6 +384,7 @@ See [RTSPS and TLS certificates](README-RTSPS-TLS.md) for the complete Bosch UI,
|
||||
| `AGENT_CAPTURE_IPCAMERA_ONVIF_XADDR` | ONVIF endpoint/address running on the camera. | "" |
|
||||
| `AGENT_CAPTURE_IPCAMERA_ONVIF_USERNAME` | ONVIF username to authenticate against. | "" |
|
||||
| `AGENT_CAPTURE_IPCAMERA_ONVIF_PASSWORD` | ONVIF password to authenticate against. | "" |
|
||||
| `AGENT_CAPTURE_IPCAMERA_ONVIF_CHANNEL` | 1-based channel on multi-channel ONVIF devices (encoders, NVRs) used for PTZ, presets and focus. | "0" (first profile) |
|
||||
| `AGENT_CAPTURE_MOTION` | Toggle for enabling or disabling motion. | "true" |
|
||||
| `AGENT_CAPTURE_LIVEVIEW` | Toggle for enabling or disabling liveview. | "true" |
|
||||
| `AGENT_CAPTURE_SNAPSHOTS` | Toggle for enabling or disabling snapshot generation. | "true" |
|
||||
|
||||
-151
@@ -38,154 +38,3 @@ If you do not receive a response in time, please resend your report and include
|
||||
## Disclosure and Credits
|
||||
|
||||
We follow coordinated disclosure. After a fix is available, we will credit reporters unless they prefer to stay anonymous.
|
||||
|
||||
## Production container contract
|
||||
|
||||
The image follows the [image hardening guide](https://learn.uug.ai/guides/security/image-hardening/)
|
||||
with explicit media/device exceptions. A Dockerfile change alone is not evidence
|
||||
that an image is hardened: native architecture builds, functional smoke tests,
|
||||
vulnerability reports and an SBOM must pass before release.
|
||||
|
||||
- Build and runtime bases are pinned to verified multi-platform digests. Refresh
|
||||
them regularly; pins do not deliver security updates automatically.
|
||||
- Public Go modules retain checksum verification. No build token is needed.
|
||||
Building machinery requires Go 1.26 or newer; CI uses Go 1.26.10.
|
||||
Go retains CGO for FFmpeg capture and uses `-w -buildid=` without `-s`, preserving
|
||||
symbols for binary vulnerability analysis.
|
||||
- The fixed runtime identity is `65532:65532`; `/home/agent/main`, `version` and
|
||||
`www` are root-owned and not writable by the process. Demo UI configuration is
|
||||
served without renaming or modifying packaged assets.
|
||||
- `/home/agent/data` remains the persistent writable path, including `config`,
|
||||
`cloud`, `snapshots`, `log`, `recordings`, `capture-test` and `tus`. Existing
|
||||
bind mounts/PVCs must grant UID/GID 65532 access before rollout. Do not change
|
||||
their paths or discard existing configuration/recordings.
|
||||
- The existing `./main -action run` command, `/health`, UI and port **80** remain.
|
||||
The executable has no file capabilities. For port 80 without capabilities,
|
||||
set `net.ipv4.ip_unprivileged_port_start=0` in the container network namespace
|
||||
(Docker normally defaults to this). Alternatively set `AGENT_PORT=8080` and
|
||||
update the service/container port mapping. Kubernetes policy must permit the
|
||||
chosen sysctl, or use the unprivileged port override.
|
||||
- Preserve required camera/audio device mappings and add the host device's
|
||||
numeric supplemental GID (`--group-add` or Kubernetes `supplementalGroups`).
|
||||
Do not use privileged mode or make device nodes world-writable. The image
|
||||
retains its `video` group; host device GIDs are not portable.
|
||||
- Debian's glibc and FFmpeg codecs are required by native capture and media
|
||||
processing. `/bin/sh` is retained for the product's optional MQTT remote
|
||||
terminal, and curl for its existing healthcheck. This is a documented
|
||||
media/remote-terminal exception to the shell-free distroless preference, not
|
||||
a general permission to add debugging tools. Package-manager executables,
|
||||
compiler toolchains and special setuid/setgid permission bits are removed.
|
||||
Do not claim complete deployment CIS compliance.
|
||||
|
||||
### Verification and release gates
|
||||
|
||||
PR, release and nightly jobs build the `test` target (Go tests/vet plus source
|
||||
and binary govulncheck), then verify the exact image before it can be published.
|
||||
The local `verify-image` action runs `image/smoke.sh`, records all severities,
|
||||
blocks HIGH/CRITICAL findings including unfixed findings, and uploads an SPDX
|
||||
SBOM. There is no blanket ignore list or non-blocking release scan.
|
||||
|
||||
The smoke uses `--read-only --cap-drop ALL --security-opt no-new-privileges`,
|
||||
a Docker volume initialized from the packaged data defaults, and the low-port
|
||||
sysctl. It checks default production startup, demo mode with `AGENT_PORT=8080`,
|
||||
health/UI/config responses, writable data, immutable assets, FFmpeg frame
|
||||
extraction and ffprobe. Hardware/RTSP camera and MQTT remote-terminal integration
|
||||
still require representative device-backed validation; synthetic media is not
|
||||
proof of hardware compatibility.
|
||||
|
||||
Run on a native Docker host:
|
||||
|
||||
```sh
|
||||
docker build --target test .
|
||||
docker build -t agent:validation .
|
||||
bash image/smoke.sh agent:validation
|
||||
```
|
||||
|
||||
Shared workflow integration remains a release prerequisite: the shared
|
||||
`worker`/`frontend` profiles do **not** match this image. An Agent profile must
|
||||
preserve `/home/agent`, its command/port/data/device contract, allow the explicit
|
||||
media/shell/healthcheck exceptions, publish to `kerberos/agent`, and retain the
|
||||
downloadable release tar assets. Centralized digest-bound signing, provenance
|
||||
and published-image verification must be integrated before treating this
|
||||
rollout as fully validated. Do not substitute the incompatible worker profile.
|
||||
|
||||
Native release validation targets AMD64 and ARM64. The legacy nightly ARMv6
|
||||
matrix is currently blocked: the pinned Go and Node upstream indexes provide
|
||||
ARMv7, not ARMv6 variants. The complete nightly manifest must therefore remain
|
||||
blocked rather than silently publishing an incomplete architecture set. ARMv6
|
||||
needs an independently validated cross-build/media-runtime strategy before its
|
||||
support can be claimed; do not resolve the failure by weakening the gates.
|
||||
|
||||
### Current native validation blocker
|
||||
|
||||
The ARM64 build and read-only/cap-drop production, demo and FFmpeg smoke passed
|
||||
in [CI run 40410591639](https://github.com/kerberos-io/agent/actions/runs/40410591639).
|
||||
The strict vulnerability gate then failed: the refreshed Debian 13.7 runtime
|
||||
reported 212 HIGH/CRITICAL package findings, including FFmpeg 7.1.5 and curl
|
||||
8.14.1, with no fixed Debian versions reported. The run uploaded the complete
|
||||
inventory and SPDX SBOM in `agent-image-evidence-ARM64-1`. This is **not** a
|
||||
security-clean image or permission to ship it; resolving these native-library
|
||||
findings without dropping required capture/codecs remains a rollout blocker.
|
||||
|
||||
### Supported-base alternative investigation (2026-10-26)
|
||||
|
||||
Compared the current **Debian 13 Trixie** runtime against **Debian 12.15
|
||||
Bookworm LTS**, using the [Debian security tracker JSON](https://security-tracker.debian.org/tracker/data/json)
|
||||
and the exact 44 distinct HIGH/CRITICAL CVEs from the native image report.
|
||||
[Bookworm remains supported until 2028-06-30](https://www.debian.org/releases/bookworm/)
|
||||
on its listed LTS architectures. Its current FFmpeg is
|
||||
`7:5.1.9-0+deb12u1`, curl is `7.88.1-10+deb12u15`, and libxml2 is
|
||||
`2.9.14+dfsg-1.3~deb12u6`.
|
||||
|
||||
This supported alternative does **not** clear the gate: **42 of the 44 CVEs
|
||||
remain open**, including the critical libxml2 `CVE-2026-6653`, both curl CVEs,
|
||||
and 15 of the 16 FFmpeg CVEs. Bookworm is marked unaffected (`fixed_version=0`)
|
||||
for FFmpeg `CVE-2026-66041` and libxml2 `CVE-2026-86139`; that improvement is
|
||||
not sufficient to release.
|
||||
|
||||
Bookworm is also not a drop-in runtime replacement: this CGO binary is linked
|
||||
to FFmpeg 7's `libavcodec.so.61`/`libavutil.so.59`, while Bookworm's FFmpeg 5
|
||||
uses different SONAMEs (`libavcodec.so.59`/`libavutil.so.57`). Both the Go/native
|
||||
builder and runtime would require a matched rebuild and codec/device regression
|
||||
validation. No Bookworm image was built or claimed compatible after the
|
||||
security metadata already demonstrated the unresolved blockers. Bookworm LTS
|
||||
also no longer covers armel; it is not a silent solution for the retained ARMv6
|
||||
requirement.
|
||||
|
||||
The tracker reports FFmpeg fixes in `7:9.0.2-1`, curl fixes in
|
||||
`8.21.0~rc2-1`/`8.21.0~rc3-1`, and the critical libxml2 fix in
|
||||
`2.14.5+dfsg-0.1` in **Forky**. However,
|
||||
[Forky is testing, without timely Security Team support](https://www.debian.org/releases/forky/),
|
||||
not a supported stable-base upgrade; cjson, libsndfile, libX11 and libXrender
|
||||
blockers also remain open there. It was not substituted into production.
|
||||
Dropping unused administration tools alone cannot resolve the required FFmpeg
|
||||
decoder/parser findings. No codec, device integration, remote terminal or
|
||||
ARMv6 target was removed to obtain a clean scan.
|
||||
|
||||
The required decision is whether to wait for supported-distribution fixes or
|
||||
approve an explicitly maintained native-package/backport source or supported
|
||||
media image, with ownership for security updates and validation across the
|
||||
retained architectures. Such a change needs fresh matching native inventory,
|
||||
image scans and camera/codec/device tests; it is not authorization to ignore
|
||||
these findings or switch to an unsupported testing distribution.
|
||||
|
||||
#### Exact blocking native CVEs
|
||||
|
||||
These are the source components behind the 212 package findings in Debian 13.7;
|
||||
several FFmpeg binaries/libraries share the same source advisories:
|
||||
|
||||
| Source component | CVEs |
|
||||
| --- | --- |
|
||||
| util-linux | CVE-2026-76642, CVE-2026-78408, CVE-2026-78409, CVE-2026-78410 |
|
||||
| curl | CVE-2026-12064, CVE-2026-8458 |
|
||||
| ffmpeg | CVE-2026-58049, CVE-2026-64830, CVE-2026-64832, CVE-2026-64833, CVE-2026-64834, CVE-2026-64835, CVE-2026-66036, CVE-2026-66039, CVE-2026-66040, CVE-2026-66041, CVE-2026-70628, CVE-2026-70632, CVE-2026-75142, CVE-2026-75143, CVE-2026-75144, CVE-2026-75146 |
|
||||
| acl | CVE-2026-54369 |
|
||||
| cjson | CVE-2026-16554, CVE-2026-29036, CVE-2026-67215, CVE-2026-67216, CVE-2026-87933 |
|
||||
| ncurses | CVE-2025-69720 |
|
||||
| libsndfile | CVE-2026-37555 |
|
||||
| systemd | CVE-2026-16742 |
|
||||
| tiff | CVE-2026-36849, CVE-2026-52490 |
|
||||
| libX11 | CVE-2026-88806 |
|
||||
| libxml2 | CVE-2026-6653 (CRITICAL), CVE-2026-74860, CVE-2026-86138, CVE-2026-86139, CVE-2026-86140, CVE-2026-86142, CVE-2026-86143, CVE-2026-86144 |
|
||||
| libXrender | CVE-2026-88807 |
|
||||
| perl | CVE-2026-9538 |
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
image=${1:?usage: image/smoke.sh IMAGE}
|
||||
test "$(docker image inspect "$image" --format '{{.Config.User}}')" = 65532:65532
|
||||
test "$(docker image inspect "$image" --format '{{json .Config.Cmd}}')" = '["./main","-action","run"]'
|
||||
test "$(docker image inspect "$image" --format '{{.Config.WorkingDir}}')" = /home/agent
|
||||
docker run --rm --read-only --cap-drop ALL --security-opt no-new-privileges \
|
||||
--entrypoint /bin/sh "$image" -ec '
|
||||
test "$(id -u)" = 65532
|
||||
test "$(stat -c %u:%g /home/agent/main)" = 0:0
|
||||
test ! -w /home/agent/main
|
||||
test ! -w /home/agent/www
|
||||
test ! -e /usr/bin/apt
|
||||
test ! -e /usr/bin/dpkg
|
||||
test -z "$(find / -xdev -type f -perm /6000 -print 2>/dev/null)"
|
||||
./main -action version
|
||||
ffmpeg -version
|
||||
ffprobe -version
|
||||
'
|
||||
container=
|
||||
volume=
|
||||
cleanup() {
|
||||
if [[ -n "$container" ]]; then
|
||||
docker logs "$container" || true
|
||||
docker rm -f "$container" >/dev/null
|
||||
fi
|
||||
if [[ -n "$volume" ]]; then docker volume rm "$volume" >/dev/null; fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
for mode in production demo; do
|
||||
volume=$(docker volume create)
|
||||
args=()
|
||||
port=80
|
||||
if [[ "$mode" == demo ]]; then
|
||||
port=8080
|
||||
args+=(-e AGENT_MODE=demo -e AGENT_PORT=8080)
|
||||
fi
|
||||
container=$(docker run -d --read-only --cap-drop ALL \
|
||||
--security-opt no-new-privileges \
|
||||
--sysctl net.ipv4.ip_unprivileged_port_start=0 \
|
||||
--mount "type=volume,src=$volume,dst=/home/agent/data" \
|
||||
-p "127.0.0.1::$port" "${args[@]}" "$image")
|
||||
address=$(docker port "$container" "$port/tcp")
|
||||
healthy=false
|
||||
for attempt in {1..60}; do
|
||||
if curl --fail --silent "http://$address/health" >/dev/null; then
|
||||
healthy=true
|
||||
break
|
||||
fi
|
||||
test "$(docker inspect "$container" --format '{{.State.Running}}')" = true
|
||||
sleep 2
|
||||
done
|
||||
test "$healthy" = true
|
||||
curl --fail --silent "http://$address/" | grep -q '<html'
|
||||
curl --fail --silent "http://$address/env.js" > "image-env-$mode.js"
|
||||
docker cp "$container:/home/agent/www/env.$([[ "$mode" == demo ]] && echo demo. || true)js" "image-expected-$mode.js"
|
||||
cmp "image-env-$mode.js" "image-expected-$mode.js"
|
||||
docker exec "$container" /bin/sh -ec '
|
||||
test -w data/config/config.json
|
||||
test -w data/config/user.json
|
||||
ffmpeg -nostdin -v error -i data/test-480p.mp4 -frames:v 1 data/capture-test/smoke.jpg
|
||||
test -s data/capture-test/smoke.jpg
|
||||
ffprobe -v error -show_entries stream=codec_type data/test-480p.mp4
|
||||
for path in cloud snapshots log recordings capture-test config tus; do
|
||||
test -w "data/$path"
|
||||
done
|
||||
test ! -w www/env.js
|
||||
'
|
||||
cleanup
|
||||
container=
|
||||
volume=
|
||||
done
|
||||
+26
-27
@@ -1,6 +1,6 @@
|
||||
module github.com/kerberos-io/agent/machinery
|
||||
|
||||
go 1.26.0
|
||||
go 1.25.0
|
||||
|
||||
replace google.golang.org/genproto => google.golang.org/genproto v0.0.0-20250519155744-55703ea1f237
|
||||
|
||||
@@ -14,7 +14,7 @@ require (
|
||||
github.com/creack/pty v1.1.24
|
||||
github.com/dromara/carbon/v2 v2.6.8
|
||||
github.com/dropbox/dropbox-sdk-go-unofficial/v6 v6.0.5
|
||||
github.com/eclipse/paho.mqtt.golang v1.5.1
|
||||
github.com/eclipse/paho.mqtt.golang v1.5.0
|
||||
github.com/elastic/go-sysinfo v1.15.3
|
||||
github.com/gin-contrib/cors v1.7.5
|
||||
github.com/gin-contrib/pprof v1.5.3
|
||||
@@ -39,13 +39,12 @@ require (
|
||||
github.com/swaggo/swag v1.16.4
|
||||
github.com/tevino/abool v1.2.0
|
||||
github.com/zaf/g711 v1.4.0
|
||||
go.mongodb.org/mongo-driver v1.17.7
|
||||
go.opentelemetry.io/otel v1.45.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.45.0
|
||||
go.opentelemetry.io/otel/sdk v1.45.0
|
||||
go.opentelemetry.io/otel/trace v1.45.0
|
||||
golang.org/x/net v0.60.0
|
||||
go.mongodb.org/mongo-driver v1.17.3
|
||||
go.opentelemetry.io/otel v1.36.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0
|
||||
go.opentelemetry.io/otel/sdk v1.36.0
|
||||
go.opentelemetry.io/otel/trace v1.36.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -56,8 +55,7 @@ require (
|
||||
github.com/bluenviron/mediacommon/v2 v2.9.2 // indirect
|
||||
github.com/bytedance/sonic v1.13.2 // indirect
|
||||
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
|
||||
github.com/clbanning/mxj v1.8.4 // indirect
|
||||
github.com/clbanning/mxj/v2 v2.7.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||
@@ -67,7 +65,7 @@ require (
|
||||
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/gin-contrib/sse v1.0.0 // indirect
|
||||
github.com/go-logr/logr v1.4.4 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
github.com/go-openapi/jsonreference v0.19.6 // indirect
|
||||
@@ -79,11 +77,11 @@ require (
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/juju/errors v1.0.0 // indirect
|
||||
github.com/klauspost/compress v1.18.7 // indirect
|
||||
github.com/klauspost/compress v1.16.7 // indirect
|
||||
github.com/klauspost/cpuid v1.2.3 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||
github.com/kylelemons/go-gypsy v1.0.0 // indirect
|
||||
@@ -123,21 +121,22 @@ require (
|
||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||
github.com/ziutek/mymysql v1.5.4 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.45.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.11.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.36.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.6.0 // indirect
|
||||
golang.org/x/arch v0.16.0 // indirect
|
||||
golang.org/x/crypto v0.57.0 // indirect
|
||||
golang.org/x/oauth2 v0.36.0 // indirect
|
||||
golang.org/x/sync v0.23.0 // indirect
|
||||
golang.org/x/sys v0.48.0 // indirect
|
||||
golang.org/x/text v0.42.0 // indirect
|
||||
golang.org/x/crypto v0.54.0 // indirect
|
||||
golang.org/x/net v0.57.0 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/sync v0.22.0 // indirect
|
||||
golang.org/x/sys v0.47.0 // indirect
|
||||
golang.org/x/text v0.40.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/tools v0.49.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d // indirect
|
||||
google.golang.org/grpc v1.83.2 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
golang.org/x/tools v0.47.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect
|
||||
google.golang.org/grpc v1.72.1 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
gopkg.in/ini.v1 v1.42.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
||||
+49
-59
@@ -408,15 +408,14 @@ github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCN
|
||||
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/cedricve/go-onvif v0.0.0-20200222191200-567e8ce298f6 h1:bzFZYgZD5vf4PWaa2GjOh90HG88uKi2a+B6VnQcDlCA=
|
||||
github.com/cedricve/go-onvif v0.0.0-20200222191200-567e8ce298f6/go.mod h1:nBrjN2nMHendp0Cvb/6GaJ1v92Qv/kzqxWtNBnKJEK0=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
|
||||
github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY=
|
||||
@@ -469,8 +468,8 @@ github.com/dropbox/dropbox-sdk-go-unofficial/v6 v6.0.5 h1:FT+t0UEDykcor4y3dMVKXI
|
||||
github.com/dropbox/dropbox-sdk-go-unofficial/v6 v6.0.5/go.mod h1:rSS3kM9XMzSQ6pw91Qgd6yB5jdt70N4OdtrAf74As5M=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE=
|
||||
github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU=
|
||||
github.com/eclipse/paho.mqtt.golang v1.5.0 h1:EH+bUVJNgttidWFkLLVKaQPGmkTUfQQqjOsyvMGvD6o=
|
||||
github.com/eclipse/paho.mqtt.golang v1.5.0/go.mod h1:du/2qNQVqJf/Sqs4MEL77kR8QTqANF7XU7Fk0aOTAgk=
|
||||
github.com/elastic/go-sysinfo v1.15.3 h1:W+RnmhKFkqPTCRoFq2VCTmsT4p/fwpo+3gKNQsn1XU0=
|
||||
github.com/elastic/go-sysinfo v1.15.3/go.mod h1:K/cNrqYTDrSoMh2oDkYEMS2+a72GRxMvNP+GC+vRIlo=
|
||||
github.com/elastic/go-windows v1.0.2 h1:yoLLsAsV5cfg9FLhZ9EXZ2n2sQFKeDYrHenkcivY4vI=
|
||||
@@ -549,9 +548,8 @@ github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpx
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
|
||||
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
@@ -748,8 +746,8 @@ github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFb
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
|
||||
github.com/hamba/avro/v2 v2.17.2/go.mod h1:Q9YK+qxAhtVrNqOhwlZTATLgLA8qxG2vtvkhK8fJ7Jo=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
@@ -783,9 +781,8 @@ github.com/kerberos-io/onvif v1.2.3/go.mod h1:XSgEQXmEDjUQTbdXvsaRJt6Az8YPGj7L+j
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE=
|
||||
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
|
||||
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/klauspost/compress v1.18.7 h1:aUyZsS4kH3QTKurYhAOwAHxllVPnOthb3vPfnF1Ehjw=
|
||||
github.com/klauspost/compress v1.18.7/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/klauspost/cpuid v1.2.3 h1:CCtW0xUnWGVINKvE/WWOYKdsPV6mawAtvQuSl8guwQs=
|
||||
github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
@@ -927,9 +924,8 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
|
||||
github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk=
|
||||
github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo=
|
||||
@@ -1013,8 +1009,8 @@ github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs=
|
||||
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
|
||||
go.einride.tech/aip v0.67.1/go.mod h1:ZGX4/zKw8dcgzdLsrvpOOGxfxI2QSk12SlP7d6c0/XI=
|
||||
go.einride.tech/aip v0.68.1/go.mod h1:XaFtaj4HuA3Zwk9xoBtTWgNubZ0ZZXv9BZJCkuKuWbg=
|
||||
go.mongodb.org/mongo-driver v1.17.7 h1:a9w+U3Vt67eYzcfq3k/OAv284/uUUkL0uP75VE5rCOU=
|
||||
go.mongodb.org/mongo-driver v1.17.7/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
|
||||
go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ=
|
||||
go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
@@ -1023,9 +1019,8 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.28.0/go.mod h1:9BIqH22qyHWAiZxQh0whuJygro59z+nbMVuc7ciiGug=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.29.0/go.mod h1:GW2aWZNwR2ZxDLdv8OyC2G8zkRoQBuURgV7RPQgcPoU=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.31.0/go.mod h1:tzQL6E1l+iV44YFTkcAeNQqzXUiekSYP9jjJjXwEd00=
|
||||
@@ -1064,12 +1059,12 @@ go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGi
|
||||
go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I=
|
||||
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
|
||||
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
||||
go.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU=
|
||||
go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0 h1:QRefszxJmfPdjXUUm3j6iDzY03mTPXMjqErFqQ67vUg=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0/go.mod h1:Tiz03lTBVBrm7eWZBOidzEaYaJa8tjwGUGv6d8mlTyk=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.45.0 h1:QBajQ2SrwQijzHyZbQlPsuIzpl/ll8DY6wPWsajeGcI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.45.0/go.mod h1:08ZQLjrPLQ6R4kAXvuOvODEer5Yh4CoFvll5qB2BCI8=
|
||||
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
|
||||
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0 h1:nRVXXvf78e00EwY6Wp0YII8ww2JVWshZ20HfTlE11AM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0/go.mod h1:r49hO7CgrxY9Voaj3Xe8pANWtr0Oq916d0XAmOoCZAQ=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0/go.mod h1:BLbf7zbNIONBLPwvFnwNHGj4zge8uTCM/UPIVW1Mq2I=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0/go.mod h1:U2R3XyVPzn0WX7wOIypPuptulsMcPDPs/oiSVOMVnHY=
|
||||
go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM=
|
||||
@@ -1085,8 +1080,8 @@ go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzau
|
||||
go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M=
|
||||
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
|
||||
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
||||
go.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M=
|
||||
go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s=
|
||||
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
|
||||
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
|
||||
go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E=
|
||||
go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc=
|
||||
go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=
|
||||
@@ -1098,17 +1093,16 @@ go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJC
|
||||
go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM=
|
||||
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
|
||||
go.opentelemetry.io/otel/sdk v1.45.0 h1:4VVSMgQ83dUgW2aoX5f6JgLvHwIvzcuLnF9lUdCSpCw=
|
||||
go.opentelemetry.io/otel/sdk v1.45.0/go.mod h1:Sr40LgXV7DsKMMJMKOhUWOgMWTfAaqvm2kF0g7ilwuA=
|
||||
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
|
||||
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.28.0/go.mod h1:cWPjykihLAPvXKi4iZc1dpER3Jdq2Z0YLse3moQUCpg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.29.0/go.mod h1:6zZLdCl2fkauYoZIOn/soQIDSWFmNSRcICarHfuhNJQ=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.30.0/go.mod h1:waS6P3YqFNzeP01kuo/MBBYqaoBJl7efRQHOaydhy1Y=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.45.0 h1:oVFszMfyj1Am6s24Vtc7wBb8BKLcwepJjNEYILuiE3o=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.45.0/go.mod h1:vUWUxDZvu1WVRj8JA8S0AdhsPrZoDpA2DdZauIh4mDA=
|
||||
go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ=
|
||||
go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo=
|
||||
go.opentelemetry.io/otel/trace v1.23.0/go.mod h1:GSGTbIClEsuZrGIzoEHqsVfxgn5UkggkflQwDScNUsk=
|
||||
@@ -1122,14 +1116,14 @@ go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L
|
||||
go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck=
|
||||
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
|
||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||
go.opentelemetry.io/otel/trace v1.45.0 h1:l/mP6Uv7oNO7/TblbhpbgMidxhq1uO/rPsikOyVhxag=
|
||||
go.opentelemetry.io/otel/trace v1.45.0/go.mod h1:qoJJA2xNMnxRrdISU/kLtfUH2wNeQbiv+jhs/CxI8bc=
|
||||
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
|
||||
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
|
||||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||
go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
|
||||
go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
|
||||
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
|
||||
go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk=
|
||||
go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E=
|
||||
go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI=
|
||||
go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U=
|
||||
@@ -1175,8 +1169,8 @@ golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5
|
||||
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
|
||||
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
|
||||
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
||||
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
||||
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
@@ -1246,8 +1240,8 @@ golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
|
||||
golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
|
||||
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
|
||||
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -1335,8 +1329,8 @@ golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||
golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU=
|
||||
golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
|
||||
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
||||
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -1386,9 +1380,8 @@ golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbht
|
||||
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -1417,8 +1410,8 @@ golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
|
||||
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -1527,8 +1520,8 @@ golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
@@ -1595,8 +1588,8 @@ golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
|
||||
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
|
||||
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
||||
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
@@ -1684,8 +1677,8 @@ golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxb
|
||||
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
|
||||
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
|
||||
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
|
||||
golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
|
||||
golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
|
||||
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
|
||||
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -1700,8 +1693,6 @@ gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJ
|
||||
gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
|
||||
gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0=
|
||||
gonum.org/v1/gonum v0.12.0/go.mod h1:73TDxJfAAHeA8Mk9mf8NlIppyhQNo5GLTcYeqgo2lvY=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
|
||||
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
|
||||
gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY=
|
||||
@@ -1875,8 +1866,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20250425173222-7b384671a197/go.
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34/go.mod h1:0awUlEkap+Pb1UMeJwJQQAdJQrt3moU7J2moTy69irI=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:pKLAc5OolXC3ViWGI62vvC0n10CpwAtRcTNCFwTKBEw=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250512202823-5a2f75b736a9/go.mod h1:W3S/3np0/dPWsWLi1h/UymYctGXaGBM2StwzD0y140U=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d h1:FarXi840EJWSHYTN3ERkADbPWjl307+FGrA22KAVjjc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260803160001-6ac0973c030d/go.mod h1:K/+WGbmBY7aNW1HDw1fJnKYo10i0DkAX6pows00dLig=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 h1:Kog3KlB4xevJlAcbbbzPfRG0+X9fdoGM+UBRKVz6Wr0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto=
|
||||
google.golang.org/genproto/googleapis/bytestream v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:ylj+BE99M198VPbBh6A8d9n3w8fChvyLK3wwBOjXBFA=
|
||||
google.golang.org/genproto/googleapis/bytestream v0.0.0-20231012201019-e917dd12ba7a/go.mod h1:+34luvCflYKiKylNwGJfn9cFBbcL/WrkciMmDmsTQ/A=
|
||||
google.golang.org/genproto/googleapis/bytestream v0.0.0-20231030173426-d783a09b4405/go.mod h1:GRUCuLdzVqZte8+Dl/D4N25yLzcGqqWaYkeVOwulFqw=
|
||||
@@ -1995,8 +1986,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250425173222-7b384671a197/go.
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d h1:IL4hdHzcUv2l/gcg98/Rj3FbtE6axwqslOW8SW0C+S0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260803160001-6ac0973c030d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
@@ -2071,8 +2062,8 @@ google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40Rmc
|
||||
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
|
||||
google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
|
||||
google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
|
||||
google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU=
|
||||
google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8=
|
||||
google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
|
||||
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
|
||||
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0/go.mod h1:Dk1tviKTvMCz5tvh7t+fh94dhmQVHuCt2OzJB3CTW9Y=
|
||||
google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20/go.mod h1:Nr5H8+MlGWr5+xX/STzdoEqJrO+YteqFbMyCsrb6mH0=
|
||||
@@ -2105,9 +2096,8 @@ google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojt
|
||||
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
@@ -319,6 +319,7 @@ func heartbeatONVIFCameraKey(camera models.IPCamera) string {
|
||||
camera.ONVIFXAddr,
|
||||
camera.ONVIFUsername,
|
||||
camera.ONVIFPassword,
|
||||
strconv.Itoa(camera.ONVIFChannel),
|
||||
}, "\x00")
|
||||
}
|
||||
|
||||
@@ -413,7 +414,7 @@ func (state *heartbeatONVIFState) refreshStaticPayload(device *goonvif.Device) h
|
||||
payload.panTilt = "true"
|
||||
}
|
||||
|
||||
presets, err := heartbeatGetPresetsFromDevice(device)
|
||||
presets, err := heartbeatGetPresetsFromDevice(device, state.cameraConfiguration.ONVIFChannel)
|
||||
if err == nil && len(presets) > 0 {
|
||||
payload.presets = "true"
|
||||
presetsList, marshalErr := json.Marshal(presets)
|
||||
|
||||
@@ -296,7 +296,7 @@ func TestHeartbeatONVIFPayloadCachesStaticAndReusesLoopSubscription(t *testing.T
|
||||
ptzFunctionCalls++
|
||||
return nil, true, true
|
||||
}
|
||||
heartbeatGetPresetsFromDevice = func(*goonvif.Device) ([]models.OnvifActionPreset, error) {
|
||||
heartbeatGetPresetsFromDevice = func(*goonvif.Device, int) ([]models.OnvifActionPreset, error) {
|
||||
presetCalls++
|
||||
return []models.OnvifActionPreset{{Name: "Lobby", Token: "1"}}, nil
|
||||
}
|
||||
@@ -397,7 +397,7 @@ func TestHeartbeatONVIFPayloadRefreshesAfterCameraConfigChange(t *testing.T) {
|
||||
heartbeatGetPTZFunctionsFromDevice = func(goonvifptz.GetConfigurationsResponse) ([]string, bool, bool) {
|
||||
return nil, false, true
|
||||
}
|
||||
heartbeatGetPresetsFromDevice = func(*goonvif.Device) ([]models.OnvifActionPreset, error) {
|
||||
heartbeatGetPresetsFromDevice = func(*goonvif.Device, int) ([]models.OnvifActionPreset, error) {
|
||||
presetCalls++
|
||||
return nil, nil
|
||||
}
|
||||
@@ -477,7 +477,7 @@ func TestHeartbeatONVIFPayloadRetriesStaticFetchFailuresWithoutReconnect(t *test
|
||||
heartbeatGetPTZFunctionsFromDevice = func(goonvifptz.GetConfigurationsResponse) ([]string, bool, bool) {
|
||||
return nil, true, true
|
||||
}
|
||||
heartbeatGetPresetsFromDevice = func(*goonvif.Device) ([]models.OnvifActionPreset, error) {
|
||||
heartbeatGetPresetsFromDevice = func(*goonvif.Device, int) ([]models.OnvifActionPreset, error) {
|
||||
presetCalls++
|
||||
if presetCalls == 1 {
|
||||
return nil, errors.New("temporary preset failure")
|
||||
@@ -618,7 +618,7 @@ func TestHeartbeatONVIFPayloadReconnectFailureInvalidatesCache(t *testing.T) {
|
||||
heartbeatGetPTZFunctionsFromDevice = func(goonvifptz.GetConfigurationsResponse) ([]string, bool, bool) {
|
||||
return nil, true, false
|
||||
}
|
||||
heartbeatGetPresetsFromDevice = func(*goonvif.Device) ([]models.OnvifActionPreset, error) {
|
||||
heartbeatGetPresetsFromDevice = func(*goonvif.Device, int) ([]models.OnvifActionPreset, error) {
|
||||
presetCalls++
|
||||
return []models.OnvifActionPreset{{Name: "Preset", Token: "1"}}, nil
|
||||
}
|
||||
|
||||
@@ -394,6 +394,9 @@ func applyAgentEnvVars(configuration *models.Configuration, prefix string, apply
|
||||
case "AGENT_CAPTURE_IPCAMERA_ONVIF_PASSWORD":
|
||||
configuration.Config.Capture.IPCamera.ONVIFPassword = value
|
||||
break
|
||||
case "AGENT_CAPTURE_IPCAMERA_ONVIF_CHANNEL":
|
||||
configuration.Config.Capture.IPCamera.ONVIFChannel, _ = strconv.Atoi(value)
|
||||
break
|
||||
|
||||
/* Recording mode */
|
||||
case "AGENT_CAPTURE_RECORDING":
|
||||
|
||||
@@ -12,6 +12,7 @@ type OnvifCredentials struct {
|
||||
ONVIFXAddr string `json:"onvif_xaddr,omitempty" bson:"onvif_xaddr"`
|
||||
ONVIFUsername string `json:"onvif_username,omitempty" bson:"onvif_username"`
|
||||
ONVIFPassword string `json:"onvif_password,omitempty" bson:"onvif_password"`
|
||||
ONVIFChannel int `json:"onvif_channel,omitempty" bson:"onvif_channel,omitempty"` // 1-based; 0 = first profile
|
||||
}
|
||||
|
||||
type CameraStreams struct {
|
||||
@@ -68,6 +69,11 @@ type OnvifZoom struct {
|
||||
Zoom float64 `json:"zoom,omitempty" bson:"zoom"`
|
||||
}
|
||||
|
||||
type OnvifFocus struct {
|
||||
OnvifCredentials OnvifCredentials `json:"onvif_credentials,omitempty" bson:"onvif_credentials"`
|
||||
Focus float64 `json:"focus,omitempty" bson:"focus"`
|
||||
}
|
||||
|
||||
type OnvifPreset struct {
|
||||
OnvifCredentials OnvifCredentials `json:"onvif_credentials,omitempty" bson:"onvif_credentials"`
|
||||
Preset string `json:"preset,omitempty" bson:"preset"`
|
||||
|
||||
@@ -105,6 +105,7 @@ type IPCamera struct {
|
||||
ONVIFXAddr string `json:"onvif_xaddr" bson:"onvif_xaddr"`
|
||||
ONVIFUsername string `json:"onvif_username" bson:"onvif_username"`
|
||||
ONVIFPassword string `json:"onvif_password" bson:"onvif_password"`
|
||||
ONVIFChannel int `json:"onvif_channel,omitempty" bson:"onvif_channel,omitempty"` // 1-based video input on multi-channel devices (encoders, NVRs); 0 = first profile
|
||||
SPSNALUs [][]byte `json:"sps_nalus,omitempty" bson:"sps_nalus,omitempty"`
|
||||
PPSNALUs [][]byte `json:"pps_nalus,omitempty" bson:"pps_nalus,omitempty"`
|
||||
VPSNALUs [][]byte `json:"vps_nalus,omitempty" bson:"vps_nalus,omitempty"`
|
||||
|
||||
@@ -12,6 +12,7 @@ type OnvifActionPTZ struct {
|
||||
Down int `json:"down" bson:"down"`
|
||||
Center int `json:"center" bson:"center"`
|
||||
Zoom float64 `json:"zoom" bson:"zoom"`
|
||||
Focus float64 `json:"focus" bson:"focus"`
|
||||
X float64 `json:"x" bson:"x"`
|
||||
Y float64 `json:"y" bson:"y"`
|
||||
Z float64 `json:"z" bson:"z"`
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
package onvif
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/kerberos-io/onvif"
|
||||
"github.com/kerberos-io/onvif/media"
|
||||
xsdonvif "github.com/kerberos-io/onvif/xsd/onvif"
|
||||
)
|
||||
|
||||
// Multi-channel devices (encoders, NVRs) expose several media profiles per
|
||||
// video input, e.g. a Hanwha SPE-420 returns DefaultProfile-01-0..03-0 for
|
||||
// channel 1 and DefaultProfile-01-1..03-1 for channel 2. PTZ, presets and
|
||||
// focus must be addressed with a profile (or video source) of the channel the
|
||||
// camera is wired to, otherwise commands drive the wrong camera.
|
||||
//
|
||||
// A channel is 1-based, counted in the order the device first lists each video
|
||||
// source in GetProfiles. Channel 0 (unset) keeps the historic behaviour of
|
||||
// using the first profile, so single-sensor cameras need no configuration.
|
||||
|
||||
// GetTokenFromChannel returns the media profile token to use for PTZ commands
|
||||
// on the given 1-based channel.
|
||||
func GetTokenFromChannel(device *onvif.Device, channel int) (xsdonvif.ReferenceToken, error) {
|
||||
if channel <= 0 {
|
||||
return GetTokenFromProfile(device, 0)
|
||||
}
|
||||
profiles, err := getMediaProfiles(device)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
profile, err := selectChannelProfile(profiles, channel)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return profile.Token, nil
|
||||
}
|
||||
|
||||
// GetVideoSourceTokenFromChannel returns the video source token of the given
|
||||
// 1-based channel, as used by the imaging (focus) service.
|
||||
func GetVideoSourceTokenFromChannel(device *onvif.Device, channel int) (xsdonvif.ReferenceToken, error) {
|
||||
if channel <= 0 {
|
||||
return GetVideoSourceTokenFromProfile(device, 0)
|
||||
}
|
||||
profiles, err := getMediaProfiles(device)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
profile, err := selectChannelProfile(profiles, channel)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return *profile.VideoSourceConfiguration.SourceToken, nil
|
||||
}
|
||||
|
||||
func getMediaProfiles(device *onvif.Device) ([]xsdonvif.Profile, error) {
|
||||
resp, err := device.CallMethod(media.GetProfiles{})
|
||||
if err != nil {
|
||||
if resp != nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decodedXML, et, err := getXMLNode(string(b), "GetProfilesResponse")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var profilesResp media.GetProfilesResponse
|
||||
if err := decodedXML.DecodeElement(&profilesResp, et); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return profilesResp.Profiles, nil
|
||||
}
|
||||
|
||||
// selectChannelProfile groups profiles by video source and returns a profile
|
||||
// of the channel-th source (1-based). Within a channel it prefers the first
|
||||
// profile that has a PTZ configuration attached.
|
||||
func selectChannelProfile(profiles []xsdonvif.Profile, channel int) (xsdonvif.Profile, error) {
|
||||
if channel <= 0 {
|
||||
return xsdonvif.Profile{}, errors.New("onvif channel must be 1 or higher")
|
||||
}
|
||||
|
||||
var sources []xsdonvif.ReferenceToken
|
||||
bySource := map[xsdonvif.ReferenceToken][]xsdonvif.Profile{}
|
||||
for _, profile := range profiles {
|
||||
if profile.VideoSourceConfiguration == nil || profile.VideoSourceConfiguration.SourceToken == nil {
|
||||
continue
|
||||
}
|
||||
source := *profile.VideoSourceConfiguration.SourceToken
|
||||
if _, seen := bySource[source]; !seen {
|
||||
sources = append(sources, source)
|
||||
}
|
||||
bySource[source] = append(bySource[source], profile)
|
||||
}
|
||||
|
||||
if channel > len(sources) {
|
||||
return xsdonvif.Profile{}, fmt.Errorf("onvif channel %d not found: device exposes %d video source(s)", channel, len(sources))
|
||||
}
|
||||
|
||||
candidates := bySource[sources[channel-1]]
|
||||
for _, profile := range candidates {
|
||||
if profile.PTZConfiguration != nil {
|
||||
return profile, nil
|
||||
}
|
||||
}
|
||||
return candidates[0], nil
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package onvif
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
xsdonvif "github.com/kerberos-io/onvif/xsd/onvif"
|
||||
)
|
||||
|
||||
func testProfile(token string, source string, ptz bool) xsdonvif.Profile {
|
||||
profile := xsdonvif.Profile{Token: xsdonvif.ReferenceToken(token)}
|
||||
if source != "" {
|
||||
sourceToken := xsdonvif.ReferenceToken(source)
|
||||
profile.VideoSourceConfiguration = &xsdonvif.VideoSourceConfiguration{SourceToken: &sourceToken}
|
||||
}
|
||||
if ptz {
|
||||
profile.PTZConfiguration = &xsdonvif.PTZConfiguration{}
|
||||
}
|
||||
return profile
|
||||
}
|
||||
|
||||
// spe420Profiles mirrors a Hanwha SPE-420: three profiles per channel, token
|
||||
// suffix -(N-1), listed grouped by profile type rather than by channel.
|
||||
func spe420Profiles() []xsdonvif.Profile {
|
||||
var profiles []xsdonvif.Profile
|
||||
for kind := 1; kind <= 3; kind++ {
|
||||
for ch := 0; ch < 4; ch++ {
|
||||
profiles = append(profiles, testProfile(
|
||||
fmt.Sprintf("DefaultProfile-0%d-%d", kind, ch),
|
||||
fmt.Sprintf("VideoSourceToken-%d", ch),
|
||||
true,
|
||||
))
|
||||
}
|
||||
}
|
||||
return profiles
|
||||
}
|
||||
|
||||
func TestSelectChannelProfileMapsEncoderChannels(t *testing.T) {
|
||||
profiles := spe420Profiles()
|
||||
for channel := 1; channel <= 4; channel++ {
|
||||
profile, err := selectChannelProfile(profiles, channel)
|
||||
if err != nil {
|
||||
t.Fatalf("channel %d: %v", channel, err)
|
||||
}
|
||||
want := fmt.Sprintf("DefaultProfile-01-%d", channel-1)
|
||||
if string(profile.Token) != want {
|
||||
t.Fatalf("channel %d: got %s, want %s", channel, profile.Token, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectChannelProfilePrefersProfileWithPTZ(t *testing.T) {
|
||||
profiles := []xsdonvif.Profile{
|
||||
testProfile("mjpeg", "src-0", false),
|
||||
testProfile("main", "src-0", true),
|
||||
}
|
||||
profile, err := selectChannelProfile(profiles, 1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if profile.Token != "main" {
|
||||
t.Fatalf("got %s, want main", profile.Token)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectChannelProfileFallsBackToFirstProfileOfChannel(t *testing.T) {
|
||||
profiles := []xsdonvif.Profile{
|
||||
testProfile("a", "src-0", false),
|
||||
testProfile("b", "src-1", false),
|
||||
testProfile("c", "src-1", false),
|
||||
}
|
||||
profile, err := selectChannelProfile(profiles, 2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if profile.Token != "b" {
|
||||
t.Fatalf("got %s, want b", profile.Token)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectChannelProfileSkipsProfilesWithoutVideoSource(t *testing.T) {
|
||||
profiles := []xsdonvif.Profile{
|
||||
testProfile("audio-only", "", false),
|
||||
testProfile("video", "src-0", false),
|
||||
}
|
||||
profile, err := selectChannelProfile(profiles, 1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if profile.Token != "video" {
|
||||
t.Fatalf("got %s, want video", profile.Token)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectChannelProfileRejectsUnknownChannel(t *testing.T) {
|
||||
if _, err := selectChannelProfile(spe420Profiles(), 5); err == nil {
|
||||
t.Fatal("expected error for channel beyond the device's video sources")
|
||||
}
|
||||
if _, err := selectChannelProfile(spe420Profiles(), 0); err == nil {
|
||||
t.Fatal("expected error for channel 0")
|
||||
}
|
||||
if _, err := selectChannelProfile(nil, 1); err == nil {
|
||||
t.Fatal("expected error when the device has no profiles")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,466 @@
|
||||
package onvif
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/kerberos-io/onvif"
|
||||
"github.com/kerberos-io/onvif/imaging"
|
||||
"github.com/kerberos-io/onvif/media"
|
||||
xsdonvif "github.com/kerberos-io/onvif/xsd/onvif"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Focus control goes through the ONVIF imaging service. The request types of
|
||||
// the onvif library serialise every field (all three move types, every imaging
|
||||
// setting), which cameras reject or which would overwrite unrelated imaging
|
||||
// settings. The types below only carry the fields we want to send. Schema
|
||||
// elements use the "onvif" prefix, which the library declares on the envelope
|
||||
// as http://www.onvif.org/ver10/schema.
|
||||
|
||||
const (
|
||||
focusModeAuto = "AUTO"
|
||||
focusModeManual = "MANUAL"
|
||||
|
||||
// Same move duration as ContinuousZoom.
|
||||
continuousFocusDuration = 500 * time.Millisecond
|
||||
// Step for cameras that only support relative focus moves.
|
||||
relativeFocusDistance = 0.1
|
||||
// Step at full speed for cameras that only support absolute focus moves
|
||||
// (e.g. Axis); scaled by the requested speed.
|
||||
absoluteFocusStep = 0.1
|
||||
// Time to let the camera settle in MANUAL before re-enabling AUTO.
|
||||
autoFocusRetriggerDelay = 300 * time.Millisecond
|
||||
)
|
||||
|
||||
type imagingMoveRequest struct {
|
||||
XMLName xml.Name `xml:"timg:Move"`
|
||||
VideoSourceToken xsdonvif.ReferenceToken `xml:"timg:VideoSourceToken"`
|
||||
Focus focusMove `xml:"timg:Focus"`
|
||||
}
|
||||
|
||||
type focusMove struct {
|
||||
Absolute *absoluteFocusMove `xml:"onvif:Absolute,omitempty"`
|
||||
Relative *relativeFocusMove `xml:"onvif:Relative,omitempty"`
|
||||
Continuous *continuousFocusMove `xml:"onvif:Continuous,omitempty"`
|
||||
}
|
||||
|
||||
type absoluteFocusMove struct {
|
||||
Position float64 `xml:"onvif:Position"`
|
||||
}
|
||||
|
||||
type relativeFocusMove struct {
|
||||
Distance float64 `xml:"onvif:Distance"`
|
||||
Speed float64 `xml:"onvif:Speed,omitempty"`
|
||||
}
|
||||
|
||||
type continuousFocusMove struct {
|
||||
Speed float64 `xml:"onvif:Speed"`
|
||||
}
|
||||
|
||||
type imagingStopRequest struct {
|
||||
XMLName xml.Name `xml:"timg:Stop"`
|
||||
VideoSourceToken xsdonvif.ReferenceToken `xml:"timg:VideoSourceToken"`
|
||||
}
|
||||
|
||||
type imagingGetSettingsRequest struct {
|
||||
XMLName xml.Name `xml:"timg:GetImagingSettings"`
|
||||
VideoSourceToken xsdonvif.ReferenceToken `xml:"timg:VideoSourceToken"`
|
||||
}
|
||||
|
||||
type imagingGetSettingsResponse struct {
|
||||
ImagingSettings struct {
|
||||
Brightness *float64 `xml:"Brightness"`
|
||||
Focus struct {
|
||||
AutoFocusMode string `xml:"AutoFocusMode"`
|
||||
} `xml:"Focus"`
|
||||
} `xml:"ImagingSettings"`
|
||||
}
|
||||
|
||||
type imagingSetFocusModeRequest struct {
|
||||
XMLName xml.Name `xml:"timg:SetImagingSettings"`
|
||||
VideoSourceToken xsdonvif.ReferenceToken `xml:"timg:VideoSourceToken"`
|
||||
ImagingSettings struct {
|
||||
Brightness *float64 `xml:"onvif:Brightness,omitempty"`
|
||||
Focus struct {
|
||||
AutoFocusMode string `xml:"onvif:AutoFocusMode"`
|
||||
} `xml:"onvif:Focus"`
|
||||
} `xml:"timg:ImagingSettings"`
|
||||
}
|
||||
|
||||
type imagingGetOptionsRequest struct {
|
||||
XMLName xml.Name `xml:"timg:GetOptions"`
|
||||
VideoSourceToken xsdonvif.ReferenceToken `xml:"timg:VideoSourceToken"`
|
||||
}
|
||||
|
||||
type imagingGetOptionsResponse struct {
|
||||
ImagingOptions struct {
|
||||
Focus struct {
|
||||
AutoFocusModes []string `xml:"AutoFocusModes"`
|
||||
} `xml:"Focus"`
|
||||
} `xml:"ImagingOptions"`
|
||||
}
|
||||
|
||||
type imagingGetMoveOptionsRequest struct {
|
||||
XMLName xml.Name `xml:"timg:GetMoveOptions"`
|
||||
VideoSourceToken xsdonvif.ReferenceToken `xml:"timg:VideoSourceToken"`
|
||||
}
|
||||
|
||||
type imagingGetMoveOptionsResponse struct {
|
||||
MoveOptions struct {
|
||||
Absolute *struct {
|
||||
Position struct {
|
||||
Min float64 `xml:"Min"`
|
||||
Max float64 `xml:"Max"`
|
||||
} `xml:"Position"`
|
||||
} `xml:"Absolute"`
|
||||
Relative *struct{} `xml:"Relative"`
|
||||
Continuous *struct{} `xml:"Continuous"`
|
||||
} `xml:"MoveOptions"`
|
||||
}
|
||||
|
||||
type imagingGetStatusRequest struct {
|
||||
XMLName xml.Name `xml:"timg:GetStatus"`
|
||||
VideoSourceToken xsdonvif.ReferenceToken `xml:"timg:VideoSourceToken"`
|
||||
}
|
||||
|
||||
type imagingGetStatusResponse struct {
|
||||
Status struct {
|
||||
FocusStatus20 *struct {
|
||||
Position *float64 `xml:"Position"`
|
||||
} `xml:"FocusStatus20"`
|
||||
FocusStatus *struct {
|
||||
Position *float64 `xml:"Position"`
|
||||
} `xml:"FocusStatus"`
|
||||
} `xml:"Status"`
|
||||
}
|
||||
|
||||
// focusMoveOptions lists the focus move types a camera supports.
|
||||
type focusMoveOptions struct {
|
||||
Continuous bool
|
||||
Relative bool
|
||||
Absolute bool
|
||||
AbsoluteMin float64
|
||||
AbsoluteMax float64
|
||||
}
|
||||
|
||||
// allFocusMoveOptions is used when a camera does not report its move options:
|
||||
// every move type is tried in turn.
|
||||
var allFocusMoveOptions = focusMoveOptions{Continuous: true, Relative: true, Absolute: true, AbsoluteMin: 0, AbsoluteMax: 1}
|
||||
|
||||
// focusController abstracts the imaging calls so the focus logic can be tested
|
||||
// without a camera.
|
||||
type focusController interface {
|
||||
FocusMode() (string, error)
|
||||
FocusModes() ([]string, error)
|
||||
SetFocusMode(mode string) error
|
||||
MoveOptions() (focusMoveOptions, error)
|
||||
FocusPosition() (float64, error)
|
||||
MoveFocus(move focusMove) error
|
||||
StopFocus() error
|
||||
}
|
||||
|
||||
type onvifFocusClient struct {
|
||||
device *onvif.Device
|
||||
endpoint string
|
||||
token xsdonvif.ReferenceToken
|
||||
}
|
||||
|
||||
func newONVIFFocusClient(device *onvif.Device, channel int) (*onvifFocusClient, error) {
|
||||
endpoint, err := device.GetEndpointByRequestStruct(&imaging.Move{})
|
||||
if err != nil {
|
||||
return nil, errors.New("camera does not expose the ONVIF imaging service: " + err.Error())
|
||||
}
|
||||
token, err := GetVideoSourceTokenFromChannel(device, channel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &onvifFocusClient{device: device, endpoint: endpoint, token: token}, nil
|
||||
}
|
||||
|
||||
var soapFaultReason = regexp.MustCompile(`<(?:[\w.-]+:)?Text[^>]*>([^<]+)</`)
|
||||
|
||||
// call sends an imaging request and returns the response body. A rejected
|
||||
// request returns an error carrying the camera's SOAP fault reason.
|
||||
func (client *onvifFocusClient) call(operation string, request interface{}) ([]byte, error) {
|
||||
body, err := xml.Marshal(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := client.device.SendSoap(client.endpoint, string(body))
|
||||
if resp == nil {
|
||||
if err == nil {
|
||||
err = errors.New("no response")
|
||||
}
|
||||
return nil, fmt.Errorf("%s: %w", operation, err)
|
||||
}
|
||||
b, readErr := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
||||
reason := http.StatusText(resp.StatusCode)
|
||||
if m := soapFaultReason.FindSubmatch(b); m != nil {
|
||||
reason = string(m[1])
|
||||
}
|
||||
return nil, fmt.Errorf("%s: camera returned HTTP %d: %s", operation, resp.StatusCode, reason)
|
||||
}
|
||||
if readErr != nil {
|
||||
return nil, fmt.Errorf("%s: %w", operation, readErr)
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (client *onvifFocusClient) query(operation string, request interface{}, responseNode string, response interface{}) error {
|
||||
b, err := client.call(operation, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
decodedXML, et, err := getXMLNode(string(b), responseNode)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", operation, err)
|
||||
}
|
||||
if err := decodedXML.DecodeElement(response, et); err != nil {
|
||||
return fmt.Errorf("%s: %w", operation, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (client *onvifFocusClient) imagingSettings() (imagingGetSettingsResponse, error) {
|
||||
var settings imagingGetSettingsResponse
|
||||
err := client.query("get_imaging_settings", imagingGetSettingsRequest{VideoSourceToken: client.token}, "GetImagingSettingsResponse", &settings)
|
||||
return settings, err
|
||||
}
|
||||
|
||||
func (client *onvifFocusClient) FocusMode() (string, error) {
|
||||
settings, err := client.imagingSettings()
|
||||
return settings.ImagingSettings.Focus.AutoFocusMode, err
|
||||
}
|
||||
|
||||
func (client *onvifFocusClient) SetFocusMode(mode string) error {
|
||||
request := imagingSetFocusModeRequest{VideoSourceToken: client.token}
|
||||
request.ImagingSettings.Focus.AutoFocusMode = mode
|
||||
_, err := client.call("set_focus_mode", request)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Some cameras (seen on Axis) fail on a SetImagingSettings that only
|
||||
// carries Focus. Retry with the current brightness, which leaves it as is.
|
||||
settings, settingsErr := client.imagingSettings()
|
||||
if settingsErr != nil || settings.ImagingSettings.Brightness == nil {
|
||||
return err
|
||||
}
|
||||
request.ImagingSettings.Brightness = settings.ImagingSettings.Brightness
|
||||
if _, retryErr := client.call("set_focus_mode", request); retryErr != nil {
|
||||
return errors.Join(err, retryErr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (client *onvifFocusClient) FocusModes() ([]string, error) {
|
||||
var response imagingGetOptionsResponse
|
||||
if err := client.query("get_imaging_options", imagingGetOptionsRequest{VideoSourceToken: client.token}, "GetOptionsResponse", &response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return response.ImagingOptions.Focus.AutoFocusModes, nil
|
||||
}
|
||||
|
||||
func (client *onvifFocusClient) MoveOptions() (focusMoveOptions, error) {
|
||||
var response imagingGetMoveOptionsResponse
|
||||
if err := client.query("get_focus_move_options", imagingGetMoveOptionsRequest{VideoSourceToken: client.token}, "GetMoveOptionsResponse", &response); err != nil {
|
||||
return focusMoveOptions{}, err
|
||||
}
|
||||
moveOptions := response.MoveOptions
|
||||
options := focusMoveOptions{
|
||||
Continuous: moveOptions.Continuous != nil,
|
||||
Relative: moveOptions.Relative != nil,
|
||||
Absolute: moveOptions.Absolute != nil,
|
||||
AbsoluteMin: 0,
|
||||
AbsoluteMax: 1,
|
||||
}
|
||||
if moveOptions.Absolute != nil && moveOptions.Absolute.Position.Max > moveOptions.Absolute.Position.Min {
|
||||
options.AbsoluteMin = moveOptions.Absolute.Position.Min
|
||||
options.AbsoluteMax = moveOptions.Absolute.Position.Max
|
||||
}
|
||||
return options, nil
|
||||
}
|
||||
|
||||
func (client *onvifFocusClient) FocusPosition() (float64, error) {
|
||||
var response imagingGetStatusResponse
|
||||
if err := client.query("get_focus_status", imagingGetStatusRequest{VideoSourceToken: client.token}, "GetStatusResponse", &response); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
status := response.Status
|
||||
if status.FocusStatus20 != nil && status.FocusStatus20.Position != nil {
|
||||
return *status.FocusStatus20.Position, nil
|
||||
}
|
||||
if status.FocusStatus != nil && status.FocusStatus.Position != nil {
|
||||
return *status.FocusStatus.Position, nil
|
||||
}
|
||||
return 0, errors.New("get_focus_status: camera did not report a focus position")
|
||||
}
|
||||
|
||||
func (client *onvifFocusClient) MoveFocus(move focusMove) error {
|
||||
_, err := client.call("focus_move", imagingMoveRequest{VideoSourceToken: client.token, Focus: move})
|
||||
return err
|
||||
}
|
||||
|
||||
func (client *onvifFocusClient) StopFocus() error {
|
||||
_, err := client.call("focus_stop", imagingStopRequest{VideoSourceToken: client.token})
|
||||
return err
|
||||
}
|
||||
|
||||
// ContinuousFocus moves the focus motor a short step, the focus equivalent of
|
||||
// ContinuousZoom. Speed is in [-1, 1]: positive focuses near, negative focuses
|
||||
// far. Cameras with continuous focus move for a moment and stop; cameras with
|
||||
// only relative or absolute focus (e.g. Axis) step by an amount scaled to the
|
||||
// speed. The camera is switched to MANUAL focus first, because cameras ignore
|
||||
// manual moves while autofocus is active; use AutoFocus to hand control back.
|
||||
func ContinuousFocus(device *onvif.Device, channel int, speed float64) error {
|
||||
client, err := newONVIFFocusClient(device, channel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return continuousFocus(client, speed, continuousFocusDuration)
|
||||
}
|
||||
|
||||
// AutoFocus triggers the camera's native ONVIF autofocus. Setting AUTO while
|
||||
// already in AUTO is a no-op on most cameras, so we toggle through MANUAL to
|
||||
// force a fresh focus cycle.
|
||||
func AutoFocus(device *onvif.Device, channel int) error {
|
||||
client, err := newONVIFFocusClient(device, channel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return autoFocus(client, autoFocusRetriggerDelay)
|
||||
}
|
||||
|
||||
func continuousFocus(client focusController, speed float64, duration time.Duration) error {
|
||||
speed = math.Max(-1, math.Min(1, speed))
|
||||
if speed == 0 {
|
||||
return client.StopFocus()
|
||||
}
|
||||
|
||||
if err := ensureFocusMode(client, focusModeManual); err != nil {
|
||||
// Some cameras have no focus mode setting but still accept moves.
|
||||
log.Debug("onvif.continuousFocus(): could not switch to manual focus: " + err.Error())
|
||||
}
|
||||
|
||||
options, err := client.MoveOptions()
|
||||
if err != nil {
|
||||
log.Debug("onvif.continuousFocus(): could not read focus move options, trying all move types: " + err.Error())
|
||||
options = allFocusMoveOptions
|
||||
}
|
||||
|
||||
var moveErrs []error
|
||||
if options.Continuous {
|
||||
err := client.MoveFocus(focusMove{Continuous: &continuousFocusMove{Speed: speed}})
|
||||
if err == nil {
|
||||
time.Sleep(duration)
|
||||
return client.StopFocus()
|
||||
}
|
||||
moveErrs = append(moveErrs, err)
|
||||
}
|
||||
if options.Relative {
|
||||
// A single relative step needs no stop.
|
||||
err := client.MoveFocus(focusMove{Relative: &relativeFocusMove{
|
||||
Distance: math.Copysign(relativeFocusDistance, speed),
|
||||
Speed: math.Abs(speed),
|
||||
}})
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
moveErrs = append(moveErrs, err)
|
||||
}
|
||||
if options.Absolute {
|
||||
position, err := client.FocusPosition()
|
||||
if err == nil {
|
||||
target := math.Max(options.AbsoluteMin, math.Min(options.AbsoluteMax, position+speed*absoluteFocusStep))
|
||||
err = client.MoveFocus(focusMove{Absolute: &absoluteFocusMove{Position: target}})
|
||||
}
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
moveErrs = append(moveErrs, err)
|
||||
}
|
||||
|
||||
if len(moveErrs) == 0 {
|
||||
return errors.New("camera does not support any focus move")
|
||||
}
|
||||
return errors.Join(moveErrs...)
|
||||
}
|
||||
|
||||
func ensureFocusMode(client focusController, mode string) error {
|
||||
current, err := client.FocusMode()
|
||||
if err == nil && current == mode {
|
||||
return nil
|
||||
}
|
||||
return client.SetFocusMode(mode)
|
||||
}
|
||||
|
||||
var errNoNativeAutoFocus = errors.New("camera does not support native ONVIF autofocus")
|
||||
|
||||
func autoFocus(client focusController, delay time.Duration) error {
|
||||
// Some cameras (seen on Dahua) only advertise MANUAL, yet accept a switch
|
||||
// to AUTO and silently ignore it.
|
||||
if modes, err := client.FocusModes(); err == nil && !slices.Contains(modes, focusModeAuto) {
|
||||
return errNoNativeAutoFocus
|
||||
}
|
||||
|
||||
mode, err := client.FocusMode()
|
||||
if err == nil && mode == focusModeAuto {
|
||||
if err := client.SetFocusMode(focusModeManual); err != nil {
|
||||
return err
|
||||
}
|
||||
time.Sleep(delay)
|
||||
}
|
||||
if err := client.SetFocusMode(focusModeAuto); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if mode, err := client.FocusMode(); err == nil && mode != focusModeAuto {
|
||||
return fmt.Errorf("%w: camera accepted AUTO but reports %s", errNoNativeAutoFocus, mode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetVideoSourceTokenFromProfile returns the video source token of a media
|
||||
// profile; the imaging service is addressed by video source, not by profile.
|
||||
func GetVideoSourceTokenFromProfile(device *onvif.Device, profileId int) (xsdonvif.ReferenceToken, error) {
|
||||
var sourceToken xsdonvif.ReferenceToken
|
||||
|
||||
resp, err := device.CallMethod(media.GetProfiles{})
|
||||
if err != nil {
|
||||
if resp != nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
return sourceToken, err
|
||||
}
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
return sourceToken, err
|
||||
}
|
||||
|
||||
decodedXML, et, err := getXMLNode(string(b), "GetProfilesResponse")
|
||||
if err != nil {
|
||||
return sourceToken, err
|
||||
}
|
||||
var mProfilesResp media.GetProfilesResponse
|
||||
if err := decodedXML.DecodeElement(&mProfilesResp, et); err != nil {
|
||||
return sourceToken, err
|
||||
}
|
||||
|
||||
for i, profile := range mProfilesResp.Profiles {
|
||||
if i == profileId && profile.VideoSourceConfiguration != nil && profile.VideoSourceConfiguration.SourceToken != nil {
|
||||
return *profile.VideoSourceConfiguration.SourceToken, nil
|
||||
}
|
||||
}
|
||||
return sourceToken, errors.New("no video source token found for profile")
|
||||
}
|
||||
@@ -0,0 +1,418 @@
|
||||
package onvif
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"math"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type fakeFocusClient struct {
|
||||
mode string
|
||||
modeErr error
|
||||
focusModes []string
|
||||
focusModesErr error
|
||||
ignoreAuto bool
|
||||
setModeErr error
|
||||
options *focusMoveOptions
|
||||
optionsErr error
|
||||
position float64
|
||||
positionErr error
|
||||
continuousErr error
|
||||
relativeErr error
|
||||
absoluteErr error
|
||||
modes []string
|
||||
moves []focusMove
|
||||
stopCalls int
|
||||
}
|
||||
|
||||
func (client *fakeFocusClient) FocusMode() (string, error) {
|
||||
return client.mode, client.modeErr
|
||||
}
|
||||
|
||||
func (client *fakeFocusClient) FocusModes() ([]string, error) {
|
||||
if client.focusModes == nil && client.focusModesErr == nil {
|
||||
return []string{focusModeManual, focusModeAuto}, nil
|
||||
}
|
||||
return client.focusModes, client.focusModesErr
|
||||
}
|
||||
|
||||
func (client *fakeFocusClient) SetFocusMode(mode string) error {
|
||||
client.modes = append(client.modes, mode)
|
||||
if client.setModeErr != nil {
|
||||
return client.setModeErr
|
||||
}
|
||||
if mode == focusModeAuto && client.ignoreAuto {
|
||||
return nil
|
||||
}
|
||||
client.mode = mode
|
||||
return nil
|
||||
}
|
||||
|
||||
func (client *fakeFocusClient) MoveOptions() (focusMoveOptions, error) {
|
||||
if client.optionsErr != nil {
|
||||
return focusMoveOptions{}, client.optionsErr
|
||||
}
|
||||
if client.options == nil {
|
||||
return allFocusMoveOptions, nil
|
||||
}
|
||||
return *client.options, nil
|
||||
}
|
||||
|
||||
func (client *fakeFocusClient) FocusPosition() (float64, error) {
|
||||
return client.position, client.positionErr
|
||||
}
|
||||
|
||||
func (client *fakeFocusClient) MoveFocus(move focusMove) error {
|
||||
client.moves = append(client.moves, move)
|
||||
switch {
|
||||
case move.Continuous != nil:
|
||||
return client.continuousErr
|
||||
case move.Relative != nil:
|
||||
return client.relativeErr
|
||||
default:
|
||||
return client.absoluteErr
|
||||
}
|
||||
}
|
||||
|
||||
func (client *fakeFocusClient) StopFocus() error {
|
||||
client.stopCalls++
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestContinuousFocusSwitchesToManualMovesAndStops(t *testing.T) {
|
||||
client := &fakeFocusClient{mode: focusModeAuto}
|
||||
|
||||
if err := continuousFocus(client, 0.5, 0); err != nil {
|
||||
t.Fatalf("continuousFocus() error = %v", err)
|
||||
}
|
||||
|
||||
if len(client.modes) != 1 || client.modes[0] != focusModeManual {
|
||||
t.Fatalf("modes = %v, want [MANUAL]", client.modes)
|
||||
}
|
||||
if len(client.moves) != 1 || client.moves[0].Continuous == nil || client.moves[0].Continuous.Speed != 0.5 {
|
||||
t.Fatalf("moves = %+v, want one continuous move at 0.5", client.moves)
|
||||
}
|
||||
if client.stopCalls != 1 {
|
||||
t.Fatalf("stopCalls = %d, want 1", client.stopCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContinuousFocusKeepsManualMode(t *testing.T) {
|
||||
client := &fakeFocusClient{mode: focusModeManual}
|
||||
|
||||
if err := continuousFocus(client, 0.5, 0); err != nil {
|
||||
t.Fatalf("continuousFocus() error = %v", err)
|
||||
}
|
||||
if len(client.modes) != 0 {
|
||||
t.Fatalf("modes = %v, want no mode change when already MANUAL", client.modes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContinuousFocusClampsSpeed(t *testing.T) {
|
||||
client := &fakeFocusClient{}
|
||||
|
||||
if err := continuousFocus(client, -3, 0); err != nil {
|
||||
t.Fatalf("continuousFocus() error = %v", err)
|
||||
}
|
||||
if got := client.moves[0].Continuous.Speed; got != -1 {
|
||||
t.Fatalf("speed = %v, want -1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContinuousFocusIgnoresUnsupportedFocusMode(t *testing.T) {
|
||||
client := &fakeFocusClient{setModeErr: errors.New("not supported")}
|
||||
|
||||
if err := continuousFocus(client, 0.5, 0); err != nil {
|
||||
t.Fatalf("continuousFocus() error = %v", err)
|
||||
}
|
||||
if len(client.moves) != 1 || client.stopCalls != 1 {
|
||||
t.Fatalf("moves = %d stopCalls = %d, want 1 and 1", len(client.moves), client.stopCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContinuousFocusFallsBackToRelative(t *testing.T) {
|
||||
client := &fakeFocusClient{continuousErr: errors.New("continuous not supported")}
|
||||
|
||||
if err := continuousFocus(client, -0.4, 0); err != nil {
|
||||
t.Fatalf("continuousFocus() error = %v", err)
|
||||
}
|
||||
if len(client.moves) != 2 || client.moves[1].Relative == nil {
|
||||
t.Fatalf("moves = %+v, want continuous then relative", client.moves)
|
||||
}
|
||||
relative := client.moves[1].Relative
|
||||
if relative.Distance != -relativeFocusDistance || relative.Speed != 0.4 {
|
||||
t.Fatalf("relative = %+v, want distance %v speed 0.4", relative, -relativeFocusDistance)
|
||||
}
|
||||
if client.stopCalls != 0 {
|
||||
t.Fatalf("stopCalls = %d, want 0 for a relative move", client.stopCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContinuousFocusUsesAbsoluteWhenOnlyAbsoluteSupported(t *testing.T) {
|
||||
// Axis cameras report only absolute focus moves.
|
||||
client := &fakeFocusClient{
|
||||
mode: focusModeManual,
|
||||
options: &focusMoveOptions{Absolute: true, AbsoluteMin: 0, AbsoluteMax: 1},
|
||||
position: 0.48,
|
||||
}
|
||||
|
||||
if err := continuousFocus(client, 0.5, 0); err != nil {
|
||||
t.Fatalf("continuousFocus() error = %v", err)
|
||||
}
|
||||
if len(client.moves) != 1 || client.moves[0].Absolute == nil {
|
||||
t.Fatalf("moves = %+v, want a single absolute move", client.moves)
|
||||
}
|
||||
if got, want := client.moves[0].Absolute.Position, 0.48+0.5*absoluteFocusStep; math.Abs(got-want) > 1e-9 {
|
||||
t.Fatalf("position = %v, want %v", got, want)
|
||||
}
|
||||
if client.stopCalls != 0 {
|
||||
t.Fatalf("stopCalls = %d, want 0 for an absolute move", client.stopCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContinuousFocusClampsAbsoluteToRange(t *testing.T) {
|
||||
client := &fakeFocusClient{
|
||||
options: &focusMoveOptions{Absolute: true, AbsoluteMin: 0, AbsoluteMax: 1},
|
||||
position: 0.02,
|
||||
}
|
||||
|
||||
if err := continuousFocus(client, -1, 0); err != nil {
|
||||
t.Fatalf("continuousFocus() error = %v", err)
|
||||
}
|
||||
if got := client.moves[0].Absolute.Position; got != 0 {
|
||||
t.Fatalf("position = %v, want clamped to 0", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContinuousFocusTriesAllMovesWithoutOptions(t *testing.T) {
|
||||
client := &fakeFocusClient{
|
||||
optionsErr: errors.New("no move options"),
|
||||
continuousErr: errors.New("continuous not supported"),
|
||||
relativeErr: errors.New("relative not supported"),
|
||||
position: 0.5,
|
||||
}
|
||||
|
||||
if err := continuousFocus(client, 0.5, 0); err != nil {
|
||||
t.Fatalf("continuousFocus() error = %v", err)
|
||||
}
|
||||
if len(client.moves) != 3 || client.moves[2].Absolute == nil {
|
||||
t.Fatalf("moves = %+v, want continuous, relative then absolute", client.moves)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContinuousFocusReturnsErrorWhenAllMovesFail(t *testing.T) {
|
||||
client := &fakeFocusClient{
|
||||
continuousErr: errors.New("continuous not supported"),
|
||||
relativeErr: errors.New("relative not supported"),
|
||||
positionErr: errors.New("no position"),
|
||||
}
|
||||
|
||||
err := continuousFocus(client, 0.5, 0)
|
||||
if err == nil || !strings.Contains(err.Error(), "continuous") || !strings.Contains(err.Error(), "relative") || !strings.Contains(err.Error(), "position") {
|
||||
t.Fatalf("continuousFocus() error = %v, want every move error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContinuousFocusReturnsErrorWhenNoMoveSupported(t *testing.T) {
|
||||
client := &fakeFocusClient{options: &focusMoveOptions{}}
|
||||
|
||||
if err := continuousFocus(client, 0.5, 0); err == nil {
|
||||
t.Fatal("continuousFocus() error = nil, want an error")
|
||||
}
|
||||
if len(client.moves) != 0 {
|
||||
t.Fatalf("moves = %+v, want none", client.moves)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContinuousFocusZeroSpeedStops(t *testing.T) {
|
||||
client := &fakeFocusClient{}
|
||||
|
||||
if err := continuousFocus(client, 0, 0); err != nil {
|
||||
t.Fatalf("continuousFocus() error = %v", err)
|
||||
}
|
||||
if len(client.moves) != 0 || len(client.modes) != 0 || client.stopCalls != 1 {
|
||||
t.Fatalf("moves = %d modes = %v stopCalls = %d, want only a stop", len(client.moves), client.modes, client.stopCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAutoFocusRetriggersWhenAlreadyAuto(t *testing.T) {
|
||||
client := &fakeFocusClient{mode: focusModeAuto}
|
||||
|
||||
if err := autoFocus(client, 0); err != nil {
|
||||
t.Fatalf("autoFocus() error = %v", err)
|
||||
}
|
||||
if strings.Join(client.modes, ",") != "MANUAL,AUTO" {
|
||||
t.Fatalf("modes = %v, want [MANUAL AUTO]", client.modes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAutoFocusFromManual(t *testing.T) {
|
||||
client := &fakeFocusClient{mode: focusModeManual}
|
||||
|
||||
if err := autoFocus(client, 0); err != nil {
|
||||
t.Fatalf("autoFocus() error = %v", err)
|
||||
}
|
||||
if strings.Join(client.modes, ",") != "AUTO" {
|
||||
t.Fatalf("modes = %v, want [AUTO]", client.modes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAutoFocusWhenModeUnreadable(t *testing.T) {
|
||||
client := &fakeFocusClient{modeErr: errors.New("unreadable")}
|
||||
|
||||
if err := autoFocus(client, 0); err != nil {
|
||||
t.Fatalf("autoFocus() error = %v", err)
|
||||
}
|
||||
if strings.Join(client.modes, ",") != "AUTO" {
|
||||
t.Fatalf("modes = %v, want [AUTO]", client.modes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAutoFocusRejectsCameraWithoutAutoMode(t *testing.T) {
|
||||
// Dahua cameras advertise only MANUAL.
|
||||
client := &fakeFocusClient{mode: focusModeManual, focusModes: []string{focusModeManual}}
|
||||
|
||||
if err := autoFocus(client, 0); !errors.Is(err, errNoNativeAutoFocus) {
|
||||
t.Fatalf("autoFocus() error = %v, want errNoNativeAutoFocus", err)
|
||||
}
|
||||
if len(client.modes) != 0 {
|
||||
t.Fatalf("modes = %v, want no mode change", client.modes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAutoFocusDetectsIgnoredAutoMode(t *testing.T) {
|
||||
client := &fakeFocusClient{mode: focusModeManual, ignoreAuto: true}
|
||||
|
||||
if err := autoFocus(client, 0); !errors.Is(err, errNoNativeAutoFocus) {
|
||||
t.Fatalf("autoFocus() error = %v, want errNoNativeAutoFocus", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAutoFocusWhenFocusModesUnreadable(t *testing.T) {
|
||||
client := &fakeFocusClient{mode: focusModeManual, focusModesErr: errors.New("no options")}
|
||||
|
||||
if err := autoFocus(client, 0); err != nil {
|
||||
t.Fatalf("autoFocus() error = %v", err)
|
||||
}
|
||||
if strings.Join(client.modes, ",") != "AUTO" {
|
||||
t.Fatalf("modes = %v, want [AUTO]", client.modes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImagingRequestsOnlyCarryFocusFields(t *testing.T) {
|
||||
move, err := xml.Marshal(imagingMoveRequest{
|
||||
VideoSourceToken: "source",
|
||||
Focus: focusMove{Continuous: &continuousFocusMove{Speed: 0.5}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal move: %v", err)
|
||||
}
|
||||
wantMove := "<timg:Move><timg:VideoSourceToken>source</timg:VideoSourceToken><timg:Focus><onvif:Continuous><onvif:Speed>0.5</onvif:Speed></onvif:Continuous></timg:Focus></timg:Move>"
|
||||
if string(move) != wantMove {
|
||||
t.Fatalf("move XML = %s\nwant %s", move, wantMove)
|
||||
}
|
||||
|
||||
absolute, err := xml.Marshal(imagingMoveRequest{
|
||||
VideoSourceToken: "source",
|
||||
Focus: focusMove{Absolute: &absoluteFocusMove{Position: 0.53}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal absolute move: %v", err)
|
||||
}
|
||||
wantAbsolute := "<timg:Move><timg:VideoSourceToken>source</timg:VideoSourceToken><timg:Focus><onvif:Absolute><onvif:Position>0.53</onvif:Position></onvif:Absolute></timg:Focus></timg:Move>"
|
||||
if string(absolute) != wantAbsolute {
|
||||
t.Fatalf("absolute move XML = %s\nwant %s", absolute, wantAbsolute)
|
||||
}
|
||||
|
||||
request := imagingSetFocusModeRequest{VideoSourceToken: "source"}
|
||||
request.ImagingSettings.Focus.AutoFocusMode = focusModeManual
|
||||
settings, err := xml.Marshal(request)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal settings: %v", err)
|
||||
}
|
||||
wantSettings := "<timg:SetImagingSettings><timg:VideoSourceToken>source</timg:VideoSourceToken><timg:ImagingSettings><onvif:Focus><onvif:AutoFocusMode>MANUAL</onvif:AutoFocusMode></onvif:Focus></timg:ImagingSettings></timg:SetImagingSettings>"
|
||||
if string(settings) != wantSettings {
|
||||
t.Fatalf("settings XML = %s\nwant %s", settings, wantSettings)
|
||||
}
|
||||
|
||||
// Brightness must come before Focus (schema order) for the Axis retry.
|
||||
brightness := 50.0
|
||||
request.ImagingSettings.Brightness = &brightness
|
||||
settings, err = xml.Marshal(request)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal settings with brightness: %v", err)
|
||||
}
|
||||
wantSettings = "<timg:SetImagingSettings><timg:VideoSourceToken>source</timg:VideoSourceToken><timg:ImagingSettings><onvif:Brightness>50</onvif:Brightness><onvif:Focus><onvif:AutoFocusMode>MANUAL</onvif:AutoFocusMode></onvif:Focus></timg:ImagingSettings></timg:SetImagingSettings>"
|
||||
if string(settings) != wantSettings {
|
||||
t.Fatalf("settings XML = %s\nwant %s", settings, wantSettings)
|
||||
}
|
||||
}
|
||||
|
||||
const testImagingEnvelope = `<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://www.w3.org/2003/05/soap-envelope" xmlns:timg="http://www.onvif.org/ver20/imaging/wsdl" xmlns:tt="http://www.onvif.org/ver10/schema"><SOAP-ENV:Body>%s</SOAP-ENV:Body></SOAP-ENV:Envelope>`
|
||||
|
||||
func decodeTestImagingResponse(t *testing.T, body string, node string, out interface{}) {
|
||||
t.Helper()
|
||||
decoder, start, err := getXMLNode(strings.Replace(testImagingEnvelope, "%s", body, 1), node)
|
||||
if err != nil {
|
||||
t.Fatalf("getXMLNode() error = %v", err)
|
||||
}
|
||||
if err := decoder.DecodeElement(out, start); err != nil {
|
||||
t.Fatalf("DecodeElement() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImagingSettingsResponseDecodesFocusModeAndBrightness(t *testing.T) {
|
||||
var settings imagingGetSettingsResponse
|
||||
decodeTestImagingResponse(t, `<timg:GetImagingSettingsResponse><timg:ImagingSettings><tt:Brightness>50</tt:Brightness><tt:Focus><tt:AutoFocusMode>AUTO</tt:AutoFocusMode></tt:Focus></timg:ImagingSettings></timg:GetImagingSettingsResponse>`, "GetImagingSettingsResponse", &settings)
|
||||
|
||||
if settings.ImagingSettings.Focus.AutoFocusMode != focusModeAuto {
|
||||
t.Fatalf("AutoFocusMode = %q, want AUTO", settings.ImagingSettings.Focus.AutoFocusMode)
|
||||
}
|
||||
if settings.ImagingSettings.Brightness == nil || *settings.ImagingSettings.Brightness != 50 {
|
||||
t.Fatalf("Brightness = %v, want 50", settings.ImagingSettings.Brightness)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImagingOptionsResponseDecodesFocusModes(t *testing.T) {
|
||||
// Response captured from a Dahua camera.
|
||||
var response imagingGetOptionsResponse
|
||||
decodeTestImagingResponse(t, `<timg:GetOptionsResponse><timg:ImagingOptions><tt:Brightness><tt:Min>0</tt:Min><tt:Max>100</tt:Max></tt:Brightness><tt:Focus><tt:AutoFocusModes>MANUAL</tt:AutoFocusModes><tt:DefaultSpeed><tt:Min>-1</tt:Min><tt:Max>1</tt:Max></tt:DefaultSpeed></tt:Focus></timg:ImagingOptions></timg:GetOptionsResponse>`, "GetOptionsResponse", &response)
|
||||
|
||||
if got := strings.Join(response.ImagingOptions.Focus.AutoFocusModes, ","); got != "MANUAL" {
|
||||
t.Fatalf("AutoFocusModes = %q, want MANUAL", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMoveOptionsResponseDecodesAbsoluteOnly(t *testing.T) {
|
||||
// Response captured from an Axis camera.
|
||||
var response imagingGetMoveOptionsResponse
|
||||
decodeTestImagingResponse(t, `<timg:GetMoveOptionsResponse><timg:MoveOptions><tt:Absolute><tt:Position><tt:Min>0</tt:Min><tt:Max>1</tt:Max></tt:Position></tt:Absolute></timg:MoveOptions></timg:GetMoveOptionsResponse>`, "GetMoveOptionsResponse", &response)
|
||||
|
||||
options := response.MoveOptions
|
||||
if options.Absolute == nil || options.Relative != nil || options.Continuous != nil {
|
||||
t.Fatalf("options = %+v, want only absolute", options)
|
||||
}
|
||||
if options.Absolute.Position.Min != 0 || options.Absolute.Position.Max != 1 {
|
||||
t.Fatalf("absolute range = %+v, want 0..1", options.Absolute.Position)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatusResponseDecodesFocusPosition(t *testing.T) {
|
||||
// Response captured from an Axis camera.
|
||||
var response imagingGetStatusResponse
|
||||
decodeTestImagingResponse(t, `<timg:GetStatusResponse><timg:Status><tt:FocusStatus20><tt:Position>0.479226589</tt:Position><tt:MoveStatus>IDLE</tt:MoveStatus></tt:FocusStatus20></timg:Status></timg:GetStatusResponse>`, "GetStatusResponse", &response)
|
||||
|
||||
if response.Status.FocusStatus20 == nil || response.Status.FocusStatus20.Position == nil || *response.Status.FocusStatus20.Position != 0.479226589 {
|
||||
t.Fatalf("status = %+v, want position 0.479226589", response.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSOAPFaultReasonExtraction(t *testing.T) {
|
||||
fault := `<SOAP-ENV:Fault><SOAP-ENV:Reason><SOAP-ENV:Text xml:lang="en">Argument value invalid</SOAP-ENV:Text></SOAP-ENV:Reason></SOAP-ENV:Fault>`
|
||||
m := soapFaultReason.FindStringSubmatch(fault)
|
||||
if m == nil || m[1] != "Argument value invalid" {
|
||||
t.Fatalf("fault reason = %v, want Argument value invalid", m)
|
||||
}
|
||||
}
|
||||
+35
-14
@@ -119,10 +119,30 @@ func HandleONVIFActions(configuration *models.Configuration, communication *mode
|
||||
// Connect to Onvif device
|
||||
cameraConfiguration := configuration.Config.Capture.IPCamera
|
||||
device, _, err := ConnectToOnvifDevice(&cameraConfiguration)
|
||||
if err == nil {
|
||||
if err == nil && (onvifAction.Action == "focus" || onvifAction.Action == "autofocus") {
|
||||
|
||||
// Get token from the first profile
|
||||
token, err := GetTokenFromProfile(device, 0)
|
||||
// Focus goes through the imaging service, so it also works on
|
||||
// cameras without PTZ (e.g. fixed cameras with a motorised lens).
|
||||
if onvifAction.Action == "focus" {
|
||||
err := ContinuousFocus(device, cameraConfiguration.ONVIFChannel, ptzAction.Focus)
|
||||
if err != nil {
|
||||
log.Error("onvif.HandleONVIFActions() - ContinuousFocus: " + err.Error())
|
||||
} else {
|
||||
log.Info("onvif.HandleONVIFActions() - ContinuousFocus: successfully focused camera")
|
||||
}
|
||||
} else {
|
||||
err := AutoFocus(device, cameraConfiguration.ONVIFChannel)
|
||||
if err != nil {
|
||||
log.Error("onvif.HandleONVIFActions() - AutoFocus: " + err.Error())
|
||||
} else {
|
||||
log.Info("onvif.HandleONVIFActions() - AutoFocus: successfully triggered autofocus")
|
||||
}
|
||||
}
|
||||
|
||||
} else if err == nil {
|
||||
|
||||
// Get the profile token of the configured channel
|
||||
token, err := GetTokenFromChannel(device, cameraConfiguration.ONVIFChannel)
|
||||
if err == nil {
|
||||
|
||||
// Get the configurations from the device
|
||||
@@ -154,7 +174,7 @@ func HandleONVIFActions(configuration *models.Configuration, communication *mode
|
||||
|
||||
// Execute the preset
|
||||
preset := ptzAction.Preset
|
||||
err := GoToPresetFromDevice(device, preset)
|
||||
err := GoToPresetFromDevice(device, cameraConfiguration.ONVIFChannel, preset)
|
||||
if err != nil {
|
||||
log.Debug("onvif.HandleONVIFActions() - GotoPreset: " + err.Error())
|
||||
} else {
|
||||
@@ -349,8 +369,8 @@ func GetPositionFromDevice(configuration models.Configuration) (xsdonvif.PTZVect
|
||||
device, _, err := ConnectToOnvifDevice(&cameraConfiguration)
|
||||
if err == nil {
|
||||
|
||||
// Get token from the first profile
|
||||
token, err := GetTokenFromProfile(device, 0)
|
||||
// Get the profile token of the configured channel
|
||||
token, err := GetTokenFromChannel(device, cameraConfiguration.ONVIFChannel)
|
||||
if err == nil {
|
||||
// Get the PTZ configurations from the device
|
||||
position, err := GetPosition(device, token)
|
||||
@@ -863,12 +883,12 @@ func GetCapabilitiesFromDevice(dev *onvif.Device) []string {
|
||||
return capabilities
|
||||
}
|
||||
|
||||
func GetPresetsFromDevice(device *onvif.Device) ([]models.OnvifActionPreset, error) {
|
||||
func GetPresetsFromDevice(device *onvif.Device, channel int) ([]models.OnvifActionPreset, error) {
|
||||
var presets []models.OnvifActionPreset
|
||||
var presetsResponse ptz.GetPresetsResponse
|
||||
|
||||
// Get token from the first profile
|
||||
token, err := GetTokenFromProfile(device, 0)
|
||||
// Get the profile token of the requested channel
|
||||
token, err := GetTokenFromChannel(device, channel)
|
||||
if err == nil {
|
||||
resp, err := device.CallMethod(ptz.GetPresets{
|
||||
ProfileToken: token,
|
||||
@@ -913,11 +933,11 @@ func GetPresetsFromDevice(device *onvif.Device) ([]models.OnvifActionPreset, err
|
||||
return presets, err
|
||||
}
|
||||
|
||||
func GoToPresetFromDevice(device *onvif.Device, presetName string) error {
|
||||
func GoToPresetFromDevice(device *onvif.Device, channel int, presetName string) error {
|
||||
var goToPresetResponse ptz.GotoPresetResponse
|
||||
|
||||
// Get token from the first profile
|
||||
token, err := GetTokenFromProfile(device, 0)
|
||||
// Get the profile token of the requested channel
|
||||
token, err := GetTokenFromChannel(device, channel)
|
||||
if err == nil {
|
||||
preset := xsdonvif.ReferenceToken(presetName)
|
||||
resp, err := device.CallMethod(ptz.GotoPreset{
|
||||
@@ -1020,6 +1040,7 @@ func VerifyOnvifConnection(c *gin.Context) {
|
||||
ONVIFXAddr: onvifCredentials.ONVIFXAddr,
|
||||
ONVIFUsername: onvifCredentials.ONVIFUsername,
|
||||
ONVIFPassword: onvifCredentials.ONVIFPassword,
|
||||
ONVIFChannel: onvifCredentials.ONVIFChannel,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1028,8 +1049,8 @@ func VerifyOnvifConnection(c *gin.Context) {
|
||||
cameraConfiguration := configuration.Config.Capture.IPCamera
|
||||
device, capabilities, err := ConnectToOnvifDevice(&cameraConfiguration)
|
||||
if err == nil {
|
||||
// Get token from the first profile
|
||||
token, err := GetTokenFromProfile(device, 0)
|
||||
// Get the profile token of the requested channel
|
||||
token, err := GetTokenFromChannel(device, cameraConfiguration.ONVIFChannel)
|
||||
if err == nil {
|
||||
c.JSON(200, gin.H{
|
||||
"device": device,
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/contrib/static"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func TestDemoEnvironmentReadOnly(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
for name, contents := range map[string]string{
|
||||
"env.js": "production", "env.demo.js": "demo", "index.html": "index",
|
||||
} {
|
||||
if err := os.WriteFile(filepath.Join(directory, name), []byte(contents), 0444); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := os.Chmod(directory, 0555); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = os.Chmod(directory, 0755) })
|
||||
router := gin.New()
|
||||
router.Use(demoEnvironmentMiddleware(filepath.Join(directory, "env.demo.js")))
|
||||
router.Use(static.Serve("/", static.LocalFile(directory, true)))
|
||||
for path, want := range map[string]string{"/env.js": "demo", "/": "index"} {
|
||||
response := httptest.NewRecorder()
|
||||
router.ServeHTTP(response, httptest.NewRequest(http.MethodGet, path, nil))
|
||||
if response.Code != http.StatusOK || response.Body.String() != want {
|
||||
t.Fatalf("%s: status %d, body %q; want %q", path, response.Code, response.Body.String(), want)
|
||||
}
|
||||
}
|
||||
contents, err := os.ReadFile(filepath.Join(directory, "env.js"))
|
||||
if err != nil || string(contents) != "production" {
|
||||
t.Fatalf("production environment was changed: %q, %v", contents, err)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -74,6 +75,7 @@ func LoginToOnvif(c *gin.Context) {
|
||||
ONVIFXAddr: onvifCredentials.ONVIFXAddr,
|
||||
ONVIFUsername: onvifCredentials.ONVIFUsername,
|
||||
ONVIFPassword: onvifCredentials.ONVIFPassword,
|
||||
ONVIFChannel: onvifCredentials.ONVIFChannel,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -82,8 +84,8 @@ func LoginToOnvif(c *gin.Context) {
|
||||
cameraConfiguration := configuration.Config.Capture.IPCamera
|
||||
device, capabilities, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
|
||||
if err == nil {
|
||||
// Get token from the first profile
|
||||
token, err := onvif.GetTokenFromProfile(device, 0)
|
||||
// Get the profile token of the requested channel
|
||||
token, err := onvif.GetTokenFromChannel(device, cameraConfiguration.ONVIFChannel)
|
||||
if err == nil {
|
||||
c.JSON(200, gin.H{
|
||||
"device": device,
|
||||
@@ -128,6 +130,7 @@ func GetOnvifCapabilities(c *gin.Context) {
|
||||
ONVIFXAddr: onvifCredentials.ONVIFXAddr,
|
||||
ONVIFUsername: onvifCredentials.ONVIFUsername,
|
||||
ONVIFPassword: onvifCredentials.ONVIFPassword,
|
||||
ONVIFChannel: onvifCredentials.ONVIFChannel,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -172,6 +175,7 @@ func DoOnvifPanTilt(c *gin.Context) {
|
||||
ONVIFXAddr: onvifPanTilt.OnvifCredentials.ONVIFXAddr,
|
||||
ONVIFUsername: onvifPanTilt.OnvifCredentials.ONVIFUsername,
|
||||
ONVIFPassword: onvifPanTilt.OnvifCredentials.ONVIFPassword,
|
||||
ONVIFChannel: onvifPanTilt.OnvifCredentials.ONVIFChannel,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -181,8 +185,8 @@ func DoOnvifPanTilt(c *gin.Context) {
|
||||
device, _, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
|
||||
|
||||
if err == nil {
|
||||
// Get token from the first profile
|
||||
token, err := onvif.GetTokenFromProfile(device, 0)
|
||||
// Get the profile token of the requested channel
|
||||
token, err := onvif.GetTokenFromChannel(device, cameraConfiguration.ONVIFChannel)
|
||||
|
||||
if err == nil {
|
||||
|
||||
@@ -246,6 +250,7 @@ func DoOnvifZoom(c *gin.Context) {
|
||||
ONVIFXAddr: onvifZoom.OnvifCredentials.ONVIFXAddr,
|
||||
ONVIFUsername: onvifZoom.OnvifCredentials.ONVIFUsername,
|
||||
ONVIFPassword: onvifZoom.OnvifCredentials.ONVIFPassword,
|
||||
ONVIFChannel: onvifZoom.OnvifCredentials.ONVIFChannel,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -255,8 +260,8 @@ func DoOnvifZoom(c *gin.Context) {
|
||||
device, _, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
|
||||
|
||||
if err == nil {
|
||||
// Get token from the first profile
|
||||
token, err := onvif.GetTokenFromProfile(device, 0)
|
||||
// Get the profile token of the requested channel
|
||||
token, err := onvif.GetTokenFromChannel(device, cameraConfiguration.ONVIFChannel)
|
||||
|
||||
if err == nil {
|
||||
|
||||
@@ -298,6 +303,54 @@ func DoOnvifZoom(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// DoOnvifFocus godoc
|
||||
// @Router /api/camera/onvif/focus [post]
|
||||
// @ID camera-onvif-focus
|
||||
// @Tags onvif
|
||||
// @Param focus body models.OnvifFocus true "OnvifFocus"
|
||||
// @Summary Focusing the camera near or far.
|
||||
// @Description Focusing the camera near (positive) or far (negative).
|
||||
// @Success 200 {object} models.APIResponse
|
||||
func DoOnvifFocus(c *gin.Context) {
|
||||
var onvifFocus models.OnvifFocus
|
||||
err := c.BindJSON(&onvifFocus)
|
||||
if err == nil && onvifFocus.OnvifCredentials.ONVIFXAddr == "" {
|
||||
err = errors.New("missing onvif_xaddr")
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
|
||||
cameraConfiguration := models.IPCamera{
|
||||
ONVIFXAddr: onvifFocus.OnvifCredentials.ONVIFXAddr,
|
||||
ONVIFUsername: onvifFocus.OnvifCredentials.ONVIFUsername,
|
||||
ONVIFPassword: onvifFocus.OnvifCredentials.ONVIFPassword,
|
||||
ONVIFChannel: onvifFocus.OnvifCredentials.ONVIFChannel,
|
||||
}
|
||||
device, _, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
|
||||
|
||||
if err == nil {
|
||||
err := onvif.ContinuousFocus(device, cameraConfiguration.ONVIFChannel, onvifFocus.Focus)
|
||||
if err == nil {
|
||||
c.JSON(200, models.APIResponse{
|
||||
Message: "Successfully focused the camera",
|
||||
})
|
||||
} else {
|
||||
c.JSON(400, models.APIResponse{
|
||||
Message: "Something went wrong: " + err.Error(),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
c.JSON(400, models.APIResponse{
|
||||
Message: "Something went wrong: " + err.Error(),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
c.JSON(400, models.APIResponse{
|
||||
Message: "Something went wrong: " + err.Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// GetOnvifPresets godoc
|
||||
// @Router /api/camera/onvif/presets [post]
|
||||
// @ID camera-onvif-presets
|
||||
@@ -319,6 +372,7 @@ func GetOnvifPresets(c *gin.Context) {
|
||||
ONVIFXAddr: onvifCredentials.ONVIFXAddr,
|
||||
ONVIFUsername: onvifCredentials.ONVIFUsername,
|
||||
ONVIFPassword: onvifCredentials.ONVIFPassword,
|
||||
ONVIFChannel: onvifCredentials.ONVIFChannel,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -327,7 +381,7 @@ func GetOnvifPresets(c *gin.Context) {
|
||||
cameraConfiguration := configuration.Config.Capture.IPCamera
|
||||
device, _, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
|
||||
if err == nil {
|
||||
presets, err := onvif.GetPresetsFromDevice(device)
|
||||
presets, err := onvif.GetPresetsFromDevice(device, cameraConfiguration.ONVIFChannel)
|
||||
if err == nil {
|
||||
c.JSON(200, gin.H{
|
||||
"presets": presets,
|
||||
@@ -370,6 +424,7 @@ func GoToOnvifPreset(c *gin.Context) {
|
||||
ONVIFXAddr: onvifPreset.OnvifCredentials.ONVIFXAddr,
|
||||
ONVIFUsername: onvifPreset.OnvifCredentials.ONVIFUsername,
|
||||
ONVIFPassword: onvifPreset.OnvifCredentials.ONVIFPassword,
|
||||
ONVIFChannel: onvifPreset.OnvifCredentials.ONVIFChannel,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -378,7 +433,7 @@ func GoToOnvifPreset(c *gin.Context) {
|
||||
cameraConfiguration := configuration.Config.Capture.IPCamera
|
||||
device, _, err := onvif.ConnectToOnvifDevice(&cameraConfiguration)
|
||||
if err == nil {
|
||||
err := onvif.GoToPresetFromDevice(device, onvifPreset.Preset)
|
||||
err := onvif.GoToPresetFromDevice(device, cameraConfiguration.ONVIFChannel, onvifPreset.Preset)
|
||||
if err == nil {
|
||||
c.JSON(200, gin.H{
|
||||
"data": "Camera preset activated: " + onvifPreset.Preset,
|
||||
@@ -425,6 +480,7 @@ func DoGetDigitalInputs(c *gin.Context) {
|
||||
ONVIFXAddr: onvifCredentials.ONVIFXAddr,
|
||||
ONVIFUsername: onvifCredentials.ONVIFUsername,
|
||||
ONVIFPassword: onvifCredentials.ONVIFPassword,
|
||||
ONVIFChannel: onvifCredentials.ONVIFChannel,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -514,6 +570,7 @@ func DoGetRelayOutputs(c *gin.Context) {
|
||||
ONVIFXAddr: onvifCredentials.ONVIFXAddr,
|
||||
ONVIFUsername: onvifCredentials.ONVIFUsername,
|
||||
ONVIFPassword: onvifCredentials.ONVIFPassword,
|
||||
ONVIFChannel: onvifCredentials.ONVIFChannel,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -587,6 +644,7 @@ func DoTriggerRelayOutput(c *gin.Context) {
|
||||
ONVIFXAddr: onvifCredentials.ONVIFXAddr,
|
||||
ONVIFUsername: onvifCredentials.ONVIFUsername,
|
||||
ONVIFPassword: onvifCredentials.ONVIFPassword,
|
||||
ONVIFChannel: onvifCredentials.ONVIFChannel,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -108,6 +108,7 @@ func AddRoutes(r *gin.Engine, authMiddleware *jwt.GinJWTMiddleware, configDirect
|
||||
api.POST("/camera/onvif/gotopreset", GoToOnvifPreset)
|
||||
api.POST("/camera/onvif/pantilt", DoOnvifPanTilt)
|
||||
api.POST("/camera/onvif/zoom", DoOnvifZoom)
|
||||
api.POST("/camera/onvif/focus", DoOnvifFocus)
|
||||
api.POST("/camera/onvif/inputs", DoGetDigitalInputs)
|
||||
api.POST("/camera/onvif/outputs", DoGetRelayOutputs)
|
||||
api.POST("/camera/onvif/outputs/:output", DoTriggerRelayOutput)
|
||||
|
||||
@@ -43,15 +43,6 @@ import (
|
||||
// @in header
|
||||
// @name Authorization
|
||||
|
||||
func demoEnvironmentMiddleware(path string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if c.Request.URL.Path == "/env.js" && (c.Request.Method == "GET" || c.Request.Method == "HEAD") {
|
||||
c.File(path)
|
||||
c.Abort()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func StartServer(configDirectory string, configuration *models.Configuration, communication *models.Communication, captureDevice *capture.Capture) {
|
||||
|
||||
// Set release mode
|
||||
@@ -93,9 +84,17 @@ func StartServer(configDirectory string, configuration *models.Configuration, co
|
||||
AddRoutes(r, authMiddleware, configDirectory, configuration, communication, captureDevice)
|
||||
|
||||
// Update environment variables
|
||||
environmentVariables := configDirectory + "/www/env.js"
|
||||
if os.Getenv("AGENT_MODE") == "demo" {
|
||||
demoEnvironmentVariables := configDirectory + "/www/env.demo.js"
|
||||
r.Use(demoEnvironmentMiddleware(demoEnvironmentVariables))
|
||||
// Move demo environment variables to environment variables
|
||||
err := os.Rename(demoEnvironmentVariables, environmentVariables)
|
||||
if err != nil {
|
||||
log.WithError(err).WithFields(log.Fields{
|
||||
"component": "http",
|
||||
"event": "demo_environment_move_failed",
|
||||
}).Fatal("Failed to activate demo UI environment")
|
||||
}
|
||||
}
|
||||
|
||||
// Add static routes to UI
|
||||
|
||||
Reference in New Issue
Block a user