From 74dc6dfc8143439058f11f7051c7201bf4e10514 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 29 Sep 2026 15:05:29 -0700 Subject: [PATCH 1/3] quest(tooling/demo-scripts): claim Co-Authored-By: Claude Opus 5.5 From a533d36aab177a789b587f382fdd500117f35036 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 29 Sep 2026 15:28:28 -0700 Subject: [PATCH 2/3] refactor(demo): move recipe logic into sh/demo scripts The demo justfiles held the last inline bash in the just tree. Recipes with real logic (HLS generation, port probing, cert and token generation, R2 sync, downloads) now call scripts under sh/demo/, linted by the shell lint. Forwarding recipes in demo/pub and demo/sub use positional arguments so quoted arguments survive, and clock validates its action with a just argument pattern. Co-Authored-By: Claude Opus 5.5 --- demo/boy/justfile | 18 +--- demo/justfile | 51 +-------- demo/pub/justfile | 171 ++++++------------------------- demo/relay/justfile | 53 +--------- demo/sub/justfile | 4 +- quest/m1/tooling/README.md | 1 - quest/m1/tooling/demo-scripts.md | 30 ------ sh/demo/ca.sh | 15 +++ sh/demo/cert.sh | 20 ++++ sh/demo/download.sh | 15 +++ sh/demo/hls.sh | 70 +++++++++++++ sh/demo/key.sh | 11 ++ sh/demo/port.sh | 18 ++++ sh/demo/run.sh | 24 +++++ sh/demo/serve-hls.sh | 43 ++++++++ sh/demo/sync.sh | 16 +++ sh/demo/token.sh | 19 ++++ sh/demo/wait.sh | 14 +++ 18 files changed, 314 insertions(+), 279 deletions(-) delete mode 100644 quest/m1/tooling/demo-scripts.md create mode 100755 sh/demo/ca.sh create mode 100755 sh/demo/cert.sh create mode 100755 sh/demo/download.sh create mode 100755 sh/demo/hls.sh create mode 100755 sh/demo/key.sh create mode 100755 sh/demo/port.sh create mode 100755 sh/demo/run.sh create mode 100755 sh/demo/serve-hls.sh create mode 100755 sh/demo/sync.sh create mode 100755 sh/demo/token.sh create mode 100755 sh/demo/wait.sh diff --git a/demo/boy/justfile b/demo/boy/justfile index 9312d15bd5..5837865a5e 100644 --- a/demo/boy/justfile +++ b/demo/boy/justfile @@ -11,13 +11,9 @@ default: "just wait && just web" # Download the given ROM from our R2 bucket. +[positional-arguments] download rom: - #!/usr/bin/env bash - if [ ! -f "rom/{{ rom }}" ]; then - mkdir -p rom - curl -fSL -o "rom/{{ rom }}.tmp" "https://rom.moq.dev/{{ rom }}" - mv "rom/{{ rom }}.tmp" "rom/{{ rom }}" - fi + @../../sh/demo/download.sh "https://rom.moq.dev/$1" "rom/$1" # Big2Small (puzzle): https://mdsteele.itch.io/big2small b2s: @@ -70,15 +66,9 @@ upload file: bun wrangler r2 object put "rom-moq-dev/{{ file }}" --file "rom/{{ file }}" --remote # Sync all ROMs to R2 +[positional-arguments] sync dir="rom": - #!/usr/bin/env bash - set -euo pipefail - for file in "{{ dir }}"/*; do - [ -e "$file" ] || continue - key=$(basename "$file") - echo "Uploading $key..." - bun wrangler r2 object put "rom-moq-dev/$key" --file "$file" --remote - done + ../../sh/demo/sync.sh rom-moq-dev "$@" # List files in the R2 bucket list: diff --git a/demo/justfile b/demo/justfile index b612873465..08b359d9ff 100644 --- a/demo/justfile +++ b/demo/justfile @@ -2,58 +2,17 @@ set fallback # Run the web demo (default). # -# Picks the first free port at/after 4443 (both UDP for QUIC and TCP for HTTP) -# so multiple worktrees can run `just dev` without colliding. The relay reads - -# the port from MOQ_LISTEN/MOQ_WEB_HTTP_LISTEN, overriding localhost.toml. +# Picks the first free port at/after 4443 so multiple worktrees can run +# `just dev` without colliding; see sh/demo/run.sh. default: - #!/usr/bin/env bash - set -euo pipefail bun install - - port=$(just port) - if [ "$port" != "4443" ]; then - echo ">>> Port 4443 is busy, using $port instead" >&2 - fi - - base="http://localhost:$port" - - # Scope the bind env vars to the relay only. The unified `moq` binary also - # reads MOQ_LISTEN, so exporting it globally would make the `pub` client - # try to start a server (and fail without a TLS cert). - bun run concurrently --kill-others --names rly,bbb,web --prefix-colors auto \ - "MOQ_LISTEN='[::]:$port' MOQ_WEB_HTTP_LISTEN='[::]:$port' just relay" \ - "just wait $base/certificate.sha256 && just pub bbb $base" \ - "just wait $base/certificate.sha256 && just web serve $base" - -# Find the first free port at/after `start` (checked on both TCP and UDP). -# `lsof` isn't available everywhere (e.g. Git Bash on Windows); without it we -# can't probe, so fall back to `start` and let the relay surface a bind error if - -# it's actually taken. -[private] -port start="4443": - #!/usr/bin/env bash - set -euo pipefail - port={{ start }} - if command -v lsof >/dev/null 2>&1; then - while lsof -nP -iTCP:"$port" -sTCP:LISTEN -t >/dev/null 2>&1 \ - || lsof -nP -iUDP:"$port" -t >/dev/null 2>&1; do - port=$((port + 1)) - done - fi - echo "$port" + ../sh/demo/run.sh # Wait for the localhost relay to be ready by repeatedly requesting the cert +[positional-arguments] [private] wait url="http://localhost:4443/certificate.sha256": - #!/usr/bin/env bash - set -euo pipefail - for i in $(seq 1 600); do - curl -sf "{{ url }}" > /dev/null 2>&1 && exit 0 - sleep 1 - done - exit 1 + ../sh/demo/wait.sh "$@" # Throttle UDP traffic for testing (macOS only, requires sudo). throttle: diff --git a/demo/pub/justfile b/demo/pub/justfile index fb780f5e73..16cb04aea6 100644 --- a/demo/pub/justfile +++ b/demo/pub/justfile @@ -1,4 +1,5 @@ set fallback +set shell := ["bash", "-euo", "pipefail", "-c"] # --- R2 bucket management (vid.moq.dev) --- @@ -18,15 +19,9 @@ upload file: bun wrangler r2 object put "video-moq-dev/{{ file }}" --file "media/{{ file }}" --remote # Sync all files in a local directory to R2 +[positional-arguments] sync dir="media": - #!/usr/bin/env bash - set -euo pipefail - for file in "{{ dir }}"/*; do - [ -e "$file" ] || continue - key=$(basename "$file") - echo "Uploading $key..." - bun wrangler r2 object put "video-moq-dev/$key" --file "$file" --remote - done + ../../sh/demo/sync.sh video-moq-dev "$@" # List files in the bucket list: @@ -36,6 +31,7 @@ list: # --- Publishing --- # Publish Big Buck Bunny to a relay server. +[positional-arguments] bbb url='http://localhost:4443' *args: just download bbb just download bbb-sd @@ -44,7 +40,7 @@ bbb url='http://localhost:4443' *args: -stream_loop -1 -re -i media/bbb.mp4 \ -stream_loop -1 -re -i media/bbb-sd.mp4 \ -map 0 -map 1:v -c copy -f mpegts -pes_payload_size 0 - \ - | cargo run --bin moq -- {{ args }} --connect "{{ url }}" --broadcast bbb.hang import ts + | cargo run --bin moq -- "${@:2}" --connect "{{ url }}" --broadcast bbb.hang import ts # Encode the video-only SD rendition, preserving source timestamps and loop duration. encode-bbb-sd: @@ -58,19 +54,22 @@ check-bbb: python3 bbb.py check # Publish Tears of Steel to a relay server. +[positional-arguments] tos url='http://localhost:4443' *args: just download tos - just ts tos "{{ url }}" {{ args }} + just ts tos "$@" # Publish a video using ffmpeg (CMAF / fMP4) to a relay server. +[positional-arguments] cmaf name url='http://localhost:4443' *args: cargo build --bin moq - just ffmpeg-cmaf "media/{{ name }}.mp4" | cargo run --bin moq -- {{ args }} --connect "{{ url }}" --broadcast "{{ name }}.hang" import fmp4 + just ffmpeg-cmaf "media/{{ name }}.mp4" | cargo run --bin moq -- "${@:3}" --connect "{{ url }}" --broadcast "{{ name }}.hang" import fmp4 # Publish a video using ffmpeg (MPEG-TS) to a relay server. +[positional-arguments] ts name url='http://localhost:4443' *args: cargo build --bin moq - just ffmpeg-ts "media/{{ name }}.mp4" | cargo run --bin moq -- {{ args }} --connect "{{ url }}" --broadcast "{{ name }}.hang" import ts + just ffmpeg-ts "media/{{ name }}.mp4" | cargo run --bin moq -- "${@:3}" --connect "{{ url }}" --broadcast "{{ name }}.hang" import ts # Publish a video via iroh. iroh name url prefix="": @@ -79,82 +78,13 @@ iroh name url prefix="": just ffmpeg-ts "media/{{ name }}.mp4" | cargo run --bin moq -- --iroh-enabled --connect "{{ url }}" --broadcast "{{ prefix }}{{ name }}.hang" import ts # Generate and ingest an HLS stream from a video file. +[positional-arguments] hls name relay="http://localhost:4443": - #!/usr/bin/env bash - set -euo pipefail - - just download "{{ name }}" - - INPUT="media/{{ name }}.mp4" - OUT_DIR="media/{{ name }}" - - rm -rf "$OUT_DIR" - mkdir -p "$OUT_DIR" - - echo ">>> Generating HLS stream to disk (1280x720 + 256x144)..." - - ffmpeg -hide_banner -loglevel warning -re -stream_loop -1 -i "$INPUT" \ - -filter_complex "\ - [0:v]split=2[v0][v1]; \ - [v0]scale=-2:720[v720]; \ - [v1]scale=-2:144[v144]" \ - -map "[v720]" -map "[v144]" -map 0:a:0 \ - -r 25 -preset veryfast -g 50 -keyint_min 50 -sc_threshold 0 \ - -c:v:0 libx264 -profile:v:0 high -level:v:0 4.1 -pix_fmt:v:0 yuv420p -tag:v:0 avc1 \ - -b:v:0 4M -maxrate:v:0 4.4M -bufsize:v:0 8M \ - -c:v:1 libx264 -profile:v:1 high -level:v:1 4.1 -pix_fmt:v:1 yuv420p -tag:v:1 avc1 \ - -b:v:1 300k -maxrate:v:1 330k -bufsize:v:1 600k \ - -c:a aac -b:a 128k \ - -f hls -hls_time 2 -hls_list_size 6 \ - -hls_flags independent_segments+delete_segments \ - -hls_segment_type fmp4 \ - -master_pl_name master.m3u8 \ - -var_stream_map "v:0,agroup:audio,name:720 v:1,agroup:audio,name:144 a:0,agroup:audio,name:audio" \ - -hls_segment_filename "$OUT_DIR/v%v/segment_%09d.m4s" \ - "$OUT_DIR/v%v/stream.m3u8" & - - FFMPEG_PID=$! - - echo ">>> Waiting for HLS playlist generation..." - for i in {1..30}; do - if [ -f "$OUT_DIR/master.m3u8" ]; then break; fi - sleep 0.5 - done - - if [ ! -f "$OUT_DIR/master.m3u8" ]; then - kill $FFMPEG_PID 2>/dev/null || true - echo "Error: master.m3u8 not generated in time" - exit 1 - fi - - echo ">>> Waiting for variant playlists..." - sleep 2 - for i in {1..20}; do - if [ -f "$OUT_DIR/v0/stream.m3u8" ] || [ -f "$OUT_DIR/v720/stream.m3u8" ] || [ -f "$OUT_DIR/v144/stream.m3u8" ] || [ -f "$OUT_DIR/vaudio/stream.m3u8" ]; then - break - fi - sleep 0.5 - done - - CLEANUP_CALLED=false - cleanup() { - if [ "$CLEANUP_CALLED" = "true" ]; then return; fi - CLEANUP_CALLED=true - echo "Shutting down..." - kill $FFMPEG_PID 2>/dev/null || true - sleep 0.5 - kill -9 $FFMPEG_PID 2>/dev/null || true - } - trap cleanup SIGINT SIGTERM EXIT - - echo ">>> Importing HLS via moq-cli" - cargo run --bin moq -- --connect "{{ relay }}" --broadcast "{{ name }}.hang" import hls "$OUT_DIR/master.m3u8" - EXIT_CODE=$? - - cleanup - exit $EXIT_CODE + just download "$1" + ../../sh/demo/hls.sh "$@" # Publish a video using H.264 Annex B format. +[positional-arguments] h264 name url="http://localhost:4443" *args: just download "{{ name }}" cargo build --bin moq @@ -165,68 +95,31 @@ h264 name url="http://localhost:4443" *args: -c:v copy -an \ -bsf:v h264_mp4toannexb \ -f h264 \ - - | cargo run --bin moq -- {{ args }} --connect "{{ url }}" --broadcast "{{ name }}.hang" import avc3 + - | cargo run --bin moq -- "${@:3}" --connect "{{ url }}" --broadcast "{{ name }}.hang" import avc3 # Publish a video using ffmpeg directly from moq to the localhost. +[positional-arguments] serve name *args: just download "{{ name }}" cargo build --bin moq - just ffmpeg-ts "media/{{ name }}.mp4" | cargo run --bin moq -- {{ args }} --listen "[::]:4443" --listen-tls-generate "localhost" --auth-public "**" --broadcast "{{ name }}.hang" import ts + just ffmpeg-ts "media/{{ name }}.mp4" | cargo run --bin moq -- "${@:2}" --listen "[::]:4443" --listen-tls-generate "localhost" --auth-public "**" --broadcast "{{ name }}.hang" import ts # Generate and serve an HLS stream from a video for testing. +[positional-arguments] serve-hls name port="8000": - #!/usr/bin/env bash - set -euo pipefail - - just download "{{ name }}" - - INPUT="media/{{ name }}.mp4" - OUT_DIR="media/{{ name }}" - - rm -rf "$OUT_DIR" - mkdir -p "$OUT_DIR" - - echo ">>> Starting HLS stream generation..." - echo ">>> Master playlist: http://localhost:{{ port }}/master.m3u8" - - cleanup() { - echo "Shutting down..." - kill $(jobs -p) 2>/dev/null || true - exit 0 - } - trap cleanup SIGINT SIGTERM - - ffmpeg -loglevel warning -re -stream_loop -1 -i "$INPUT" \ - -map 0:v:0 -map 0:v:0 -map 0:a:0 \ - -r 25 -preset veryfast -g 50 -keyint_min 50 -sc_threshold 0 \ - -c:v:0 libx264 -profile:v:0 high -level:v:0 4.1 -pix_fmt:v:0 yuv420p -tag:v:0 avc1 -bsf:v:0 dump_extra -b:v:0 4M -vf:0 "scale=1920:-2" \ - -c:v:1 libx264 -profile:v:1 high -level:v:1 4.1 -pix_fmt:v:1 yuv420p -tag:v:1 avc1 -bsf:v:1 dump_extra -b:v:1 300k -vf:1 "scale=256:-2" \ - -c:a aac -b:a 128k \ - -f hls \ - -hls_time 2 -hls_list_size 12 \ - -hls_flags independent_segments+delete_segments \ - -hls_segment_type fmp4 \ - -master_pl_name master.m3u8 \ - -var_stream_map "v:0,agroup:audio v:1,agroup:audio a:0,agroup:audio" \ - -hls_segment_filename "$OUT_DIR/v%v/segment_%09d.m4s" \ - "$OUT_DIR/v%v/stream.m3u8" & - - sleep 2 - echo ">>> HTTP server: http://localhost:{{ port }}/" - cd "$OUT_DIR" && python3 -m http.server {{ port }} + just download "$1" + ../../sh/demo/serve-hls.sh "$@" # Publish the clock broadcast. +[arg("action", pattern="publish|subscribe")] +[positional-arguments] clock action url="http://localhost:4443" *args: - @if [ "{{ action }}" != "publish" ] && [ "{{ action }}" != "subscribe" ]; then \ - echo "Error: action must be 'publish' or 'subscribe', got '{{ action }}'" >&2; \ - exit 1; \ - fi - - cargo run -p moq-tokio --example clock -- --connect "{{ url }}" --broadcast "clock" {{ args }} {{ action }} + cargo run -p moq-tokio --example clock -- --connect "{{ url }}" --broadcast "clock" "${@:3}" "{{ action }}" # --- GStreamer --- # Publish a video using GStreamer to a relay server. +[positional-arguments] gst name url='http://localhost:4443' *args: just download "{{ name }}" cargo build -p moq-gst @@ -235,34 +128,34 @@ gst name url='http://localhost:4443' *args: gst-launch-1.0 -v -e multifilesrc location="media/{{ name }}.mp4" loop=true ! parsebin name=parse \ parse. ! queue ! identity sync=true ! mux.sink_0 \ parse. ! queue ! identity sync=true ! mux.sink_1 \ - moqsink name=mux url="{{ url }}" broadcast="{{ name }}.hang" {{ args }} + moqsink name=mux url="{{ url }}" broadcast="{{ name }}.hang" "${@:3}" # --- Private helpers --- # Download a test video (already fragmented on vid.moq.dev). +[positional-arguments] [private] download name: - @if [ ! -f "media/{{ name }}.mp4" ]; then \ - mkdir -p media; \ - curl -fsSL "https://vid.moq.dev/{{ name }}.mp4" -o "media/{{ name }}.mp4"; \ - fi + @../../sh/demo/download.sh "https://vid.moq.dev/$1.mp4" "media/$1.mp4" # Convert an h264 input file to CMAF (fmp4) format to stdout, with one fragment per sample. # -frag_duration rather than frag_every_frame: the latter repeats a fragment's tfdt when it # interleaves audio and video, landing two samples on one timestamp. +[positional-arguments] [private] ffmpeg-cmaf input output='-' *args: ffmpeg -hide_banner -v quiet \ -stream_loop -1 -re \ -i "{{ input }}" \ -c copy \ - -f mp4 -movflags cmaf+separate_moof+delay_moov+skip_trailer -frag_duration 1000 {{ args }} {{ output }} + -f mp4 -movflags cmaf+separate_moof+delay_moov+skip_trailer -frag_duration 1000 "${@:3}" "{{ output }}" # Convert an h264 input file to MPEG-TS format to stdout, with one PES per audio frame. +[positional-arguments] [private] ffmpeg-ts input output='-' *args: ffmpeg -hide_banner -v quiet \ -stream_loop -1 -re \ -i "{{ input }}" \ -c copy \ - -f mpegts -pes_payload_size 0 {{ args }} {{ output }} + -f mpegts -pes_payload_size 0 "${@:3}" "{{ output }}" diff --git a/demo/relay/justfile b/demo/relay/justfile index c0e33a3197..db04682e60 100644 --- a/demo/relay/justfile +++ b/demo/relay/justfile @@ -34,60 +34,17 @@ leaf1: (cert "leaf1") # Generate a self-signed CA ca: - #!/usr/bin/env bash - set -euo pipefail - umask 077 - - [ -f ca.pem ] && [ -f ca.key ] && exit 0 - rm -f ca.pem ca.key - printf '[req]\ndistinguished_name = req_dn\n[req_dn]\n' > ca.cnf - export OPENSSL_CONF="ca.cnf" - openssl req -x509 -sha256 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes \ - -days 365 -subj "/CN=moq cluster CA" \ - -keyout ca.key -out ca.pem + ../../sh/demo/ca.sh # Generate a self-signed certificate using the root CA. - -# Chrome requires a <=14 day validity for self-signed certs. +[positional-arguments] cert name: ca - #!/usr/bin/env bash - set -euo pipefail - umask 077 - - export OPENSSL_CONF="ca.cnf" - openssl req -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes \ - -subj "/CN={{ name }}" \ - -keyout "{{ name }}.key" -out "{{ name }}.csr" - openssl x509 -req -sha256 -in "{{ name }}.csr" \ - -CA ca.pem -CAkey ca.key -CAcreateserial \ - -days 14 -extfile <(printf "subjectAltName=DNS:localhost\n") \ - -out "{{ name }}.crt" - rm "{{ name }}.csr" + ../../sh/demo/cert.sh "$@" # Generate a random secret key for authentication. key: - #!/usr/bin/env bash - set -euo pipefail - umask 077 - - [ -f root.jwk ] && exit 0 - rm -f *.jwt - cargo run --bin moq -- auth generate --out root.jwk + ../../sh/demo/key.sh # Generate authentication tokens for local development. token: key - #!/usr/bin/env bash - set -euo pipefail - umask 077 - - if [ ! -f demo-web.jwt ]; then - cargo run --quiet --bin moq -- auth sign --key root.jwk \ - --root demo --subscribe '**' --publish 'me/**' \ - > demo-web.jwt - fi - - if [ ! -f demo-cli.jwt ]; then - cargo run --quiet --bin moq -- auth sign --key root.jwk \ - --root demo --publish '**' \ - > demo-cli.jwt - fi + ../../sh/demo/token.sh diff --git a/demo/sub/justfile b/demo/sub/justfile index e7ec69ed85..824fd6674a 100644 --- a/demo/sub/justfile +++ b/demo/sub/justfile @@ -1,6 +1,8 @@ set fallback +set shell := ["bash", "-euo", "pipefail", "-c"] # Subscribe to a broadcast using GStreamer and render to the screen. +[positional-arguments] gst name url='http://localhost:4443' *args: cargo build -p moq-gst @@ -8,6 +10,6 @@ gst name url='http://localhost:4443' *args: # by name so a bare `! decodebin3` can't latch onto the audio pad and leave the # video sink empty; route audio to its own sink so it doesn't stall the session. GST_PLUGIN_PATH_1_0="${PWD}/../../target/debug${GST_PLUGIN_PATH_1_0:+:$GST_PLUGIN_PATH_1_0}" \ - gst-launch-1.0 -v -e moqsrc name=s url="{{ url }}" broadcast="{{ name }}" {{ args }} \ + gst-launch-1.0 -v -e moqsrc name=s url="{{ url }}" broadcast="{{ name }}" "${@:3}" \ s.video_0 ! queue ! decodebin3 ! videoconvert ! autovideosink \ s.audio_0 ! queue ! decodebin3 ! audioconvert ! autoaudiosink diff --git a/quest/m1/tooling/README.md b/quest/m1/tooling/README.md index 3820b828aa..28e81dc194 100644 --- a/quest/m1/tooling/README.md +++ b/quest/m1/tooling/README.md @@ -23,6 +23,5 @@ inline; line count is not the test. ## Required -- [Demo scripts](/quest/m1/tooling/demo-scripts.md) - the demo justfiles' inline bash moves into scripts, the last logic left inside recipes - [Forward arguments](/quest/m1/tooling/forward-args.md) - recipes pass a quoted nextest filterset through intact instead of re-splitting it - [Check scope](/quest/m1/tooling/check-scope.md) - check.yml's build-or-skip decision comes from sh/dispatch.sh instead of an inline diff grep diff --git a/quest/m1/tooling/demo-scripts.md b/quest/m1/tooling/demo-scripts.md deleted file mode 100644 index b2e721fd28..0000000000 --- a/quest/m1/tooling/demo-scripts.md +++ /dev/null @@ -1,30 +0,0 @@ -# [S] Demo recipes call scripts - -## Goal - -The demo justfiles hold the last inline bash in the `just` tree. Move each -recipe body with real logic into a script the recipe calls, so every recipe -is a thin command sequence: - -- `demo/pub`: `hls` (a background ffmpeg, playlist polling, and a cleanup - trap) and `serve-hls`. -- `demo/justfile`: `default` (port probing and the `concurrently` launch), - `port`, and `wait`. -- `demo/relay`: `ca`, `cert`, `key`, and `token`, the idempotent certificate - and token generation. -- `demo/pub` and `demo/boy`: `sync`, the same R2 upload loop with a different - bucket, which becomes one script taking the bucket. `download` in both, and - the argument check in `demo/pub`'s `clock`, go with them. - -Behavior stays the same, including Windows Git Bash, where `port` falls back -without `lsof`. - -## Plan - -- Put the scripts under `sh/demo/`, matching the `sh//` layout of the - other modules, and run `shellcheck` and `shfmt` on them through the existing - `shell` lint. -- Short command sequences (`just rs features`, `just py check`, the `ffmpeg` - pipelines) are thin already and stay inline. - -Public API: none. Wire: none. diff --git a/sh/demo/ca.sh b/sh/demo/ca.sh new file mode 100755 index 0000000000..01cfbd0c05 --- /dev/null +++ b/sh/demo/ca.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# Generate the relay cluster's self-signed CA (ca.pem, ca.key, ca.cnf) in the +# current directory, unless it already exists. +# +# Usage: sh/demo/ca.sh, from demo/relay. +set -euo pipefail +umask 077 + +[ -f ca.pem ] && [ -f ca.key ] && exit 0 +rm -f ca.pem ca.key +printf '[req]\ndistinguished_name = req_dn\n[req_dn]\n' >ca.cnf +export OPENSSL_CONF="ca.cnf" +openssl req -x509 -sha256 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes \ + -days 365 -subj "/CN=moq cluster CA" \ + -keyout ca.key -out ca.pem diff --git a/sh/demo/cert.sh b/sh/demo/cert.sh new file mode 100755 index 0000000000..916ece90f6 --- /dev/null +++ b/sh/demo/cert.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# Issue NAME.crt and NAME.key for localhost from the CA that sh/demo/ca.sh +# generates in the current directory. Chrome requires a <=14 day validity for +# self-signed certs. +# +# Usage: sh/demo/cert.sh NAME, from demo/relay. +set -euo pipefail +umask 077 + +name=${1:?usage: sh/demo/cert.sh NAME} + +export OPENSSL_CONF="ca.cnf" +openssl req -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes \ + -subj "/CN=$name" \ + -keyout "$name.key" -out "$name.csr" +openssl x509 -req -sha256 -in "$name.csr" \ + -CA ca.pem -CAkey ca.key -CAcreateserial \ + -days 14 -extfile <(printf "subjectAltName=DNS:localhost\n") \ + -out "$name.crt" +rm "$name.csr" diff --git a/sh/demo/download.sh b/sh/demo/download.sh new file mode 100755 index 0000000000..3b1a4f57a5 --- /dev/null +++ b/sh/demo/download.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# Download URL to FILE unless FILE already exists. The download lands in a +# temporary file first, so an interrupted one is retried instead of cached. +# +# Usage: sh/demo/download.sh URL FILE +set -euo pipefail + +usage="usage: sh/demo/download.sh URL FILE" +url=${1:?$usage} +file=${2:?$usage} + +[ -f "$file" ] && exit 0 +mkdir -p "$(dirname "$file")" +curl -fsSL -o "$file.tmp" "$url" +mv "$file.tmp" "$file" diff --git a/sh/demo/hls.sh b/sh/demo/hls.sh new file mode 100755 index 0000000000..c203866bbd --- /dev/null +++ b/sh/demo/hls.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# Encode media/NAME.mp4 into a looping HLS stream (1280x720 + 256x144 + audio) +# under media/NAME/, and import it into RELAY as NAME.hang. +# +# Usage: sh/demo/hls.sh NAME RELAY, from demo/pub. +set -euo pipefail + +usage="usage: sh/demo/hls.sh NAME RELAY" +name=${1:?$usage} +relay=${2:?$usage} + +input="media/$name.mp4" +out="media/$name" + +rm -rf "$out" +mkdir -p "$out" + +echo ">>> Generating HLS stream to disk (1280x720 + 256x144)..." + +ffmpeg -hide_banner -loglevel warning -re -stream_loop -1 -i "$input" \ + -filter_complex "[0:v]split=2[v0][v1]; [v0]scale=-2:720[v720]; [v1]scale=-2:144[v144]" \ + -map "[v720]" -map "[v144]" -map 0:a:0 \ + -r 25 -preset veryfast -g 50 -keyint_min 50 -sc_threshold 0 \ + -c:v:0 libx264 -profile:v:0 high -level:v:0 4.1 -pix_fmt:v:0 yuv420p -tag:v:0 avc1 \ + -b:v:0 4M -maxrate:v:0 4.4M -bufsize:v:0 8M \ + -c:v:1 libx264 -profile:v:1 high -level:v:1 4.1 -pix_fmt:v:1 yuv420p -tag:v:1 avc1 \ + -b:v:1 300k -maxrate:v:1 330k -bufsize:v:1 600k \ + -c:a aac -b:a 128k \ + -f hls -hls_time 2 -hls_list_size 6 \ + -hls_flags independent_segments+delete_segments \ + -hls_segment_type fmp4 \ + -master_pl_name master.m3u8 \ + -var_stream_map "v:0,agroup:audio,name:720 v:1,agroup:audio,name:144 a:0,agroup:audio,name:audio" \ + -hls_segment_filename "$out/v%v/segment_%09d.m4s" \ + "$out/v%v/stream.m3u8" & +ffmpeg=$! + +cleaned=false +cleanup() { + if [ "$cleaned" = true ]; then return; fi + cleaned=true + echo "Shutting down..." + kill "$ffmpeg" 2>/dev/null || true + sleep 0.5 + kill -9 "$ffmpeg" 2>/dev/null || true +} +trap cleanup INT TERM EXIT + +echo ">>> Waiting for HLS playlist generation..." +for _ in {1..30}; do + if [ -f "$out/master.m3u8" ]; then break; fi + sleep 0.5 +done + +if [ ! -f "$out/master.m3u8" ]; then + echo "Error: master.m3u8 not generated in time" >&2 + exit 1 +fi + +echo ">>> Waiting for variant playlists..." +sleep 2 +for _ in {1..20}; do + if [ -f "$out/v720/stream.m3u8" ] || [ -f "$out/v144/stream.m3u8" ] || [ -f "$out/vaudio/stream.m3u8" ]; then + break + fi + sleep 0.5 +done + +echo ">>> Importing HLS via moq-cli" +cargo run --bin moq -- --connect "$relay" --broadcast "$name.hang" import hls "$out/master.m3u8" diff --git a/sh/demo/key.sh b/sh/demo/key.sh new file mode 100755 index 0000000000..1c199dafcb --- /dev/null +++ b/sh/demo/key.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# Generate the relay's signing key (root.jwk) in the current directory, unless +# it already exists. A new key invalidates every token, so they are removed. +# +# Usage: sh/demo/key.sh, from demo/relay. +set -euo pipefail +umask 077 + +[ -f root.jwk ] && exit 0 +rm -f ./*.jwt +cargo run --bin moq -- auth generate --out root.jwk diff --git a/sh/demo/port.sh b/sh/demo/port.sh new file mode 100755 index 0000000000..814314a703 --- /dev/null +++ b/sh/demo/port.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# Print the first port at/after START that is free on both TCP and UDP. +# +# `lsof` isn't available everywhere (e.g. Git Bash on Windows); without it we +# can't probe, so print START and let the relay surface a bind error if it's +# actually taken. +# +# Usage: sh/demo/port.sh START +set -euo pipefail + +port=${1:?usage: sh/demo/port.sh START} +if command -v lsof >/dev/null 2>&1; then + while lsof -nP -iTCP:"$port" -sTCP:LISTEN -t >/dev/null 2>&1 || + lsof -nP -iUDP:"$port" -t >/dev/null 2>&1; do + port=$((port + 1)) + done +fi +echo "$port" diff --git a/sh/demo/run.sh b/sh/demo/run.sh new file mode 100755 index 0000000000..af9d85555b --- /dev/null +++ b/sh/demo/run.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# Run the web demo: a localhost relay, Big Buck Bunny, and the web player. +# +# Picks the first free port at/after 4443 (both UDP for QUIC and TCP for HTTP) +# so multiple worktrees can run `just dev` without colliding. The relay reads +# the port from MOQ_LISTEN/MOQ_WEB_HTTP_LISTEN, overriding localhost.toml. +# +# Usage: sh/demo/run.sh, from demo/ so `just` resolves the demo recipes. +set -euo pipefail + +port=$("$(dirname "$0")/port.sh" 4443) +if [ "$port" != "4443" ]; then + echo ">>> Port 4443 is busy, using $port instead" >&2 +fi + +base="http://localhost:$port" + +# Scope the bind env vars to the relay only. The unified `moq` binary also +# reads MOQ_LISTEN, so exporting it globally would make the `pub` client +# try to start a server (and fail without a TLS cert). +bun run concurrently --kill-others --names rly,bbb,web --prefix-colors auto \ + "MOQ_LISTEN='[::]:$port' MOQ_WEB_HTTP_LISTEN='[::]:$port' just relay" \ + "just wait $base/certificate.sha256 && just pub bbb $base" \ + "just wait $base/certificate.sha256 && just web serve $base" diff --git a/sh/demo/serve-hls.sh b/sh/demo/serve-hls.sh new file mode 100755 index 0000000000..bf5257bc11 --- /dev/null +++ b/sh/demo/serve-hls.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# Encode media/NAME.mp4 into a looping HLS stream under media/NAME/ and serve +# it over HTTP on PORT, for testing HLS import. +# +# Usage: sh/demo/serve-hls.sh NAME PORT, from demo/pub. +set -euo pipefail + +usage="usage: sh/demo/serve-hls.sh NAME PORT" +name=${1:?$usage} +port=${2:?$usage} + +input="media/$name.mp4" +out="media/$name" + +rm -rf "$out" +mkdir -p "$out" + +echo ">>> Starting HLS stream generation..." +echo ">>> Master playlist: http://localhost:$port/master.m3u8" + +ffmpeg -loglevel warning -re -stream_loop -1 -i "$input" \ + -map 0:v:0 -map 0:v:0 -map 0:a:0 \ + -r 25 -preset veryfast -g 50 -keyint_min 50 -sc_threshold 0 \ + -c:v:0 libx264 -profile:v:0 high -level:v:0 4.1 -pix_fmt:v:0 yuv420p -tag:v:0 avc1 -bsf:v:0 dump_extra -b:v:0 4M -vf:0 "scale=1920:-2" \ + -c:v:1 libx264 -profile:v:1 high -level:v:1 4.1 -pix_fmt:v:1 yuv420p -tag:v:1 avc1 -bsf:v:1 dump_extra -b:v:1 300k -vf:1 "scale=256:-2" \ + -c:a aac -b:a 128k \ + -f hls \ + -hls_time 2 -hls_list_size 12 \ + -hls_flags independent_segments+delete_segments \ + -hls_segment_type fmp4 \ + -master_pl_name master.m3u8 \ + -var_stream_map "v:0,agroup:audio v:1,agroup:audio a:0,agroup:audio" \ + -hls_segment_filename "$out/v%v/segment_%09d.m4s" \ + "$out/v%v/stream.m3u8" & +ffmpeg=$! + +trap 'echo "Shutting down..."; exit 0' INT TERM +trap 'kill "$ffmpeg" 2>/dev/null || true' EXIT + +sleep 2 +echo ">>> HTTP server: http://localhost:$port/" +cd "$out" +python3 -m http.server "$port" diff --git a/sh/demo/sync.sh b/sh/demo/sync.sh new file mode 100755 index 0000000000..af5f2fb886 --- /dev/null +++ b/sh/demo/sync.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# Upload every file in DIR to the R2 bucket BUCKET, keyed by its file name. +# +# Usage: sh/demo/sync.sh BUCKET DIR +set -euo pipefail + +usage="usage: sh/demo/sync.sh BUCKET DIR" +bucket=${1:?$usage} +dir=${2:?$usage} + +for file in "$dir"/*; do + [ -e "$file" ] || continue + key=$(basename "$file") + echo "Uploading $key..." + bun wrangler r2 object put "$bucket/$key" --file "$file" --remote +done diff --git a/sh/demo/token.sh b/sh/demo/token.sh new file mode 100755 index 0000000000..e85ebc7bac --- /dev/null +++ b/sh/demo/token.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# Sign the local development tokens (demo-web.jwt, demo-cli.jwt) with root.jwk +# in the current directory, skipping any that already exist. +# +# Usage: sh/demo/token.sh, from demo/relay. +set -euo pipefail +umask 077 + +if [ ! -f demo-web.jwt ]; then + cargo run --quiet --bin moq -- auth sign --key root.jwk \ + --root demo --subscribe '**' --publish 'me/**' \ + >demo-web.jwt +fi + +if [ ! -f demo-cli.jwt ]; then + cargo run --quiet --bin moq -- auth sign --key root.jwk \ + --root demo --publish '**' \ + >demo-cli.jwt +fi diff --git a/sh/demo/wait.sh b/sh/demo/wait.sh new file mode 100755 index 0000000000..e556127c3a --- /dev/null +++ b/sh/demo/wait.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# Wait up to ten minutes for URL to respond, e.g. a relay's certificate +# fingerprint once it is listening. +# +# Usage: sh/demo/wait.sh URL +set -euo pipefail + +url=${1:?usage: sh/demo/wait.sh URL} +for _ in $(seq 1 600); do + curl -sf "$url" >/dev/null 2>&1 && exit 0 + sleep 1 +done +echo "timed out waiting for $url" >&2 +exit 1 From 23882d8b3b4b15ef2d25bdbabd9a2af4e7d24516 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 29 Sep 2026 17:06:23 -0700 Subject: [PATCH 3/3] fix(demo): a signal ends hls.sh instead of resuming its waits Co-Authored-By: Claude Opus 5.5 --- sh/demo/hls.sh | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/sh/demo/hls.sh b/sh/demo/hls.sh index c203866bbd..f2c15aa1f6 100755 --- a/sh/demo/hls.sh +++ b/sh/demo/hls.sh @@ -35,16 +35,15 @@ ffmpeg -hide_banner -loglevel warning -re -stream_loop -1 -i "$input" \ "$out/v%v/stream.m3u8" & ffmpeg=$! -cleaned=false cleanup() { - if [ "$cleaned" = true ]; then return; fi - cleaned=true echo "Shutting down..." kill "$ffmpeg" 2>/dev/null || true sleep 0.5 kill -9 "$ffmpeg" 2>/dev/null || true } -trap cleanup INT TERM EXIT +trap cleanup EXIT +# A signal exits, so the EXIT trap cleans up instead of the script resuming its waits. +trap 'exit 130' INT TERM echo ">>> Waiting for HLS playlist generation..." for _ in {1..30}; do