mirror of
https://github.com/kerberos-io/agent.git
synced 2026-08-23 15:08:32 +00:00
Merge pull request #310 from kerberos-io/feature/integrate-moq-streaming-protocol
feature/integrate-moq-streaming-protocol
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
FROM mcr.microsoft.com/devcontainers/go:1.24-bookworm
|
FROM mcr.microsoft.com/devcontainers/go:1.24-trixie
|
||||||
|
|
||||||
# Install node environment
|
# Install node environment
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||||
// README at: https://github.com/devcontainers/templates/tree/main/src/python
|
// README at: https://github.com/devcontainers/templates/tree/main/src/python
|
||||||
{
|
{
|
||||||
"name": "go:1.24-bookworm",
|
"name": "go:1.24-trixie",
|
||||||
"runArgs": [
|
"runArgs": [
|
||||||
"--name=agent",
|
"--name=agent",
|
||||||
"--network=host"
|
"--network=host"
|
||||||
@@ -20,5 +20,5 @@
|
|||||||
3000,
|
3000,
|
||||||
8080
|
8080
|
||||||
],
|
],
|
||||||
"postCreateCommand": "cd ui && yarn install && yarn build && cd ../machinery && go mod download"
|
"postCreateCommand": "cd ui && yarn install && yarn build && cd ../machinery && go mod download && bash ./verify-moq-devcontainer.sh"
|
||||||
}
|
}
|
||||||
4
.github/workflows/pr-build.yml
vendored
4
.github/workflows/pr-build.yml
vendored
@@ -16,10 +16,8 @@ jobs:
|
|||||||
include:
|
include:
|
||||||
- architecture: amd64
|
- architecture: amd64
|
||||||
runner: ubuntu-24.04
|
runner: ubuntu-24.04
|
||||||
dockerfile: Dockerfile
|
|
||||||
- architecture: arm64
|
- architecture: arm64
|
||||||
runner: ubuntu-24.04-arm
|
runner: ubuntu-24.04-arm
|
||||||
dockerfile: Dockerfile.arm64
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
@@ -34,7 +32,7 @@ jobs:
|
|||||||
length: 7
|
length: 7
|
||||||
- name: Run Build
|
- name: Run Build
|
||||||
run: |
|
run: |
|
||||||
docker build -t ${{ matrix.architecture }} -f ${{ matrix.dockerfile }} .
|
docker build -t ${{ matrix.architecture }} .
|
||||||
CID=$(docker create ${{matrix.architecture}})
|
CID=$(docker create ${{matrix.architecture}})
|
||||||
docker cp ${CID}:/home/agent ./output-${{matrix.architecture}}
|
docker cp ${CID}:/home/agent ./output-${{matrix.architecture}}
|
||||||
docker rm ${CID}
|
docker rm ${CID}
|
||||||
|
|||||||
2
.github/workflows/release-bump.yml
vendored
2
.github/workflows/release-bump.yml
vendored
@@ -108,7 +108,7 @@ jobs:
|
|||||||
length: 7
|
length: 7
|
||||||
- name: Run Build
|
- name: Run Build
|
||||||
run: |
|
run: |
|
||||||
docker build --provenance=false --build-arg VERSION=${{ needs.bump-release.outputs.tag }} -t ${{matrix.architecture}} -f Dockerfile.arm64 .
|
docker build --provenance=false --build-arg VERSION=${{ needs.bump-release.outputs.tag }} -t ${{matrix.architecture}} .
|
||||||
CID=$(docker create ${{matrix.architecture}})
|
CID=$(docker create ${{matrix.architecture}})
|
||||||
docker cp ${CID}:/home/agent ./output-${{matrix.architecture}}
|
docker cp ${CID}:/home/agent ./output-${{matrix.architecture}}
|
||||||
docker rm ${CID}
|
docker rm ${CID}
|
||||||
|
|||||||
2
.github/workflows/release-create.yml
vendored
2
.github/workflows/release-create.yml
vendored
@@ -71,7 +71,7 @@ jobs:
|
|||||||
length: 7
|
length: 7
|
||||||
- name: Run Build
|
- name: Run Build
|
||||||
run: |
|
run: |
|
||||||
docker build --provenance=false --build-arg VERSION=${{github.event.inputs.tag || github.ref_name}} -t ${{matrix.architecture}} -f Dockerfile.arm64 .
|
docker build --provenance=false --build-arg VERSION=${{github.event.inputs.tag || github.ref_name}} -t ${{matrix.architecture}} .
|
||||||
CID=$(docker create ${{matrix.architecture}})
|
CID=$(docker create ${{matrix.architecture}})
|
||||||
docker cp ${CID}:/home/agent ./output-${{matrix.architecture}}
|
docker cp ${CID}:/home/agent ./output-${{matrix.architecture}}
|
||||||
docker rm ${CID}
|
docker rm ${CID}
|
||||||
|
|||||||
14
.vscode/tasks.json
vendored
Normal file
14
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "agent: moq verify",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "bash ./verify-moq-devcontainer.sh",
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}/machinery"
|
||||||
|
},
|
||||||
|
"problemMatcher": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
27
Dockerfile
27
Dockerfile
@@ -1,13 +1,15 @@
|
|||||||
|
|
||||||
ARG BASE_IMAGE_VERSION=amd64-ddbe40e
|
ARG GO_IMAGE=golang:1.24-trixie
|
||||||
|
ARG RUNTIME_IMAGE=debian:trixie-slim
|
||||||
ARG VERSION=0.0.0
|
ARG VERSION=0.0.0
|
||||||
FROM kerberos/base:${BASE_IMAGE_VERSION} AS build-machinery
|
FROM ${GO_IMAGE} AS build-machinery
|
||||||
LABEL AUTHOR=uug.ai
|
LABEL AUTHOR=uug.ai
|
||||||
|
|
||||||
# Re-declare VERSION inside this stage so the value passed via
|
# Re-declare VERSION inside this stage so the value passed via
|
||||||
# `--build-arg VERSION=...` (e.g. the release tag) is available below.
|
# `--build-arg VERSION=...` (e.g. the release tag) is available below.
|
||||||
# ARGs declared before the first FROM are not visible inside build stages.
|
# ARGs declared before the first FROM are not visible inside build stages.
|
||||||
ARG VERSION
|
ARG VERSION
|
||||||
|
ARG TARGETARCH
|
||||||
|
|
||||||
ENV GOROOT=/usr/local/go
|
ENV GOROOT=/usr/local/go
|
||||||
ENV GOPATH=/go
|
ENV GOPATH=/go
|
||||||
@@ -17,9 +19,10 @@ ENV GOSUMDB=off
|
|||||||
##########################################
|
##########################################
|
||||||
# Installing some additional dependencies.
|
# Installing some additional dependencies.
|
||||||
|
|
||||||
RUN apt-get upgrade -y && apt-get update && apt-get install -y --fix-missing --no-install-recommends \
|
RUN apt-get update && apt-get install -y --fix-missing --no-install-recommends \
|
||||||
git build-essential cmake pkg-config unzip libgtk2.0-dev \
|
git build-essential cmake pkg-config unzip libgtk2.0-dev \
|
||||||
curl ca-certificates libcurl4-openssl-dev libssl-dev libjpeg62-turbo-dev && \
|
curl ca-certificates libavcodec-dev libavutil-dev libcurl4-openssl-dev \
|
||||||
|
libssl-dev libjpeg62-turbo-dev libswscale-dev && \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
@@ -43,7 +46,9 @@ RUN cd /go/src/github.com/kerberos-io/agent/machinery && \
|
|||||||
if [ -z "${VERSION}" ] || [ "${VERSION}" = "0.0.0" ]; then \
|
if [ -z "${VERSION}" ] || [ "${VERSION}" = "0.0.0" ]; then \
|
||||||
VERSION=$(cd /go/src/github.com/kerberos-io/agent && git describe --tags --always 2>/dev/null || echo "0.0.0"); \
|
VERSION=$(cd /go/src/github.com/kerberos-io/agent && git describe --tags --always 2>/dev/null || echo "0.0.0"); \
|
||||||
fi && \
|
fi && \
|
||||||
go build -tags timetzdata,netgo,osusergo --ldflags "-s -w -X github.com/kerberos-io/agent/machinery/src/utils.VERSION=${VERSION} -extldflags '-static -latomic'" main.go && \
|
BUILD_TAGS=timetzdata,netgo,osusergo && \
|
||||||
|
case "${TARGETARCH:-$(go env GOARCH)}" in amd64|arm64) BUILD_TAGS="moq,${BUILD_TAGS}" ;; esac && \
|
||||||
|
go build -tags "${BUILD_TAGS}" --ldflags "-s -w -X github.com/kerberos-io/agent/machinery/src/utils.VERSION=${VERSION}" main.go && \
|
||||||
mkdir -p /agent && \
|
mkdir -p /agent && \
|
||||||
mv main /agent && \
|
mv main /agent && \
|
||||||
mv version /agent && \
|
mv version /agent && \
|
||||||
@@ -89,12 +94,16 @@ RUN mkdir -p ./agent && cp -r /go/src/github.com/kerberos-io/agent/machinery/www
|
|||||||
############################################
|
############################################
|
||||||
# Publish main binary to GitHub release
|
# Publish main binary to GitHub release
|
||||||
|
|
||||||
FROM alpine:latest
|
FROM ${RUNTIME_IMAGE}
|
||||||
|
|
||||||
############################
|
############################
|
||||||
# Protect by non-root user.
|
# Protect by non-root user.
|
||||||
|
|
||||||
RUN addgroup -S kerberosio && adduser -S agent -G kerberosio && addgroup agent video
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
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
|
||||||
|
|
||||||
#################################
|
#################################
|
||||||
# Copy files from previous images
|
# Copy files from previous images
|
||||||
@@ -102,8 +111,6 @@ RUN addgroup -S kerberosio && adduser -S agent -G kerberosio && addgroup agent v
|
|||||||
COPY --chown=0:0 --from=build-machinery /dist /
|
COPY --chown=0:0 --from=build-machinery /dist /
|
||||||
COPY --chown=0:0 --from=build-ui /dist /
|
COPY --chown=0:0 --from=build-ui /dist /
|
||||||
|
|
||||||
RUN apk update && apk add ca-certificates curl ffmpeg libstdc++ libc6-compat --no-cache && rm -rf /var/cache/apk/*
|
|
||||||
|
|
||||||
##################
|
##################
|
||||||
# Try running agent
|
# Try running agent
|
||||||
|
|
||||||
@@ -123,7 +130,7 @@ RUN chown -R agent:kerberosio /home/agent/www
|
|||||||
|
|
||||||
###########################
|
###########################
|
||||||
# Grant the necessary root capabilities to the process trying to bind to the privileged port
|
# Grant the necessary root capabilities to the process trying to bind to the privileged port
|
||||||
RUN apk add libcap && setcap 'cap_net_bind_service=+ep' /home/agent/main
|
RUN setcap 'cap_net_bind_service=+ep' /home/agent/main
|
||||||
|
|
||||||
###################
|
###################
|
||||||
# Run non-root user
|
# Run non-root user
|
||||||
|
|||||||
147
Dockerfile.arm64
147
Dockerfile.arm64
@@ -1,147 +0,0 @@
|
|||||||
|
|
||||||
ARG BASE_IMAGE_VERSION=arm64-ddbe40e
|
|
||||||
ARG VERSION=0.0.0
|
|
||||||
FROM kerberos/base:${BASE_IMAGE_VERSION} AS build-machinery
|
|
||||||
LABEL AUTHOR=uug.ai
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
ENV GOROOT=/usr/local/go
|
|
||||||
ENV GOPATH=/go
|
|
||||||
ENV PATH=$GOPATH/bin:$GOROOT/bin:/usr/local/lib:$PATH
|
|
||||||
ENV GOSUMDB=off
|
|
||||||
|
|
||||||
##########################################
|
|
||||||
# Installing some additional dependencies.
|
|
||||||
|
|
||||||
RUN apt-get upgrade -y && apt-get update && apt-get install -y --fix-missing --no-install-recommends \
|
|
||||||
git build-essential cmake pkg-config unzip libgtk2.0-dev \
|
|
||||||
curl ca-certificates libcurl4-openssl-dev libssl-dev libjpeg62-turbo-dev && \
|
|
||||||
rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
##############################################################################
|
|
||||||
# Copy all the relevant source code in the Docker image, so we can build this.
|
|
||||||
|
|
||||||
RUN mkdir -p /go/src/github.com/kerberos-io/agent
|
|
||||||
COPY machinery /go/src/github.com/kerberos-io/agent/machinery
|
|
||||||
RUN rm -rf /go/src/github.com/kerberos-io/agent/machinery/.env
|
|
||||||
|
|
||||||
##################################################################
|
|
||||||
# Get the latest commit hash, so we know which version we're running
|
|
||||||
COPY .git /go/src/github.com/kerberos-io/agent/.git
|
|
||||||
RUN cd /go/src/github.com/kerberos-io/agent/.git && git log --format="%H" -n 1 | head -c7 > /go/src/github.com/kerberos-io/agent/machinery/version
|
|
||||||
RUN cat /go/src/github.com/kerberos-io/agent/machinery/version
|
|
||||||
|
|
||||||
##################
|
|
||||||
# Build Machinery
|
|
||||||
|
|
||||||
RUN cd /go/src/github.com/kerberos-io/agent/machinery && \
|
|
||||||
go mod download && \
|
|
||||||
if [ -z "${VERSION}" ] || [ "${VERSION}" = "0.0.0" ]; then \
|
|
||||||
VERSION=$(cd /go/src/github.com/kerberos-io/agent && git describe --tags --always 2>/dev/null || echo "0.0.0"); \
|
|
||||||
fi && \
|
|
||||||
go build -tags timetzdata,netgo,osusergo --ldflags "-s -w -X github.com/kerberos-io/agent/machinery/src/utils.VERSION=${VERSION} -extldflags '-static -latomic'" main.go && \
|
|
||||||
mkdir -p /agent && \
|
|
||||||
mv main /agent && \
|
|
||||||
mv version /agent && \
|
|
||||||
mv data /agent && \
|
|
||||||
mkdir -p /agent/data/cloud && \
|
|
||||||
mkdir -p /agent/data/snapshots && \
|
|
||||||
mkdir -p /agent/data/log && \
|
|
||||||
mkdir -p /agent/data/recordings && \
|
|
||||||
mkdir -p /agent/data/capture-test && \
|
|
||||||
mkdir -p /agent/data/config
|
|
||||||
|
|
||||||
####################################
|
|
||||||
# Let's create a /dist folder containing just the files necessary for runtime.
|
|
||||||
# Later, it will be copied as the / (root) of the output image.
|
|
||||||
|
|
||||||
WORKDIR /dist
|
|
||||||
RUN cp -r /agent ./
|
|
||||||
|
|
||||||
####################################################################################
|
|
||||||
# This will collect dependent libraries so they're later copied to the final image.
|
|
||||||
|
|
||||||
RUN /dist/agent/main version
|
|
||||||
|
|
||||||
FROM node: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
|
|
||||||
COPY ui /go/src/github.com/kerberos-io/agent/ui
|
|
||||||
RUN cd /go/src/github.com/kerberos-io/agent/ui && rm -rf yarn.lock && yarn config set network-timeout 300000 && \
|
|
||||||
yarn && yarn build
|
|
||||||
|
|
||||||
####################################
|
|
||||||
# Let's create a /dist folder containing just the files necessary for runtime.
|
|
||||||
# Later, it will be copied as the / (root) of the output image.
|
|
||||||
|
|
||||||
WORKDIR /dist
|
|
||||||
RUN mkdir -p ./agent && cp -r /go/src/github.com/kerberos-io/agent/machinery/www ./agent/
|
|
||||||
|
|
||||||
############################################
|
|
||||||
# Publish main binary to GitHub release
|
|
||||||
|
|
||||||
FROM alpine:latest
|
|
||||||
|
|
||||||
############################
|
|
||||||
# Protect by non-root user.
|
|
||||||
|
|
||||||
RUN addgroup -S kerberosio && adduser -S agent -G kerberosio && addgroup agent video
|
|
||||||
|
|
||||||
#################################
|
|
||||||
# Copy files from previous images
|
|
||||||
|
|
||||||
COPY --chown=0:0 --from=build-machinery /dist /
|
|
||||||
COPY --chown=0:0 --from=build-ui /dist /
|
|
||||||
|
|
||||||
RUN apk update && apk add ca-certificates curl ffmpeg libstdc++ libc6-compat --no-cache && rm -rf /var/cache/apk/*
|
|
||||||
|
|
||||||
##################
|
|
||||||
# Try running agent
|
|
||||||
|
|
||||||
RUN mv /agent/* /home/agent/
|
|
||||||
RUN /home/agent/main version
|
|
||||||
|
|
||||||
#######################
|
|
||||||
# Make template config
|
|
||||||
|
|
||||||
RUN cp /home/agent/data/config/config.json /home/agent/data/config.template.json
|
|
||||||
|
|
||||||
###########################
|
|
||||||
# Set permissions correctly
|
|
||||||
|
|
||||||
RUN chown -R agent:kerberosio /home/agent/data
|
|
||||||
RUN chown -R agent:kerberosio /home/agent/www
|
|
||||||
|
|
||||||
###########################
|
|
||||||
# Grant the necessary root capabilities to the process trying to bind to the privileged port
|
|
||||||
RUN apk add libcap && setcap 'cap_net_bind_service=+ep' /home/agent/main
|
|
||||||
|
|
||||||
###################
|
|
||||||
# Run non-root user
|
|
||||||
|
|
||||||
USER agent
|
|
||||||
|
|
||||||
######################################
|
|
||||||
# By default the app runs on port 80
|
|
||||||
|
|
||||||
EXPOSE 80
|
|
||||||
|
|
||||||
######################################
|
|
||||||
# Check if agent is still running
|
|
||||||
|
|
||||||
HEALTHCHECK CMD curl --fail http://localhost:80 || exit 1
|
|
||||||
|
|
||||||
###################################################
|
|
||||||
# Leeeeettttt'ssss goooooo!!!
|
|
||||||
# Run the shizzle from the right working directory.
|
|
||||||
WORKDIR /home/agent
|
|
||||||
CMD ["./main", "-action", "run", "-port", "80"]
|
|
||||||
34
README.md
34
README.md
@@ -412,13 +412,43 @@ Remember the build step of the `web` part, during build time we move the build d
|
|||||||
|
|
||||||
## Building for Docker
|
## Building for Docker
|
||||||
|
|
||||||
Inside the root of this `agent` repository, you will find a `Dockerfile`. This file contains the instructions for building and shipping a **Kerberos Agent**. Important to note is that you start from a prebuilt base image, `kerberos/base:xxx`.
|
Inside the root of this `agent` repository, you will find a `Dockerfile`. This file contains the instructions for building and shipping a **Kerberos Agent**. It uses Debian Trixie to support the native dependencies used by the Agent, including Media over QUIC.
|
||||||
This base image already contains a couple of tools, such as Golang, FFmpeg and OpenCV. We do this for faster compilation times.
|
|
||||||
|
|
||||||
By running the `docker build` command, you will create the Kerberos Agent Docker image. After building you can simply run the image as a Docker container.
|
By running the `docker build` command, you will create the Kerberos Agent Docker image. After building you can simply run the image as a Docker container.
|
||||||
|
|
||||||
docker build -t kerberos/agent .
|
docker build -t kerberos/agent .
|
||||||
|
|
||||||
|
### Media over QUIC
|
||||||
|
|
||||||
|
The standard AMD64 and ARM64 images include the optional MoQ publisher. Its Rust
|
||||||
|
FFI archive requires CGO and glibc 2.38 or newer, which is why the standard image
|
||||||
|
uses Debian Trixie. The publisher is disabled unless explicitly enabled at runtime:
|
||||||
|
|
||||||
|
docker run --rm -p 80:80 \
|
||||||
|
-e AGENT_LIVE_MOQ_ENABLED=true \
|
||||||
|
-e AGENT_LIVE_MOQ_URL=https://relay.uug.ai/anon \
|
||||||
|
kerberos/agent
|
||||||
|
|
||||||
|
`AGENT_LIVE_MOQ_BROADCAST_PREFIX` defaults to `devices`, producing the broadcast
|
||||||
|
`devices/<agent-key>/live.hang`. `AGENT_LIVE_MOQ_QUALITY` accepts `auto` (the
|
||||||
|
default), `high`, or `low` and selects the main or sub camera stream when the
|
||||||
|
Agent starts. The initial implementation publishes H.264 video only.
|
||||||
|
|
||||||
|
The `/anon` relay route is intended for interoperability testing. Production
|
||||||
|
deployments must set `AGENT_LIVE_MOQ_URL` to a short-lived, device-scoped
|
||||||
|
publisher URL issued by Hub API.
|
||||||
|
|
||||||
|
To verify the native SDK in a development container, rebuild the Agent or shared
|
||||||
|
monorepo devcontainer so it uses the Trixie base, then run the VS Code task
|
||||||
|
`agent: moq verify`. The same check is available from a terminal:
|
||||||
|
|
||||||
|
cd machinery
|
||||||
|
bash ./verify-moq-devcontainer.sh
|
||||||
|
|
||||||
|
The check requires glibc 2.38 or newer, runs the tagged package tests, links the
|
||||||
|
complete Agent with `-tags moq`, and executes the resulting binary's version
|
||||||
|
command. Both devcontainers also run this check during their post-create setup.
|
||||||
|
|
||||||
## What is new?
|
## What is new?
|
||||||
|
|
||||||
This repository contains the next generation of Kerberos.io, **Kerberos Agent (v3)**, and is the successor of the machinery and web repositories. A switch in technologies and architecture has been made. This version is still under active development and can be followed on the [develop branch](https://github.com/kerberos-io/agent/tree/develop) and [project overview](https://github.com/kerberos-io/agent/projects/1).
|
This repository contains the next generation of Kerberos.io, **Kerberos Agent (v3)**, and is the successor of the machinery and web repositories. A switch in technologies and architecture has been made. This version is still under active development and can be followed on the [develop branch](https://github.com/kerberos-io/agent/tree/develop) and [project overview](https://github.com/kerberos-io/agent/projects/1).
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ require (
|
|||||||
github.com/kerberos-io/joy4 v1.0.64
|
github.com/kerberos-io/joy4 v1.0.64
|
||||||
github.com/kerberos-io/onvif v1.2.2
|
github.com/kerberos-io/onvif v1.2.2
|
||||||
github.com/minio/minio-go/v6 v6.0.57
|
github.com/minio/minio-go/v6 v6.0.57
|
||||||
|
github.com/moq-dev/moq-go v0.5.7
|
||||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
|
||||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||||
github.com/pion/interceptor v0.1.40
|
github.com/pion/interceptor v0.1.40
|
||||||
@@ -95,6 +96,7 @@ require (
|
|||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/montanaflynn/stats v0.7.1 // indirect
|
github.com/montanaflynn/stats v0.7.1 // indirect
|
||||||
|
github.com/moq-dev/moq-go-ffi v0.3.7 // indirect
|
||||||
github.com/nxadm/tail v1.4.11 // indirect
|
github.com/nxadm/tail v1.4.11 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||||
github.com/pion/datachannel v1.5.10 // indirect
|
github.com/pion/datachannel v1.5.10 // indirect
|
||||||
|
|||||||
@@ -845,6 +845,10 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
|
|||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
|
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
|
||||||
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||||
|
github.com/moq-dev/moq-go v0.5.7 h1:LfFpgAU8FRMcnU85L5Lb03HzxhYbQ2+BhT/pJQdjy5U=
|
||||||
|
github.com/moq-dev/moq-go v0.5.7/go.mod h1:5K8zjKKjWe5lzfCtlfxoAfdCr3KX6EBuTOZsz0WBnzw=
|
||||||
|
github.com/moq-dev/moq-go-ffi v0.3.7 h1:+xwPOzTJHvB0tuTnW6znbvvOgT4yLnN7sMqXXZYh02M=
|
||||||
|
github.com/moq-dev/moq-go-ffi v0.3.7/go.mod h1:zxpOlUetvaoxWBnbXTdILtLLUytaoVuLvO36lftwWO0=
|
||||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||||
|
|||||||
65
machinery/src/cloud/livemoq/annexb.go
Normal file
65
machinery/src/cloud/livemoq/annexb.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package livemoq
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/bluenviron/mediacommon/pkg/codecs/h264"
|
||||||
|
)
|
||||||
|
|
||||||
|
var annexBStartCode = []byte{0x00, 0x00, 0x00, 0x01}
|
||||||
|
|
||||||
|
// EnsureAnnexB restores the start code stripped by the Agent capture queue.
|
||||||
|
func EnsureAnnexB(payload []byte) []byte {
|
||||||
|
if hasAnnexBStartCode(payload) {
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
framed := make([]byte, 0, len(annexBStartCode)+len(payload))
|
||||||
|
framed = append(framed, annexBStartCode...)
|
||||||
|
return append(framed, payload...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NormalizeH264AccessUnit removes delimiters and duplicate parameter sets that
|
||||||
|
// can make older MoQ splitters emit a parameter-only frame before the IDR.
|
||||||
|
func NormalizeH264AccessUnit(payload []byte) ([]byte, error) {
|
||||||
|
nalus, err := h264.AnnexBUnmarshal(EnsureAnnexB(payload))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
normalized := make([][]byte, 0, len(nalus))
|
||||||
|
for _, nalu := range nalus {
|
||||||
|
if len(nalu) == 0 || nalu[0]&0x1f == 9 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if nalu[0]&0x1f == 7 || nalu[0]&0x1f == 8 {
|
||||||
|
duplicate := false
|
||||||
|
for _, existing := range normalized {
|
||||||
|
if bytes.Equal(existing, nalu) {
|
||||||
|
duplicate = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if duplicate {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
normalized = append(normalized, nalu)
|
||||||
|
}
|
||||||
|
|
||||||
|
return h264.AnnexBMarshal(normalized)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BroadcastPath(prefix string, deviceKey string) string {
|
||||||
|
prefix = strings.Trim(prefix, "/")
|
||||||
|
if prefix == "" {
|
||||||
|
prefix = "devices"
|
||||||
|
}
|
||||||
|
return prefix + "/" + strings.Trim(deviceKey, "/") + "/live.hang"
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasAnnexBStartCode(payload []byte) bool {
|
||||||
|
return len(payload) >= 4 && payload[0] == 0 && payload[1] == 0 &&
|
||||||
|
((payload[2] == 0 && payload[3] == 1) || payload[2] == 1)
|
||||||
|
}
|
||||||
74
machinery/src/cloud/livemoq/annexb_test.go
Normal file
74
machinery/src/cloud/livemoq/annexb_test.go
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
package livemoq
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEnsureAnnexB(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
payload []byte
|
||||||
|
want []byte
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "missing start code",
|
||||||
|
payload: []byte{0x41, 0x01},
|
||||||
|
want: []byte{0x00, 0x00, 0x00, 0x01, 0x41, 0x01},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "four byte start code",
|
||||||
|
payload: []byte{0x00, 0x00, 0x00, 0x01, 0x65},
|
||||||
|
want: []byte{0x00, 0x00, 0x00, 0x01, 0x65},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "three byte start code",
|
||||||
|
payload: []byte{0x00, 0x00, 0x01, 0x41},
|
||||||
|
want: []byte{0x00, 0x00, 0x01, 0x41},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
if got := EnsureAnnexB(test.payload); !bytes.Equal(got, test.want) {
|
||||||
|
t.Fatalf("EnsureAnnexB() = %x, want %x", got, test.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeH264AccessUnit(t *testing.T) {
|
||||||
|
startCode := []byte{0x00, 0x00, 0x00, 0x01}
|
||||||
|
sps := []byte{0x67, 0x42, 0x00, 0x1f}
|
||||||
|
pps := []byte{0x68, 0xce, 0x06, 0xe2}
|
||||||
|
aud := []byte{0x09, 0xf0}
|
||||||
|
idr := []byte{0x65, 0x88, 0x84}
|
||||||
|
|
||||||
|
payload := make([]byte, 0)
|
||||||
|
for _, nalu := range [][]byte{sps, pps, aud, sps, pps, idr} {
|
||||||
|
payload = append(payload, startCode...)
|
||||||
|
payload = append(payload, nalu...)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := NormalizeH264AccessUnit(payload)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want := make([]byte, 0)
|
||||||
|
for _, nalu := range [][]byte{sps, pps, idr} {
|
||||||
|
want = append(want, startCode...)
|
||||||
|
want = append(want, nalu...)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(got, want) {
|
||||||
|
t.Fatalf("NormalizeH264AccessUnit() = %x, want %x", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBroadcastPath(t *testing.T) {
|
||||||
|
if got := BroadcastPath("/devices/", "/camera-1/"); got != "devices/camera-1/live.hang" {
|
||||||
|
t.Fatalf("BroadcastPath() = %q", got)
|
||||||
|
}
|
||||||
|
if got := BroadcastPath("", "camera-1"); got != "devices/camera-1/live.hang" {
|
||||||
|
t.Fatalf("BroadcastPath() default = %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
8
machinery/src/cloud/livemoq_disabled.go
Normal file
8
machinery/src/cloud/livemoq_disabled.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
//go:build !moq
|
||||||
|
|
||||||
|
package cloud
|
||||||
|
|
||||||
|
import "github.com/kerberos-io/agent/machinery/src/models"
|
||||||
|
|
||||||
|
// StartLiveStreamMoQ is disabled in the standard Agent build.
|
||||||
|
func StartLiveStreamMoQ(_ *models.Configuration, _ *models.Communication, _ bool) {}
|
||||||
171
machinery/src/cloud/livemoq_enabled.go
Normal file
171
machinery/src/cloud/livemoq_enabled.go
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
//go:build moq
|
||||||
|
|
||||||
|
package cloud
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/kerberos-io/agent/machinery/src/cloud/livemoq"
|
||||||
|
"github.com/kerberos-io/agent/machinery/src/log"
|
||||||
|
"github.com/kerberos-io/agent/machinery/src/models"
|
||||||
|
"github.com/kerberos-io/agent/machinery/src/packets"
|
||||||
|
"github.com/moq-dev/moq-go/moq"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultMoQRelayURL = "https://relay.uug.ai/anon"
|
||||||
|
minMoQRetryDelay = time.Second
|
||||||
|
maxMoQRetryDelay = 30 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
type liveMoQConfig struct {
|
||||||
|
relayURL string
|
||||||
|
broadcast string
|
||||||
|
quality string
|
||||||
|
sourceLabel string
|
||||||
|
queue *packets.Queue
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartLiveStreamMoQ starts the publisher only in the dedicated MoQ build and
|
||||||
|
// only when explicitly enabled by the deployment.
|
||||||
|
func StartLiveStreamMoQ(configuration *models.Configuration, communication *models.Communication, subStreamEnabled bool) {
|
||||||
|
if os.Getenv("AGENT_LIVE_MOQ_ENABLED") != "true" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
config := configuration.Config
|
||||||
|
if config.Offline == "true" || config.Capture.Liveview == "false" {
|
||||||
|
log.Log.Info("cloud.StartLiveStreamMoQ(): disabled by Agent live-view configuration")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if config.Key == "" {
|
||||||
|
log.Log.Warning("cloud.StartLiveStreamMoQ(): AGENT_KEY is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
quality := os.Getenv("AGENT_LIVE_MOQ_QUALITY")
|
||||||
|
if quality == "" {
|
||||||
|
quality = models.StreamQualityAuto
|
||||||
|
}
|
||||||
|
useSub := models.SelectSubStreamForQuality(config, quality, subStreamEnabled)
|
||||||
|
queue := communication.Queue
|
||||||
|
sourceLabel := "main"
|
||||||
|
if useSub && communication.SubQueue != nil {
|
||||||
|
queue = communication.SubQueue
|
||||||
|
sourceLabel = "sub"
|
||||||
|
}
|
||||||
|
if queue == nil {
|
||||||
|
log.Log.Warning("cloud.StartLiveStreamMoQ(): selected packet queue is unavailable")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
relayURL := os.Getenv("AGENT_LIVE_MOQ_URL")
|
||||||
|
if relayURL == "" {
|
||||||
|
relayURL = defaultMoQRelayURL
|
||||||
|
}
|
||||||
|
publisherConfig := liveMoQConfig{
|
||||||
|
relayURL: relayURL,
|
||||||
|
broadcast: livemoq.BroadcastPath(os.Getenv("AGENT_LIVE_MOQ_BROADCAST_PREFIX"), config.Key),
|
||||||
|
quality: quality,
|
||||||
|
sourceLabel: sourceLabel,
|
||||||
|
queue: queue,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
if communication.Context != nil {
|
||||||
|
ctx = *communication.Context
|
||||||
|
}
|
||||||
|
go runLiveStreamMoQ(ctx, publisherConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runLiveStreamMoQ(ctx context.Context, config liveMoQConfig) {
|
||||||
|
log.Log.Info(fmt.Sprintf(
|
||||||
|
"cloud.runLiveStreamMoQ(): publishing %s stream (quality=%s) to %s/%s",
|
||||||
|
config.sourceLabel, config.quality, strings.TrimRight(config.relayURL, "/"), config.broadcast,
|
||||||
|
))
|
||||||
|
|
||||||
|
retryDelay := minMoQRetryDelay
|
||||||
|
for ctx.Err() == nil {
|
||||||
|
connectedAt := time.Now()
|
||||||
|
err := publishLiveStreamMoQ(ctx, config)
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Log.Warning("cloud.runLiveStreamMoQ(): publisher stopped: " + err.Error())
|
||||||
|
if time.Since(connectedAt) >= time.Minute {
|
||||||
|
retryDelay = minMoQRetryDelay
|
||||||
|
}
|
||||||
|
|
||||||
|
timer := time.NewTimer(retryDelay)
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
timer.Stop()
|
||||||
|
return
|
||||||
|
case <-timer.C:
|
||||||
|
}
|
||||||
|
if retryDelay < maxMoQRetryDelay {
|
||||||
|
retryDelay *= 2
|
||||||
|
if retryDelay > maxMoQRetryDelay {
|
||||||
|
retryDelay = maxMoQRetryDelay
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func publishLiveStreamMoQ(ctx context.Context, config liveMoQConfig) error {
|
||||||
|
client, err := moq.Dial(ctx, config.relayURL)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("connect to relay: %w", err)
|
||||||
|
}
|
||||||
|
defer client.Close()
|
||||||
|
|
||||||
|
broadcast, err := client.CreateBroadcast(config.broadcast)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create broadcast: %w", err)
|
||||||
|
}
|
||||||
|
defer broadcast.Finish()
|
||||||
|
|
||||||
|
stream, err := broadcast.PublishMedia("avc3", nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create H.264 media stream: %w", err)
|
||||||
|
}
|
||||||
|
defer stream.Finish()
|
||||||
|
|
||||||
|
cursor := config.queue.Latest()
|
||||||
|
writing := false
|
||||||
|
for {
|
||||||
|
packet, err := cursor.ReadPacket()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read packet: %w", err)
|
||||||
|
}
|
||||||
|
if !packet.IsVideo || len(packet.Data) == 0 || !strings.EqualFold(packet.Codec, "H264") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !writing {
|
||||||
|
if !packet.IsKeyFrame {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
writing = true
|
||||||
|
log.Log.Info("cloud.publishLiveStreamMoQ(): first H.264 keyframe received; broadcast is live")
|
||||||
|
}
|
||||||
|
presentationTimeMs := packet.Time + packet.CompositionTime
|
||||||
|
if presentationTimeMs < 0 {
|
||||||
|
presentationTimeMs = 0
|
||||||
|
}
|
||||||
|
payload, err := livemoq.NormalizeH264AccessUnit(packet.Data)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("normalize H.264 access unit: %w", err)
|
||||||
|
}
|
||||||
|
frame := moq.Frame{
|
||||||
|
Payload: payload,
|
||||||
|
TimestampUs: uint64(presentationTimeMs) * 1000,
|
||||||
|
}
|
||||||
|
if err := stream.WriteFrame(frame); err != nil {
|
||||||
|
return fmt.Errorf("write H.264 access unit: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -305,6 +305,10 @@ func RunAgent(configDirectory string, configuration *models.Configuration, commu
|
|||||||
// watching.
|
// watching.
|
||||||
go cloud.HandleLiveStreamHLS(configuration, communication, mqttClient, subStreamEnabled)
|
go cloud.HandleLiveStreamHLS(configuration, communication, mqttClient, subStreamEnabled)
|
||||||
|
|
||||||
|
// MoQ is available only in the dedicated CGO/glibc build. The standard
|
||||||
|
// static Alpine build resolves this hook to a no-op.
|
||||||
|
cloud.StartLiveStreamMoQ(configuration, communication, subStreamEnabled)
|
||||||
|
|
||||||
// Handle livestream HD (high resolution over WEBRTC). Both the main and sub
|
// Handle livestream HD (high resolution over WEBRTC). Both the main and sub
|
||||||
// stream are exposed as separate broadcasters so a viewer can request the
|
// stream are exposed as separate broadcasters so a viewer can request the
|
||||||
// high (main) or low (sub) resolution per peer connection; "auto" prefers the
|
// high (main) or low (sub) resolution per peer connection; "auto" prefers the
|
||||||
|
|||||||
23
machinery/verify-moq-devcontainer.sh
Normal file
23
machinery/verify-moq-devcontainer.sh
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
required_glibc="2.38"
|
||||||
|
current_glibc="$(getconf GNU_LIBC_VERSION | awk '{print $2}')"
|
||||||
|
|
||||||
|
if ! dpkg --compare-versions "$current_glibc" ge "$required_glibc"; then
|
||||||
|
echo "MoQ requires glibc ${required_glibc}+; this container has ${current_glibc}." >&2
|
||||||
|
echo "Rebuild the devcontainer with the Trixie base, then run this check again." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> Testing MoQ packages (glibc ${current_glibc})"
|
||||||
|
GOWORK=off go test -tags moq ./src/cloud/livemoq ./src/cloud
|
||||||
|
|
||||||
|
binary="${TMPDIR:-/tmp}/agent-moq"
|
||||||
|
trap 'rm -f "$binary"' EXIT
|
||||||
|
|
||||||
|
echo "==> Linking the MoQ Agent"
|
||||||
|
GOWORK=off go build -tags moq -o "$binary" ./main.go
|
||||||
|
|
||||||
|
echo "==> Running the linked binary"
|
||||||
|
"$binary" -action version
|
||||||
Reference in New Issue
Block a user