mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
test(sandbox): add ffmpeg + Mux live/DVR test-stream scripts and orchestrator
apps/sandbox/scripts/live/ — spin up an ephemeral Mux live source for exercising the live path in any HLS-capable sandbox page: - create-stream.sh: create a low-latency Mux live stream (creds via --token-id/--token-secret flags or MUX_TOKEN_ID/MUX_TOKEN_SECRET env; --quiet for scripting) - broadcast.sh: push an ffmpeg testsrc + tone feed to the stream - dvr-url.sh: resolve active_asset_id to the DVR/EVENT playback URL - live-test.sh: orchestrate create -> broadcast -> wait -> open the page, parameterized by --page (renderer-agnostic), --flavor sliding|dvr|both, --port, --params, --latency; deletes the stream on exit unless --keep - README.md: quick-start, manual steps, and cross-renderer reuse notes Credentials are read only from flags/env; none are committed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
78f33556dc
commit
da33b10952
@@ -0,0 +1,99 @@
|
||||
# Live / DVR test streams (ffmpeg + Mux Video)
|
||||
|
||||
Spin up an ephemeral live HLS source to exercise the live path in the sandbox.
|
||||
Uses `ffmpeg`'s built-in test-pattern generator pushed to a Mux live stream.
|
||||
|
||||
The stream is **renderer-agnostic** — it's a normal Mux HLS source, playable by
|
||||
any HLS-capable sandbox page (the SPF engine, an hls.js-backed player, or native
|
||||
HLS). Reuse comes from pointing `--page` at a different template, not from
|
||||
per-renderer scripts.
|
||||
|
||||
One broadcast yields **two** sources to test:
|
||||
|
||||
- **Sliding-window live** — the live stream's own playback id (small window that
|
||||
rolls off the back).
|
||||
- **DVR / EVENT** — the recording asset's playback id (`#EXT-X-PLAYLIST-TYPE:EVENT`,
|
||||
grows from the start, seekable all the way back), reached via the stream's
|
||||
`active_asset_id` while it's broadcasting.
|
||||
|
||||
Both are CMAF/fMP4, which is what the SPF MSE pipeline appends (it does not
|
||||
transmux MPEG-TS, so generic TS live test streams won't play there).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- `ffmpeg`, `curl`, `jq` on `PATH`.
|
||||
- A Mux access token (Mux dashboard → Settings → Access Tokens). Provide it via
|
||||
env vars (**preferred** — flag values show up in the process list) or flags:
|
||||
|
||||
```sh
|
||||
export MUX_TOKEN_ID=...
|
||||
export MUX_TOKEN_SECRET=... # never commit these
|
||||
```
|
||||
|
||||
- The sandbox dev server running: `pnpm dev` (from `apps/sandbox`).
|
||||
|
||||
## Quick start — `live-test.sh` (orchestrator)
|
||||
|
||||
Creates a stream, broadcasts, waits for it to go live, and opens the page.
|
||||
Ctrl-C stops the broadcast and deletes the stream (use `--keep` to retain it).
|
||||
|
||||
```sh
|
||||
cd apps/sandbox/scripts/live
|
||||
|
||||
# SPF segment-loading harness, sliding-window live (defaults):
|
||||
./live-test.sh
|
||||
|
||||
# DVR / EVENT source on the SPF harness:
|
||||
./live-test.sh --flavor dvr
|
||||
|
||||
# Open both flavors at once:
|
||||
./live-test.sh --flavor both
|
||||
```
|
||||
|
||||
Reuse across renderers — just change `--page` (and `--params` if the page
|
||||
doesn't need SPF's `preload=auto` load quirk):
|
||||
|
||||
```sh
|
||||
./live-test.sh --page live-hls-engine # bare SPF engine harness
|
||||
./live-test.sh --page html-hls-video # hls.js-backed player component
|
||||
./live-test.sh --page html-native-hls-video # native HLS (Safari)
|
||||
```
|
||||
|
||||
Options: `--page`, `--flavor sliding|dvr|both`, `--port`, `--params`,
|
||||
`--latency low|reduced|standard`, `--no-open`, `--keep`, `--token-id`,
|
||||
`--token-secret`. See `./live-test.sh --help`.
|
||||
|
||||
## Manual steps (the individual scripts)
|
||||
|
||||
```sh
|
||||
# 1. Create a low-latency live stream. Note the Stream ID, Stream key, playback URL.
|
||||
./create-stream.sh # or: --token-id ID --token-secret SECRET --latency low
|
||||
|
||||
# 2. Broadcast a test feed (color bars + 440 Hz tone). Leave running; Ctrl-C to stop.
|
||||
./broadcast.sh <STREAM_KEY>
|
||||
|
||||
# 3. Play it in the sandbox (default dev port 5173):
|
||||
open "http://localhost:5173/spf-segment-loading/?src=https://stream.mux.com/<PLAYBACK_ID>.m3u8&muted=true&autoplay=true&preload=auto"
|
||||
|
||||
# 4. DVR / EVENT — resolve the recording asset's playback URL (stream must be
|
||||
# broadcasting), then open it the same way:
|
||||
./dvr-url.sh <STREAM_ID>
|
||||
```
|
||||
|
||||
The `spf-segment-loading` page's Live / DVR panel classifies the source
|
||||
(`sliding live` vs `DVR (… seekable)`) and the seek controls drive
|
||||
seek-to-live / DVR back-seek.
|
||||
|
||||
## Notes
|
||||
|
||||
- Live streams are **ephemeral**. `live-test.sh` deletes its stream on exit
|
||||
(unless `--keep`); the manual scripts leave it — clean up from the Mux
|
||||
dashboard so they don't accumulate.
|
||||
- `preload=auto` is in the default params because the SPF page needs it to
|
||||
load+play a live source (autoplay alone isn't sufficient). Player-component
|
||||
pages manage their own loading; override `--params` for them as needed.
|
||||
- `--latency low` gives LL-HLS (≈2 s segments); `reduced` / `standard` test
|
||||
other latencies.
|
||||
- For an **on-demand** CMAF baseline (no live machinery), the sandbox default
|
||||
`https://stream.mux.com/JX01bG8eB4uaoV3OpDuK602rBfvdSgrMObjwuUOBn4JrQ.m3u8`
|
||||
is a public VOD asset.
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
# Push an ffmpeg-generated test feed (color bars + 440 Hz tone) to a Mux live
|
||||
# stream. Run in its own terminal; Ctrl-C to stop. Mux turns this into both a
|
||||
# sliding-window live playlist and (because the stream records) a growing
|
||||
# DVR / EVENT playlist.
|
||||
#
|
||||
# Usage: ./broadcast.sh <STREAM_KEY>
|
||||
# STREAM_KEY is printed by create-stream.sh.
|
||||
set -euo pipefail
|
||||
|
||||
key="${1:-}"
|
||||
if [ -z "$key" ]; then
|
||||
echo "Usage: $0 <STREAM_KEY> (the stream key printed by create-stream.sh)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# -re paces input at real time; zerolatency + short GOP keep segments small for
|
||||
# low-latency live. CMAF/fMP4 output is what the SPF MSE pipeline appends.
|
||||
exec ffmpeg -re \
|
||||
-f lavfi -i "testsrc=size=1280x720:rate=30" \
|
||||
-f lavfi -i "sine=frequency=440:sample_rate=48000" \
|
||||
-c:v libx264 -preset veryfast -tune zerolatency -g 60 -keyint_min 60 \
|
||||
-b:v 2500k -pix_fmt yuv420p \
|
||||
-c:a aac -b:a 128k -ar 48000 \
|
||||
-f flv "rtmps://global-live.mux.com:443/app/$key"
|
||||
Executable
+72
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env bash
|
||||
# Create a low-latency Mux live stream for testing live / DVR playback in any
|
||||
# HLS-capable sandbox page. Prints the stream id, stream key, RTMPS ingest URL,
|
||||
# and the live playback URL.
|
||||
#
|
||||
# Requires: curl, jq, and Mux API credentials. Credentials resolve from (in
|
||||
# precedence order) the --token-id / --token-secret flags, else the
|
||||
# MUX_TOKEN_ID / MUX_TOKEN_SECRET environment variables. Get them from the Mux
|
||||
# dashboard → Settings → Access Tokens; never commit them. (Prefer the env vars:
|
||||
# flag values are visible in the process list.)
|
||||
#
|
||||
# Usage: ./create-stream.sh [--token-id ID] [--token-secret SECRET]
|
||||
# [--latency low|reduced|standard] [--quiet]
|
||||
# --quiet print only "<id> <key> <playback-id>" on one line (for scripting).
|
||||
set -euo pipefail
|
||||
|
||||
token_id="${MUX_TOKEN_ID:-}"
|
||||
token_secret="${MUX_TOKEN_SECRET:-}"
|
||||
latency="low"
|
||||
quiet=0
|
||||
|
||||
usage() { sed -n '2,16p' "$0" | sed 's/^# \{0,1\}//'; }
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--token-id) token_id="$2"; shift 2 ;;
|
||||
--token-secret) token_secret="$2"; shift 2 ;;
|
||||
--latency) latency="$2"; shift 2 ;;
|
||||
--quiet) quiet=1; shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "unknown argument: $1" >&2; usage >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[ -n "$token_id" ] || { echo "missing Mux token id (--token-id or MUX_TOKEN_ID)" >&2; exit 1; }
|
||||
[ -n "$token_secret" ] || { echo "missing Mux token secret (--token-secret or MUX_TOKEN_SECRET)" >&2; exit 1; }
|
||||
|
||||
# latency_mode "low" → LL-HLS (PART-INF, ~2s target duration). new_asset_settings
|
||||
# makes the stream record, so its recording asset can serve the DVR / EVENT
|
||||
# manifest (see dvr-url.sh).
|
||||
resp=$(curl -sS -u "$token_id:$token_secret" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"latency_mode\":\"$latency\",\"playback_policy\":[\"public\"],\"new_asset_settings\":{\"playback_policy\":[\"public\"]}}" \
|
||||
https://api.mux.com/video/v1/live-streams)
|
||||
|
||||
id=$(echo "$resp" | jq -r '.data.id // empty')
|
||||
key=$(echo "$resp" | jq -r '.data.stream_key // empty')
|
||||
pb=$(echo "$resp" | jq -r '.data.playback_ids[0].id // empty')
|
||||
|
||||
if [ -z "$id" ]; then
|
||||
echo "Mux API error:" >&2
|
||||
echo "$resp" | jq . >&2 2>/dev/null || echo "$resp" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$quiet" -eq 1 ]; then
|
||||
echo "$id $key $pb"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
Live stream created.
|
||||
|
||||
Stream ID: $id
|
||||
Stream key: $key
|
||||
RTMPS ingest: rtmps://global-live.mux.com:443/app/$key
|
||||
Live playback: https://stream.mux.com/$pb.m3u8 (sliding-window live)
|
||||
|
||||
Next:
|
||||
./broadcast.sh $key # push a test feed in a separate terminal
|
||||
./dvr-url.sh $id # once broadcasting, get the DVR / EVENT playback URL
|
||||
EOF
|
||||
Executable
+46
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env bash
|
||||
# Resolve the DVR / EVENT playback URL for a live stream that is currently
|
||||
# broadcasting. Mux records by default; while the stream is active its recording
|
||||
# asset (active_asset_id) serves a growing #EXT-X-PLAYLIST-TYPE:EVENT manifest —
|
||||
# the DVR source, seekable back to the start — which is distinct from the live
|
||||
# stream's own sliding-window playback id (from create-stream.sh).
|
||||
#
|
||||
# Usage: ./dvr-url.sh [--quiet] <STREAM_ID>
|
||||
# --quiet print only the playback .m3u8 URL (for scripting).
|
||||
# Requires curl, jq, and MUX_TOKEN_ID / MUX_TOKEN_SECRET in the environment.
|
||||
# Exits non-zero (no asset yet) until the stream has started broadcasting.
|
||||
set -euo pipefail
|
||||
|
||||
: "${MUX_TOKEN_ID:?set MUX_TOKEN_ID}"
|
||||
: "${MUX_TOKEN_SECRET:?set MUX_TOKEN_SECRET}"
|
||||
|
||||
quiet=0
|
||||
id=""
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--quiet) quiet=1; shift ;;
|
||||
-h|--help) sed -n '2,12p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
||||
*) id="$1"; shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[ -n "$id" ] || { echo "Usage: $0 [--quiet] <STREAM_ID>" >&2; exit 1; }
|
||||
|
||||
asset=$(curl -sS -u "$MUX_TOKEN_ID:$MUX_TOKEN_SECRET" \
|
||||
"https://api.mux.com/video/v1/live-streams/$id" | jq -r '.data.active_asset_id // empty')
|
||||
|
||||
if [ -z "$asset" ]; then
|
||||
echo "No active_asset_id yet — is the stream broadcasting? Start ./broadcast.sh first." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
pb=$(curl -sS -u "$MUX_TOKEN_ID:$MUX_TOKEN_SECRET" \
|
||||
"https://api.mux.com/video/v1/assets/$asset" | jq -r '.data.playback_ids[0].id // empty')
|
||||
|
||||
[ -n "$pb" ] || { echo "recording asset $asset has no public playback id" >&2; exit 1; }
|
||||
|
||||
if [ "$quiet" -eq 1 ]; then
|
||||
echo "https://stream.mux.com/$pb.m3u8"
|
||||
else
|
||||
echo "DVR / EVENT playback: https://stream.mux.com/$pb.m3u8 (growing window, back-seek to start)"
|
||||
fi
|
||||
Executable
+120
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env bash
|
||||
# Orchestrate a live / DVR sandbox test end to end: create a Mux live stream,
|
||||
# start an ffmpeg test broadcast, wait for the playlist to go live, and open the
|
||||
# chosen sandbox page pointed at it. On exit (Ctrl-C) the broadcast stops and the
|
||||
# stream is deleted (unless --keep).
|
||||
#
|
||||
# The stream itself is renderer-agnostic — any HLS-capable sandbox page works.
|
||||
# Point --page at the SPF harness or an hls.js / native player page, e.g.:
|
||||
# spf-segment-loading, live-hls-engine, html-hls-video, html-native-hls-video
|
||||
#
|
||||
# Usage: ./live-test.sh [options]
|
||||
# --page <name> sandbox template dir (default: spf-segment-loading)
|
||||
# --flavor <f> sliding | dvr | both (default: sliding)
|
||||
# --port <n> dev server port (default: 5173)
|
||||
# --params <qs> query string appended to the page URL
|
||||
# (default: muted=true&autoplay=true&preload=auto)
|
||||
# --latency <m> low | reduced | standard (default: low)
|
||||
# --no-open print the URL(s) instead of opening a browser
|
||||
# --keep do not delete the Mux stream on exit
|
||||
# --token-id ID / --token-secret SECRET Mux creds (default: env vars)
|
||||
#
|
||||
# Requires: curl, jq, ffmpeg, and Mux credentials (flags or MUX_TOKEN_ID /
|
||||
# MUX_TOKEN_SECRET). Start the sandbox dev server (`pnpm dev`) first.
|
||||
set -euo pipefail
|
||||
here="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
page="spf-segment-loading"
|
||||
flavor="sliding"
|
||||
port="5173"
|
||||
params="muted=true&autoplay=true&preload=auto"
|
||||
latency="low"
|
||||
do_open=1
|
||||
keep=0
|
||||
token_id="${MUX_TOKEN_ID:-}"
|
||||
token_secret="${MUX_TOKEN_SECRET:-}"
|
||||
|
||||
usage() { sed -n '2,28p' "$0" | sed 's/^# \{0,1\}//'; }
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--page) page="$2"; shift 2 ;;
|
||||
--flavor) flavor="$2"; shift 2 ;;
|
||||
--port) port="$2"; shift 2 ;;
|
||||
--params) params="$2"; shift 2 ;;
|
||||
--latency) latency="$2"; shift 2 ;;
|
||||
--no-open) do_open=0; shift ;;
|
||||
--keep) keep=1; shift ;;
|
||||
--token-id) token_id="$2"; shift 2 ;;
|
||||
--token-secret) token_secret="$2"; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "unknown argument: $1" >&2; usage >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
case "$flavor" in sliding|dvr|both) ;; *) echo "--flavor must be sliding|dvr|both" >&2; exit 1 ;; esac
|
||||
[ -n "$token_id" ] && [ -n "$token_secret" ] || { echo "missing Mux creds (--token-id/--token-secret or MUX_TOKEN_ID/MUX_TOKEN_SECRET)" >&2; exit 1; }
|
||||
# Export so the child scripts (create-stream, dvr-url) inherit the resolved creds.
|
||||
export MUX_TOKEN_ID="$token_id" MUX_TOKEN_SECRET="$token_secret"
|
||||
|
||||
open_url() {
|
||||
if [ "$do_open" -eq 0 ]; then echo " open: $1"; return; fi
|
||||
if command -v open >/dev/null 2>&1; then open "$1"
|
||||
elif command -v xdg-open >/dev/null 2>&1; then xdg-open "$1"
|
||||
else echo " open manually: $1"; fi
|
||||
}
|
||||
|
||||
wait_for_m3u8() {
|
||||
for _ in $(seq 1 30); do
|
||||
curl -sf -o /dev/null --max-time 5 "$1" && return 0
|
||||
sleep 1
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
stream_id=""; bpid=""
|
||||
cleanup() {
|
||||
trap - INT TERM EXIT
|
||||
[ -n "$bpid" ] && kill "$bpid" 2>/dev/null || true
|
||||
if [ "$keep" -eq 0 ] && [ -n "$stream_id" ]; then
|
||||
curl -sS -u "$MUX_TOKEN_ID:$MUX_TOKEN_SECRET" -X DELETE \
|
||||
"https://api.mux.com/video/v1/live-streams/$stream_id" >/dev/null 2>&1 || true
|
||||
echo "deleted live stream $stream_id"
|
||||
elif [ -n "$stream_id" ]; then
|
||||
echo "kept live stream $stream_id (delete it from the Mux dashboard when done)"
|
||||
fi
|
||||
}
|
||||
trap cleanup INT TERM EXIT
|
||||
|
||||
read -r stream_id stream_key live_pb < <("$here/create-stream.sh" --quiet --latency "$latency") || true
|
||||
[ -n "$stream_id" ] || { echo "failed to create stream (check Mux credentials)" >&2; exit 1; }
|
||||
echo "created live stream $stream_id"
|
||||
|
||||
log="${TMPDIR:-/tmp}/live-test-ffmpeg-$stream_id.log"
|
||||
"$here/broadcast.sh" "$stream_key" >"$log" 2>&1 &
|
||||
bpid=$!
|
||||
echo "broadcasting (ffmpeg pid $bpid; log: $log)"
|
||||
|
||||
base="http://localhost:$port"
|
||||
curl -sf -o /dev/null --max-time 2 "$base/$page/" \
|
||||
|| echo "warning: dev server not responding at $base/$page/ — run 'pnpm dev' in apps/sandbox"
|
||||
|
||||
if [ "$flavor" = sliding ] || [ "$flavor" = both ]; then
|
||||
m="https://stream.mux.com/$live_pb.m3u8"
|
||||
echo "waiting for sliding-window live to go live…"
|
||||
wait_for_m3u8 "$m" && open_url "$base/$page/?src=$m&$params" || echo "sliding live did not come up in time"
|
||||
fi
|
||||
|
||||
if [ "$flavor" = dvr ] || [ "$flavor" = both ]; then
|
||||
echo "waiting for the DVR / EVENT recording asset…"
|
||||
dvr=""
|
||||
for _ in $(seq 1 40); do
|
||||
dvr="$("$here/dvr-url.sh" --quiet "$stream_id" 2>/dev/null)" && [ -n "$dvr" ] && break
|
||||
dvr=""; sleep 2
|
||||
done
|
||||
if [ -n "$dvr" ] && wait_for_m3u8 "$dvr"; then open_url "$base/$page/?src=$dvr&$params"
|
||||
else echo "DVR / EVENT source did not come up in time"; fi
|
||||
fi
|
||||
|
||||
echo "broadcasting — Ctrl-C to stop and clean up."
|
||||
wait "$bpid"
|
||||
Reference in New Issue
Block a user