Skip to content

Commit 133f66a

Browse files
winlinvipclaude
andcommitted
Edge: Fix HTTP-FLV 404 and RTMP late-join missing sequence headers. v7.0.150 (#4678)
Cherry-pick of the v8.0.2 fix to the 7.0 release line. Two edge-cluster regressions surface when validating an RTMP origin/edge setup: - **HTTP-FLV play on edge always 404'd.** `SrsHttpStreamServer::assemble()` registered the dynamic matcher only when the mux cast was `NULL` (inverted guard), so the matcher was never wired up. On edge the FLV mount is created lazily by the dynamic matcher, so every HTTP-FLV client got 404. Invert the guard to register when the mux is valid, mirroring the destructor. (`trunk/src/app/srs_app_http_stream.cpp`) - **RTMP players that join an edge stream after the first player fail to decode.** After v7.0.94 (#4513) stopped creating `SrsOriginHub` on edge, the `hub_active` gate in `SrsLiveSource::consumer_dumps()` always evaluated false on edge. That gate guards the dump of cached `onMetaData` + AVC sequence header + AAC sequence header + GOP cache to a new consumer. Result: the first player attaches before the edge-pull starts and gets headers via the live fan-out, but every subsequent player gets coded payload with no codec config and ffmpeg aborts with `dimensions not set` / `Could not write header`. Fall back to the meta cache state when `hub_` is `NULL`, so the dump path runs once the edge-pull has populated the cache. (`trunk/src/app/srs_app_rtmp_source.cpp`) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 386a376 commit 133f66a

9 files changed

Lines changed: 412 additions & 4 deletions

File tree

internal/version/version.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ func VersionMinor() int {
1515
}
1616

1717
func VersionRevision() int {
18-
return 149
18+
return 150
1919
}
2020

2121
func Version() string {

skills/srs-develop/SKILL.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,10 @@ Only after the user confirms the routing do you proceed to Step 2.
152152
```
153153
bash scripts/proxy-e2e-cluster-test.sh
154154
```
155+
- Proxy + SRS edge + SRS origin three-tier topology (starts proxy + one SRS edge in `mode remote` registered with the proxy + one upstream SRS origin, publishes RTMP via proxy→edge→origin, then plays the same stream with two concurrent RTMP players where the second joins after a delay as a late joiner on the active edge-pull):
156+
```
157+
bash scripts/proxy-e2e-edge-test.sh
158+
```
155159
- Redis multi-proxy routing test (requires local Redis; starts two proxy instances with Redis LB, publishes through one proxy, verifies playback through the other):
156160
```
157161
bash scripts/proxy-e2e-redis-test.sh
Lines changed: 342 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,342 @@
1+
#!/bin/bash
2+
# E2E test for proxy + edge + origin: starts proxy + one SRS edge (registered
3+
# with proxy) + one SRS upstream origin (not registered). Publishes a single
4+
# RTMP stream through proxy -> edge -> origin, then plays the stream twice
5+
# concurrently from the proxy -> edge with a delay so the second player is a
6+
# late joiner on an already-active edge-pull. Both players must succeed.
7+
set -e
8+
9+
SCRIPT_DIR="$(cd -P "$(dirname "$0")" && pwd)"
10+
# Walk up from SCRIPT_DIR looking for go.mod. This avoids brittle "../../../.."
11+
# counting when the skills directory is reached via a symlink (which changes
12+
# the symbolic vs. physical depth).
13+
WORKSPACE="$SCRIPT_DIR"
14+
while [[ "$WORKSPACE" != "/" && ! -f "$WORKSPACE/go.mod" ]]; do
15+
WORKSPACE="$(dirname "$WORKSPACE")"
16+
done
17+
18+
if [[ ! -f "$WORKSPACE/go.mod" ]]; then
19+
echo "Error: go.mod not found walking up from: $SCRIPT_DIR" >&2
20+
exit 1
21+
fi
22+
23+
# Proxy ports — high range, avoids the SRS port range.
24+
PROXY_RTMP_PORT=11935
25+
PROXY_HTTP_API_PORT=11985
26+
PROXY_HTTP_SERVER_PORT=18080
27+
PROXY_WEBRTC_PORT=18000
28+
PROXY_SRT_PORT=20080
29+
PROXY_SYSTEM_API_PORT=12025
30+
31+
# Origin ports (from origin-for-edge.conf) — upstream of the edge, NOT
32+
# registered with the proxy. Distinct from origin1/2/3 to avoid collisions
33+
# when running this test alongside the other proxy E2E tests.
34+
ORIGIN_RTMP_PORT=19360
35+
ORIGIN_API_PORT=19860
36+
37+
# Edge ports (from edge-for-proxy.conf) — what the proxy treats as its backend.
38+
EDGE_RTMP_PORT=19361
39+
EDGE_HTTP_PORT=8091
40+
EDGE_API_PORT=19861
41+
42+
SOURCE_FLV="$WORKSPACE/trunk/doc/source.flv"
43+
SRS_BINARY="$WORKSPACE/trunk/objs/srs"
44+
ORIGIN_CONF="$WORKSPACE/trunk/conf/origin-for-edge.conf"
45+
EDGE_CONF="$WORKSPACE/trunk/conf/edge-for-proxy.conf"
46+
# Randomize per run so each invocation starts from clean state and never
47+
# shares state with sibling E2E tests publishing to live/livestream.
48+
STREAM_NAME="edge$(date +%s)"
49+
STREAM_PATH="live/$STREAM_NAME"
50+
51+
# PIDs to clean up on exit.
52+
PROXY_PID=""
53+
ORIGIN_PID=""
54+
EDGE_PID=""
55+
PUBLISH_PID=""
56+
PLAYER1_PID=""
57+
PLAYER2_PID=""
58+
59+
cleanup() {
60+
echo ""
61+
echo "=== Cleaning up ==="
62+
for pid in $PUBLISH_PID $PLAYER1_PID $PLAYER2_PID $EDGE_PID $ORIGIN_PID $PROXY_PID; do
63+
if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
64+
kill "$pid" 2>/dev/null || true
65+
fi
66+
done
67+
sleep 1
68+
for pid in $PUBLISH_PID $PLAYER1_PID $PLAYER2_PID $EDGE_PID $ORIGIN_PID $PROXY_PID; do
69+
if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
70+
kill -9 "$pid" 2>/dev/null || true
71+
fi
72+
done
73+
echo "Cleanup done."
74+
}
75+
trap cleanup EXIT
76+
77+
wait_for_http() {
78+
local url=$1
79+
local name=$2
80+
local i
81+
82+
for i in $(seq 1 30); do
83+
if curl -fsS --max-time 2 "$url" >/dev/null 2>&1; then
84+
echo "$name is ready."
85+
return 0
86+
fi
87+
sleep 1
88+
done
89+
90+
echo "Error: $name is not ready after 30s: $url" >&2
91+
return 1
92+
}
93+
94+
api_has_stream() {
95+
local api_port=$1
96+
local stream=$2
97+
98+
curl -fsS --max-time 3 "http://127.0.0.1:$api_port/api/v1/streams/" 2>/dev/null | grep -q "$stream"
99+
}
100+
101+
verify_probe_has_av() {
102+
local url=$1
103+
local label=$2
104+
local probe_output
105+
106+
probe_output=$(ffprobe -v error -rw_timeout 5000000 -show_streams "$url" 2>&1 || true)
107+
108+
if ! echo "$probe_output" | grep -q "codec_type=video"; then
109+
echo "FAIL: No video stream detected for $label." >&2
110+
echo "ffprobe output:" >&2
111+
echo "$probe_output" >&2
112+
exit 1
113+
fi
114+
115+
if ! echo "$probe_output" | grep -q "codec_type=audio"; then
116+
echo "FAIL: No audio stream detected for $label." >&2
117+
echo "ffprobe output:" >&2
118+
echo "$probe_output" >&2
119+
exit 1
120+
fi
121+
122+
echo "PASS: Audio/video detected for $label."
123+
}
124+
125+
echo "=== E2E Proxy + Edge + Origin Test ==="
126+
echo "Workspace: $WORKSPACE"
127+
echo "Stream: $STREAM_PATH"
128+
echo ""
129+
130+
# --- Pre-checks ---
131+
if [[ ! -f "$SOURCE_FLV" ]]; then
132+
echo "Error: test source not found: $SOURCE_FLV" >&2
133+
exit 1
134+
fi
135+
if [[ ! -f "$ORIGIN_CONF" ]]; then
136+
echo "Error: origin conf not found: $ORIGIN_CONF" >&2
137+
exit 1
138+
fi
139+
if [[ ! -f "$EDGE_CONF" ]]; then
140+
echo "Error: edge conf not found: $EDGE_CONF" >&2
141+
exit 1
142+
fi
143+
for tool in ffmpeg ffprobe curl; do
144+
if ! command -v "$tool" &>/dev/null; then
145+
echo "Error: $tool not found in PATH" >&2
146+
exit 1
147+
fi
148+
done
149+
150+
# --- Step 0: Clean up stale state ---
151+
rm -f "$WORKSPACE/trunk/objs/origin-for-edge.pid" "$WORKSPACE/trunk/objs/edge-for-proxy.pid"
152+
ALL_PORTS="$PROXY_RTMP_PORT $PROXY_HTTP_API_PORT $PROXY_HTTP_SERVER_PORT $PROXY_WEBRTC_PORT $PROXY_SRT_PORT $PROXY_SYSTEM_API_PORT"
153+
ALL_PORTS="$ALL_PORTS $ORIGIN_RTMP_PORT $ORIGIN_API_PORT $EDGE_RTMP_PORT $EDGE_HTTP_PORT $EDGE_API_PORT"
154+
for port in $ALL_PORTS; do
155+
lsof -ti :"$port" 2>/dev/null | xargs kill 2>/dev/null || true
156+
done
157+
sleep 1
158+
159+
# --- Step 1: Build proxy ---
160+
echo "=== Step 1: Building proxy ==="
161+
cd "$WORKSPACE"
162+
make -s 2>&1
163+
echo "Proxy built: $WORKSPACE/bin/srs-proxy"
164+
165+
# --- Step 2: Build SRS (if not already built) ---
166+
if [[ ! -f "$SRS_BINARY" ]]; then
167+
echo "=== Step 2: Building SRS ==="
168+
cd "$WORKSPACE/trunk"
169+
./configure && make 2>&1 | tail -3
170+
echo "SRS built: $SRS_BINARY"
171+
else
172+
echo "=== Step 2: SRS already built ==="
173+
fi
174+
175+
# --- Step 3: Start proxy ---
176+
echo "=== Step 3: Starting proxy (RTMP :$PROXY_RTMP_PORT, System API :$PROXY_SYSTEM_API_PORT) ==="
177+
cd "$WORKSPACE"
178+
env PROXY_RTMP_SERVER=$PROXY_RTMP_PORT \
179+
PROXY_HTTP_API=$PROXY_HTTP_API_PORT \
180+
PROXY_HTTP_SERVER=$PROXY_HTTP_SERVER_PORT \
181+
PROXY_WEBRTC_SERVER=$PROXY_WEBRTC_PORT \
182+
PROXY_SRT_SERVER=$PROXY_SRT_PORT \
183+
PROXY_SYSTEM_API=$PROXY_SYSTEM_API_PORT \
184+
PROXY_LOAD_BALANCER_TYPE=memory \
185+
./bin/srs-proxy >/tmp/srs-proxy-edge-e2e.log 2>&1 &
186+
PROXY_PID=$!
187+
echo "Proxy PID: $PROXY_PID"
188+
189+
wait_for_http "http://127.0.0.1:$PROXY_SYSTEM_API_PORT/api/v1/versions" "Proxy System API"
190+
191+
if ! kill -0 "$PROXY_PID" 2>/dev/null; then
192+
echo "Error: proxy failed to start. Logs:" >&2
193+
cat /tmp/srs-proxy-edge-e2e.log >&2
194+
exit 1
195+
fi
196+
echo "Proxy started."
197+
198+
# --- Step 4: Start upstream origin (no proxy heartbeat) ---
199+
echo "=== Step 4: Starting upstream SRS origin (RTMP :$ORIGIN_RTMP_PORT) ==="
200+
ulimit -n 10000 2>/dev/null || true
201+
cd "$WORKSPACE/trunk"
202+
./objs/srs -c conf/origin-for-edge.conf >/tmp/srs-origin-edge-e2e.log 2>&1 &
203+
ORIGIN_PID=$!
204+
echo "Origin PID: $ORIGIN_PID"
205+
206+
wait_for_http "http://127.0.0.1:$ORIGIN_API_PORT/api/v1/versions" "Origin HTTP API"
207+
208+
if ! kill -0 "$ORIGIN_PID" 2>/dev/null; then
209+
echo "Error: origin failed to start. Logs:" >&2
210+
cat /tmp/srs-origin-edge-e2e.log >&2
211+
exit 1
212+
fi
213+
echo "Origin started."
214+
215+
# --- Step 5: Start edge (mode remote, registered with proxy) ---
216+
echo "=== Step 5: Starting SRS edge (RTMP :$EDGE_RTMP_PORT, upstream :$ORIGIN_RTMP_PORT) ==="
217+
./objs/srs -c conf/edge-for-proxy.conf >/tmp/srs-edge-e2e.log 2>&1 &
218+
EDGE_PID=$!
219+
echo "Edge PID: $EDGE_PID"
220+
221+
wait_for_http "http://127.0.0.1:$EDGE_API_PORT/api/v1/versions" "Edge HTTP API"
222+
223+
# Wait for the edge to register with the proxy (heartbeat interval is 9s).
224+
echo "Waiting for edge to register with proxy (up to 20s)..."
225+
for i in $(seq 1 20); do
226+
if grep -q "Register SRS media server" /tmp/srs-proxy-edge-e2e.log 2>/dev/null; then
227+
echo "Edge registered with proxy."
228+
break
229+
fi
230+
sleep 1
231+
done
232+
233+
if ! grep -q "Register SRS media server" /tmp/srs-proxy-edge-e2e.log 2>/dev/null; then
234+
echo "Error: edge did not register with proxy after 20s. Proxy logs:" >&2
235+
cat /tmp/srs-proxy-edge-e2e.log >&2
236+
exit 1
237+
fi
238+
239+
if ! kill -0 "$EDGE_PID" 2>/dev/null; then
240+
echo "Error: edge failed to start. Logs:" >&2
241+
cat /tmp/srs-edge-e2e.log >&2
242+
exit 1
243+
fi
244+
echo "Edge started and registered."
245+
246+
# --- Step 6: Publish RTMP stream to proxy ---
247+
# Path: ffmpeg -> proxy (:$PROXY_RTMP_PORT) -> edge (:$EDGE_RTMP_PORT, mode remote forwards
248+
# publish) -> origin (:$ORIGIN_RTMP_PORT). Verify the publish reached the
249+
# upstream origin via the origin's HTTP API.
250+
echo "=== Step 6: Publishing $STREAM_PATH through proxy -> edge -> origin ==="
251+
ffmpeg -stream_loop -1 -re -i "$SOURCE_FLV" -c copy -f flv \
252+
"rtmp://localhost:$PROXY_RTMP_PORT/$STREAM_PATH" >/tmp/srs-ffmpeg-edge-e2e.log 2>&1 &
253+
PUBLISH_PID=$!
254+
echo "Publisher PID: $PUBLISH_PID"
255+
256+
echo "Waiting for stream to reach origin (up to 15s)..."
257+
reached_origin=0
258+
for i in $(seq 1 15); do
259+
if api_has_stream "$ORIGIN_API_PORT" "$STREAM_NAME"; then
260+
reached_origin=1
261+
echo "Stream visible on origin after ${i}s."
262+
break
263+
fi
264+
sleep 1
265+
done
266+
267+
if [[ $reached_origin -ne 1 ]]; then
268+
echo "FAIL: stream did not reach upstream origin via edge." >&2
269+
echo "Publisher logs:" >&2
270+
cat /tmp/srs-ffmpeg-edge-e2e.log >&2
271+
echo "Edge logs:" >&2
272+
cat /tmp/srs-edge-e2e.log >&2
273+
exit 1
274+
fi
275+
276+
if ! kill -0 "$PUBLISH_PID" 2>/dev/null; then
277+
echo "Error: publisher exited unexpectedly. Logs:" >&2
278+
cat /tmp/srs-ffmpeg-edge-e2e.log >&2
279+
exit 1
280+
fi
281+
echo "PASS: publish path proxy -> edge -> origin works."
282+
283+
# --- Step 7: Two concurrent players on the same stream ---
284+
# Player 1 attaches first and triggers the edge-pull from origin. Player 2
285+
# joins a few seconds later as a late joiner on the already-active edge-pull.
286+
# Both must succeed — this is the proxy-side analogue of the C++ edge late-
287+
# join fix.
288+
echo "=== Step 7: Two concurrent RTMP players via proxy ==="
289+
PLAY_DURATION=8
290+
PLAYER_URL="rtmp://localhost:$PROXY_RTMP_PORT/$STREAM_PATH"
291+
292+
echo "Starting player 1 (immediate)..."
293+
ffmpeg -rw_timeout 5000000 -i "$PLAYER_URL" -t $PLAY_DURATION -c copy -f flv -y /dev/null \
294+
>/tmp/srs-player1-edge-e2e.log 2>&1 &
295+
PLAYER1_PID=$!
296+
297+
sleep 3
298+
299+
echo "Starting player 2 (late joiner, +3s)..."
300+
ffmpeg -rw_timeout 5000000 -i "$PLAYER_URL" -t $PLAY_DURATION -c copy -f flv -y /dev/null \
301+
>/tmp/srs-player2-edge-e2e.log 2>&1 &
302+
PLAYER2_PID=$!
303+
304+
# Wait for both players to finish.
305+
player1_rc=0
306+
player2_rc=0
307+
wait "$PLAYER1_PID" || player1_rc=$?
308+
wait "$PLAYER2_PID" || player2_rc=$?
309+
# Clear PIDs so cleanup() doesn't try to re-kill exited processes.
310+
PLAYER1_PID=""
311+
PLAYER2_PID=""
312+
313+
check_player() {
314+
local label=$1
315+
local rc=$2
316+
local log=$3
317+
318+
if [[ $rc -ne 0 ]]; then
319+
echo "FAIL: $label exited with code $rc. Logs:" >&2
320+
cat "$log" >&2
321+
exit 1
322+
fi
323+
# Decoded-frames check — ffmpeg's progress lines contain `frame=` once it has
324+
# successfully started decoding. Catches "dimensions not set"-style failures
325+
# where ffmpeg returns 0 but never produced output.
326+
if ! grep -qE 'frame= *[1-9]' "$log"; then
327+
echo "FAIL: $label produced no frames. Logs:" >&2
328+
cat "$log" >&2
329+
exit 1
330+
fi
331+
echo "PASS: $label played successfully."
332+
}
333+
334+
check_player "player 1" "$player1_rc" /tmp/srs-player1-edge-e2e.log
335+
check_player "player 2 (late joiner)" "$player2_rc" /tmp/srs-player2-edge-e2e.log
336+
337+
# --- Step 8: Final probe via proxy confirms A/V is still queryable ---
338+
echo "=== Step 8: Final ffprobe verification via proxy ==="
339+
verify_probe_has_av "rtmp://localhost:$PROXY_RTMP_PORT/$STREAM_PATH" "proxy $STREAM_PATH"
340+
341+
echo ""
342+
echo "=== E2E Proxy + Edge + Origin Test PASSED ==="

0 commit comments

Comments
 (0)