- Refactor the Go proxy for dependency injection: every proxy server, the bootstrap, the signal handler, the load balancers, and AMF0 now accept functional-option seams (factories/closures) so tests can inject fakes without binding real sockets, talking to real Redis, or racing on package globals. - Drop the package-global `lb.SrsLoadBalancer`. The bootstrap creates the LB locally and threads it through every proxy server constructor. Two old global indirections in `internal/signal` and `internal/rtmp/amf0` are likewise replaced by per-instance fields. - Rename `internal/server` → `internal/proxy` and rename the `lb` public surface for clarity: `SRSLoadBalancer` is split into `OriginService` / `HLSService` / `RTCService` and recomposed as `OriginLoadBalancer`; `SRSServer` → `OriginServer`; all proxy server types gain a `Proxy` qualifier (e.g. `RTMPServer` → `RTMPProxyServer`). - Extract the Redis client behind a new `internal/redisclient` package with a minimal `RedisClient` interface and a counterfeiter fake. - Add counterfeiter fakes (`proxyfakes`, `lbfakes`, `redisclientfakes`) and ~7.5k lines of unit tests covering bootstrap, memory + Redis LBs, all five proxy servers, the signal handler, and AMF0. - Add two new E2E flows — `proxy-e2e-srt-test.sh` (SRT publish through proxy, verify SRT/RTMP/HTTP-FLV/HLS playback) and `proxy-e2e-whip-test.sh` (WHIP publish, verify RTMP/HTTP-FLV/HLS via origin `rtc_to_rtmp`) — plus `setup-ffmpeg-with-whip.sh`, a macOS builder for an ffmpeg with openssl-DTLS WHIP and SRT support that the two scripts auto-invoke when needed. - Workspace reorg: move `memory/` and `skills/` to the repo root so all agent tools (Claude / Codex / Kiro / OpenClaw) share one source of truth via symlinks. Sync `docs/proxy/proxy-load-balancer.md` and `memory/srs-codebase-map.md` with the new names. No protocol, log, HTTP API, or wire-format changes. Refactor only — all externally observable proxy behavior is unchanged. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: chatgpt-codex-connector[bot] <199175422+chatgpt-codex-connector[bot]@users.noreply.github.com>
351 lines
12 KiB
Bash
Executable File
351 lines
12 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)"
|
|
# Walk up from SCRIPT_DIR looking for go.mod. This avoids brittle "../../../.."
|
|
# counting when the skills directory is reached via a symlink (which changes
|
|
# the symbolic vs. physical depth).
|
|
WORKSPACE="$SCRIPT_DIR"
|
|
while [[ "$WORKSPACE" != "/" && ! -f "$WORKSPACE/go.mod" ]]; do
|
|
WORKSPACE="$(dirname "$WORKSPACE")"
|
|
done
|
|
|
|
if [[ ! -f "$WORKSPACE/go.mod" ]]; then
|
|
echo "Error: go.mod not found walking up from: $SCRIPT_DIR" >&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 ==="
|