srs/.openclaw/skills/srs-develop/scripts/proxy-e2e-whip-test.sh
winlin de69339785 Codex: Stabilize proxy E2E stream isolation.
Randomize proxy E2E stream names to avoid stale shared state between runs, and make the WHIP HLS check skip the first possibly incomplete segment before requiring audio/video playback.

Verified with the full srs-develop proxy script set: proxy unit coverage and all proxy E2E scripts passed.

Co-authored-by: chatgpt-codex-connector[bot] <199175422+chatgpt-codex-connector[bot]@users.noreply.github.com>
2026-05-10 20:20:27 -04:00

346 lines
11 KiB
Bash
Executable File

#!/bin/bash
# E2E test for WHIP proxy: starts proxy + SRS origin, publishes a WebRTC
# stream via WHIP through the proxy, then verifies playback through the proxy
# in every form the origin can transmux from WebRTC (rtc_to_rtmp + http_remux
# + hls):
# - RTMP play (via rtc_to_rtmp on origin)
# - HTTP-FLV (HTTP remux of the bridged RTMP)
# - HLS (m3u8 + TS segments)
# - WebRTC WHEP (placeholder only, not actually verified here)
set -e
SCRIPT_DIR="$(cd -P "$(dirname "$0")" && pwd)"
# Navigate: scripts/ -> srs-develop/ -> skills/ -> .openclaw/ -> srs
WORKSPACE="$(cd -P "$SCRIPT_DIR/../../../.." && pwd)"
if [[ ! -f "$WORKSPACE/go.mod" ]]; then
echo "Error: go.mod not found in WORKSPACE: $WORKSPACE" >&2
exit 1
fi
# Proxy ports — same layout as proxy-e2e-srt-test.sh / proxy-e2e-transmux-test.sh.
PROXY_RTMP_PORT=11935
PROXY_HTTP_API_PORT=11985
PROXY_HTTP_SERVER_PORT=18080
PROXY_WEBRTC_PORT=18000
PROXY_SRT_PORT=20080
PROXY_SYSTEM_API_PORT=12025
# Origin ports (from origin1-for-proxy.conf).
ORIGIN_RTMP_PORT=19351
ORIGIN_HTTP_PORT=8081
ORIGIN_API_PORT=19851
ORIGIN_RTC_PORT=8001
ORIGIN_SRT_PORT=10081
SOURCE_FLV="$WORKSPACE/trunk/doc/source.flv"
SRS_BINARY="$WORKSPACE/trunk/objs/srs"
# Randomize the stream name per run so each test starts from a clean origin
# state (HLS segments, RTMP source, proxy stream registry) and never shares
# state with sibling E2E tests that publish to "live/livestream".
STREAM_NAME="whip$(date +%s)"
STREAM_URL="live/$STREAM_NAME"
# WHIP endpoint exposed by the proxy. The proxy parses ?app=&stream= via
# utils.ConvertURLToStreamURL, then forwards the SDP exchange to the backend
# SRS origin. @see internal/proxy/api.go and internal/proxy/rtc.go.
WHIP_PUBLISH_URL="http://localhost:$PROXY_HTTP_API_PORT/rtc/v1/whip/?app=live&stream=$STREAM_NAME"
# Make the SRS origin advertise a host candidate that loops back through the
# proxy. The proxy rewrites only the port in the SDP answer (origin RTC port
# -> proxy WebRTC port), so the candidate IP must already be reachable for
# the publisher; 127.0.0.1 works for all-local E2E.
ORIGIN_CANDIDATE="127.0.0.1"
# PIDs to clean up on exit.
PROXY_PID=""
ORIGIN_PID=""
FFMPEG_PID=""
cleanup() {
echo ""
echo "=== Cleaning up ==="
for pid in $PROXY_PID $ORIGIN_PID $FFMPEG_PID; do
if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
kill "$pid" 2>/dev/null || true
fi
done
sleep 1
for pid in $PROXY_PID $ORIGIN_PID $FFMPEG_PID; do
if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
kill -9 "$pid" 2>/dev/null || true
fi
done
echo "Cleanup done."
}
trap cleanup EXIT
probe_has_audio_video() {
local name="$1"
local url="$2"
echo "Verifying $name playback: $url"
local output
output=$("$FFPROBE_BIN" -v error -show_streams "$url" 2>&1 || true)
if echo "$output" | grep -q "codec_type=video"; then
echo "PASS: $name video stream detected."
else
echo "FAIL: $name no video stream detected." >&2
echo "ffprobe output:" >&2
echo "$output" >&2
exit 1
fi
if echo "$output" | grep -q "codec_type=audio"; then
echo "PASS: $name audio stream detected."
else
echo "FAIL: $name no audio stream detected." >&2
echo "ffprobe output:" >&2
echo "$output" >&2
exit 1
fi
}
wait_for_hls_playlist() {
local url="$1"
local deadline=60
echo "Waiting for HLS playlist to be generated (up to ${deadline}s): $url"
for ((i = 1; i <= deadline; i++)); do
if curl -fsS "$url" 2>/dev/null | grep -q "#EXTM3U"; then
echo "HLS playlist is ready."
return
fi
sleep 1
done
echo "FAIL: HLS playlist was not generated in ${deadline}s." >&2
echo "Last HLS response:" >&2
curl -v "$url" 2>&1 || true
exit 1
}
first_hls_segment() {
local url="$1"
curl -fsS "$url" 2>/dev/null | awk '
/^[[:space:]]*$/ { next }
/^#/ { next }
{ print; exit }
'
}
wait_for_hls_to_skip_first_segment() {
local url="$1"
local deadline=60
local first_segment current_segment output
first_segment="$(first_hls_segment "$url")"
if [[ -z "$first_segment" ]]; then
echo "FAIL: HLS playlist has no media segment: $url" >&2
curl -fsS "$url" 2>&1 || true
exit 1
fi
echo "Waiting for HLS to skip the first possibly incomplete segment (up to ${deadline}s): $first_segment"
for ((i = 1; i <= deadline; i++)); do
current_segment="$(first_hls_segment "$url")"
if [[ -n "$current_segment" && "$current_segment" != "$first_segment" ]]; then
output=$("$FFPROBE_BIN" -v error -show_streams "$url" 2>&1 || true)
if echo "$output" | grep -q "codec_type=video" && echo "$output" | grep -q "codec_type=audio"; then
echo "HLS first segment advanced and audio/video is ready: $current_segment"
return
fi
fi
sleep 1
done
echo "FAIL: HLS did not skip the first segment and expose audio/video in ${deadline}s." >&2
echo "Last HLS response:" >&2
curl -fsS "$url" 2>&1 || true
echo "Last ffprobe output:" >&2
echo "$output" >&2
exit 1
}
echo "=== E2E WHIP Proxy Test ==="
echo "Workspace: $WORKSPACE"
echo "Stream: $STREAM_URL"
echo ""
# --- Pre-checks ---
if [[ ! -f "$SOURCE_FLV" ]]; then
echo "Error: test source not found: $SOURCE_FLV" >&2
exit 1
fi
if ! command -v curl &>/dev/null; then
echo "Error: curl not found in PATH" >&2
exit 1
fi
# WHIP needs an ffmpeg with the `whip` muxer (added in ffmpeg 7.1, requires
# --enable-openssl at build time for DTLS-SRTP). Neither vanilla brew nor the
# homebrew-ffmpeg tap enable it. Resolution order:
# 1. Use ffmpeg/ffprobe from PATH if they include the whip muxer.
# 2. Otherwise, use ~/.local/bin/ffmpeg/ffprobe if previously built there.
# 3. Otherwise, build from source via setup-ffmpeg-with-whip.sh (installs
# into ~/.local/) and use the freshly built binaries.
ffmpeg_has_whip() {
local bin="$1"
[[ -x "$bin" ]] && "$bin" -hide_banner -muxers 2>/dev/null | grep -qw whip
}
resolve_ffmpeg() {
local sys_ffmpeg sys_ffprobe local_ffmpeg local_ffprobe
sys_ffmpeg="$(command -v ffmpeg || true)"
sys_ffprobe="$(command -v ffprobe || true)"
local_ffmpeg="$HOME/.local/bin/ffmpeg"
local_ffprobe="$HOME/.local/bin/ffprobe"
if [[ -n "$sys_ffprobe" ]] && ffmpeg_has_whip "$sys_ffmpeg"; then
FFMPEG_BIN="$sys_ffmpeg"
FFPROBE_BIN="$sys_ffprobe"
return 0
fi
if [[ -x "$local_ffprobe" ]] && ffmpeg_has_whip "$local_ffmpeg"; then
FFMPEG_BIN="$local_ffmpeg"
FFPROBE_BIN="$local_ffprobe"
return 0
fi
return 1
}
if ! resolve_ffmpeg; then
echo "No ffmpeg with WHIP muxer found on PATH or in ~/.local/bin."
echo "Building ffmpeg from source via setup-ffmpeg-with-whip.sh — this can take several minutes."
bash "$SCRIPT_DIR/setup-ffmpeg-with-whip.sh"
FFMPEG_BIN="$HOME/.local/bin/ffmpeg"
FFPROBE_BIN="$HOME/.local/bin/ffprobe"
if ! ffmpeg_has_whip "$FFMPEG_BIN"; then
echo "Error: ffmpeg still lacks WHIP muxer after running setup-ffmpeg-with-whip.sh." >&2
exit 1
fi
if [[ ! -x "$FFPROBE_BIN" ]]; then
echo "Error: ffprobe missing at $FFPROBE_BIN after running setup-ffmpeg-with-whip.sh." >&2
exit 1
fi
fi
echo "ffmpeg : $FFMPEG_BIN"
echo "ffprobe: $FFPROBE_BIN"
# --- Step 0: Clean up stale state ---
rm -f "$WORKSPACE/trunk/objs/origin1.pid"
ALL_PORTS="$PROXY_RTMP_PORT $PROXY_HTTP_API_PORT $PROXY_HTTP_SERVER_PORT $PROXY_WEBRTC_PORT $PROXY_SRT_PORT $PROXY_SYSTEM_API_PORT $ORIGIN_RTMP_PORT $ORIGIN_HTTP_PORT $ORIGIN_API_PORT $ORIGIN_RTC_PORT $ORIGIN_SRT_PORT"
for port in $ALL_PORTS; do
lsof -ti :"$port" 2>/dev/null | xargs kill 2>/dev/null || true
done
sleep 1
# --- Step 1: Build proxy ---
echo "=== Step 1: Building proxy ==="
cd "$WORKSPACE"
make -s 2>&1
echo "Proxy built: $WORKSPACE/bin/srs-proxy"
# --- Step 2: Build SRS origin (if not already built) ---
if [[ ! -f "$SRS_BINARY" ]]; then
echo "=== Step 2: Building SRS origin ==="
cd "$WORKSPACE/trunk"
./configure && make 2>&1 | tail -3
echo "SRS origin built: $SRS_BINARY"
else
echo "=== Step 2: SRS origin already built ==="
fi
# --- Step 3: Start proxy ---
echo "=== Step 3: Starting proxy (HTTP API :$PROXY_HTTP_API_PORT, WebRTC :$PROXY_WEBRTC_PORT) ==="
cd "$WORKSPACE"
env PROXY_RTMP_SERVER=$PROXY_RTMP_PORT \
PROXY_HTTP_API=$PROXY_HTTP_API_PORT \
PROXY_HTTP_SERVER=$PROXY_HTTP_SERVER_PORT \
PROXY_WEBRTC_SERVER=$PROXY_WEBRTC_PORT \
PROXY_SRT_SERVER=$PROXY_SRT_PORT \
PROXY_SYSTEM_API=$PROXY_SYSTEM_API_PORT \
PROXY_LOAD_BALANCER_TYPE=memory \
./bin/srs-proxy >/tmp/srs-proxy-whip-e2e.log 2>&1 &
PROXY_PID=$!
echo "Proxy PID: $PROXY_PID"
sleep 1
if ! kill -0 "$PROXY_PID" 2>/dev/null; then
echo "Error: proxy failed to start. Logs:" >&2
cat /tmp/srs-proxy-whip-e2e.log >&2
exit 1
fi
echo "Proxy started."
# --- Step 4: Start SRS origin (with CANDIDATE=$ORIGIN_CANDIDATE for WebRTC) ---
echo "=== Step 4: Starting SRS origin (CANDIDATE=$ORIGIN_CANDIDATE) ==="
ulimit -n 10000 2>/dev/null || true
cd "$WORKSPACE/trunk"
env CANDIDATE="$ORIGIN_CANDIDATE" \
./objs/srs -c conf/origin1-for-proxy.conf >/tmp/srs-origin-whip-e2e.log 2>&1 &
ORIGIN_PID=$!
echo "SRS origin PID: $ORIGIN_PID"
# Wait for SRS to start and register with proxy (heartbeat interval is 9s).
echo "Waiting for SRS origin to register with proxy (up to 15s)..."
sleep 12
if ! kill -0 "$ORIGIN_PID" 2>/dev/null; then
echo "Error: SRS origin failed to start. Logs:" >&2
cat /tmp/srs-origin-whip-e2e.log >&2
exit 1
fi
echo "SRS origin started and registered."
# --- Step 5: Publish WHIP stream ---
# WebRTC requires H.264 (baseline-friendly) + Opus. source.flv is H.264 High
# profile + AAC, so transcode video to baseline and audio to Opus. Use
# zerolatency/ultrafast so the encoder keeps up with -re.
echo "=== Step 5: Publishing WHIP stream to proxy ==="
echo "Publish URL: $WHIP_PUBLISH_URL"
"$FFMPEG_BIN" -stream_loop -1 -re -i "$SOURCE_FLV" \
-c:v libx264 -profile:v baseline -level 3.1 -pix_fmt yuv420p \
-tune zerolatency -preset ultrafast \
-c:a libopus -ar 48000 -ac 2 \
-f whip "$WHIP_PUBLISH_URL" >/tmp/srs-ffmpeg-whip-e2e.log 2>&1 &
FFMPEG_PID=$!
echo "FFmpeg publisher PID: $FFMPEG_PID"
# Wait for WHIP SDP exchange + DTLS-SRTP handshake + the origin's
# rtc_to_rtmp pipeline to spin up the bridged RTMP stream.
sleep 8
if ! kill -0 "$FFMPEG_PID" 2>/dev/null; then
echo "Error: FFmpeg WHIP publisher failed. Logs:" >&2
cat /tmp/srs-ffmpeg-whip-e2e.log >&2
exit 1
fi
echo "Stream publishing."
# --- Step 6: Verify RTMP playback (rtc_to_rtmp) ---
echo "=== Step 6: Verifying RTMP playback via proxy ==="
probe_has_audio_video "RTMP" "rtmp://localhost:$PROXY_RTMP_PORT/$STREAM_URL"
# --- Step 7: Verify HTTP-FLV playback ---
echo "=== Step 7: Verifying HTTP-FLV playback via proxy ==="
probe_has_audio_video "HTTP-FLV" "http://localhost:$PROXY_HTTP_SERVER_PORT/$STREAM_URL.flv"
# --- Step 8: Verify HLS playback ---
echo "=== Step 8: Verifying HLS playback via proxy ==="
HLS_URL="http://localhost:$PROXY_HTTP_SERVER_PORT/$STREAM_URL.m3u8"
wait_for_hls_playlist "$HLS_URL"
wait_for_hls_to_skip_first_segment "$HLS_URL"
probe_has_audio_video "HLS" "$HLS_URL"
# --- Step 9: WebRTC WHEP playback (placeholder) ---
echo "=== Step 9: WebRTC WHEP playback (placeholder) ==="
echo "SKIP: WebRTC WHEP playback is not verified by this script."
echo " The origin has rtmp_to_rtc enabled, so WHIP->RTMP->RTC should work end-to-end,"
echo " but actual playback verification is intentionally left as a TODO here."
echo ""
echo "=== E2E WHIP Proxy Test PASSED ==="