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 3cf8cf0a80..0d62a6a3b0 100644 --- a/quest/m1/tooling/README.md +++ b/quest/m1/tooling/README.md @@ -23,5 +23,4 @@ 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 - [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..f2c15aa1f6 --- /dev/null +++ b/sh/demo/hls.sh @@ -0,0 +1,69 @@ +#!/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=$! + +cleanup() { + echo "Shutting down..." + kill "$ffmpeg" 2>/dev/null || true + sleep 0.5 + kill -9 "$ffmpeg" 2>/dev/null || true +} +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 + 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