From 4395fe2417f08b77c4c6b0836220b3c3d1e70b4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Verstraeten?= Date: Wed, 5 Aug 2026 20:53:37 +0200 Subject: [PATCH 1/5] Add optional MoQ live-stream publisher Adds a dedicated MoQ build path that publishes H.264 live streams to a configurable relay, with retry handling, stream selection, Annex B framing, and Docker packaging. Standard builds retain a no-op implementation. --- Dockerfile.moq | 74 ++++++++++ machinery/go.mod | 2 + machinery/go.sum | 4 + machinery/src/cloud/livemoq/annexb.go | 29 ++++ machinery/src/cloud/livemoq/annexb_test.go | 47 ++++++ machinery/src/cloud/livemoq_disabled.go | 8 ++ machinery/src/cloud/livemoq_enabled.go | 159 +++++++++++++++++++++ machinery/src/components/kerberos.go | 4 + 8 files changed, 327 insertions(+) create mode 100644 Dockerfile.moq create mode 100644 machinery/src/cloud/livemoq/annexb.go create mode 100644 machinery/src/cloud/livemoq/annexb_test.go create mode 100644 machinery/src/cloud/livemoq_disabled.go create mode 100644 machinery/src/cloud/livemoq_enabled.go diff --git a/Dockerfile.moq b/Dockerfile.moq new file mode 100644 index 0000000..6f5fcfe --- /dev/null +++ b/Dockerfile.moq @@ -0,0 +1,74 @@ +ARG GO_IMAGE=golang:1.24-trixie +ARG RUNTIME_IMAGE=debian:trixie-slim +ARG VERSION=0.0.0 + +FROM ${GO_IMAGE} AS build-machinery + +ARG VERSION +ENV CGO_ENABLED=1 + +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + ca-certificates \ + git \ + libavcodec-dev \ + libavutil-dev \ + libswscale-dev \ + pkg-config \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /src +COPY machinery ./machinery +COPY .git ./.git + +RUN cd machinery && \ + go mod download && \ + if [ -z "${VERSION}" ] || [ "${VERSION}" = "0.0.0" ]; then \ + VERSION=$(git -C /src describe --tags --always 2>/dev/null || echo "0.0.0"); \ + fi && \ + go build \ + -tags moq,timetzdata,netgo,osusergo \ + -ldflags "-s -w -X github.com/kerberos-io/agent/machinery/src/utils.VERSION=${VERSION}" \ + -o /out/main \ + ./main.go && \ + cp -r data /out/data && \ + printf '%s' "${VERSION}" > /out/version && \ + mkdir -p /out/data/cloud /out/data/snapshots /out/data/log /out/data/recordings /out/data/capture-test /out/data/config + +FROM node:22-alpine AS build-ui + +WORKDIR /src/ui +COPY ui ./ +RUN mkdir -p /src/machinery && \ + yarn config set network-timeout 300000 && \ + yarn install --frozen-lockfile && \ + yarn build + +FROM ${RUNTIME_IMAGE} + +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + ffmpeg \ + libcap2-bin \ + libstdc++6 \ + && rm -rf /var/lib/apt/lists/* && \ + groupadd --system kerberosio && \ + useradd --system --gid kerberosio --groups video --create-home agent + +COPY --from=build-machinery --chown=root:root /out/main /home/agent/main +COPY --from=build-machinery --chown=agent:kerberosio /out/data /home/agent/data +COPY --from=build-machinery --chown=root:root /out/version /home/agent/version +COPY --from=build-ui --chown=agent:kerberosio /src/machinery/www /home/agent/www + +RUN cp /home/agent/data/config/config.json /home/agent/data/config.template.json && \ + chown agent:kerberosio /home/agent/data/config.template.json && \ + setcap 'cap_net_bind_service=+ep' /home/agent/main && \ + /home/agent/main -action version + +USER agent +WORKDIR /home/agent + +EXPOSE 80 +HEALTHCHECK CMD curl --fail http://localhost:80 || exit 1 +CMD ["./main", "-action", "run", "-port", "80"] \ No newline at end of file diff --git a/machinery/go.mod b/machinery/go.mod index 1364d91..496bb53 100644 --- a/machinery/go.mod +++ b/machinery/go.mod @@ -26,6 +26,7 @@ require ( github.com/kerberos-io/joy4 v1.0.64 github.com/kerberos-io/onvif v1.2.2 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/op/go-logging v0.0.0-20160315200505-970db520ece7 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/reflect2 v1.0.2 // 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/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pion/datachannel v1.5.10 // indirect diff --git a/machinery/go.sum b/machinery/go.sum index 8e11c52..bb469ba 100644 --- a/machinery/go.sum +++ b/machinery/go.sum @@ -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/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= 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/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= diff --git a/machinery/src/cloud/livemoq/annexb.go b/machinery/src/cloud/livemoq/annexb.go new file mode 100644 index 0000000..52c68d9 --- /dev/null +++ b/machinery/src/cloud/livemoq/annexb.go @@ -0,0 +1,29 @@ +package livemoq + +import "strings" + +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...) +} + +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) +} diff --git a/machinery/src/cloud/livemoq/annexb_test.go b/machinery/src/cloud/livemoq/annexb_test.go new file mode 100644 index 0000000..7ba2ae7 --- /dev/null +++ b/machinery/src/cloud/livemoq/annexb_test.go @@ -0,0 +1,47 @@ +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 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) + } +} diff --git a/machinery/src/cloud/livemoq_disabled.go b/machinery/src/cloud/livemoq_disabled.go new file mode 100644 index 0000000..40e7a55 --- /dev/null +++ b/machinery/src/cloud/livemoq_disabled.go @@ -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) {} diff --git a/machinery/src/cloud/livemoq_enabled.go b/machinery/src/cloud/livemoq_enabled.go new file mode 100644 index 0000000..b275b1a --- /dev/null +++ b/machinery/src/cloud/livemoq_enabled.go @@ -0,0 +1,159 @@ +//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.PublishMediaStream("avc3") + 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") + } + if err := stream.Write(livemoq.EnsureAnnexB(packet.Data)); err != nil { + return fmt.Errorf("write H.264 access unit: %w", err) + } + } +} diff --git a/machinery/src/components/kerberos.go b/machinery/src/components/kerberos.go index 5637824..68544e2 100644 --- a/machinery/src/components/kerberos.go +++ b/machinery/src/components/kerberos.go @@ -305,6 +305,10 @@ func RunAgent(configDirectory string, configuration *models.Configuration, commu // watching. 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 // stream are exposed as separate broadcasters so a viewer can request the // high (main) or low (sub) resolution per peer connection; "auto" prefers the From 8f04a6d42fcb2ca1ba6d471c8eea9fd8c4e51faf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Verstraeten?= Date: Wed, 5 Aug 2026 20:54:06 +0200 Subject: [PATCH 2/5] Add MoQ image build workflow Build and verify MoQ images on amd64 and arm64 pull requests, and document MoQ Docker build and runtime configuration. --- .github/workflows/moq-build.yml | 32 ++++++++++++++++++++++++++++++++ README.md | 24 ++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 .github/workflows/moq-build.yml diff --git a/.github/workflows/moq-build.yml b/.github/workflows/moq-build.yml new file mode 100644 index 0000000..6d5307d --- /dev/null +++ b/.github/workflows/moq-build.yml @@ -0,0 +1,32 @@ +name: Build MoQ image + +on: + pull_request: + paths: + - Dockerfile.moq + - machinery/** + - ui/** + - .github/workflows/moq-build.yml + workflow_dispatch: + +jobs: + build: + name: MoQ (${{ matrix.architecture }}) + runs-on: ${{ matrix.runner }} + strategy: + matrix: + include: + - architecture: amd64 + runner: ubuntu-24.04 + - architecture: arm64 + runner: ubuntu-24.04-arm + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Build image + run: docker build --file Dockerfile.moq --tag agent-moq:${{ matrix.architecture }} . + + - name: Verify binary + run: docker run --rm agent-moq:${{ matrix.architecture }} ./main -action version \ No newline at end of file diff --git a/README.md b/README.md index 3ef365d..2644cff 100644 --- a/README.md +++ b/README.md @@ -419,6 +419,30 @@ By running the `docker build` command, you will create the Kerberos Agent Docker docker build -t kerberos/agent . +### Building with Media over QUIC + +The optional MoQ publisher uses a Rust FFI archive that requires CGO and glibc +2.38 or newer. Build it with the Debian-based image instead of the standard +static Alpine image: + + docker build -f Dockerfile.moq -t kerberos/agent:moq . + +The publisher is still 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:moq + +`AGENT_LIVE_MOQ_BROADCAST_PREFIX` defaults to `devices`, producing the broadcast +`devices//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. + ## 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). From ff643d21efbf39b4b63a89234fdc8f82012ab40f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Verstraeten?= Date: Wed, 5 Aug 2026 21:07:32 +0200 Subject: [PATCH 3/5] Add MoQ devcontainer verification Switch devcontainers to Debian Trixie for the required glibc version and add automated MoQ package tests, Agent linking, and version checks through a script and VS Code task. --- .devcontainer/Dockerfile | 2 +- .devcontainer/devcontainer.json | 4 ++-- .vscode/tasks.json | 14 ++++++++++++++ README.md | 11 +++++++++++ machinery/verify-moq-devcontainer.sh | 23 +++++++++++++++++++++++ 5 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 .vscode/tasks.json create mode 100644 machinery/verify-moq-devcontainer.sh diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index a61e860..92d0950 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -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 RUN apt-get update && \ diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 452cee8..4af61c6 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,7 +1,7 @@ // 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 { - "name": "go:1.24-bookworm", + "name": "go:1.24-trixie", "runArgs": [ "--name=agent", "--network=host" @@ -20,5 +20,5 @@ 3000, 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" } \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..bc0dcb4 --- /dev/null +++ b/.vscode/tasks.json @@ -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": [] + } + ] +} diff --git a/README.md b/README.md index 2644cff..0ba64dd 100644 --- a/README.md +++ b/README.md @@ -443,6 +443,17 @@ 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? 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). diff --git a/machinery/verify-moq-devcontainer.sh b/machinery/verify-moq-devcontainer.sh new file mode 100644 index 0000000..0c58308 --- /dev/null +++ b/machinery/verify-moq-devcontainer.sh @@ -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 From 2ffb210ccbef476033f74e966e019233888b89c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Verstraeten?= Date: Wed, 5 Aug 2026 20:25:05 +0000 Subject: [PATCH 4/5] Implement NormalizeH264AccessUnit for H.264 payload normalization and update live stream publishing to use the new function --- ...ncrypted_0-0-0-0_-1_20373.mp4.primary.json | 1 + machinery/src/cloud/livemoq/annexb.go | 38 ++++++++++++++++++- machinery/src/cloud/livemoq/annexb_test.go | 27 +++++++++++++ machinery/src/cloud/livemoq_enabled.go | 16 +++++++- 4 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 machinery/data/tus/1785961483_3-259_encrypted_0-0-0-0_-1_20373.mp4.primary.json diff --git a/machinery/data/tus/1785961483_3-259_encrypted_0-0-0-0_-1_20373.mp4.primary.json b/machinery/data/tus/1785961483_3-259_encrypted_0-0-0-0_-1_20373.mp4.primary.json new file mode 100644 index 0000000..f9a394e --- /dev/null +++ b/machinery/data/tus/1785961483_3-259_encrypted_0-0-0-0_-1_20373.mp4.primary.json @@ -0,0 +1 @@ +{"upload_url":"https://vault.kerberos.io/api/storage/tus/7a192d02cd47ede3939ddfa5f8819465","vault_uri":"https://vault.kerberos.io/api/storage/tus/","size":4739581} \ No newline at end of file diff --git a/machinery/src/cloud/livemoq/annexb.go b/machinery/src/cloud/livemoq/annexb.go index 52c68d9..106c659 100644 --- a/machinery/src/cloud/livemoq/annexb.go +++ b/machinery/src/cloud/livemoq/annexb.go @@ -1,6 +1,11 @@ package livemoq -import "strings" +import ( + "bytes" + "strings" + + "github.com/bluenviron/mediacommon/pkg/codecs/h264" +) var annexBStartCode = []byte{0x00, 0x00, 0x00, 0x01} @@ -15,6 +20,37 @@ func EnsureAnnexB(payload []byte) []byte { 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 == "" { diff --git a/machinery/src/cloud/livemoq/annexb_test.go b/machinery/src/cloud/livemoq/annexb_test.go index 7ba2ae7..1ece27a 100644 --- a/machinery/src/cloud/livemoq/annexb_test.go +++ b/machinery/src/cloud/livemoq/annexb_test.go @@ -37,6 +37,33 @@ func TestEnsureAnnexB(t *testing.T) { } } +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) diff --git a/machinery/src/cloud/livemoq_enabled.go b/machinery/src/cloud/livemoq_enabled.go index b275b1a..3569aac 100644 --- a/machinery/src/cloud/livemoq_enabled.go +++ b/machinery/src/cloud/livemoq_enabled.go @@ -129,7 +129,7 @@ func publishLiveStreamMoQ(ctx context.Context, config liveMoQConfig) error { } defer broadcast.Finish() - stream, err := broadcast.PublishMediaStream("avc3") + stream, err := broadcast.PublishMedia("avc3", nil) if err != nil { return fmt.Errorf("create H.264 media stream: %w", err) } @@ -152,7 +152,19 @@ func publishLiveStreamMoQ(ctx context.Context, config liveMoQConfig) error { writing = true log.Log.Info("cloud.publishLiveStreamMoQ(): first H.264 keyframe received; broadcast is live") } - if err := stream.Write(livemoq.EnsureAnnexB(packet.Data)); err != nil { + 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) } } From 63b352b5e271ce289210697efa3aa990ba9b51f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Verstraeten?= Date: Wed, 5 Aug 2026 20:36:26 +0000 Subject: [PATCH 5/5] Remove obsolete MoQ Dockerfile and update build workflows to streamline image creation --- .github/workflows/moq-build.yml | 32 ---- .github/workflows/pr-build.yml | 4 +- .github/workflows/release-bump.yml | 2 +- .github/workflows/release-create.yml | 2 +- Dockerfile | 27 ++-- Dockerfile.arm64 | 147 ------------------ Dockerfile.moq | 74 --------- README.md | 17 +- ...ncrypted_0-0-0-0_-1_20373.mp4.primary.json | 1 - 9 files changed, 26 insertions(+), 280 deletions(-) delete mode 100644 .github/workflows/moq-build.yml delete mode 100644 Dockerfile.arm64 delete mode 100644 Dockerfile.moq delete mode 100644 machinery/data/tus/1785961483_3-259_encrypted_0-0-0-0_-1_20373.mp4.primary.json diff --git a/.github/workflows/moq-build.yml b/.github/workflows/moq-build.yml deleted file mode 100644 index 6d5307d..0000000 --- a/.github/workflows/moq-build.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Build MoQ image - -on: - pull_request: - paths: - - Dockerfile.moq - - machinery/** - - ui/** - - .github/workflows/moq-build.yml - workflow_dispatch: - -jobs: - build: - name: MoQ (${{ matrix.architecture }}) - runs-on: ${{ matrix.runner }} - strategy: - matrix: - include: - - architecture: amd64 - runner: ubuntu-24.04 - - architecture: arm64 - runner: ubuntu-24.04-arm - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Build image - run: docker build --file Dockerfile.moq --tag agent-moq:${{ matrix.architecture }} . - - - name: Verify binary - run: docker run --rm agent-moq:${{ matrix.architecture }} ./main -action version \ No newline at end of file diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index 8ef3c9c..7cf66ca 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -16,10 +16,8 @@ jobs: include: - architecture: amd64 runner: ubuntu-24.04 - dockerfile: Dockerfile - architecture: arm64 runner: ubuntu-24.04-arm - dockerfile: Dockerfile.arm64 steps: - name: Checkout uses: actions/checkout@v3 @@ -34,7 +32,7 @@ jobs: length: 7 - name: Run Build run: | - docker build -t ${{ matrix.architecture }} -f ${{ matrix.dockerfile }} . + docker build -t ${{ matrix.architecture }} . CID=$(docker create ${{matrix.architecture}}) docker cp ${CID}:/home/agent ./output-${{matrix.architecture}} docker rm ${CID} diff --git a/.github/workflows/release-bump.yml b/.github/workflows/release-bump.yml index b3677d3..6b282dd 100644 --- a/.github/workflows/release-bump.yml +++ b/.github/workflows/release-bump.yml @@ -108,7 +108,7 @@ jobs: length: 7 - name: Run Build 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}}) docker cp ${CID}:/home/agent ./output-${{matrix.architecture}} docker rm ${CID} diff --git a/.github/workflows/release-create.yml b/.github/workflows/release-create.yml index d765fac..4e4b0b7 100644 --- a/.github/workflows/release-create.yml +++ b/.github/workflows/release-create.yml @@ -71,7 +71,7 @@ jobs: length: 7 - name: Run Build run: | - docker build --provenance=false --build-arg VERSION=${{github.event.inputs.tag || github.ref_name}} -t ${{matrix.architecture}} -f Dockerfile.arm64 . + 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} diff --git a/Dockerfile b/Dockerfile index ddb8d72..8186227 100644 --- a/Dockerfile +++ b/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 -FROM kerberos/base:${BASE_IMAGE_VERSION} AS build-machinery +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 GOROOT=/usr/local/go ENV GOPATH=/go @@ -17,9 +19,10 @@ ENV GOSUMDB=off ########################################## # Installing some additional dependencies. -RUN apt-get upgrade -y && apt-get update && apt-get install -y --fix-missing --no-install-recommends \ +RUN apt-get 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 && \ + curl ca-certificates libavcodec-dev libavutil-dev libcurl4-openssl-dev \ + libssl-dev libjpeg62-turbo-dev libswscale-dev && \ 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 \ 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 && \ + 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 && \ mv main /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 -FROM alpine:latest +FROM ${RUNTIME_IMAGE} ############################ # 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 @@ -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-ui /dist / -RUN apk update && apk add ca-certificates curl ffmpeg libstdc++ libc6-compat --no-cache && rm -rf /var/cache/apk/* - ################## # 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 -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 diff --git a/Dockerfile.arm64 b/Dockerfile.arm64 deleted file mode 100644 index e324e6d..0000000 --- a/Dockerfile.arm64 +++ /dev/null @@ -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"] \ No newline at end of file diff --git a/Dockerfile.moq b/Dockerfile.moq deleted file mode 100644 index 6f5fcfe..0000000 --- a/Dockerfile.moq +++ /dev/null @@ -1,74 +0,0 @@ -ARG GO_IMAGE=golang:1.24-trixie -ARG RUNTIME_IMAGE=debian:trixie-slim -ARG VERSION=0.0.0 - -FROM ${GO_IMAGE} AS build-machinery - -ARG VERSION -ENV CGO_ENABLED=1 - -RUN apt-get update && apt-get install -y --no-install-recommends \ - build-essential \ - ca-certificates \ - git \ - libavcodec-dev \ - libavutil-dev \ - libswscale-dev \ - pkg-config \ - && rm -rf /var/lib/apt/lists/* - -WORKDIR /src -COPY machinery ./machinery -COPY .git ./.git - -RUN cd machinery && \ - go mod download && \ - if [ -z "${VERSION}" ] || [ "${VERSION}" = "0.0.0" ]; then \ - VERSION=$(git -C /src describe --tags --always 2>/dev/null || echo "0.0.0"); \ - fi && \ - go build \ - -tags moq,timetzdata,netgo,osusergo \ - -ldflags "-s -w -X github.com/kerberos-io/agent/machinery/src/utils.VERSION=${VERSION}" \ - -o /out/main \ - ./main.go && \ - cp -r data /out/data && \ - printf '%s' "${VERSION}" > /out/version && \ - mkdir -p /out/data/cloud /out/data/snapshots /out/data/log /out/data/recordings /out/data/capture-test /out/data/config - -FROM node:22-alpine AS build-ui - -WORKDIR /src/ui -COPY ui ./ -RUN mkdir -p /src/machinery && \ - yarn config set network-timeout 300000 && \ - yarn install --frozen-lockfile && \ - yarn build - -FROM ${RUNTIME_IMAGE} - -RUN apt-get update && apt-get install -y --no-install-recommends \ - ca-certificates \ - curl \ - ffmpeg \ - libcap2-bin \ - libstdc++6 \ - && rm -rf /var/lib/apt/lists/* && \ - groupadd --system kerberosio && \ - useradd --system --gid kerberosio --groups video --create-home agent - -COPY --from=build-machinery --chown=root:root /out/main /home/agent/main -COPY --from=build-machinery --chown=agent:kerberosio /out/data /home/agent/data -COPY --from=build-machinery --chown=root:root /out/version /home/agent/version -COPY --from=build-ui --chown=agent:kerberosio /src/machinery/www /home/agent/www - -RUN cp /home/agent/data/config/config.json /home/agent/data/config.template.json && \ - chown agent:kerberosio /home/agent/data/config.template.json && \ - setcap 'cap_net_bind_service=+ep' /home/agent/main && \ - /home/agent/main -action version - -USER agent -WORKDIR /home/agent - -EXPOSE 80 -HEALTHCHECK CMD curl --fail http://localhost:80 || exit 1 -CMD ["./main", "-action", "run", "-port", "80"] \ No newline at end of file diff --git a/README.md b/README.md index 0ba64dd..748d626 100644 --- a/README.md +++ b/README.md @@ -412,27 +412,22 @@ Remember the build step of the `web` part, during build time we move the build d ## Building for Docker -Inside the root of this `agent` repository, you will find a `Dockerfile`. This file contains the instructions for building and shipping a **Kerberos Agent**. Important to note is that you start from a prebuilt base image, `kerberos/base:xxx`. -This base image already contains a couple of tools, such as Golang, FFmpeg and OpenCV. We do this for faster compilation times. +Inside the root of this `agent` repository, you will find a `Dockerfile`. This file contains the instructions for building and shipping a **Kerberos Agent**. It uses Debian Trixie to support the native dependencies used by the Agent, including Media over QUIC. 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 . -### Building with Media over QUIC +### Media over QUIC -The optional MoQ publisher uses a Rust FFI archive that requires CGO and glibc -2.38 or newer. Build it with the Debian-based image instead of the standard -static Alpine image: - - docker build -f Dockerfile.moq -t kerberos/agent:moq . - -The publisher is still disabled unless explicitly enabled at runtime: +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:moq + kerberos/agent `AGENT_LIVE_MOQ_BROADCAST_PREFIX` defaults to `devices`, producing the broadcast `devices//live.hang`. `AGENT_LIVE_MOQ_QUALITY` accepts `auto` (the diff --git a/machinery/data/tus/1785961483_3-259_encrypted_0-0-0-0_-1_20373.mp4.primary.json b/machinery/data/tus/1785961483_3-259_encrypted_0-0-0-0_-1_20373.mp4.primary.json deleted file mode 100644 index f9a394e..0000000 --- a/machinery/data/tus/1785961483_3-259_encrypted_0-0-0-0_-1_20373.mp4.primary.json +++ /dev/null @@ -1 +0,0 @@ -{"upload_url":"https://vault.kerberos.io/api/storage/tus/7a192d02cd47ede3939ddfa5f8819465","vault_uri":"https://vault.kerberos.io/api/storage/tus/","size":4739581} \ No newline at end of file