diff --git a/.forgejo/workflows/ci-gates.yml b/.forgejo/workflows/ci-gates.yml new file mode 100644 index 0000000..ff6bff8 --- /dev/null +++ b/.forgejo/workflows/ci-gates.yml @@ -0,0 +1,121 @@ +name: ci-gates + +on: + pull_request: {} + push: + branches: [main] + workflow_dispatch: {} + +jobs: + checks: + if: ${{ github.server_url != 'https://codeberg.org' }} + runs-on: codeberg-medium-lazy + steps: + - name: Fetch source (no git required) + env: + GITHUB_TOKEN: ${{ github.token }} + shell: bash + run: | + set -euo pipefail + if [[ -z "${GITHUB_TOKEN:-}" ]]; then + echo "error: missing github.token" + exit 2 + fi + if ! command -v curl >/dev/null 2>&1; then + echo "error: curl is required" + exit 2 + fi + if ! command -v tar >/dev/null 2>&1; then + echo "error: tar is required" + exit 2 + fi + if [[ -z "${GITHUB_SHA:-}" ]]; then + echo "error: missing GITHUB_SHA" + exit 2 + fi + if [[ -z "${GITHUB_SERVER_URL:-}" ]]; then + echo "error: missing GITHUB_SERVER_URL" + exit 2 + fi + if [[ -z "${GITHUB_REPOSITORY:-}" ]]; then + echo "error: missing GITHUB_REPOSITORY" + exit 2 + fi + + rm -rf .repo + mkdir -p .repo + curl -fsSL -H "Authorization: token ${GITHUB_TOKEN}" \ + "${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/archive/${GITHUB_SHA}.tar.gz?rev=${GITHUB_SHA}" \ + -o .repo/src.tgz + tar -xzf .repo/src.tgz -C .repo --strip-components=1 + rm -f .repo/src.tgz + + - name: Bootstrap Rust + web build tools + shell: bash + run: | + set -euo pipefail + cd .repo + install -d -m 755 "$HOME/.local/bin" + echo "PATH=$HOME/.local/bin:$PATH" >> "$GITHUB_ENV" + export PATH="$HOME/.local/bin:$PATH" + + if ! command -v curl >/dev/null 2>&1; then + echo "error: curl is required" + exit 2 + fi + + if ! command -v cargo >/dev/null 2>&1; then + curl -fsSL https://sh.rustup.rs | sh -s -- -y --profile minimal + . "$HOME/.cargo/env" + elif [[ -f "$HOME/.cargo/env" ]]; then + . "$HOME/.cargo/env" + fi + + rustup target add wasm32-unknown-unknown + + if ! command -v trunk >/dev/null 2>&1; then + trunk_version="0.21.14" + arch="$(uname -m)" + case "${arch}" in + x86_64|amd64) trunk_target="x86_64-unknown-linux-gnu" ;; + aarch64|arm64) trunk_target="aarch64-unknown-linux-gnu" ;; + *) + echo "error: unsupported runner arch for trunk prebuilt binary: ${arch}" + exit 2 + ;; + esac + curl -fsSL "https://github.com/trunk-rs/trunk/releases/download/v${trunk_version}/trunk-${trunk_target}.tar.gz" \ + | tar -xz -C "$HOME/.local/bin" trunk + fi + + cargo --version + rustc --version + trunk --version + + - name: ECP lint + shell: bash + run: | + set -euo pipefail + cd .repo + bash ./scripts/ecp-lint.sh + + - name: Rust tests (core subset) + shell: bash + run: | + set -euo pipefail + cd .repo + if [[ -f "$HOME/.cargo/env" ]]; then + . "$HOME/.cargo/env" + fi + cargo test -p ec-core -p ec-crypto -p ec-moq -p ec-iroh -p ec-linux-iptv + + - name: Build web (apps/web) + shell: bash + run: | + set -euo pipefail + cd .repo + if [[ -f "$HOME/.cargo/env" ]]; then + . "$HOME/.cargo/env" + fi + cd apps/web + env -u NO_COLOR trunk build --release --public-url / diff --git a/.forgejo/workflows/deploy-cloudflare.yml b/.forgejo/workflows/deploy-cloudflare.yml index dfe203b..2a5dc2e 100644 --- a/.forgejo/workflows/deploy-cloudflare.yml +++ b/.forgejo/workflows/deploy-cloudflare.yml @@ -10,7 +10,8 @@ concurrency: cancel-in-progress: true jobs: - deploy: + checks: + if: ${{ github.server_url != 'https://codeberg.org' }} runs-on: codeberg-medium-lazy steps: - name: Fetch Source (no git required) @@ -35,13 +36,131 @@ jobs: echo "error: missing GITHUB_SHA" exit 2 fi + if [[ -z "${GITHUB_SERVER_URL:-}" ]]; then + echo "error: missing GITHUB_SERVER_URL" + exit 2 + fi + if [[ -z "${GITHUB_REPOSITORY:-}" ]]; then + echo "error: missing GITHUB_REPOSITORY" + exit 2 + fi + + rm -rf .repo + mkdir -p .repo + + curl -fsSL -H "Authorization: token ${GITHUB_TOKEN}" \ + "${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/archive/${GITHUB_SHA}.tar.gz?rev=${GITHUB_SHA}" \ + -o .repo/src.tgz + tar -xzf .repo/src.tgz -C .repo --strip-components=1 + rm -f .repo/src.tgz + + - name: Bootstrap Rust + web build tools + shell: bash + run: | + set -euo pipefail + cd .repo + install -d -m 755 "$HOME/.local/bin" + echo "PATH=$HOME/.local/bin:$PATH" >> "$GITHUB_ENV" + export PATH="$HOME/.local/bin:$PATH" + + if ! command -v curl >/dev/null 2>&1; then + echo "error: curl is required" + exit 2 + fi + + if ! command -v cargo >/dev/null 2>&1; then + curl -fsSL https://sh.rustup.rs | sh -s -- -y --profile minimal + . "$HOME/.cargo/env" + elif [[ -f "$HOME/.cargo/env" ]]; then + . "$HOME/.cargo/env" + fi + rustup target add wasm32-unknown-unknown + + if ! command -v trunk >/dev/null 2>&1; then + trunk_version="0.21.14" + arch="$(uname -m)" + case "${arch}" in + x86_64|amd64) trunk_target="x86_64-unknown-linux-gnu" ;; + aarch64|arm64) trunk_target="aarch64-unknown-linux-gnu" ;; + *) + echo "error: unsupported runner arch for trunk prebuilt binary: ${arch}" + exit 2 + ;; + esac + curl -fsSL "https://github.com/trunk-rs/trunk/releases/download/v${trunk_version}/trunk-${trunk_target}.tar.gz" \ + | tar -xz -C "$HOME/.local/bin" trunk + fi + + - name: ECP lint + shell: bash + run: | + set -euo pipefail + cd .repo + bash ./scripts/ecp-lint.sh + + - name: Rust tests (core subset) + shell: bash + run: | + set -euo pipefail + cd .repo + if [[ -f "$HOME/.cargo/env" ]]; then + . "$HOME/.cargo/env" + fi + cargo test -p ec-core -p ec-crypto -p ec-moq -p ec-iroh -p ec-linux-iptv + + - name: Build site (web) + shell: bash + run: | + set -euo pipefail + cd .repo + if [[ -f "$HOME/.cargo/env" ]]; then + . "$HOME/.cargo/env" + fi + cd apps/web + env -u NO_COLOR trunk build --release --public-url / + + deploy: + if: ${{ github.server_url != 'https://codeberg.org' }} + needs: checks + runs-on: codeberg-medium-lazy + steps: + - name: Fetch Source (no git required) + env: + GITHUB_TOKEN: ${{ github.token }} + shell: bash + run: | + set -euo pipefail + if [[ -z "${GITHUB_TOKEN:-}" ]]; then + echo "error: missing github.token" + exit 2 + fi + if ! command -v curl >/dev/null 2>&1; then + echo "error: curl is required" + exit 2 + fi + if ! command -v tar >/dev/null 2>&1; then + echo "error: tar is required" + exit 2 + fi + if [[ -z "${GITHUB_SHA:-}" ]]; then + echo "error: missing GITHUB_SHA" + exit 2 + fi + if [[ -z "${GITHUB_SERVER_URL:-}" ]]; then + echo "error: missing GITHUB_SERVER_URL" + exit 2 + fi + if [[ -z "${GITHUB_REPOSITORY:-}" ]]; then + echo "error: missing GITHUB_REPOSITORY" + exit 2 + fi rm -rf .repo mkdir -p .repo # Use the authenticated API archive endpoint (works for private repos). curl -fsSL -H "Authorization: token ${GITHUB_TOKEN}" \ - "https://codeberg.org/api/v1/repos/every-channel/every.channel/archive/${GITHUB_SHA}.tar.gz?rev=${GITHUB_SHA}" \ + "${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/archive/${GITHUB_SHA}.tar.gz?rev=${GITHUB_SHA}" \ -o .repo/src.tgz tar -xzf .repo/src.tgz -C .repo --strip-components=1 rm -f .repo/src.tgz @@ -110,7 +229,7 @@ jobs: cd .repo curl -fsSL -X POST -H "Authorization: token ${GITHUB_TOKEN}" \ -H "content-type: application/json" \ - "https://codeberg.org/api/v1/repos/every-channel/every.channel/statuses/${GITHUB_SHA}" \ + "${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/statuses/${GITHUB_SHA}" \ -d '{"context":"deploy-cloudflare/breadcrumb","state":"pending","description":"bootstrap ok"}' >/dev/null - name: Configure CI Age identity @@ -135,7 +254,7 @@ jobs: curl -fsSL -X POST -H "Authorization: token ${GITHUB_TOKEN}" \ -H "content-type: application/json" \ - "https://codeberg.org/api/v1/repos/every-channel/every.channel/statuses/${GITHUB_SHA}" \ + "${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/statuses/${GITHUB_SHA}" \ -d '{"context":"deploy-cloudflare/breadcrumb","state":"pending","description":"age key ok"}' >/dev/null - name: Decrypt CI secrets from repo @@ -161,7 +280,7 @@ jobs: curl -fsSL -X POST -H "Authorization: token ${GITHUB_TOKEN}" \ -H "content-type: application/json" \ - "https://codeberg.org/api/v1/repos/every-channel/every.channel/statuses/${GITHUB_SHA}" \ + "${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/statuses/${GITHUB_SHA}" \ -d '{"context":"deploy-cloudflare/breadcrumb","state":"pending","description":"decrypt ok"}' >/dev/null - name: Build site (web) @@ -200,7 +319,7 @@ jobs: curl -fsSL -X POST -H "Authorization: token ${GITHUB_TOKEN}" \ -H "content-type: application/json" \ - "https://codeberg.org/api/v1/repos/every-channel/every.channel/statuses/${GITHUB_SHA}" \ + "${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/statuses/${GITHUB_SHA}" \ -d '{"context":"deploy-cloudflare/breadcrumb","state":"pending","description":"build ok"}' >/dev/null - name: Deploy worker @@ -216,5 +335,5 @@ jobs: curl -fsSL -X POST -H "Authorization: token ${GITHUB_TOKEN}" \ -H "content-type: application/json" \ - "https://codeberg.org/api/v1/repos/every-channel/every.channel/statuses/${GITHUB_SHA}" \ + "${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/statuses/${GITHUB_SHA}" \ -d '{"context":"deploy-cloudflare/breadcrumb","state":"success","description":"deploy ok"}' >/dev/null diff --git a/.forgejo/workflows/deploy-runner-images.yml b/.forgejo/workflows/deploy-runner-images.yml new file mode 100644 index 0000000..d7463cc --- /dev/null +++ b/.forgejo/workflows/deploy-runner-images.yml @@ -0,0 +1,276 @@ +name: deploy-runner-images + +on: + push: + tags: [boot-v*] + workflow_dispatch: + inputs: + release_tag: + description: "Release tag override (manual runs only)" + required: false + default: "" + publish_release: + description: "Publish artifacts to Forgejo release (true/false)" + required: false + default: "true" + build_x86_64_netboot: + description: "Build x86_64 netboot tarball (true/false)" + required: false + default: "true" + build_x86_64_iso: + description: "Build x86_64 installer ISO (true/false)" + required: false + default: "true" + +concurrency: + group: runner-image-deploy-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-and-release: + if: ${{ github.server_url != 'https://codeberg.org' }} + runs-on: codeberg-medium-lazy + steps: + - name: Fetch source (no git required) + env: + GITHUB_TOKEN: ${{ github.token }} + shell: bash + run: | + set -euo pipefail + if [[ -z "${GITHUB_TOKEN:-}" ]]; then + echo "error: missing github.token" + exit 2 + fi + if ! command -v curl >/dev/null 2>&1; then + echo "error: curl is required" + exit 2 + fi + if ! command -v tar >/dev/null 2>&1; then + echo "error: tar is required" + exit 2 + fi + if [[ -z "${GITHUB_SHA:-}" ]]; then + echo "error: missing GITHUB_SHA" + exit 2 + fi + if [[ -z "${GITHUB_SERVER_URL:-}" ]]; then + echo "error: missing GITHUB_SERVER_URL" + exit 2 + fi + if [[ -z "${GITHUB_REPOSITORY:-}" ]]; then + echo "error: missing GITHUB_REPOSITORY" + exit 2 + fi + + rm -rf .repo + mkdir -p .repo + curl -fsSL -H "Authorization: token ${GITHUB_TOKEN}" \ + "${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/archive/${GITHUB_SHA}.tar.gz?rev=${GITHUB_SHA}" \ + -o .repo/src.tgz + tar -xzf .repo/src.tgz -C .repo --strip-components=1 + rm -f .repo/src.tgz + + - name: Bootstrap Nix + shell: bash + run: | + set -euo pipefail + if ! command -v nix >/dev/null 2>&1; then + curl -fsSL https://nixos.org/nix/install -o /tmp/install-nix.sh + sh /tmp/install-nix.sh --no-daemon --yes + fi + + if [[ -f "$HOME/.nix-profile/etc/profile.d/nix.sh" ]]; then + # shellcheck disable=SC1091 + . "$HOME/.nix-profile/etc/profile.d/nix.sh" + fi + if [[ -d "$HOME/.nix-profile/bin" ]]; then + echo "PATH=$HOME/.nix-profile/bin:$PATH" >> "$GITHUB_ENV" + export PATH="$HOME/.nix-profile/bin:$PATH" + fi + nix --version + + - name: Resolve build plan + id: plan + env: + INPUT_RELEASE_TAG: ${{ github.event.inputs.release_tag }} + INPUT_PUBLISH_RELEASE: ${{ github.event.inputs.publish_release }} + INPUT_BUILD_X86_64_NETBOOT: ${{ github.event.inputs.build_x86_64_netboot }} + INPUT_BUILD_X86_64_ISO: ${{ github.event.inputs.build_x86_64_iso }} + shell: bash + run: | + set -euo pipefail + + bool_norm() { + local raw + raw="$(printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]')" + case "${raw}" in + ''|true|1|yes|y|on) echo "true" ;; + false|0|no|n|off) echo "false" ;; + *) + echo "error: invalid boolean value '${1}'" >&2 + exit 2 + ;; + esac + } + + short_sha="${GITHUB_SHA:0:12}" + if [[ "${GITHUB_REF:-}" == refs/tags/* ]]; then + release_tag="${GITHUB_REF#refs/tags/}" + else + release_tag="${INPUT_RELEASE_TAG:-}" + if [[ -z "${release_tag}" ]]; then + release_tag="boot-${short_sha}" + fi + fi + + if [[ ! "${release_tag}" =~ ^[A-Za-z0-9._-]+$ ]]; then + echo "error: release tag contains unsupported characters: ${release_tag}" >&2 + exit 2 + fi + + publish_release="$(bool_norm "${INPUT_PUBLISH_RELEASE:-true}")" + build_x86_64_netboot="$(bool_norm "${INPUT_BUILD_X86_64_NETBOOT:-true}")" + build_x86_64_iso="$(bool_norm "${INPUT_BUILD_X86_64_ISO:-true}")" + + if [[ "${build_x86_64_netboot}" != "true" && "${build_x86_64_iso}" != "true" ]]; then + echo "error: at least one image build must be enabled" >&2 + exit 2 + fi + + artifact_suffix="${short_sha}-${GITHUB_RUN_NUMBER:-0}-${GITHUB_RUN_ATTEMPT:-1}" + + echo "release_tag=${release_tag}" >> "$GITHUB_OUTPUT" + echo "publish_release=${publish_release}" >> "$GITHUB_OUTPUT" + echo "build_x86_64_netboot=${build_x86_64_netboot}" >> "$GITHUB_OUTPUT" + echo "build_x86_64_iso=${build_x86_64_iso}" >> "$GITHUB_OUTPUT" + echo "artifact_suffix=${artifact_suffix}" >> "$GITHUB_OUTPUT" + + - name: Build runner boot images + env: + BUILD_X86_64_NETBOOT: ${{ steps.plan.outputs.build_x86_64_netboot }} + BUILD_X86_64_ISO: ${{ steps.plan.outputs.build_x86_64_iso }} + ARTIFACT_SUFFIX: ${{ steps.plan.outputs.artifact_suffix }} + shell: bash + run: | + set -euo pipefail + cd .repo + + if [[ -f "$HOME/.nix-profile/etc/profile.d/nix.sh" ]]; then + # shellcheck disable=SC1091 + . "$HOME/.nix-profile/etc/profile.d/nix.sh" + fi + export PATH="$HOME/.nix-profile/bin:$PATH" + + artifacts_dir="$PWD/.artifacts" + rm -rf "${artifacts_dir}" + mkdir -p "${artifacts_dir}" + + nix_args=(--accept-flake-config --extra-experimental-features "nix-command flakes") + + if [[ "${BUILD_X86_64_NETBOOT}" == "true" ]]; then + nix build "${nix_args[@]}" \ + .#nixosConfigurations.ec-runner-x86_64-netboot.config.system.build.netboot \ + -o result-netboot-x86_64 + tar -C result-netboot-x86_64 \ + -czf "${artifacts_dir}/ec-runner-x86_64-netboot-${ARTIFACT_SUFFIX}.tar.gz" \ + kernel initrd netboot.ipxe + fi + + if [[ "${BUILD_X86_64_ISO}" == "true" ]]; then + nix build "${nix_args[@]}" \ + .#nixosConfigurations.ec-runner-x86_64-iso.config.system.build.isoImage \ + -o result-iso-x86_64 + iso_source="" + if [[ -f result-iso-x86_64 ]]; then + iso_source="result-iso-x86_64" + else + iso_source="$(find -L result-iso-x86_64 -type f -name '*.iso' | head -n 1 || true)" + fi + if [[ -z "${iso_source}" ]]; then + echo "error: could not locate ISO output from result-iso-x86_64" >&2 + exit 2 + fi + cp -f "${iso_source}" "${artifacts_dir}/ec-runner-x86_64-iso-${ARTIFACT_SUFFIX}.iso" + fi + + if ! find "${artifacts_dir}" -maxdepth 1 -type f | grep -q .; then + echo "error: no image artifacts were produced" >&2 + exit 2 + fi + + ( + cd "${artifacts_dir}" + sha256sum -- * > SHA256SUMS.txt + ls -lh + ) + + - name: Publish artifacts to Forgejo release + if: ${{ steps.plan.outputs.publish_release == 'true' }} + env: + GITHUB_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ steps.plan.outputs.release_tag }} + shell: bash + run: | + set -euo pipefail + cd .repo + + if [[ -z "${GITHUB_TOKEN:-}" ]]; then + echo "error: missing github.token" + exit 2 + fi + if [[ -z "${GITHUB_SERVER_URL:-}" ]]; then + echo "error: missing GITHUB_SERVER_URL" + exit 2 + fi + if [[ -z "${GITHUB_REPOSITORY:-}" ]]; then + echo "error: missing GITHUB_REPOSITORY" + exit 2 + fi + + api_base="${GITHUB_SERVER_URL%/}/api/v1/repos/${GITHUB_REPOSITORY}" + release_json="$(curl -fsSL \ + -H "Authorization: token ${GITHUB_TOKEN}" \ + "${api_base}/releases/tags/${RELEASE_TAG}" 2>/dev/null || true)" + + if [[ -z "${release_json}" ]]; then + payload="$(cat </dev/null 2>&1; then + release_id="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])' <<<"${release_json}" 2>/dev/null || true)" + fi + if [[ -z "${release_id}" ]]; then + release_id="$(printf '%s' "${release_json}" \ + | sed -nE 's/.*"id"[[:space:]]*:[[:space:]]*([0-9]+).*/\1/p' \ + | head -n 1)" + fi + if [[ -z "${release_id}" ]]; then + echo "error: failed to resolve release id for ${RELEASE_TAG}" >&2 + exit 2 + fi + + for asset_path in .artifacts/*; do + [[ -f "${asset_path}" ]] || continue + asset_name="$(basename "${asset_path}")" + curl -fsSL -X POST \ + -H "Authorization: token ${GITHUB_TOKEN}" \ + -H "content-type: application/octet-stream" \ + --data-binary @"${asset_path}" \ + "${api_base}/releases/${release_id}/assets?name=${asset_name}" >/dev/null + echo "uploaded: ${asset_name}" + done diff --git a/.forgejo/workflows/runner-smoke.yml b/.forgejo/workflows/runner-smoke.yml index f3c470e..2d1f2e3 100644 --- a/.forgejo/workflows/runner-smoke.yml +++ b/.forgejo/workflows/runner-smoke.yml @@ -5,6 +5,7 @@ on: jobs: smoke: + if: ${{ github.server_url != 'https://codeberg.org' }} runs-on: codeberg-medium-lazy steps: - name: Basic runner + secret smoke test diff --git a/README.md b/README.md index f4861f8..6124b6f 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,18 @@ Runbook: cat docs/USAGE.md ``` +Git hosting topology: + +```sh +cat docs/GIT_HOSTING.md +``` + +NUC PXE rollout (Unifi + ProxyDHCP): + +```sh +cat docs/NUC_UNIFI_NETBOOT.md +``` + ## WebTransport Watch (MoQ) Publish (node -> Cloudflare relay): diff --git a/apps/web/app.js b/apps/web/app.js index 7c5dab5..5842106 100644 --- a/apps/web/app.js +++ b/apps/web/app.js @@ -162,6 +162,7 @@ function mountPlayer(relayUrl, name) { watch.setAttribute("name", name); watch.setAttribute("path", name); watch.setAttribute("volume", "1"); + watch.setAttribute("muted", ""); // Force WebTransport in-browser; websocket fallback has shown degraded // media behavior (especially audio) against public relay paths. @@ -169,15 +170,16 @@ function mountPlayer(relayUrl, name) { watch.connection.websocket = { enabled: false }; } - // Use a media element for live playback so browser audio controls/policies apply naturally. + // Prefer a video element for native controls/audio routing. + // Start muted to satisfy autoplay policy, then unlock audio on user gesture. const video = document.createElement("video"); video.className = "archiveVideo"; video.controls = true; video.autoplay = true; - video.muted = false; + video.muted = true; + video.volume = 1; video.playsInline = true; watch.appendChild(video); - mount.appendChild(watch); const forceAudioOn = () => { try { @@ -187,9 +189,19 @@ function mountPlayer(relayUrl, name) { // Best effort only. } }; - forceAudioOn(); - window.setTimeout(forceAudioOn, 1000); - window.setTimeout(forceAudioOn, 4000); + const unlockAudio = () => { + forceAudioOn(); + watch.backend?.paused?.set?.(true); + watch.backend?.paused?.set?.(false); + video.muted = false; + video.volume = 1; + void video.play().catch(() => {}); + setHint(`Live: subscribed to ${name} (audio unlocked)`, "ok"); + }; + document.addEventListener("pointerdown", unlockAudio, { once: true }); + video.addEventListener("pointerdown", unlockAudio, { once: true }); + setHint(`Live: subscribed to ${name} (tap video to unmute)`, "warn"); + void video.play().catch(() => {}); bindPlayerSignals(watch, name); } diff --git a/crates/ec-node/src/main.rs b/crates/ec-node/src/main.rs index b4b68b3..3b1be70 100644 --- a/crates/ec-node/src/main.rs +++ b/crates/ec-node/src/main.rs @@ -6312,6 +6312,10 @@ async fn wt_publish(args: WtPublishArgs) -> Result<()> { if args.transcode { cmd.args([ + "-map", + "0:v:0", + "-map", + "0:a:0?", "-c:v", "libx264", "-preset", @@ -6332,8 +6336,10 @@ async fn wt_publish(args: WtPublishArgs) -> Result<()> { "1", "-c:a", "aac", + "-profile:a", + "aac_low", "-b:a", - "128k", + "160k", "-ac", "2", "-ar", diff --git a/docs/BRANCH_PROTECTION.md b/docs/BRANCH_PROTECTION.md new file mode 100644 index 0000000..d0b3e02 --- /dev/null +++ b/docs/BRANCH_PROTECTION.md @@ -0,0 +1,37 @@ +# Branch Protection (Forgejo Primary) + +`main` should be protected to satisfy constitutional governance (`all changes merge through pull requests`) and to require CI before merge. + +## Required settings + +- Protected branch: `main` +- Direct pushes disabled +- Required approvals: `1` (or stricter) +- Required status checks: + - `ci-gates / checks` +- Require signed commits: enabled + +## Apply via script + +```sh +./scripts/fj-enforce-branch-protection.sh +``` + +Optional overrides: + +```sh +EVERY_CHANNEL_FORGE_HOST=https://forge.every.channel \ +EVERY_CHANNEL_FORGE_REPO=every-channel/every.channel \ +EVERY_CHANNEL_PROTECTED_BRANCH=main \ +EVERY_CHANNEL_REQUIRED_CHECKS="ci-gates / checks" \ +EVERY_CHANNEL_REQUIRED_APPROVALS=1 \ +./scripts/fj-enforce-branch-protection.sh +``` + +Token source order: + +1. `EVERY_CHANNEL_FORGE_TOKEN` / `FORGE_TOKEN` / `CODEBERG_TOKEN` env var +2. `secrets/forge-token.age` (preferred) via `agenix` or `age` +3. `secrets/codeberg-token.age` (compat) via `agenix` or `age` + +The token must have repository admin scope to edit branch protection. diff --git a/docs/DEPLOY_CLOUDFLARE.md b/docs/DEPLOY_CLOUDFLARE.md index 6993c60..45dbef7 100644 --- a/docs/DEPLOY_CLOUDFLARE.md +++ b/docs/DEPLOY_CLOUDFLARE.md @@ -1,22 +1,25 @@ # Cloudflare Deploy (Forgejo Actions) This repo deploys `https://every.channel` via Wrangler. +The deploy workflow is intended to run on the primary Forgejo host (not Codeberg/GitHub mirrors). ## Prereqs - Forgejo Actions enabled on the repo. -- A Cloudflare API token stored as a Forgejo Actions secret: - - name: `CLOUDFLARE_API_TOKEN` +- Forgejo Actions secret `AGE_FORGE_SSH_KEY` set to the SSH private key used to decrypt repo-encrypted age secrets. +- `secrets/cloudflare-api-token.age` present in-repo and decryptable by `AGE_FORGE_SSH_KEY`. -The workflow is defined in `.forgejo/workflows/deploy-cloudflare.yml`. +CI and deploy workflows: + +- PR/main checks: `.forgejo/workflows/ci-gates.yml` +- Deploy (main only, depends on checks): `.forgejo/workflows/deploy-cloudflare.yml` + +Mirror behavior: + +- Workflow jobs are guarded to skip execution on `https://codeberg.org`. ## Manual deploy (local) ```sh -cd apps/tauri/ui -trunk build --release --public-url / - -cd deploy/cloudflare-worker -npm ci -npm run deploy +./scripts/deploy-workers.sh ``` diff --git a/docs/GIT_HOSTING.md b/docs/GIT_HOSTING.md new file mode 100644 index 0000000..602abd6 --- /dev/null +++ b/docs/GIT_HOSTING.md @@ -0,0 +1,45 @@ +# Git Hosting Topology + +Primary host: + +- Forgejo (`origin`) + +Mirrors (push-only): + +- Codeberg (`mirror-codeberg`) +- GitHub (`mirror-github`) + +Codeberg and GitHub are distribution mirrors only. CI/actions should run on Forgejo primary. + +## Configure local remotes + +```sh +./scripts/git-configure-hosting.sh +``` + +Defaults: + +- `origin`: `git@forge.every.channel:every-channel/every.channel.git` +- `mirror-codeberg`: `git@codeberg.org:every-channel/every.channel.git` +- `mirror-github`: `git@github.com:every-channel/every.channel.git` + +You can override via env vars: + +- `EVERY_CHANNEL_PRIMARY_GIT_URL` +- `EVERY_CHANNEL_CODEBERG_GIT_URL` +- `EVERY_CHANNEL_GITHUB_GIT_URL` + +## Push mirrors + +```sh +./scripts/git-push-mirrors.sh +``` + +## Disable actions on Codeberg mirror + +```sh +EVERY_CHANNEL_FORGE_HOST=https://codeberg.org \ +EVERY_CHANNEL_FORGE_REPO=every-channel/every.channel \ +EVERY_CHANNEL_FORGE_ACTIONS_ENABLED=false \ +./scripts/forge-set-repo-actions.sh +``` diff --git a/docs/NUC_UNIFI_NETBOOT.md b/docs/NUC_UNIFI_NETBOOT.md new file mode 100644 index 0000000..e3fbe55 --- /dev/null +++ b/docs/NUC_UNIFI_NETBOOT.md @@ -0,0 +1,102 @@ +# NUC Fleet Netboot (Unifi + ProxyDHCP) + +This runbook provisions x86_64 NUCs from runner netboot artifacts without USB image flashing. + +It uses: + +- Unifi DHCP for IP leases. +- Local `dnsmasq` ProxyDHCP for PXE/iPXE bootfile logic. +- Local HTTP + TFTP service for boot artifacts. + +## Why ProxyDHCP + +iPXE commonly needs two boot stages: + +1. firmware PXE -> `ipxe.efi` +2. iPXE -> `netboot.ipxe` + +If DHCP always returns `ipxe.efi`, clients can loop forever. ProxyDHCP handles stage-specific boot responses cleanly while leaving Unifi as the DHCP lease server. + +## Prerequisites + +- A Linux boot server on the same VLAN/L2 domain as the NUCs. +- Unifi network with normal DHCP enabled. +- Local DNS record on that VLAN: `boot.every.channel -> `. +- `curl`, `tar`, `python3`, `dnsmasq` installed on the boot server. +- Runner netboot artifact already published to Forgejo Releases (or available as a local tarball). + +## 1) Stage artifacts + +From repository root on the boot server: + +```sh +./scripts/netboot-stage.sh +``` + +Optional inputs: + +- `EVERY_CHANNEL_NETBOOT_RELEASE_TAG=boot-v2026.02.28` +- `EVERY_CHANNEL_NETBOOT_TARBALL=/path/to/ec-runner-x86_64-netboot-....tar.gz` +- `EVERY_CHANNEL_FORGE_TOKEN=` for private releases +- `EVERY_CHANNEL_NETBOOT_HOSTNAME=boot.every.channel` + +This stages: + +- `tmp/netboot/http/{kernel,initrd,netboot.ipxe}` +- `tmp/netboot/tftp/ipxe.efi` + +## 2) Serve HTTP + TFTP + ProxyDHCP + +Example (replace values for your VLAN): + +```sh +sudo \ + EVERY_CHANNEL_NETBOOT_LISTEN_IP=10.20.30.2 \ + EVERY_CHANNEL_NETBOOT_INTERFACE=eth0 \ + EVERY_CHANNEL_NETBOOT_PROXY_SUBNET=10.20.30.0/24 \ + EVERY_CHANNEL_NETBOOT_HOSTNAME=boot.every.channel \ + ./scripts/netboot-serve.sh +``` + +Notes: + +- Keep this process running during provisioning. +- Do not set Unifi DHCP bootfile options while this proxy mode is active. +- Ensure `boot.every.channel` resolves to the boot server IP from NUC clients. + +## 3) Unifi / NUC settings + +Unifi: + +- Keep DHCP enabled for the provisioning VLAN. +- Leave DHCP boot/TFTP overrides unset when using `netboot-serve.sh`. +- Create/verify local DNS host override: `boot.every.channel -> `. + +NUC BIOS: + +- Enable UEFI network boot (IPv4 PXE). +- Disable Legacy/CSM if possible. +- Put network boot before disk for first install cycle. + +## 4) Provision the fleet + +1. Boot each NUC on the provisioning VLAN. +2. PXE will chainload into iPXE and then runner `netboot.ipxe`. +3. Complete install/bootstrap flow on each node. +4. After successful install, switch boot order back to local disk. + +## Troubleshooting + +- Symptom: iPXE loop (`ipxe.efi` repeatedly) + - Cause: static DHCP bootfile without iPXE-aware logic. + - Fix: use ProxyDHCP flow (`netboot-serve.sh`) or set conditional DHCP rules. +- Symptom: NUC gets IP but never downloads boot artifacts + - Verify firewall allows UDP 67/68, UDP 69, and TCP 8080 between NUCs and boot server. +- Symptom: no `dnsmasq` offers seen + - Verify `EVERY_CHANNEL_NETBOOT_INTERFACE` and `EVERY_CHANNEL_NETBOOT_PROXY_SUBNET`. + +## Security / networking + +- Tailscale is not required for provisioning. +- Keep the provisioning VLAN isolated from regular clients. +- Stop `netboot-serve.sh` when rollout is complete. diff --git a/docs/RUNNER_IMAGES.md b/docs/RUNNER_IMAGES.md index ffe5c22..4d32893 100644 --- a/docs/RUNNER_IMAGES.md +++ b/docs/RUNNER_IMAGES.md @@ -50,6 +50,36 @@ Build an aarch64 SD image: nix build .#nixosConfigurations.ec-runner-aarch64-sdimage.config.system.build.sdImage ``` +## CI Deploy (Forgejo Releases) + +Boot images can be built and published from CI via: + +- `.forgejo/workflows/deploy-runner-images.yml` + +Triggers: + +- Manual: `workflow_dispatch` +- Tags: `boot-v*` (for example `boot-v2026.02.28`) + +Manual inputs (all optional): + +- `release_tag` (defaults to `boot-`) +- `publish_release` (`true`/`false`, default `true`) +- `build_x86_64_netboot` (`true`/`false`, default `true`) +- `build_x86_64_iso` (`true`/`false`, default `true`) + +Published assets are attached to the resolved Forgejo release tag and include: + +- x86_64 netboot bundle (`kernel`, `initrd`, `netboot.ipxe`) as `.tar.gz` +- x86_64 installer `.iso` +- `SHA256SUMS.txt` + +Notes: + +- CI image publish is disabled on the Codeberg mirror host. +- Current CI scope is x86_64 targets; aarch64 image builds remain local/manual unless an aarch64-capable runner is added. +- For multi-NUC PXE rollout on Unifi networks, use `docs/NUC_UNIFI_NETBOOT.md`. + ## Outputs After building, artifacts will be in `./result` (a symlink into the Nix store). diff --git a/evolution/proposals/ECP-0063-cloudflare-moq-webtransport.md b/evolution/proposals/ECP-0063-cloudflare-moq-webtransport.md index b75a2ad..89ecf55 100644 --- a/evolution/proposals/ECP-0063-cloudflare-moq-webtransport.md +++ b/evolution/proposals/ECP-0063-cloudflare-moq-webtransport.md @@ -1,6 +1,6 @@ # ECP-0063: Cloudflare MoQ Relay + WebTransport-Only Web Watch -Status: Draft +Status: Implemented ## Decision @@ -77,6 +77,11 @@ Implementation choice: Web share link: - `https://every.channel/watch?url=&name=` +## Alternatives considered + +- Keep the legacy WebRTC/WS path as primary. Rejected because it does not align with relay-native MoQ fanout goals. +- Wait for full draft parity across all relays before shipping. Rejected because live interop was already sufficient on the chosen relay path. + ## Rollout / Reversibility - Keep existing `/api/*` bootstrap endpoints during migration. diff --git a/evolution/proposals/ECP-0064-nixos-ec-node-publisher-module.md b/evolution/proposals/ECP-0064-nixos-ec-node-publisher-module.md index 122d4ec..916dc37 100644 --- a/evolution/proposals/ECP-0064-nixos-ec-node-publisher-module.md +++ b/evolution/proposals/ECP-0064-nixos-ec-node-publisher-module.md @@ -1,6 +1,6 @@ # ECP-0064: NixOS Module For `ec-node` WebTransport Publisher (Tower) -Status: Draft +Status: Implemented ## Decision @@ -41,8 +41,12 @@ Out of scope (defer): - Automatic lineup-based channel selection by callsign. - Secrets management (publisher doesn't require secrets for Cloudflare relay preview). +## Alternatives considered + +- Continue running publishers manually via shells/tmux. Rejected because it is not reproducible or restart-safe. +- Build a separate external deployment repo first. Rejected because this delays in-repo infrastructure ownership. + ## Rollout / Reversibility - Enabling the module is per-host. - Reversible by removing the module import and disabling the service(s); roll back with the existing deployment tooling. - diff --git a/evolution/proposals/ECP-0065-nixos-runner-images.md b/evolution/proposals/ECP-0065-nixos-runner-images.md index 3899324..43f223c 100644 --- a/evolution/proposals/ECP-0065-nixos-runner-images.md +++ b/evolution/proposals/ECP-0065-nixos-runner-images.md @@ -1,6 +1,6 @@ # ECP-0065: NixOS Runner Images + Netboot Artifacts -Status: Draft +Status: Implemented ## Decision @@ -40,6 +40,11 @@ Out of scope (defer): - Remote runtime provisioning (fetching per-node channel lists). - Hardware-accelerated transcode changes (keep current CPU x264 baseline). +## Alternatives considered + +- Keep runner images out-of-repo and publish ad hoc artifacts. Rejected because it weakens reproducibility and provenance. +- Restrict to one install path only (disk install only). Rejected because netboot/bootstrap is required for fleet recovery. + ## Rollout / Reversibility - Rollout begins with local builds and a single test machine. diff --git a/evolution/proposals/ECP-0066-iroh-control-protocol.md b/evolution/proposals/ECP-0066-iroh-control-protocol.md index 359c0cd..f5edd22 100644 --- a/evolution/proposals/ECP-0066-iroh-control-protocol.md +++ b/evolution/proposals/ECP-0066-iroh-control-protocol.md @@ -1,6 +1,6 @@ # ECP-0066: iroh-Gossip Control Protocol For Hybrid MoQ Discovery -Status: Draft +Status: Implemented ## Decision @@ -39,6 +39,11 @@ Out of scope: - Security policy beyond existing iroh/gossip trust boundaries. - Replacing existing catalog gossip immediately (coexist first). +## Alternatives considered + +- Keep relay and direct discovery completely separate. Rejected because it forces duplicated consumer logic. +- Replace existing catalog gossip in one cutover. Rejected because additive coexistence is safer for rollout. + ## Rollout / Reversibility - Additive and reversible: removing control commands and topic does not affect existing media paths. diff --git a/evolution/proposals/ECP-0067-control-resolve-and-nixos-wiring.md b/evolution/proposals/ECP-0067-control-resolve-and-nixos-wiring.md index d15dfde..21b9052 100644 --- a/evolution/proposals/ECP-0067-control-resolve-and-nixos-wiring.md +++ b/evolution/proposals/ECP-0067-control-resolve-and-nixos-wiring.md @@ -1,6 +1,6 @@ # ECP-0067: Control Transport Resolution And NixOS Control Wiring -Status: Draft +Status: Implemented ## Decision @@ -32,6 +32,11 @@ Out of scope: - End-to-end automatic failover execution (resolve + launch subscribe) in one command. - Cryptographic policy hardening beyond current control-topic trust model. +## Alternatives considered + +- Keep transport selection in ad hoc shell logic. Rejected because policy behavior becomes inconsistent across operators. +- Wire control flags per host manually. Rejected because it is error-prone and not declarative. + ## Rollout / Reversibility - Additive only: existing relay and direct publish/subscribe paths remain unchanged. diff --git a/evolution/proposals/ECP-0068-iroh-control-web-directory-bridge.md b/evolution/proposals/ECP-0068-iroh-control-web-directory-bridge.md index 7157e92..87d0649 100644 --- a/evolution/proposals/ECP-0068-iroh-control-web-directory-bridge.md +++ b/evolution/proposals/ECP-0068-iroh-control-web-directory-bridge.md @@ -1,6 +1,6 @@ # ECP-0068: Iroh Control To Web Directory Bridge -Status: Draft +Status: Implemented ## Decision @@ -34,6 +34,11 @@ Out of scope: - Signed/authenticated control announcements. - Replacing relay playback with direct iroh in browsers. +## Alternatives considered + +- Keep manual stream naming/link entry on the website. Rejected because it blocks one-click discovery. +- Bridge directly from browser clients instead of a node command. Rejected because browser trust/availability constraints are higher. + ## Rollout / Reversibility - Additive change; existing `/api/directory` and watch-by-link behavior remain intact. diff --git a/evolution/proposals/ECP-0069-nixos-control-bridge-autobootstrap.md b/evolution/proposals/ECP-0069-nixos-control-bridge-autobootstrap.md index 0f4e220..bcec81d 100644 --- a/evolution/proposals/ECP-0069-nixos-control-bridge-autobootstrap.md +++ b/evolution/proposals/ECP-0069-nixos-control-bridge-autobootstrap.md @@ -1,6 +1,6 @@ # ECP-0069: NixOS Control Bridge Auto-Bootstrap -Status: Draft +Status: Implemented ## Decision @@ -31,6 +31,11 @@ Out of scope: - Signed control announcements. - Browser-native iroh direct transport playback. +## Alternatives considered + +- Continue manual gossip peer bootstrapping for the bridge. Rejected because restarts/reboots cause repeated operational toil. +- Use static peer lists only. Rejected because local publisher sets are dynamic and should be discovered from runtime endpoint files. + ## Rollout / Reversibility - Additive: existing publisher behavior is unchanged when `control.bridgeWeb.enable = false`. diff --git a/evolution/proposals/ECP-0070-relay-cas-archival.md b/evolution/proposals/ECP-0070-relay-cas-archival.md index 0a20108..9487316 100644 --- a/evolution/proposals/ECP-0070-relay-cas-archival.md +++ b/evolution/proposals/ECP-0070-relay-cas-archival.md @@ -1,5 +1,7 @@ # ECP-0070: Relay-Native CAS Archival + NixOS Auto-Archive Service +Status: Implemented + ## Summary Add a first-party archival path for MoQ relay streams: @@ -48,6 +50,11 @@ Tradeoffs: - Discovery source is the web public stream list (not full control-topic gossip ingestion). - Per-broadcast workers are process-based and best-effort supervised. +## Alternatives considered + +- Rely on browser-side replay caches only. Rejected because it does not provide durable archival storage. +- Archive only manifests without CAS payloads. Rejected because replay/integrity requires retained object bytes. + ## Rollout 1. Ship `wt-archive` command in `ec-node`. diff --git a/evolution/proposals/ECP-0071-archive-replay-dvr.md b/evolution/proposals/ECP-0071-archive-replay-dvr.md index 872ad4b..aa902fc 100644 --- a/evolution/proposals/ECP-0071-archive-replay-dvr.md +++ b/evolution/proposals/ECP-0071-archive-replay-dvr.md @@ -1,5 +1,7 @@ # ECP-0071: Archive Replay DVR Endpoints +Status: Implemented + ## Context ECP-0070 added relay archival (`wt-archive`) into CAS objects plus JSONL indexes, but there is no read path for viewers to scrub historical content. @@ -26,6 +28,16 @@ Add an archive replay path with these pieces: - Preserves CAS as source of truth; playlists are derived views. - Uses standard HLS+DVR semantics so browser playback + scrubbing works without custom protocol work in the short term. +## Alternatives considered + +- Build a custom replay protocol/UI instead of HLS. Rejected because browser DVR support is stronger with standard HLS tooling. +- Serve archive from a separate domain only. Rejected because same-domain replay keeps watch links and CORS simpler. + +## Rollout / teardown + +- Enable archive serve mode on archive hosts and deploy worker proxy routing to `/api/archive/*`. +- Teardown by disabling `archive.serve.enable` and removing proxy routing. + ## Reversibility - Disable `archive.serve.enable` and remove worker proxy route to revert to archive-only mode. diff --git a/evolution/proposals/ECP-0072-cmaf-seedbox-invariant.md b/evolution/proposals/ECP-0072-cmaf-seedbox-invariant.md index d33a44d..0a64487 100644 --- a/evolution/proposals/ECP-0072-cmaf-seedbox-invariant.md +++ b/evolution/proposals/ECP-0072-cmaf-seedbox-invariant.md @@ -1,5 +1,7 @@ # ECP-0072: CMAF Seedbox Invariant For Relay Archive +Status: Implemented + ## Context Archive replay currently stores and serves relay groups exactly as received, but many existing broadcasts were published in `legacy` container mode. Those bytes are not browser-HLS compatible, so archive playback fails despite a valid timeline and object store. @@ -20,6 +22,16 @@ Update the NixOS module default `services.every-channel.ec-node.passthrough = tr - Exact-byte retention avoids drift between live and replay. - Browsers can play CMAF fragments via standard HLS tooling; no custom legacy converter is required for new streams. +## Alternatives considered + +- Keep `passthrough=false` as default for all publishers. Rejected because archive replay needs byte-compatible CMAF fragments. +- Re-encode archived payloads during replay. Rejected because it adds complexity and breaks exact-byte history semantics. + +## Rollout / teardown + +- Flip default `passthrough` to true in CLI and Nix module, then verify new archives play via HLS. +- Teardown by explicitly setting `passthrough=false` on hosts needing legacy framing. + ## Reversibility - Operators can explicitly set `passthrough = false` per host to revert to legacy framing. diff --git a/evolution/proposals/ECP-0073-archive-relay-affinity-override.md b/evolution/proposals/ECP-0073-archive-relay-affinity-override.md index d945432..50cbf86 100644 --- a/evolution/proposals/ECP-0073-archive-relay-affinity-override.md +++ b/evolution/proposals/ECP-0073-archive-relay-affinity-override.md @@ -1,5 +1,7 @@ # ECP-0073: Archive Relay Affinity Override +Status: Implemented + ## Context `wt-archive` workers discover streams from `/api/public-streams` and subscribe to the listed `relay_url`. In practice, `cdn.moq.dev` resolves to region-local relay IPs, and broadcasts published from one region are not consistently visible from another region endpoint. @@ -22,6 +24,11 @@ This allows operators to pin archive ingestion to the same relay endpoint used b - Keeps deployment-level control in Nix (no app-level migration needed). - Reversible with a single config change. +## Alternatives considered + +- Keep subscribing to directory-provided `relay_url` only. Rejected because cross-region visibility is inconsistent in practice. +- Rewrite directory entries per-region. Rejected because this mixes deployment affinity into public directory payloads. + ## Rollout 1. Set `archive.relayUrlOverride` on archive hosts that need relay affinity. diff --git a/evolution/proposals/ECP-0074-archive-hls-engine-selection.md b/evolution/proposals/ECP-0074-archive-hls-engine-selection.md index 0e334b7..382ddf8 100644 --- a/evolution/proposals/ECP-0074-archive-hls-engine-selection.md +++ b/evolution/proposals/ECP-0074-archive-hls-engine-selection.md @@ -1,5 +1,7 @@ # ECP-0074: Archive HLS Engine Selection For Chromium +Status: Implemented + ## Context Archive mode currently chooses native HLS whenever `video.canPlayType("application/vnd.apple.mpegurl")` is non-empty. @@ -16,6 +18,16 @@ Use native HLS only on Safari/iOS user agents. For all other browsers (including - Keeps Safari native path where it is reliable. - Preserves a single URL and UI flow (`/api/archive/.../master.m3u8`). +## Alternatives considered + +- Keep `canPlayType` as the only gate. Rejected because Chromium reports support but fails event-style playback. +- Force `hls.js` for all browsers including Safari. Rejected because Safari native playback is already reliable and simpler. + +## Rollout / teardown + +- Deploy UA-gated engine selection in web app and validate archive playback on Chromium and Safari. +- Teardown by reverting to the previous generic `canPlayType` gate. + ## Reversibility Revert the UA gate and return to the previous `canPlayType`-only check. diff --git a/evolution/proposals/ECP-0075-moq-watch-0.2.0-live-stability.md b/evolution/proposals/ECP-0075-moq-watch-0.2.0-live-stability.md index e2d4311..8146efb 100644 --- a/evolution/proposals/ECP-0075-moq-watch-0.2.0-live-stability.md +++ b/evolution/proposals/ECP-0075-moq-watch-0.2.0-live-stability.md @@ -1,5 +1,7 @@ # ECP-0075: Bump Web Watcher To `@moq/watch@0.2.0` +Status: Implemented + ## Context Production web watchers currently load `@moq/watch@0.1.1`. Under live OTA relay streams, Chromium sessions frequently emit runtime failures (`VideoFrame clone` errors and repeated stream resets), leaving playback stalled even after successful subscribe. @@ -15,6 +17,16 @@ Set both `name` and `path` attributes on `` so minor-version attribut - Pulls in upstream runtime fixes without introducing new local playback logic. - Preserves multi-CDN fallback behavior already used for dependency resilience. +## Alternatives considered + +- Keep pin at `0.1.1` and add larger local workarounds. Rejected because upstream fixes already address core runtime failures. +- Switch to a different browser player stack immediately. Rejected because this is higher risk than a targeted minor-version bump. + +## Rollout / teardown + +- Roll out `@moq/watch@0.2.0` on all CDN import fallbacks and verify live subscribe/playback. +- Teardown by repinning imports to `0.1.1`. + ## Reversibility - Roll back by pinning imports back to `0.1.1` if regressions appear. diff --git a/evolution/proposals/ECP-0076-webtransport-only-web-watcher.md b/evolution/proposals/ECP-0076-webtransport-only-web-watcher.md index 31e78bc..cb23aca 100644 --- a/evolution/proposals/ECP-0076-webtransport-only-web-watcher.md +++ b/evolution/proposals/ECP-0076-webtransport-only-web-watcher.md @@ -1,5 +1,7 @@ # ECP-0076: WebTransport-Only Browser Watcher Path +Status: Implemented + ## Context The browser watcher (`@moq/watch`) races WebTransport against WebSocket fallback by default. In production relay sessions this fallback path correlates with degraded playback behavior (frequent stream resets and unreliable audio despite active subscription). @@ -10,7 +12,7 @@ In `apps/web/app.js`, configure each `` instance to disable WebSocket - `watch.connection.websocket = { enabled: false }` -Also set default watcher volume to full (`volume="1"`) and mount live playback on a `