diff --git a/internal/version/version.go b/internal/version/version.go index 0fe33ae5c7..bf69fc8bdc 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -15,7 +15,7 @@ func VersionMinor() int { } func VersionRevision() int { - return 41 + return 44 } func Version() string { diff --git a/skills/srs-develop/references/integration-tests.md b/skills/srs-develop/references/integration-tests.md index b81615bf80..0e6ce5c09c 100644 --- a/skills/srs-develop/references/integration-tests.md +++ b/skills/srs-develop/references/integration-tests.md @@ -24,15 +24,15 @@ Run focused and component-native tests first, then run every command below seque ```bash bash skills/srs-develop/scripts/proxy-e2e-redis-test.sh ``` -6. RTMP publish with RTMP, HTTP-FLV, and HLS playback verification; WHEP remains a placeholder: +6. RTMP publish with RTMP, HTTP-FLV, HLS, and WHEP playback verification: ```bash bash skills/srs-develop/scripts/proxy-e2e-transmux-test.sh ``` -7. SRT publish with SRT, RTMP, HTTP-FLV, and HLS playback verification; WHEP remains a placeholder: +7. SRT publish with SRT, RTMP, HTTP-FLV, HLS, and WHEP playback verification: ```bash bash skills/srs-develop/scripts/proxy-e2e-srt-test.sh ``` -8. WHIP publish with RTMP, HTTP-FLV, and HLS playback verification; WHEP remains a placeholder: +8. WHIP publish with RTMP, HTTP-FLV, HLS, and WHEP playback verification: ```bash bash skills/srs-develop/scripts/proxy-e2e-whip-test.sh ``` @@ -41,7 +41,7 @@ Run focused and component-native tests first, then run every command below seque bash skills/srs-develop/scripts/proxy-e2e-bearer-auth-test.sh ``` -The SRT test requires an FFmpeg build with libsrt. The WHIP test requires the `whip` muxer and OpenSSL. Both scripts automatically run `skills/srs-develop/scripts/setup-ffmpeg-with-whip.sh` on macOS when no suitable FFmpeg is available. If an environmental dependency is unavailable, run the script, preserve its exact result, and report the blocked coverage instead of claiming full verification. +The SRT test requires an FFmpeg build with libsrt. The WHIP test requires the `whip` muxer and OpenSSL. Both scripts automatically run `skills/srs-develop/scripts/setup-ffmpeg-with-whip.sh` on macOS when no suitable FFmpeg is available. If an environmental dependency is unavailable, run the script, preserve its exact result, and report the blocked coverage instead of claiming full verification. The transmux, SRT, and WHIP tests play WHEP with `tools/pion-whep`, because FFmpeg has no WHEP demuxer; they build it with `go` when the binary is missing or stale. Run feature-specific bundled tests in addition to this matrix when the routed workflow or the touched area requires them: diff --git a/skills/srs-develop/scripts/proxy-e2e-srt-test.sh b/skills/srs-develop/scripts/proxy-e2e-srt-test.sh index 0e78743cc7..f0be1baffe 100755 --- a/skills/srs-develop/scripts/proxy-e2e-srt-test.sh +++ b/skills/srs-develop/scripts/proxy-e2e-srt-test.sh @@ -6,7 +6,7 @@ # - RTMP play (via srt_to_rtmp on origin) # - HTTP-FLV (HTTP remux of the bridged RTMP) # - HLS (m3u8 + TS segments) -# - WebRTC WHEP (placeholder only, not actually verified here) +# - WebRTC WHEP (via rtmp_to_rtc on origin, played with tools/pion-whep) set -e SCRIPT_DIR="$(cd -P "$(dirname "$0")" && pwd)" @@ -100,6 +100,50 @@ probe_has_audio_video() { fi } +# Play over WHEP with tools/pion-whep, because FFmpeg has no WHEP demuxer, and +# require both video and audio RTP packets. The tool is rebuilt when its binary +# is missing or older than its sources. +verify_whep_playback() { + local url="$1" + local log="$2" + local tool_dir="$WORKSPACE/tools/pion-whep" + local tool_bin="$tool_dir/objs/pion-whep" + local summary video audio + + if [[ ! -x "$tool_bin" ]] || [[ -n "$(find "$tool_dir" -maxdepth 1 \( -name '*.go' -o -name 'go.mod' -o -name 'go.sum' \) -newer "$tool_bin")" ]]; then + echo "Building pion-whep: $tool_bin" + (cd "$tool_dir" && mkdir -p objs && go build -o objs/pion-whep .) + fi + + echo "Verifying WHEP playback: $url" + if ! "$tool_bin" -hide_banner -f whep -i "$url" -t 5 -f null - >"$log" 2>&1; then + echo "FAIL: WHEP playback failed. pion-whep log:" >&2 + cat "$log" >&2 + exit 1 + fi + + summary="$(grep -a "^Received video=" "$log" | tail -1)" + video="$(echo "$summary" | sed -n 's/^Received video=\([0-9]*\),.*/\1/p')" + audio="$(echo "$summary" | sed -n 's/^Received video=[0-9]*, audio=\([0-9]*\) .*/\1/p')" + echo "pion-whep: $summary" + + if [[ -n "$video" && "$video" -gt 0 ]]; then + echo "PASS: WHEP video packets received." + else + echo "FAIL: WHEP no video packets received." >&2 + cat "$log" >&2 + exit 1 + fi + + if [[ -n "$audio" && "$audio" -gt 0 ]]; then + echo "PASS: WHEP audio packets received." + else + echo "FAIL: WHEP no audio packets received." >&2 + cat "$log" >&2 + exit 1 + fi +} + wait_for_hls_playlist() { local url="$1" local deadline=45 @@ -133,6 +177,10 @@ if ! command -v curl &>/dev/null; then echo "Error: curl not found in PATH" >&2 exit 1 fi +if ! command -v go &>/dev/null; then + echo "Error: go not found in PATH" >&2 + exit 1 +fi # SRT URLs need libsrt compiled into ffmpeg/ffprobe. The default Homebrew # ffmpeg formula does NOT include libsrt. Resolution order: @@ -233,7 +281,10 @@ echo "Proxy started." echo "=== Step 4: Starting SRS origin ===" ulimit -n 10000 2>/dev/null || true cd "$WORKSPACE/trunk" -./objs/srs -c conf/origin1-for-proxy.conf >/tmp/srs-origin-srt-e2e.log 2>&1 & +# The proxy rewrites only the port of the WebRTC candidate, so the origin must +# advertise an IP the WHEP player can reach. +env CANDIDATE="127.0.0.1" \ + ./objs/srs -c conf/origin1-for-proxy.conf >/tmp/srs-origin-srt-e2e.log 2>&1 & ORIGIN_PID=$! echo "SRS origin PID: $ORIGIN_PID" @@ -285,11 +336,10 @@ HLS_URL="http://localhost:$PROXY_HTTP_SERVER_PORT/$STREAM_URL.m3u8" wait_for_hls_playlist "$HLS_URL" probe_has_audio_video "HLS" "$HLS_URL" -# --- Step 10: WebRTC WHEP playback (placeholder) --- -echo "=== Step 10: WebRTC WHEP playback (placeholder) ===" -echo "SKIP: WebRTC WHEP playback is not verified by this script." -echo " The origin has rtmp_to_rtc enabled, so SRT->RTMP->RTC should work end-to-end," -echo " but actual playback verification is intentionally left as a TODO here." +# --- Step 10: Verify WebRTC WHEP playback (srt_to_rtmp + rtmp_to_rtc) --- +echo "=== Step 10: Verifying WebRTC WHEP playback via proxy ===" +verify_whep_playback "http://localhost:$PROXY_HTTP_API_PORT/rtc/v1/whep/?app=live&stream=${STREAM_URL#live/}" \ + /tmp/srs-pion-whep-srt-e2e.log echo "" echo "=== E2E SRT Proxy Test PASSED ===" diff --git a/skills/srs-develop/scripts/proxy-e2e-transmux-test.sh b/skills/srs-develop/scripts/proxy-e2e-transmux-test.sh index 61ba84a7cc..fa01b8d678 100755 --- a/skills/srs-develop/scripts/proxy-e2e-transmux-test.sh +++ b/skills/srs-develop/scripts/proxy-e2e-transmux-test.sh @@ -1,8 +1,8 @@ #!/bin/bash # E2E test for RTMP-to-multiple-protocol transmuxing through the proxy: # starts one proxy with memory load balancer + one SRS origin, publishes one -# RTMP stream, then verifies RTMP, HTTP-FLV, and HLS playback through the -# proxy. WebRTC WHEP verification is intentionally a placeholder (not run). +# RTMP stream, then verifies RTMP, HTTP-FLV, HLS, and WebRTC WHEP playback +# through the proxy. WHEP is played with tools/pion-whep. set -e SCRIPT_DIR="$(cd -P "$(dirname "$0")" && pwd)" @@ -92,6 +92,50 @@ probe_has_audio_video() { fi } +# Play over WHEP with tools/pion-whep, because FFmpeg has no WHEP demuxer, and +# require both video and audio RTP packets. The tool is rebuilt when its binary +# is missing or older than its sources. +verify_whep_playback() { + local url="$1" + local log="$2" + local tool_dir="$WORKSPACE/tools/pion-whep" + local tool_bin="$tool_dir/objs/pion-whep" + local summary video audio + + if [[ ! -x "$tool_bin" ]] || [[ -n "$(find "$tool_dir" -maxdepth 1 \( -name '*.go' -o -name 'go.mod' -o -name 'go.sum' \) -newer "$tool_bin")" ]]; then + echo "Building pion-whep: $tool_bin" + (cd "$tool_dir" && mkdir -p objs && go build -o objs/pion-whep .) + fi + + echo "Verifying WHEP playback: $url" + if ! "$tool_bin" -hide_banner -f whep -i "$url" -t 5 -f null - >"$log" 2>&1; then + echo "FAIL: WHEP playback failed. pion-whep log:" >&2 + cat "$log" >&2 + exit 1 + fi + + summary="$(grep -a "^Received video=" "$log" | tail -1)" + video="$(echo "$summary" | sed -n 's/^Received video=\([0-9]*\),.*/\1/p')" + audio="$(echo "$summary" | sed -n 's/^Received video=[0-9]*, audio=\([0-9]*\) .*/\1/p')" + echo "pion-whep: $summary" + + if [[ -n "$video" && "$video" -gt 0 ]]; then + echo "PASS: WHEP video packets received." + else + echo "FAIL: WHEP no video packets received." >&2 + cat "$log" >&2 + exit 1 + fi + + if [[ -n "$audio" && "$audio" -gt 0 ]]; then + echo "PASS: WHEP audio packets received." + else + echo "FAIL: WHEP no audio packets received." >&2 + cat "$log" >&2 + exit 1 + fi +} + wait_for_hls_playlist() { local url="$1" local deadline=45 @@ -133,6 +177,10 @@ if ! command -v curl &>/dev/null; then echo "Error: curl not found in PATH" >&2 exit 1 fi +if ! command -v go &>/dev/null; then + echo "Error: go not found in PATH" >&2 + exit 1 +fi # --- Step 0: Clean up stale state --- rm -f "$WORKSPACE/trunk/objs/origin1.pid" @@ -184,7 +232,10 @@ echo "Proxy started." echo "=== Step 4: Starting SRS origin ===" ulimit -n 10000 2>/dev/null || true cd "$WORKSPACE/trunk" -./objs/srs -c conf/origin1-for-proxy.conf >/tmp/srs-origin-transmux-e2e.log 2>&1 & +# The proxy rewrites only the port of the WebRTC candidate, so the origin must +# advertise an IP the WHEP player can reach. +env CANDIDATE="127.0.0.1" \ + ./objs/srs -c conf/origin1-for-proxy.conf >/tmp/srs-origin-transmux-e2e.log 2>&1 & ORIGIN_PID=$! echo "SRS origin PID: $ORIGIN_PID" @@ -230,11 +281,10 @@ HLS_URL="http://localhost:$PROXY_HTTP_SERVER_PORT/$STREAM_URL.m3u8" wait_for_hls_playlist "$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 RTMP->RTC should work end-to-end," -echo " but actual playback verification is intentionally left as a TODO here." +# --- Step 9: Verify WebRTC WHEP playback (rtmp_to_rtc) --- +echo "=== Step 9: Verifying WebRTC WHEP playback via proxy ===" +verify_whep_playback "http://localhost:$PROXY_HTTP_API_PORT/rtc/v1/whep/?app=live&stream=${STREAM_URL#live/}" \ + /tmp/srs-pion-whep-transmux-e2e.log echo "" echo "NOTE: RTSP is not tested here because the Go proxy currently has no RTSP listener." diff --git a/skills/srs-develop/scripts/proxy-e2e-whip-test.sh b/skills/srs-develop/scripts/proxy-e2e-whip-test.sh index 05a50ec855..307481e6b5 100755 --- a/skills/srs-develop/scripts/proxy-e2e-whip-test.sh +++ b/skills/srs-develop/scripts/proxy-e2e-whip-test.sh @@ -6,7 +6,7 @@ # - 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) +# - WebRTC WHEP (RTC passthrough, played with tools/pion-whep) set -e SCRIPT_DIR="$(cd -P "$(dirname "$0")" && pwd)" @@ -107,6 +107,50 @@ probe_has_audio_video() { fi } +# Play over WHEP with tools/pion-whep, because FFmpeg has no WHEP demuxer, and +# require both video and audio RTP packets. The tool is rebuilt when its binary +# is missing or older than its sources. +verify_whep_playback() { + local url="$1" + local log="$2" + local tool_dir="$WORKSPACE/tools/pion-whep" + local tool_bin="$tool_dir/objs/pion-whep" + local summary video audio + + if [[ ! -x "$tool_bin" ]] || [[ -n "$(find "$tool_dir" -maxdepth 1 \( -name '*.go' -o -name 'go.mod' -o -name 'go.sum' \) -newer "$tool_bin")" ]]; then + echo "Building pion-whep: $tool_bin" + (cd "$tool_dir" && mkdir -p objs && go build -o objs/pion-whep .) + fi + + echo "Verifying WHEP playback: $url" + if ! "$tool_bin" -hide_banner -f whep -i "$url" -t 5 -f null - >"$log" 2>&1; then + echo "FAIL: WHEP playback failed. pion-whep log:" >&2 + cat "$log" >&2 + exit 1 + fi + + summary="$(grep -a "^Received video=" "$log" | tail -1)" + video="$(echo "$summary" | sed -n 's/^Received video=\([0-9]*\),.*/\1/p')" + audio="$(echo "$summary" | sed -n 's/^Received video=[0-9]*, audio=\([0-9]*\) .*/\1/p')" + echo "pion-whep: $summary" + + if [[ -n "$video" && "$video" -gt 0 ]]; then + echo "PASS: WHEP video packets received." + else + echo "FAIL: WHEP no video packets received." >&2 + cat "$log" >&2 + exit 1 + fi + + if [[ -n "$audio" && "$audio" -gt 0 ]]; then + echo "PASS: WHEP audio packets received." + else + echo "FAIL: WHEP no audio packets received." >&2 + cat "$log" >&2 + exit 1 + fi +} + wait_for_hls_playlist() { local url="$1" local deadline=60 @@ -183,6 +227,10 @@ if ! command -v curl &>/dev/null; then echo "Error: curl not found in PATH" >&2 exit 1 fi +if ! command -v go &>/dev/null; then + echo "Error: go 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 @@ -340,11 +388,10 @@ 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." +# --- Step 9: Verify WebRTC WHEP playback (RTC passthrough) --- +echo "=== Step 9: Verifying WebRTC WHEP playback via proxy ===" +verify_whep_playback "http://localhost:$PROXY_HTTP_API_PORT/rtc/v1/whep/?app=live&stream=$STREAM_NAME" \ + /tmp/srs-pion-whep-whip-e2e.log echo "" echo "=== E2E WHIP Proxy Test PASSED ===" diff --git a/trunk/configure b/trunk/configure index 087d0ef3ec..ce2b44739e 100755 --- a/trunk/configure +++ b/trunk/configure @@ -388,7 +388,8 @@ if [[ $SRS_UTEST == YES ]]; then MODULE_FILES+=("srs_utest_ai01" "srs_utest_ai02" "srs_utest_ai03" "srs_utest_ai04" "srs_utest_ai05" "srs_utest_ai06" "srs_utest_ai07" "srs_utest_ai08" "srs_utest_ai09" "srs_utest_ai10" "srs_utest_ai11" "srs_utest_ai12" "srs_utest_ai13" "srs_utest_ai14" "srs_utest_ai15" "srs_utest_ai16" "srs_utest_ai17" - "srs_utest_ai18" "srs_utest_ai19" "srs_utest_ai20" "srs_utest_ai24" "srs_utest_ai25" "srs_utest_ai26" "srs_utest_ai27") + "srs_utest_ai18" "srs_utest_ai19" "srs_utest_ai20" "srs_utest_ai24" "srs_utest_ai25" "srs_utest_ai26" "srs_utest_ai27" + "srs_utest_ai28" "srs_utest_ai29" "srs_utest_ai30" "srs_utest_ai31" "srs_utest_ai32") if [[ $SRS_GB28181 == YES ]]; then MODULE_FILES+=("srs_utest_manual_gb28181" "srs_utest_ai23") fi diff --git a/trunk/doc/CHANGELOG.md b/trunk/doc/CHANGELOG.md index 4363a9d1e3..d4ec39ef52 100644 --- a/trunk/doc/CHANGELOG.md +++ b/trunk/doc/CHANGELOG.md @@ -7,6 +7,9 @@ The changelog for SRS. ## SRS 8.0 Changelog +* v8.0, 2026-09-22, Timer: Notify every subscriber when one unsubscribes during the round. v8.0.44 +* v8.0, 2026-09-22, Log: Clear the log file descriptor when the logger reopens it. v8.0.43 +* v8.0, 2026-09-22, HTTP: Clamp the VOD MP4 range end to the last byte of the file. v8.0.42 * v8.0, 2026-09-21, Merge [#4746](https://github.com/ossrs/srs/pull/4746): WebRTC: Support RFC 4588 RTX retransmission with SSRC multiplexing, preferred by nack_prefer_rtx. v8.0.41 (#4746) * v8.0, 2026-09-21, RTC: Reset the play track cache when the publisher republishes with new SSRCs. v8.0.40 * v8.0, 2026-09-21, RTC: Resolve the publish and play tracks once per SSRC through the fast cache. v8.0.39 diff --git a/trunk/src/app/srs_app_config.hpp b/trunk/src/app/srs_app_config.hpp index 572ab443fc..732c97ddde 100644 --- a/trunk/src/app/srs_app_config.hpp +++ b/trunk/src/app/srs_app_config.hpp @@ -352,6 +352,9 @@ class ISrsAppConfig : public ISrsConfig virtual std::string get_https_stream_ssl_cert() = 0; virtual std::string get_http_stream_dir() = 0; virtual bool get_http_stream_crossdomain() = 0; + virtual bool get_vhost_http_enabled(std::string vhost) = 0; + virtual std::string get_vhost_http_mount(std::string vhost) = 0; + virtual std::string get_vhost_http_dir(std::string vhost) = 0; public: // WebRTC config @@ -377,8 +380,21 @@ class ISrsAppConfig : public ISrsConfig public: // SRT config virtual std::vector get_srt_listens() = 0; + virtual int64_t get_srto_maxbw() = 0; + virtual int get_srto_mss() = 0; + virtual bool get_srto_tsbpdmode() = 0; + virtual int get_srto_latency() = 0; + virtual int get_srto_recv_latency() = 0; + virtual int get_srto_peer_latency() = 0; + virtual bool get_srto_tlpktdrop() = 0; + virtual srs_utime_t get_srto_conntimeout() = 0; // Get the srt SRTO_PEERIDLETIMEO, peer idle timeout, default is 10000ms. virtual srs_utime_t get_srto_peeridletimeout() = 0; + virtual int get_srto_sendbuf() = 0; + virtual int get_srto_recvbuf() = 0; + virtual int get_srto_payloadsize() = 0; + virtual std::string get_srto_passphrase() = 0; + virtual int get_srto_pbkeylen() = 0; public: // Stream caster config @@ -640,6 +656,13 @@ class ISrsAppConfig : public ISrsConfig virtual std::string get_engine_output(SrsConfDirective *conf) = 0; virtual bool get_security_enabled(std::string vhost) = 0; virtual SrsConfDirective *get_security_rules(std::string vhost) = 0; + // Whether write log to file, otherwise to console. + virtual bool get_log_tank_file() = 0; + // The file to write log to, empty if not configured. + virtual std::string get_log_file() = 0; + virtual std::string get_log_level() = 0; + virtual std::string get_log_level_v2() = 0; + virtual bool get_utc_time() = 0; }; // The config service provider. diff --git a/trunk/src/app/srs_app_factory.cpp b/trunk/src/app/srs_app_factory.cpp index da41a504ed..252b9493e2 100644 --- a/trunk/src/app/srs_app_factory.cpp +++ b/trunk/src/app/srs_app_factory.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -92,7 +93,9 @@ ISrsOriginHub *SrsAppFactory::create_origin_hub() ISrsHourGlass *SrsAppFactory::create_hourglass(const std::string &name, ISrsHourGlassHandler *handler, srs_utime_t interval) { - return new SrsHourGlass(name, handler, interval); + SrsHourGlass *timer = new SrsHourGlass(name, handler, interval); + timer->assemble(); + return timer; } ISrsBasicRtmpClient *SrsAppFactory::create_rtmp_client(std::string url, srs_utime_t cto, srs_utime_t sto) @@ -184,6 +187,11 @@ ISrsIpListener *SrsAppFactory::create_tcp_listener(ISrsTcpHandler *handler) return new SrsTcpListener(handler); } +ISrsSrtListener *SrsAppFactory::create_srt_listener(ISrsSrtHandler *handler, std::string ip, int port) +{ + return new SrsSrtListener(handler, ip, port); +} + ISrsRtcConnection *SrsAppFactory::create_rtc_connection(ISrsExecRtcAsyncTask *exec, const SrsContextId &cid) { SrsRtcConnection *session = new SrsRtcConnection(exec, cid); diff --git a/trunk/src/app/srs_app_factory.hpp b/trunk/src/app/srs_app_factory.hpp index 8fd41ed7f3..947b939b12 100644 --- a/trunk/src/app/srs_app_factory.hpp +++ b/trunk/src/app/srs_app_factory.hpp @@ -55,6 +55,8 @@ class SrsRtcFrameBuilder; class ISrsFrameTarget; class ISrsRtcFrameBuilderAudioPacketCache; class ISrsAudioTranscoder; +class ISrsSrtListener; +class ISrsSrtHandler; // The factory to create app objects. class ISrsAppFactory : public ISrsKernelFactory @@ -92,6 +94,7 @@ class ISrsAppFactory : public ISrsKernelFactory virtual ISrsFragmentedMp4 *create_fragmented_mp4() = 0; virtual SrsHlsM4sSegment *create_hls_m4s_segment(ISrsFileWriter *fw) = 0; virtual ISrsIpListener *create_tcp_listener(ISrsTcpHandler *handler) = 0; + virtual ISrsSrtListener *create_srt_listener(ISrsSrtHandler *handler, std::string ip, int port) = 0; virtual ISrsRtcConnection *create_rtc_connection(ISrsExecRtcAsyncTask *exec, const SrsContextId &cid) = 0; virtual ISrsFFMPEG *create_ffmpeg(std::string ffmpeg_bin) = 0; virtual ISrsIngesterFFMPEG *create_ingester_ffmpeg() = 0; @@ -146,6 +149,7 @@ class SrsAppFactory : public ISrsAppFactory virtual ISrsFragmentedMp4 *create_fragmented_mp4(); virtual SrsHlsM4sSegment *create_hls_m4s_segment(ISrsFileWriter *fw); virtual ISrsIpListener *create_tcp_listener(ISrsTcpHandler *handler); + virtual ISrsSrtListener *create_srt_listener(ISrsSrtHandler *handler, std::string ip, int port); virtual ISrsRtcConnection *create_rtc_connection(ISrsExecRtcAsyncTask *exec, const SrsContextId &cid); virtual ISrsFFMPEG *create_ffmpeg(std::string ffmpeg_bin); virtual ISrsIngesterFFMPEG *create_ingester_ffmpeg(); diff --git a/trunk/src/app/srs_app_gb28181.cpp b/trunk/src/app/srs_app_gb28181.cpp index 51be1af693..b92dca6440 100644 --- a/trunk/src/app/srs_app_gb28181.cpp +++ b/trunk/src/app/srs_app_gb28181.cpp @@ -76,6 +76,7 @@ SrsGbSession::SrsGbSession() : media_(new SrsGbMediaTcpConn()) reinviting_starttime_ = 0; ppp_ = new SrsAlonePithyPrint(); + ppp_->assemble(); startime_ = srs_time_now_realtime(); connecting_starttime_ = startime_; media_connect_timeout_ = 0; diff --git a/trunk/src/app/srs_app_http_conn.cpp b/trunk/src/app/srs_app_http_conn.cpp index eb6b5f273c..f500e15099 100644 --- a/trunk/src/app/srs_app_http_conn.cpp +++ b/trunk/src/app/srs_app_http_conn.cpp @@ -566,11 +566,14 @@ ISrsHttpServer::~ISrsHttpServer() SrsHttpServer::SrsHttpServer() { http_stream_ = new SrsHttpStreamServer(); - http_stream_->assemble(); - http_static_ = new SrsHttpStaticServer(); } +void SrsHttpServer::assemble() +{ + http_stream_->assemble(); +} + SrsHttpServer::~SrsHttpServer() { srs_freep(http_stream_); @@ -597,7 +600,6 @@ srs_error_t SrsHttpServer::initialize() return err; } -// LCOV_EXCL_START srs_error_t SrsHttpServer::handle(std::string pattern, ISrsHttpHandler *handler) { return http_static_->mux()->handle(pattern, handler); @@ -631,7 +633,6 @@ srs_error_t SrsHttpServer::serve_http(ISrsHttpResponseWriter *w, ISrsHttpMessage // Use http static as default server. return http_static_->mux()->serve_http(w, r); } -// LCOV_EXCL_STOP srs_error_t SrsHttpServer::http_mount(ISrsRequest *r) { diff --git a/trunk/src/app/srs_app_http_conn.hpp b/trunk/src/app/srs_app_http_conn.hpp index d696b16145..8420d91c72 100644 --- a/trunk/src/app/srs_app_http_conn.hpp +++ b/trunk/src/app/srs_app_http_conn.hpp @@ -276,6 +276,7 @@ class SrsHttpServer : public ISrsHttpServer virtual ~SrsHttpServer(); public: + void assemble(); // Construct object, to avoid call function in constructor. virtual srs_error_t initialize(); // Interface ISrsCommonHttpHandler public: diff --git a/trunk/src/app/srs_app_http_static.cpp b/trunk/src/app/srs_app_http_static.cpp index c3ae3b175d..f88264b82a 100644 --- a/trunk/src/app/srs_app_http_static.cpp +++ b/trunk/src/app/srs_app_http_static.cpp @@ -134,6 +134,16 @@ srs_error_t SrsHlsStream::serve_m3u8_ctx(ISrsHttpResponseWriter *w, ISrsHttpMess } else { // Create a m3u8 in memory, contains the session id(ctx). err = serve_new_session(w, r, req, ctx); + + // Never keep the ctx of a refused viewer alive, because the client may choose the ctx, and a + // retry with it would be served as an existing session, which skips the security check and + // the on_play hook. The viewer was added to statistic before the checks and has no session + // to expire, so remove it now. + if (err != srs_success) { + stat_->on_disconnect(ctx, err); + srs_http_stream_serve_error(w, err); + return err; + } } // Always make the ctx alive now. @@ -411,6 +421,7 @@ void SrsHlsStream::http_hooks_on_stop(ISrsRequest *req) return; } +// LCOV_EXCL_STOP srs_error_t SrsHlsStream::on_timer(srs_utime_t interval) { @@ -421,16 +432,15 @@ srs_error_t SrsHlsStream::on_timer(srs_utime_t interval) string ctx = it->first; SrsHlsVirtualConn *info = it->second; - srs_utime_t hls_window = _srs_config->get_hls_window(info->req_->vhost_); + srs_utime_t hls_window = config_->get_hls_window(info->req_->vhost_); if (info->request_time_ + (2 * hls_window) < srs_time_now_cached()) { SrsContextRestore(_srs_context->get_id()); _srs_context->set_id(SrsContextId().set_value(ctx)); http_hooks_on_stop(info->req_); - SrsStatistic *stat = _srs_stat; // TODO: FIXME: Should finger out the err. - stat->on_disconnect(ctx, srs_success); + stat_->on_disconnect(ctx, srs_success); srs_freep(info); map_ctx_info_.erase(it++); @@ -441,7 +451,6 @@ srs_error_t SrsHlsStream::on_timer(srs_utime_t interval) return err; } -// LCOV_EXCL_STOP bool SrsHlsStream::is_interrupt(std::string id) { @@ -454,6 +463,7 @@ bool SrsHlsStream::is_interrupt(std::string id) SrsVodStream::SrsVodStream(string root_dir) : SrsHttpFileServer(root_dir) { + config_ = _srs_config; } void SrsVodStream::assemble() @@ -463,6 +473,7 @@ void SrsVodStream::assemble() SrsVodStream::~SrsVodStream() { + config_ = NULL; } srs_error_t SrsVodStream::serve_flv_stream(ISrsHttpResponseWriter *w, ISrsHttpMessage *r, string fullpath, int64_t offset) @@ -562,7 +573,13 @@ srs_error_t SrsVodStream::serve_mp4_stream(ISrsHttpResponseWriter *w, ISrsHttpMe end = fs->filesize() - 1; } - if (end > fs->filesize() || start > end || end < 0) { + // The end is the last byte position and it is inclusive, so the last byte a client may ask for is filesize-1. + // Clamp an end that reaches or passes the end of the file, as the clients expect the bytes that do exist. + if (end >= fs->filesize()) { + end = fs->filesize() - 1; + } + + if (start > end || end < 0) { return srs_error_new(ERROR_HTTP_REMUX_OFFSET_OVERFLOW, "http mp4 streaming %s overflow. size=%" PRId64 ", offset=%d", fullpath.c_str(), fs->filesize(), start); } @@ -602,7 +619,7 @@ srs_error_t SrsVodStream::serve_m3u8_ctx(ISrsHttpResponseWriter *w, ISrsHttpMess SrsUniquePtr req(hr->to_request(hr->host())->as_http()); // discovery vhost, resolve the vhost from config - SrsConfDirective *parsed_vhost = _srs_config->get_vhost(req->vhost_); + SrsConfDirective *parsed_vhost = config_->get_vhost(req->vhost_); if (parsed_vhost) { req->vhost_ = parsed_vhost->arg0(); } @@ -657,11 +674,15 @@ ISrsHttpStaticServer::~ISrsHttpStaticServer() SrsHttpStaticServer::SrsHttpStaticServer() { mux_ = new SrsHttpServeMux(); + + config_ = _srs_config; } SrsHttpStaticServer::~SrsHttpStaticServer() { srs_freep(mux_); + + config_ = NULL; } // LCOV_EXCL_START @@ -669,6 +690,7 @@ srs_error_t SrsHttpStaticServer::serve_http(ISrsHttpResponseWriter *w, ISrsHttpM { return mux_->serve_http(w, r); } +// LCOV_EXCL_STOP srs_error_t SrsHttpStaticServer::initialize() { @@ -677,7 +699,7 @@ srs_error_t SrsHttpStaticServer::initialize() bool default_root_exists = false; // http static file and flv vod stream mount for each vhost. - SrsConfDirective *root = _srs_config->get_root(); + SrsConfDirective *root = config_->get_root(); for (int i = 0; i < (int)root->directives_.size(); i++) { SrsConfDirective *conf = root->at(i); @@ -693,14 +715,14 @@ srs_error_t SrsHttpStaticServer::initialize() if (pmount == "/") { default_root_exists = true; - std::string dir = _srs_config->get_vhost_http_dir(vhost); + std::string dir = config_->get_vhost_http_dir(vhost); srs_warn("http: root mount to %s", dir.c_str()); } } if (!default_root_exists) { // add root - std::string dir = _srs_config->get_http_stream_dir(); + std::string dir = config_->get_http_stream_dir(); SrsVodStream *stream = new SrsVodStream(dir); stream->assemble(); if ((err = mux_->handle("/", stream)) != srs_success) { @@ -711,30 +733,28 @@ srs_error_t SrsHttpStaticServer::initialize() return err; } -// LCOV_EXCL_STOP ISrsHttpServeMux *SrsHttpStaticServer::mux() { return mux_; } -// LCOV_EXCL_START srs_error_t SrsHttpStaticServer::mount_vhost(string vhost, string &pmount) { srs_error_t err = srs_success; // when vhost disabled, ignore. - if (!_srs_config->get_vhost_enabled(vhost)) { + if (!config_->get_vhost_enabled(vhost)) { return err; } // when vhost http_static disabled, ignore. - if (!_srs_config->get_vhost_http_enabled(vhost)) { + if (!config_->get_vhost_http_enabled(vhost)) { return err; } - std::string mount = _srs_config->get_vhost_http_mount(vhost); - std::string dir = _srs_config->get_vhost_http_dir(vhost); + std::string mount = config_->get_vhost_http_mount(vhost); + std::string dir = config_->get_vhost_http_dir(vhost); // replace the vhost variable mount = srs_strings_replace(mount, "[vhost]", vhost); @@ -760,4 +780,3 @@ srs_error_t SrsHttpStaticServer::mount_vhost(string vhost, string &pmount) return err; } -// LCOV_EXCL_STOP diff --git a/trunk/src/app/srs_app_http_static.hpp b/trunk/src/app/srs_app_http_static.hpp index 30123104aa..b69b4f742e 100644 --- a/trunk/src/app/srs_app_http_static.hpp +++ b/trunk/src/app/srs_app_http_static.hpp @@ -90,6 +90,10 @@ class SrsVodStream : public SrsHttpFileServer SRS_DECLARE_PRIVATE: // clang-format on SrsHlsStream hls_; +// clang-format off +SRS_DECLARE_PRIVATE: // clang-format on + ISrsAppConfig *config_; + public: SrsVodStream(std::string root_dir); virtual ~SrsVodStream(); @@ -132,6 +136,10 @@ class SrsHttpStaticServer : public ISrsHttpStaticServer SRS_DECLARE_PRIVATE: // clang-format on ISrsHttpServeMux *mux_; +// clang-format off +SRS_DECLARE_PRIVATE: // clang-format on + ISrsAppConfig *config_; + public: SrsHttpStaticServer(); virtual ~SrsHttpStaticServer(); diff --git a/trunk/src/app/srs_app_latest_version.hpp b/trunk/src/app/srs_app_latest_version.hpp index 22b8db06c2..99a356cd30 100644 --- a/trunk/src/app/srs_app_latest_version.hpp +++ b/trunk/src/app/srs_app_latest_version.hpp @@ -23,6 +23,8 @@ class ISrsAppFactory; // Build features string for version query extern void srs_build_features(std::stringstream &ss); +// Deprecated: the latest version query is not used anymore, so do not add tests or +// testability refactors for this class. class SrsLatestVersion : public ISrsCoroutineHandler { // clang-format off diff --git a/trunk/src/app/srs_app_log.cpp b/trunk/src/app/srs_app_log.cpp index 22cecc133b..7a10352089 100644 --- a/trunk/src/app/srs_app_log.cpp +++ b/trunk/src/app/srs_app_log.cpp @@ -27,6 +27,49 @@ // reserved for the end of log data, it must be strlen(LOG_TAIL) #define LOG_TAIL_SIZE 1 +ISrsLogWriter::ISrsLogWriter() +{ +} + +ISrsLogWriter::~ISrsLogWriter() +{ +} + +SrsLogWriter::SrsLogWriter() +{ +} + +SrsLogWriter::~SrsLogWriter() +{ +} + +int SrsLogWriter::open_file(const std::string &path) +{ + return ::open(path.c_str(), + O_RDWR | O_CREAT | O_APPEND, + S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH); +} + +void SrsLogWriter::close_file(int fd) +{ + ::close(fd); +} + +void SrsLogWriter::write_file(int fd, const char *str_log, int size) +{ + ::write(fd, str_log, size); +} + +void SrsLogWriter::write_console(const char *color, const char *str_log, int size) +{ + if (!color || !*color) { + printf("%.*s", size, str_log); + } else { + printf("%s%.*s\033[0m", color, size, str_log); + } + fflush(stdout); +} + SrsFileLog::SrsFileLog() { level_ = SrsLogLevelTrace; @@ -35,43 +78,60 @@ SrsFileLog::SrsFileLog() fd_ = -1; log_to_file_tank_ = false; utc_ = false; + + // The config global does not exist yet when the logger is created, so it is captured by initialize(). + config_ = NULL; + + writer_ = new SrsLogWriter(); } SrsFileLog::~SrsFileLog() { srs_freepa(log_data_); - if (fd_ > 0) { - ::close(fd_); - fd_ = -1; + if (writer_ && fd_ >= 0) { + writer_->close_file(fd_); } + fd_ = -1; - if (_srs_config) { - _srs_config->unsubscribe(this); + if (config_) { + config_->unsubscribe(this); } + + config_ = NULL; + srs_freep(writer_); } // LCOV_EXCL_START srs_error_t SrsFileLog::initialize() { - if (_srs_config) { - _srs_config->subscribe(this); + // Capture the config here rather than in the constructor: the logger is one of the first objects created, before + // the config global exists. + config_ = _srs_config; - log_to_file_tank_ = _srs_config->get_log_tank_file(); - utc_ = _srs_config->get_utc_time(); + if (config_) { + config_->subscribe(this); - std::string level = _srs_config->get_log_level(); - std::string level_v2 = _srs_config->get_log_level_v2(); + log_to_file_tank_ = config_->get_log_tank_file(); + utc_ = config_->get_utc_time(); + + std::string level = config_->get_log_level(); + std::string level_v2 = config_->get_log_level_v2(); level_ = level_v2.empty() ? srs_get_log_level(level) : srs_get_log_level_v2(level_v2); } return srs_success; } +// LCOV_EXCL_STOP void SrsFileLog::reopen() { - if (fd_ > 0) { - ::close(fd_); + // Clear the descriptor with the close. Every path below may leave without opening a new file, and write_log() + // opens one only when the descriptor is negative, so a closed descriptor left here would be written to after the + // number has been handed to another socket or file. + if (fd_ >= 0) { + writer_->close_file(fd_); + fd_ = -1; } if (!log_to_file_tank_) { @@ -133,13 +193,12 @@ void SrsFileLog::write_log(int &fd, char *str_log, int size, int level) // \033[33m : yellow text code in shell // \033[0m : normal text code if (level <= SrsLogLevelTrace) { - printf("%.*s", size, str_log); + writer_->write_console("", str_log, size); } else if (level == SrsLogLevelWarn) { - printf("\033[33m%.*s\033[0m", size, str_log); + writer_->write_console("\033[33m", str_log, size); } else { - printf("\033[31m%.*s\033[0m", size, str_log); + writer_->write_console("\033[31m", str_log, size); } - fflush(stdout); return; } @@ -149,26 +208,24 @@ void SrsFileLog::write_log(int &fd, char *str_log, int size, int level) open_log_file(); } - // write log to file. - if (fd > 0) { - ::write(fd, str_log, size); + // write log to file. A descriptor of 0 is a log file like any other: the process may have been started with its + // standard input closed, so open() hands out 0. Only the negative sentinel means there is no file. + if (fd >= 0) { + writer_->write_file(fd, str_log, size); } } void SrsFileLog::open_log_file() { - if (!_srs_config) { + if (!config_) { return; } - std::string filename = _srs_config->get_log_file(); + std::string filename = config_->get_log_file(); if (filename.empty()) { return; } - fd_ = ::open(filename.c_str(), - O_RDWR | O_CREAT | O_APPEND, - S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH); + fd_ = writer_->open_file(filename); } -// LCOV_EXCL_STOP diff --git a/trunk/src/app/srs_app_log.hpp b/trunk/src/app/srs_app_log.hpp index 79cf686d6f..b46bcfa810 100644 --- a/trunk/src/app/srs_app_log.hpp +++ b/trunk/src/app/srs_app_log.hpp @@ -15,6 +15,41 @@ #include #include +class ISrsAppConfig; + +// The output operations of the file logger, the log file and the console. A narrow seam over the system calls, so a +// test can drive the logger and read back what it wrote without opening a file or writing to the terminal. +class ISrsLogWriter +{ +public: + ISrsLogWriter(); + virtual ~ISrsLogWriter(); + +public: + // Open path for append, return the descriptor, or a negative value when it cannot be opened. + virtual int open_file(const std::string &path) = 0; + // Close a descriptor returned by open_file(). + virtual void close_file(int fd) = 0; + // Write size bytes of str_log to fd. + virtual void write_file(int fd, const char *str_log, int size) = 0; + // Write size bytes of str_log to the console, wrapped in the shell color code when color is not empty. + virtual void write_console(const char *color, const char *str_log, int size) = 0; +}; + +// Write the log to the real log file and the real console. +class SrsLogWriter : public ISrsLogWriter +{ +public: + SrsLogWriter(); + virtual ~SrsLogWriter(); + // Interface ISrsLogWriter +public: + virtual int open_file(const std::string &path); + virtual void close_file(int fd); + virtual void write_file(int fd, const char *str_log, int size); + virtual void write_console(const char *color, const char *str_log, int size); +}; + // Use memory/disk cache and donot flush when write log. // it's ok to use it without config, which will log to console, and default trace level. // when you want to use different level, override this classs, set the protected _level. @@ -35,6 +70,11 @@ class SrsFileLog : public ISrsLog, public ISrsReloadHandler // Whether use utc time. bool utc_; +// clang-format off +SRS_DECLARE_PRIVATE: // clang-format on + ISrsAppConfig *config_; + ISrsLogWriter *writer_; + public: SrsFileLog(); virtual ~SrsFileLog(); diff --git a/trunk/src/app/srs_app_rtc_source.cpp b/trunk/src/app/srs_app_rtc_source.cpp index bd3202a3cb..c14adff460 100644 --- a/trunk/src/app/srs_app_rtc_source.cpp +++ b/trunk/src/app/srs_app_rtc_source.cpp @@ -270,6 +270,7 @@ SrsRtcSourceManager::SrsRtcSourceManager() { lock_ = srs_mutex_new(); timer_ = new SrsHourGlass("sources", this, 1 * SRS_UTIME_SECONDS); + timer_->assemble(); } SrsRtcSourceManager::~SrsRtcSourceManager() diff --git a/trunk/src/app/srs_app_rtmp_conn.cpp b/trunk/src/app/srs_app_rtmp_conn.cpp index 32fee7a175..be2f9bb4f1 100644 --- a/trunk/src/app/srs_app_rtmp_conn.cpp +++ b/trunk/src/app/srs_app_rtmp_conn.cpp @@ -207,22 +207,17 @@ const char *SrsRtmpsTransport::transport_type() SrsRtmpConn::SrsRtmpConn(ISrsRtmpTransport *transport, string cip, int cport) { - // Create a identify for this client. - _srs_context->set_id(_srs_context->generate_id()); - transport_ = transport; ip_ = cip; port_ = cport; - create_time_ = srsu2ms(srs_time_now_cached()); + create_time_ = 0; - trd_ = new SrsSTCoroutine("rtmp", this, _srs_context->get_id()); + trd_ = NULL; kbps_ = new SrsNetworkKbps(); - kbps_->set_io(transport_->io(), transport_->io()); delta_ = new SrsNetworkDelta(); - delta_->set_io(transport_->io(), transport_->io()); - rtmp_ = new SrsRtmpServer(transport_->io()); + rtmp_ = NULL; refer_ = new SrsRefer(); security_ = new SrsSecurity(); duration_ = 0; @@ -239,6 +234,7 @@ SrsRtmpConn::SrsRtmpConn(ISrsRtmpTransport *transport, string cip, int cport) publish_normal_timeout_ = 0; app_factory_ = _srs_app_factory; + context_ = _srs_context; config_ = _srs_config; manager_ = _srs_conn_manager; stream_publish_tokens_ = _srs_stream_publish_tokens; @@ -254,6 +250,18 @@ SrsRtmpConn::SrsRtmpConn(ISrsRtmpTransport *transport, string cip, int cport) void SrsRtmpConn::assemble() { + // Create a identify for this client. + context_->set_id(context_->generate_id()); + + create_time_ = srsu2ms(srs_time_now_cached()); + + trd_ = app_factory_->create_coroutine("rtmp", this, context_->get_id()); + + kbps_->set_io(transport_->io(), transport_->io()); + delta_->set_io(transport_->io(), transport_->io()); + + rtmp_ = new SrsRtmpServer(transport_->io()); + config_->subscribe(this); } @@ -263,7 +271,9 @@ SrsRtmpConn::~SrsRtmpConn() config_->unsubscribe(this); } - trd_->interrupt(); + if (trd_) { + trd_->interrupt(); + } // wakeup the handler which need to notice. if (wakable_) { wakable_->wakeup(); @@ -280,6 +290,7 @@ SrsRtmpConn::~SrsRtmpConn() srs_freep(security_); app_factory_ = NULL; + context_ = NULL; config_ = NULL; manager_ = NULL; stream_publish_tokens_ = NULL; @@ -591,7 +602,7 @@ srs_error_t SrsRtmpConn::stream_service_cycle() switch (info_->type_) { case SrsRtmpConnPlay: { // We must do stat the client before hooks, because hooks depends on it. - if ((err = stat_->on_client(_srs_context->get_id().c_str(), req, this, info_->type_)) != srs_success) { + if ((err = stat_->on_client(context_->get_id().c_str(), req, this, info_->type_)) != srs_success) { return srs_error_wrap(err, "rtmp: stat client"); } @@ -732,7 +743,7 @@ srs_error_t SrsRtmpConn::playing(SrsSharedPtr source) } // Use receiving thread to receive packets from peer. - SrsQueueRecvThread trd(consumer.get(), rtmp_, SRS_PERF_MW_SLEEP, _srs_context->get_id()); + SrsQueueRecvThread trd(consumer.get(), rtmp_, SRS_PERF_MW_SLEEP, context_->get_id()); if ((err = trd.start()) != srs_success) { return srs_error_wrap(err, "rtmp: start receive thread"); @@ -873,7 +884,7 @@ srs_error_t SrsRtmpConn::do_playing(SrsSharedPtr source, SrsLiveC if (count <= 0) { #ifndef SRS_PERF_QUEUE_COND_WAIT - srs_usleep(mw_sleep); + srs_usleep(mw_sleep_); #endif // ignore when nothing got. continue; @@ -944,7 +955,7 @@ srs_error_t SrsRtmpConn::authorize_publish() } // We must do stat the client before hooks, because hooks depends on it. - if ((err = stat_->on_client(_srs_context->get_id().c_str(), req, this, info_->type_)) != srs_success) { + if ((err = stat_->on_client(context_->get_id().c_str(), req, this, info_->type_)) != srs_success) { return srs_error_wrap(err, "rtmp: stat client"); } @@ -977,7 +988,7 @@ srs_error_t SrsRtmpConn::publishing(SrsSharedPtr source) if ((err = acquire_err) == srs_success) { // use isolate thread to recv, // @see: https://github.com/ossrs/srs/issues/237 - SrsPublishRecvThread rtrd(rtmp_, req, transport_->osfd(), 0, this, source, _srs_context->get_id()); + SrsPublishRecvThread rtrd(rtmp_, req, transport_->osfd(), 0, this, source, context_->get_id()); rtrd.assemble(); err = do_publishing(source, &rtrd); diff --git a/trunk/src/app/srs_app_rtmp_conn.hpp b/trunk/src/app/srs_app_rtmp_conn.hpp index f80391b229..094886074b 100644 --- a/trunk/src/app/srs_app_rtmp_conn.hpp +++ b/trunk/src/app/srs_app_rtmp_conn.hpp @@ -54,6 +54,7 @@ class ISrsRtspSourceManager; class ISrsRtmpServer; class ISrsRtmpTransport; class ISrsSecurity; +class ISrsContext; // The simple rtmp client for SRS. class SrsSimpleRtmpClient : public SrsBasicRtmpClient @@ -176,6 +177,7 @@ class SrsRtmpConn : public ISrsConnection, // It's a resource. // clang-format off SRS_DECLARE_PRIVATE: // clang-format on ISrsAppFactory *app_factory_; + ISrsContext *context_; ISrsResourceManager *manager_; ISrsAppConfig *config_; ISrsStreamPublishTokenManager *stream_publish_tokens_; @@ -235,7 +237,7 @@ class SrsRtmpConn : public ISrsConnection, // It's a resource. public: SrsRtmpConn(ISrsRtmpTransport *transport, std::string cip, int port); - void assemble(); + void assemble(); // Construct object, to avoid call function in constructor. virtual ~SrsRtmpConn(); // Interface ISrsResource. public: diff --git a/trunk/src/app/srs_app_rtmp_source.cpp b/trunk/src/app/srs_app_rtmp_source.cpp index 25b034fe40..2707a31f94 100644 --- a/trunk/src/app/srs_app_rtmp_source.cpp +++ b/trunk/src/app/srs_app_rtmp_source.cpp @@ -1622,7 +1622,9 @@ SrsLiveSourceManager *_srs_sources = NULL; SrsLiveSourceManager::SrsLiveSourceManager() { lock_ = srs_mutex_new(); - timer_ = new SrsHourGlass("sources", this, 1 * SRS_UTIME_SECONDS); + SrsHourGlass *timer = new SrsHourGlass("sources", this, 1 * SRS_UTIME_SECONDS); + timer->assemble(); + timer_ = timer; app_factory_ = _srs_app_factory; } diff --git a/trunk/src/app/srs_app_rtsp_conn.cpp b/trunk/src/app/srs_app_rtsp_conn.cpp index c005c4fa80..d0ed5f3be3 100644 --- a/trunk/src/app/srs_app_rtsp_conn.cpp +++ b/trunk/src/app/srs_app_rtsp_conn.cpp @@ -19,7 +19,6 @@ using namespace std; #include #endif #include -#include #include #include #include @@ -205,7 +204,7 @@ srs_error_t SrsRtspPlayStream::start() } srs_freep(trd_); - trd_ = new SrsFastCoroutine("rtsp_sender", this, cid_); + trd_ = app_factory_->create_coroutine("rtsp_sender", this, cid_); if ((err = trd_->start()) != srs_success) { return srs_error_wrap(err, "rtsp_sender"); @@ -382,8 +381,6 @@ ISrsRtspConnection::~ISrsRtspConnection() SrsRtspConnection::SrsRtspConnection(ISrsResourceManager *cm, ISrsProtocolReadWriter *skt, std::string cip, int port) { manager_ = cm; - cid_ = _srs_context->generate_id(); - _srs_context->set_id(cid_); // Initialize timeout management fields from SrsRtspConnection2 last_stun_time = 0; @@ -395,7 +392,7 @@ SrsRtspConnection::SrsRtspConnection(ISrsResourceManager *cm, ISrsProtocolReadWr ip_ = cip; port_ = port; rtsp_ = new SrsRtspStack(skt); - trd_ = new SrsSTCoroutine("rtsp", this, _srs_context->get_id()); + trd_ = NULL; // Initialize merged SrsRtspSession members skt_ = skt; @@ -415,16 +412,26 @@ SrsRtspConnection::SrsRtspConnection(ISrsResourceManager *cm, ISrsProtocolReadWr config_ = _srs_config; rtsp_sources_ = _srs_rtsp_sources; hooks_ = _srs_hooks; + app_factory_ = _srs_app_factory; + context_ = _srs_context; } void SrsRtspConnection::assemble() { + // Create a identify for this session. + cid_ = context_->generate_id(); + context_->set_id(cid_); + + trd_ = app_factory_->create_coroutine("rtsp", this, context_->get_id()); + rtsp_manager_->subscribe(this); } SrsRtspConnection::~SrsRtspConnection() { - rtsp_manager_->unsubscribe(this); + if (rtsp_manager_) { + rtsp_manager_->unsubscribe(this); + } srs_freep(request_); srs_freep(rtsp_); @@ -457,6 +464,8 @@ SrsRtspConnection::~SrsRtspConnection() config_ = NULL; rtsp_sources_ = NULL; hooks_ = NULL; + app_factory_ = NULL; + context_ = NULL; } // LCOV_EXCL_START @@ -465,7 +474,10 @@ srs_error_t SrsRtspConnection::do_send_packet(SrsRtpPacket *pkt) srs_error_t err = srs_success; uint32_t ssrc = pkt->header_.get_ssrc(); - ISrsStreamWriter *network = networks_[ssrc]; + // Look up without inserting: this runs for every RTP packet, and operator[] would default-insert + // a NULL entry on each miss. + std::map::iterator it = networks_.find(ssrc); + ISrsStreamWriter *network = (it == networks_.end()) ? NULL : it->second; if (!network) { return srs_error_new(ERROR_RTSP_NO_TRACK, "network not found for ssrc: %u", ssrc); } @@ -642,7 +654,20 @@ srs_error_t SrsRtspConnection::on_rtsp_request(SrsRtspRequest *req_raw) std::string local_sdp_escaped = srs_strings_replace(sdp.c_str(), "\r\n", "\\r\\n"); srs_trace("RTSP: DESCRIBE cseq=%ld, session=%s, sdp: %s", req->seq_, session_id_.c_str(), local_sdp_escaped.c_str()); } else if (req->is_setup()) { - srs_assert(req->transport_); + // SETUP carries its parameters in the Transport header, which the parser leaves NULL when the + // client omits it, while is_setup() only looks at the method. Refuse such a request here: the + // response and do_setup() below both dereference the transport, and asserting on it would end + // the process, and every other session with it, on input any client can send. + if (!req->transport_) { + SrsUniquePtr res(new SrsRtspResponse((int)req->seq_)); + res->status_ = SRS_CONSTS_RTSP_BadRequest; + res->session_ = session_id_; + if ((err = rtsp_->send_message(res.get())) != srs_success) { + return srs_error_wrap(err, "response setup"); + } + srs_warn("RTSP: SETUP cseq=%ld without transport, session=%s", req->seq_, session_id_.c_str()); + return err; + } SrsUniquePtr res(new SrsRtspSetupResponse((int)req->seq_)); res->session_ = session_id_; @@ -712,7 +737,7 @@ void SrsRtspConnection::on_before_dispose(ISrsResource *c) } if (session && session == this) { - _srs_context->set_id(cid_); + context_->set_id(cid_); srs_trace("RTSP: session detach from [%s](%s), disposing=%d", c->get_id().c_str(), c->desc().c_str(), disposing_); } @@ -727,7 +752,7 @@ void SrsRtspConnection::on_disposing(ISrsResource *c) void SrsRtspConnection::switch_to_context() { - _srs_context->set_id(cid_); + context_->set_id(cid_); } const SrsContextId &SrsRtspConnection::context_id() @@ -785,6 +810,14 @@ srs_error_t SrsRtspConnection::do_describe(SrsRtspRequest *req, std::string &sdp local_sdp.control_ = req->uri_; local_sdp.ice_lite_ = ""; // Disable this line. + // A client may DESCRIBE more than once, and each one rebuilds the track set from the source, so + // drop the previous one first. The track ids restart at 0 below, so a stale entry would collide + // on that id in get_ssrc_by_stream_id() and bind SETUP to an SSRC that is no longer published. + for (std::map::iterator it = tracks_.begin(); it != tracks_.end(); ++it) { + srs_freep(it->second); + } + tracks_.clear(); + uint32_t track_id = 0; SrsRtcTrackDescription *audio_desc = source_->audio_desc(); if (audio_desc) { @@ -878,7 +911,11 @@ srs_error_t SrsRtspConnection::do_setup(SrsRtspRequest *req, uint32_t *pssrc) "UDP transport not supported, only TCP/interleaved mode is supported"); } + // A client may re-SETUP a track, for instance to move it to another interleaved channel. The + // writer installed by the previous SETUP is owned here, so free it before taking the new one. SrsRtspTcpNetwork *network = new SrsRtspTcpNetwork(skt_, req->transport_->interleaved_min_); + ISrsStreamWriter *&slot = networks_[ssrc]; + srs_freep(slot); networks_[ssrc] = network; *pssrc = ssrc; diff --git a/trunk/src/app/srs_app_rtsp_conn.hpp b/trunk/src/app/srs_app_rtsp_conn.hpp index e66f0a2869..20b5348d27 100644 --- a/trunk/src/app/srs_app_rtsp_conn.hpp +++ b/trunk/src/app/srs_app_rtsp_conn.hpp @@ -41,6 +41,7 @@ class ISrsStatistic; class ISrsRtspSourceManager; class ISrsHttpHooks; class ISrsAppConfig; +class ISrsContext; // The handler for RTSP play stream. class ISrsRtspPlayStream @@ -149,6 +150,8 @@ class SrsRtspConnection : public ISrsResource, // It's a resource. ISrsStatistic *stat_; ISrsAppConfig *config_; ISrsHttpHooks *hooks_; + ISrsAppFactory *app_factory_; + ISrsContext *context_; // clang-format off SRS_DECLARE_PRIVATE: // clang-format on diff --git a/trunk/src/app/srs_app_rtsp_source.cpp b/trunk/src/app/srs_app_rtsp_source.cpp index 5c530c6294..2dcfa49fcf 100644 --- a/trunk/src/app/srs_app_rtsp_source.cpp +++ b/trunk/src/app/srs_app_rtsp_source.cpp @@ -123,6 +123,7 @@ SrsRtspSourceManager::SrsRtspSourceManager() { lock_ = srs_mutex_new(); timer_ = new SrsHourGlass("sources", this, 1 * SRS_UTIME_SECONDS); + timer_->assemble(); } SrsRtspSourceManager::~SrsRtspSourceManager() diff --git a/trunk/src/app/srs_app_server.cpp b/trunk/src/app/srs_app_server.cpp index 140972f44a..8b20fedb0d 100644 --- a/trunk/src/app/srs_app_server.cpp +++ b/trunk/src/app/srs_app_server.cpp @@ -200,6 +200,7 @@ SrsServer::SrsServer() #endif http_server_ = new SrsHttpServer(); + http_server_->assemble(); reuse_api_over_server_ = false; reuse_rtc_over_server_ = false; diff --git a/trunk/src/app/srs_app_srt_listener.cpp b/trunk/src/app/srs_app_srt_listener.cpp index a9299f3310..5c9a0081ba 100644 --- a/trunk/src/app/srs_app_srt_listener.cpp +++ b/trunk/src/app/srs_app_srt_listener.cpp @@ -18,6 +18,14 @@ ISrsSrtHandler::~ISrsSrtHandler() { } +ISrsSrtListener::ISrsSrtListener() +{ +} + +ISrsSrtListener::~ISrsSrtListener() +{ +} + SrsSrtListener::SrsSrtListener(ISrsSrtHandler *h, std::string i, int p) { handler_ = h; diff --git a/trunk/src/app/srs_app_srt_listener.hpp b/trunk/src/app/srs_app_srt_listener.hpp index 5cda7237ae..80f24a7cce 100644 --- a/trunk/src/app/srs_app_srt_listener.hpp +++ b/trunk/src/app/srs_app_srt_listener.hpp @@ -25,8 +25,21 @@ class ISrsSrtHandler virtual srs_error_t on_srt_client(srs_srt_t srt_fd) = 0; }; +// The SRT listener, which creates the socket, then listens after the options are set. +class ISrsSrtListener +{ +public: + ISrsSrtListener(); + virtual ~ISrsSrtListener(); + +public: + virtual srs_srt_t fd() = 0; + virtual srs_error_t create_socket() = 0; + virtual srs_error_t listen() = 0; +}; + // Bind and listen SRT(udp) port, use handler to process the client. -class SrsSrtListener : public ISrsCoroutineHandler +class SrsSrtListener : public ISrsSrtListener, public ISrsCoroutineHandler { // clang-format off SRS_DECLARE_PRIVATE: // clang-format on diff --git a/trunk/src/app/srs_app_srt_server.cpp b/trunk/src/app/srs_app_srt_server.cpp index fef0faf9fd..a9cb422f0b 100644 --- a/trunk/src/app/srs_app_srt_server.cpp +++ b/trunk/src/app/srs_app_srt_server.cpp @@ -9,6 +9,7 @@ using namespace std; #include +#include #include #include #include @@ -27,17 +28,24 @@ ISrsSrtClientHandler::~ISrsSrtClientHandler() { } -// LCOV_EXCL_START SrsSrtAcceptor::SrsSrtAcceptor(ISrsSrtClientHandler *srt_handler) { port_ = 0; srt_handler_ = srt_handler; listener_ = NULL; + + config_ = _srs_config; + app_factory_ = _srs_app_factory; + srt_options_ = new SrsSrtOptions(); } SrsSrtAcceptor::~SrsSrtAcceptor() { srs_freep(listener_); + srs_freep(srt_options_); + + config_ = NULL; + app_factory_ = NULL; } srs_error_t SrsSrtAcceptor::listen(std::string ip, int port) @@ -48,7 +56,7 @@ srs_error_t SrsSrtAcceptor::listen(std::string ip, int port) port_ = port; srs_freep(listener_); - listener_ = new SrsSrtListener(this, ip_, port_); + listener_ = app_factory_->create_srt_listener(this, ip_, port_); // Create srt socket. if ((err = listener_->create_socket()) != srs_success) { @@ -74,62 +82,62 @@ srs_error_t SrsSrtAcceptor::set_srt_opt() { srs_error_t err = srs_success; - if ((err = srs_srt_set_maxbw(listener_->fd(), _srs_config->get_srto_maxbw())) != srs_success) { - return srs_error_wrap(err, "set opt maxbw=%" PRId64 " failed", _srs_config->get_srto_maxbw()); + if ((err = srt_options_->set_maxbw(listener_->fd(), config_->get_srto_maxbw())) != srs_success) { + return srs_error_wrap(err, "set opt maxbw=%" PRId64 " failed", config_->get_srto_maxbw()); } - if ((err = srs_srt_set_mss(listener_->fd(), _srs_config->get_srto_mss())) != srs_success) { - return srs_error_wrap(err, "set opt mss=%d failed", _srs_config->get_srto_mss()); + if ((err = srt_options_->set_mss(listener_->fd(), config_->get_srto_mss())) != srs_success) { + return srs_error_wrap(err, "set opt mss=%d failed", config_->get_srto_mss()); } - if ((err = srs_srt_set_tsbpdmode(listener_->fd(), _srs_config->get_srto_tsbpdmode())) != srs_success) { - return srs_error_wrap(err, "set opt tsbpdmode=%d failed", _srs_config->get_srto_tsbpdmode()); + if ((err = srt_options_->set_tsbpdmode(listener_->fd(), config_->get_srto_tsbpdmode())) != srs_success) { + return srs_error_wrap(err, "set opt tsbpdmode=%d failed", config_->get_srto_tsbpdmode()); } - if ((err = srs_srt_set_latency(listener_->fd(), _srs_config->get_srto_latency())) != srs_success) { - return srs_error_wrap(err, "set opt latency=%d failed", _srs_config->get_srto_latency()); + if ((err = srt_options_->set_latency(listener_->fd(), config_->get_srto_latency())) != srs_success) { + return srs_error_wrap(err, "set opt latency=%d failed", config_->get_srto_latency()); } - if ((err = srs_srt_set_rcv_latency(listener_->fd(), _srs_config->get_srto_recv_latency())) != srs_success) { - return srs_error_wrap(err, "set opt recvlatency=%d failed", _srs_config->get_srto_recv_latency()); + if ((err = srt_options_->set_rcv_latency(listener_->fd(), config_->get_srto_recv_latency())) != srs_success) { + return srs_error_wrap(err, "set opt recvlatency=%d failed", config_->get_srto_recv_latency()); } - if ((err = srs_srt_set_peer_latency(listener_->fd(), _srs_config->get_srto_peer_latency())) != srs_success) { - return srs_error_wrap(err, "set opt peerlatency=%d failed", _srs_config->get_srto_peer_latency()); + if ((err = srt_options_->set_peer_latency(listener_->fd(), config_->get_srto_peer_latency())) != srs_success) { + return srs_error_wrap(err, "set opt peerlatency=%d failed", config_->get_srto_peer_latency()); } - if ((err = srs_srt_set_tlpktdrop(listener_->fd(), _srs_config->get_srto_tlpktdrop())) != srs_success) { - return srs_error_wrap(err, "set opt tlpktdrop=%d failed", _srs_config->get_srto_tlpktdrop()); + if ((err = srt_options_->set_tlpktdrop(listener_->fd(), config_->get_srto_tlpktdrop())) != srs_success) { + return srs_error_wrap(err, "set opt tlpktdrop=%d failed", config_->get_srto_tlpktdrop()); } - if ((err = srs_srt_set_connect_timeout(listener_->fd(), srsu2msi(_srs_config->get_srto_conntimeout()))) != srs_success) { - return srs_error_wrap(err, "set opt connect_timeout=%d failed", _srs_config->get_srto_conntimeout()); + if ((err = srt_options_->set_connect_timeout(listener_->fd(), srsu2msi(config_->get_srto_conntimeout()))) != srs_success) { + return srs_error_wrap(err, "set opt connect_timeout=%d failed", config_->get_srto_conntimeout()); } - if ((err = srs_srt_set_peer_idle_timeout(listener_->fd(), srsu2msi(_srs_config->get_srto_peeridletimeout()))) != srs_success) { - return srs_error_wrap(err, "set opt peer_idle_timeout=%d failed", _srs_config->get_srto_peeridletimeout()); + if ((err = srt_options_->set_peer_idle_timeout(listener_->fd(), srsu2msi(config_->get_srto_peeridletimeout()))) != srs_success) { + return srs_error_wrap(err, "set opt peer_idle_timeout=%d failed", config_->get_srto_peeridletimeout()); } - if ((err = srs_srt_set_sndbuf(listener_->fd(), _srs_config->get_srto_sendbuf())) != srs_success) { - return srs_error_wrap(err, "set opt sendbuf=%d failed", _srs_config->get_srto_sendbuf()); + if ((err = srt_options_->set_sndbuf(listener_->fd(), config_->get_srto_sendbuf())) != srs_success) { + return srs_error_wrap(err, "set opt sendbuf=%d failed", config_->get_srto_sendbuf()); } - if ((err = srs_srt_set_rcvbuf(listener_->fd(), _srs_config->get_srto_recvbuf())) != srs_success) { - return srs_error_wrap(err, "set opt recvbuf=%d failed", _srs_config->get_srto_recvbuf()); + if ((err = srt_options_->set_rcvbuf(listener_->fd(), config_->get_srto_recvbuf())) != srs_success) { + return srs_error_wrap(err, "set opt recvbuf=%d failed", config_->get_srto_recvbuf()); } - if ((err = srs_srt_set_payload_size(listener_->fd(), _srs_config->get_srto_payloadsize())) != srs_success) { - return srs_error_wrap(err, "set opt payload_size=%d failed", _srs_config->get_srto_payloadsize()); + if ((err = srt_options_->set_payload_size(listener_->fd(), config_->get_srto_payloadsize())) != srs_success) { + return srs_error_wrap(err, "set opt payload_size=%d failed", config_->get_srto_payloadsize()); } - string passphrase = _srs_config->get_srto_passphrase(); + string passphrase = config_->get_srto_passphrase(); if (!passphrase.empty()) { - if ((err = srs_srt_set_passphrase(listener_->fd(), passphrase)) != srs_success) { + if ((err = srt_options_->set_passphrase(listener_->fd(), passphrase)) != srs_success) { return srs_error_wrap(err, "set opt passphrase=%s failed", passphrase.c_str()); } - int pbkeylen = _srs_config->get_srto_pbkeylen(); - if ((err = srs_srt_set_pbkeylen(listener_->fd(), pbkeylen)) != srs_success) { + int pbkeylen = config_->get_srto_pbkeylen(); + if ((err = srt_options_->set_pbkeylen(listener_->fd(), pbkeylen)) != srs_success) { return srs_error_wrap(err, "set opt pbkeylen=%d failed", pbkeylen); } } @@ -137,6 +145,7 @@ srs_error_t SrsSrtAcceptor::set_srt_opt() return err; } +// LCOV_EXCL_START srs_error_t SrsSrtAcceptor::on_srt_client(srs_srt_t srt_fd) { srs_error_t err = srs_success; diff --git a/trunk/src/app/srs_app_srt_server.hpp b/trunk/src/app/srs_app_srt_server.hpp index 5f26ccd453..3e8691d4b9 100644 --- a/trunk/src/app/srs_app_srt_server.hpp +++ b/trunk/src/app/srs_app_srt_server.hpp @@ -15,6 +15,8 @@ class SrsSrtServer; class SrsHourGlass; class ISrsSrtClientHandler; +class ISrsAppConfig; +class ISrsAppFactory; // Interface for SRT client acceptance class ISrsSrtClientHandler @@ -38,7 +40,13 @@ class SrsSrtAcceptor : public ISrsSrtHandler // clang-format off SRS_DECLARE_PRIVATE: // clang-format on - SrsSrtListener *listener_; + ISrsAppConfig *config_; + ISrsAppFactory *app_factory_; + ISrsSrtOptions *srt_options_; + +// clang-format off +SRS_DECLARE_PRIVATE: // clang-format on + ISrsSrtListener *listener_; public: SrsSrtAcceptor(ISrsSrtClientHandler *srt_handler); diff --git a/trunk/src/app/srs_app_srt_source.cpp b/trunk/src/app/srs_app_srt_source.cpp index 318985ffcf..486591429e 100644 --- a/trunk/src/app/srs_app_srt_source.cpp +++ b/trunk/src/app/srs_app_srt_source.cpp @@ -109,6 +109,7 @@ SrsSrtSourceManager::SrsSrtSourceManager() { lock_ = srs_mutex_new(); timer_ = new SrsHourGlass("sources", this, 1 * SRS_UTIME_SECONDS); + timer_->assemble(); } SrsSrtSourceManager::~SrsSrtSourceManager() @@ -327,6 +328,7 @@ SrsSrtFrameBuilder::SrsSrtFrameBuilder(ISrsFrameTarget *target) audio_streamid_ = 2; pp_audio_duration_ = new SrsAlonePithyPrint(); + pp_audio_duration_->assemble(); } SrsSrtFrameBuilder::~SrsSrtFrameBuilder() diff --git a/trunk/src/core/srs_core_version8.hpp b/trunk/src/core/srs_core_version8.hpp index 7ff0071dd0..d0ad688e67 100644 --- a/trunk/src/core/srs_core_version8.hpp +++ b/trunk/src/core/srs_core_version8.hpp @@ -9,6 +9,6 @@ #define VERSION_MAJOR 8 #define VERSION_MINOR 0 -#define VERSION_REVISION 41 +#define VERSION_REVISION 44 #endif diff --git a/trunk/src/kernel/srs_kernel_hourglass.cpp b/trunk/src/kernel/srs_kernel_hourglass.cpp index 3aa9360b93..d760be41a2 100644 --- a/trunk/src/kernel/srs_kernel_hourglass.cpp +++ b/trunk/src/kernel/srs_kernel_hourglass.cpp @@ -52,14 +52,26 @@ SrsHourGlass::SrsHourGlass(string label, ISrsHourGlassHandler *h, srs_utime_t re handler_ = h; resolution_ = resolution; total_elapse_ = 0; - trd_ = _srs_kernel_factory->create_coroutine("timer-" + label, this, _srs_context->get_id()); - time_ = _srs_kernel_factory->create_time(); + trd_ = NULL; + time_ = NULL; + + factory_ = _srs_kernel_factory; + context_ = _srs_context; } SrsHourGlass::~SrsHourGlass() { srs_freep(trd_); srs_freep(time_); + + factory_ = NULL; + context_ = NULL; +} + +void SrsHourGlass::assemble() +{ + trd_ = factory_->create_coroutine("timer-" + label_, this, context_->get_id()); + time_ = factory_->create_time(); } srs_error_t SrsHourGlass::start() @@ -155,14 +167,27 @@ ISrsFastTimer::~ISrsFastTimer() SrsFastTimer::SrsFastTimer(std::string label, srs_utime_t interval) { interval_ = interval; - trd_ = _srs_kernel_factory->create_coroutine(label, this, _srs_context->get_id()); - time_ = _srs_kernel_factory->create_time(); + label_ = label; + trd_ = NULL; + time_ = NULL; + + factory_ = _srs_kernel_factory; + context_ = _srs_context; } SrsFastTimer::~SrsFastTimer() { srs_freep(trd_); srs_freep(time_); + + factory_ = NULL; + context_ = NULL; +} + +void SrsFastTimer::assemble() +{ + trd_ = factory_->create_coroutine(label_, this, context_->get_id()); + time_ = factory_->create_time(); } srs_error_t SrsFastTimer::start() @@ -189,6 +214,12 @@ void SrsFastTimer::unsubscribe(ISrsFastTimerHandler *timer) if (it != handlers_.end()) { handlers_.erase(it); } + + // Also take it out of the round in progress, so it is not notified after it left. + deque::iterator p = std::find(pending_.begin(), pending_.end(), timer); + if (p != pending_.end()) { + pending_.erase(p); + } } srs_error_t SrsFastTimer::cycle() @@ -202,8 +233,14 @@ srs_error_t SrsFastTimer::cycle() ++_srs_pps_timer->sugar_; - for (int i = 0; i < (int)handlers_.size(); i++) { - ISrsFastTimerHandler *timer = handlers_.at(i); + // Notify from a queue rather than walking handlers_, because a handler may unsubscribe during + // its callback, or from another coroutine while the callback yields, and erasing from the + // vector under the walk would skip the handler after it. Popping from the queue does not + // depend on positions, so unsubscribe() can remove from it freely. + pending_.assign(handlers_.begin(), handlers_.end()); + while (!pending_.empty()) { + ISrsFastTimerHandler *timer = pending_.front(); + pending_.pop_front(); if ((err = timer->on_timer(interval_)) != srs_success) { srs_freep(err); // Ignore any error for shared timer. @@ -296,9 +333,13 @@ srs_error_t SrsSharedTimer::initialize() // Initialize global shared timers timer20ms_ = new SrsFastTimer("shared", 20 * SRS_UTIME_MILLISECONDS); + timer20ms_->assemble(); timer100ms_ = new SrsFastTimer("shared", 100 * SRS_UTIME_MILLISECONDS); + timer100ms_->assemble(); timer1s_ = new SrsFastTimer("shared", 1 * SRS_UTIME_SECONDS); + timer1s_->assemble(); timer5s_ = new SrsFastTimer("shared", 5 * SRS_UTIME_SECONDS); + timer5s_->assemble(); clock_monitor_ = new SrsClockWallMonitor(); // Start all timers diff --git a/trunk/src/kernel/srs_kernel_hourglass.hpp b/trunk/src/kernel/srs_kernel_hourglass.hpp index 5c89f60e2e..6f8c830837 100644 --- a/trunk/src/kernel/srs_kernel_hourglass.hpp +++ b/trunk/src/kernel/srs_kernel_hourglass.hpp @@ -11,11 +11,14 @@ #include +#include #include #include #include class ISrsCoroutine; +class ISrsContext; +class ISrsKernelFactory; // The handler for the tick. class ISrsHourGlassHandler @@ -61,6 +64,7 @@ class ISrsHourGlass // // Usage: // SrsHourGlass* hg = new SrsHourGlass("nack", handler, 100 * SRS_UTIME_MILLISECONDS); +// hg->assemble(); // // hg->tick(1, 300 * SRS_UTIME_MILLISECONDS); // hg->tick(2, 500 * SRS_UTIME_MILLISECONDS); @@ -77,6 +81,8 @@ class SrsHourGlass : public ISrsCoroutineHandler, public ISrsHourGlass ISrsHourGlassHandler *handler_; srs_utime_t resolution_; ISrsTime *time_; + ISrsKernelFactory *factory_; + ISrsContext *context_; // The ticks: // key: the event of tick. // value: the interval of tick. @@ -90,6 +96,9 @@ class SrsHourGlass : public ISrsCoroutineHandler, public ISrsHourGlass SrsHourGlass(std::string label, ISrsHourGlassHandler *h, srs_utime_t resolution); virtual ~SrsHourGlass(); +public: + void assemble(); // Construct object, to avoid call function in constructor. + public: // Start or stop the hourglass. virtual srs_error_t start(); @@ -144,13 +153,23 @@ class SrsFastTimer : public ISrsCoroutineHandler, public ISrsFastTimer SRS_DECLARE_PRIVATE: // clang-format on ISrsCoroutine *trd_; srs_utime_t interval_; + std::string label_; std::vector handlers_; + // The handlers the round in progress has not notified yet. The round pops them off the front, and + // unsubscribe() removes a handler from here as well, so a handler that leaves during the round is + // never notified after it left, even if it was destroyed. + std::deque pending_; ISrsTime *time_; + ISrsKernelFactory *factory_; + ISrsContext *context_; public: SrsFastTimer(std::string label, srs_utime_t interval); virtual ~SrsFastTimer(); +public: + void assemble(); // Construct object, to avoid call function in constructor. + public: srs_error_t start(); diff --git a/trunk/src/kernel/srs_kernel_pithy_print.cpp b/trunk/src/kernel/srs_kernel_pithy_print.cpp index b9429195fa..68fe76164d 100644 --- a/trunk/src/kernel/srs_kernel_pithy_print.cpp +++ b/trunk/src/kernel/srs_kernel_pithy_print.cpp @@ -11,6 +11,7 @@ using namespace std; #include #include +#include #include #include @@ -20,15 +21,25 @@ SrsStageInfo::SrsStageInfo(int _stage_id, double ratio) nb_clients_ = 0; age_ = 0; nn_count_ = 0; + interval_ = 0; interval_ratio_ = ratio; - config_ = _srs_kernel_factory->create_config(); + config_ = NULL; - update_print_time(); + factory_ = _srs_kernel_factory; } SrsStageInfo::~SrsStageInfo() { srs_freep(config_); + + factory_ = NULL; +} + +void SrsStageInfo::assemble() +{ + config_ = factory_->create_config(); + + update_print_time(); } void SrsStageInfo::update_print_time() @@ -53,6 +64,14 @@ bool SrsStageInfo::can_print() return can_print; } +ISrsStageManager::ISrsStageManager() +{ +} + +ISrsStageManager::~ISrsStageManager() +{ +} + SrsStageManager::SrsStageManager() { } @@ -73,6 +92,7 @@ SrsStageInfo *SrsStageManager::fetch_or_create(int stage_id, bool *pnew) // Create one if not exists. if (it == stages_.end()) { SrsStageInfo *stage = new SrsStageInfo(stage_id); + stage->assemble(); stages_[stage_id] = stage; if (pnew) { @@ -96,10 +116,12 @@ SrsErrorPithyPrint::SrsErrorPithyPrint(double ratio) { nn_count_ = 0; ratio_ = ratio; + clk_ = _srs_clock; } SrsErrorPithyPrint::~SrsErrorPithyPrint() { + clk_ = NULL; } bool SrsErrorPithyPrint::can_print(srs_error_t err, uint32_t *pnn) @@ -129,14 +151,14 @@ bool SrsErrorPithyPrint::can_print(int error_code, uint32_t *pnn) srs_utime_t tick = ticks_[error_code]; if (!tick) { - ticks_[error_code] = tick = srs_time_now_cached(); + ticks_[error_code] = tick = clk_->now(); } - srs_utime_t diff = srs_time_now_cached() - tick; + srs_utime_t diff = clk_->now() - tick; diff = srs_max(0, diff); stage->elapse(diff); - ticks_[error_code] = srs_time_now_cached(); + ticks_[error_code] = clk_->now(); return new_stage || stage->can_print(); } @@ -146,17 +168,26 @@ SrsAlonePithyPrint::SrsAlonePithyPrint() : info_(0) // stage work for one print info_.nb_clients_ = 1; - previous_tick_ = srs_time_now_cached(); + previous_tick_ = 0; + clk_ = _srs_clock; } SrsAlonePithyPrint::~SrsAlonePithyPrint() { + clk_ = NULL; +} + +void SrsAlonePithyPrint::assemble() +{ + info_.assemble(); + + previous_tick_ = clk_->now(); } void SrsAlonePithyPrint::elapse() { - srs_utime_t diff = srs_time_now_cached() - previous_tick_; - previous_tick_ = srs_time_now_cached(); + srs_utime_t diff = clk_->now() - previous_tick_; + previous_tick_ = clk_->now(); diff = srs_max(0, diff); @@ -183,9 +214,25 @@ SrsPithyPrint::SrsPithyPrint(int _stage_id) { stage_id_ = _stage_id; cache_ = NULL; - client_id_ = enter_stage(); - previous_tick_ = srs_time_now_cached(); + client_id_ = 0; + previous_tick_ = 0; age_ = 0; + + stages_ = _srs_stages; + clk_ = _srs_clock; +} + +void SrsPithyPrint::assemble() +{ + client_id_ = enter_stage(); + previous_tick_ = clk_->now(); +} + +SrsPithyPrint *SrsPithyPrint::create(int stage_id) +{ + SrsPithyPrint *pprint = new SrsPithyPrint(stage_id); + pprint->assemble(); + return pprint; } /////////////////////////////////////////////////////////// @@ -227,92 +274,98 @@ SrsPithyPrint::SrsPithyPrint(int _stage_id) SrsPithyPrint *SrsPithyPrint::create_rtmp_play() { - return new SrsPithyPrint(SRS_CONSTS_STAGE_PLAY_USER); + return create(SRS_CONSTS_STAGE_PLAY_USER); } SrsPithyPrint *SrsPithyPrint::create_rtmp_publish() { - return new SrsPithyPrint(SRS_CONSTS_STAGE_PUBLISH_USER); + return create(SRS_CONSTS_STAGE_PUBLISH_USER); } SrsPithyPrint *SrsPithyPrint::create_hls() { - return new SrsPithyPrint(SRS_CONSTS_STAGE_HLS); + return create(SRS_CONSTS_STAGE_HLS); } SrsPithyPrint *SrsPithyPrint::create_forwarder() { - return new SrsPithyPrint(SRS_CONSTS_STAGE_FORWARDER); + return create(SRS_CONSTS_STAGE_FORWARDER); } SrsPithyPrint *SrsPithyPrint::create_encoder() { - return new SrsPithyPrint(SRS_CONSTS_STAGE_ENCODER); + return create(SRS_CONSTS_STAGE_ENCODER); } SrsPithyPrint *SrsPithyPrint::create_exec() { - return new SrsPithyPrint(SRS_CONSTS_STAGE_EXEC); + return create(SRS_CONSTS_STAGE_EXEC); } SrsPithyPrint *SrsPithyPrint::create_ingester() { - return new SrsPithyPrint(SRS_CONSTS_STAGE_INGESTER); + return create(SRS_CONSTS_STAGE_INGESTER); } SrsPithyPrint *SrsPithyPrint::create_edge() { - return new SrsPithyPrint(SRS_CONSTS_STAGE_EDGE); + return create(SRS_CONSTS_STAGE_EDGE); } SrsPithyPrint *SrsPithyPrint::create_caster() { - return new SrsPithyPrint(SRS_CONSTS_STAGE_CASTER); + return create(SRS_CONSTS_STAGE_CASTER); } SrsPithyPrint *SrsPithyPrint::create_http_stream() { - return new SrsPithyPrint(SRS_CONSTS_STAGE_HTTP_STREAM); + return create(SRS_CONSTS_STAGE_HTTP_STREAM); } SrsPithyPrint *SrsPithyPrint::create_http_stream_cache() { - return new SrsPithyPrint(SRS_CONSTS_STAGE_HTTP_STREAM_CACHE); + return create(SRS_CONSTS_STAGE_HTTP_STREAM_CACHE); } SrsPithyPrint *SrsPithyPrint::create_rtc_play() { - return new SrsPithyPrint(SRS_CONSTS_STAGE_RTC_PLAY); + return create(SRS_CONSTS_STAGE_RTC_PLAY); } SrsPithyPrint *SrsPithyPrint::create_rtc_send(int fd) { - return new SrsPithyPrint(fd << 16 | SRS_CONSTS_STAGE_RTC_SEND); + return create(fd << 16 | SRS_CONSTS_STAGE_RTC_SEND); } SrsPithyPrint *SrsPithyPrint::create_rtc_recv(int fd) { - return new SrsPithyPrint(fd << 16 | SRS_CONSTS_STAGE_RTC_RECV); + return create(fd << 16 | SRS_CONSTS_STAGE_RTC_RECV); } SrsPithyPrint *SrsPithyPrint::create_srt_play() { - return new SrsPithyPrint(SRS_CONSTS_STAGE_SRT_PLAY); + return create(SRS_CONSTS_STAGE_SRT_PLAY); } SrsPithyPrint *SrsPithyPrint::create_srt_publish() { - return new SrsPithyPrint(SRS_CONSTS_STAGE_SRT_PUBLISH); + return create(SRS_CONSTS_STAGE_SRT_PUBLISH); } SrsPithyPrint::~SrsPithyPrint() { - leave_stage(); + if (stages_) { + leave_stage(); + } + + cache_ = NULL; + stages_ = NULL; + clk_ = NULL; } int SrsPithyPrint::enter_stage() { - SrsStageInfo *stage = _srs_stages->fetch_or_create(stage_id_); + SrsStageInfo *stage = stages_->fetch_or_create(stage_id_); srs_assert(stage != NULL); client_id_ = stage->nb_clients_++; @@ -324,7 +377,7 @@ int SrsPithyPrint::enter_stage() void SrsPithyPrint::leave_stage() { - SrsStageInfo *stage = _srs_stages->fetch_or_create(stage_id_); + SrsStageInfo *stage = stages_->fetch_or_create(stage_id_); srs_assert(stage != NULL); stage->nb_clients_--; @@ -337,23 +390,23 @@ void SrsPithyPrint::elapse() { SrsStageInfo *stage = cache_; if (!stage) { - stage = cache_ = _srs_stages->fetch_or_create(stage_id_); + stage = cache_ = stages_->fetch_or_create(stage_id_); } srs_assert(stage != NULL); - srs_utime_t diff = srs_time_now_cached() - previous_tick_; + srs_utime_t diff = clk_->now() - previous_tick_; diff = srs_max(0, diff); stage->elapse(diff); age_ += diff; - previous_tick_ = srs_time_now_cached(); + previous_tick_ = clk_->now(); } bool SrsPithyPrint::can_print() { SrsStageInfo *stage = cache_; if (!stage) { - stage = cache_ = _srs_stages->fetch_or_create(stage_id_); + stage = cache_ = stages_->fetch_or_create(stage_id_); } srs_assert(stage != NULL); diff --git a/trunk/src/kernel/srs_kernel_pithy_print.hpp b/trunk/src/kernel/srs_kernel_pithy_print.hpp index 35e036db77..7cd88abfa0 100644 --- a/trunk/src/kernel/srs_kernel_pithy_print.hpp +++ b/trunk/src/kernel/srs_kernel_pithy_print.hpp @@ -11,7 +11,9 @@ #include +class ISrsClock; class ISrsConfig; +class ISrsKernelFactory; // The stage info to calc the age. class SrsStageInfo @@ -25,6 +27,7 @@ class SrsStageInfo // The ratio for interval, 1.0 means no change. double interval_ratio_; ISrsConfig *config_; + ISrsKernelFactory *factory_; public: srs_utime_t age_; @@ -32,6 +35,7 @@ class SrsStageInfo public: SrsStageInfo(int _stage_id, double ratio = 1.0); virtual ~SrsStageInfo(); + void assemble(); // Construct object, to avoid call function in constructor. virtual void update_print_time(); public: @@ -39,9 +43,21 @@ class SrsStageInfo virtual bool can_print(); }; +// The interface for the stage manager, which owns the stages shared by clients. +class ISrsStageManager +{ +public: + ISrsStageManager(); + virtual ~ISrsStageManager(); + +public: + // Fetch a stage, create one if not exists. + virtual SrsStageInfo *fetch_or_create(int stage_id, bool *pnew = NULL) = 0; +}; + // The manager for stages, it's used for a single client stage. // Of course, we can add the multiple user support, which is SrsPithyPrint. -class SrsStageManager +class SrsStageManager : public ISrsStageManager { // clang-format off SRS_DECLARE_PRIVATE: // clang-format on @@ -53,7 +69,7 @@ class SrsStageManager public: // Fetch a stage, create one if not exists. - SrsStageInfo *fetch_or_create(int stage_id, bool *pnew = NULL); + virtual SrsStageInfo *fetch_or_create(int stage_id, bool *pnew = NULL); }; // The error pithy print is a single client stage manager, each stage only has one client. @@ -69,6 +85,7 @@ class SrsErrorPithyPrint double ratio_; SrsStageManager stages_; std::map ticks_; + ISrsClock *clk_; public: SrsErrorPithyPrint(double ratio = 1.0); @@ -88,10 +105,12 @@ class SrsAlonePithyPrint SRS_DECLARE_PRIVATE: // clang-format on SrsStageInfo info_; srs_utime_t previous_tick_; + ISrsClock *clk_; public: SrsAlonePithyPrint(); virtual ~SrsAlonePithyPrint(); + void assemble(); // Construct object, to avoid call function in constructor. public: virtual void elapse(); @@ -137,10 +156,15 @@ class SrsPithyPrint : public ISrsPithyPrint int stage_id_; srs_utime_t age_; srs_utime_t previous_tick_; + ISrsStageManager *stages_; + ISrsClock *clk_; // clang-format off SRS_DECLARE_PRIVATE: // clang-format on SrsPithyPrint(int _stage_id); + void assemble(); // Construct object, to avoid call function in constructor. + // Create a printer for the stage, the only construction site of this class. + static SrsPithyPrint *create(int stage_id); public: static SrsPithyPrint *create_rtmp_play(); diff --git a/trunk/src/protocol/srs_protocol_srt.cpp b/trunk/src/protocol/srs_protocol_srt.cpp index 4ed30238d8..1ff65ec9b0 100644 --- a/trunk/src/protocol/srs_protocol_srt.cpp +++ b/trunk/src/protocol/srs_protocol_srt.cpp @@ -709,6 +709,94 @@ ISrsSrtPoller *srs_srt_poller_new() return new SrsSrtPoller(); } +ISrsSrtOptions::ISrsSrtOptions() +{ +} + +ISrsSrtOptions::~ISrsSrtOptions() +{ +} + +// LCOV_EXCL_START +SrsSrtOptions::SrsSrtOptions() +{ +} + +SrsSrtOptions::~SrsSrtOptions() +{ +} + +srs_error_t SrsSrtOptions::set_maxbw(srs_srt_t srt_fd, int64_t maxbw) +{ + return srs_srt_set_maxbw(srt_fd, maxbw); +} + +srs_error_t SrsSrtOptions::set_mss(srs_srt_t srt_fd, int mss) +{ + return srs_srt_set_mss(srt_fd, mss); +} + +srs_error_t SrsSrtOptions::set_payload_size(srs_srt_t srt_fd, int payload_size) +{ + return srs_srt_set_payload_size(srt_fd, payload_size); +} + +srs_error_t SrsSrtOptions::set_connect_timeout(srs_srt_t srt_fd, int timeout) +{ + return srs_srt_set_connect_timeout(srt_fd, timeout); +} + +srs_error_t SrsSrtOptions::set_peer_idle_timeout(srs_srt_t srt_fd, int timeout) +{ + return srs_srt_set_peer_idle_timeout(srt_fd, timeout); +} + +srs_error_t SrsSrtOptions::set_tsbpdmode(srs_srt_t srt_fd, bool tsbpdmode) +{ + return srs_srt_set_tsbpdmode(srt_fd, tsbpdmode); +} + +srs_error_t SrsSrtOptions::set_sndbuf(srs_srt_t srt_fd, int sndbuf) +{ + return srs_srt_set_sndbuf(srt_fd, sndbuf); +} + +srs_error_t SrsSrtOptions::set_rcvbuf(srs_srt_t srt_fd, int rcvbuf) +{ + return srs_srt_set_rcvbuf(srt_fd, rcvbuf); +} + +srs_error_t SrsSrtOptions::set_tlpktdrop(srs_srt_t srt_fd, bool tlpktdrop) +{ + return srs_srt_set_tlpktdrop(srt_fd, tlpktdrop); +} + +srs_error_t SrsSrtOptions::set_latency(srs_srt_t srt_fd, int latency) +{ + return srs_srt_set_latency(srt_fd, latency); +} + +srs_error_t SrsSrtOptions::set_rcv_latency(srs_srt_t srt_fd, int rcv_latency) +{ + return srs_srt_set_rcv_latency(srt_fd, rcv_latency); +} + +srs_error_t SrsSrtOptions::set_peer_latency(srs_srt_t srt_fd, int peer_latency) +{ + return srs_srt_set_peer_latency(srt_fd, peer_latency); +} + +srs_error_t SrsSrtOptions::set_passphrase(srs_srt_t srt_fd, const std::string &passphrase) +{ + return srs_srt_set_passphrase(srt_fd, passphrase); +} + +srs_error_t SrsSrtOptions::set_pbkeylen(srs_srt_t srt_fd, int pbkeylen) +{ + return srs_srt_set_pbkeylen(srt_fd, pbkeylen); +} +// LCOV_EXCL_STOP + ISrsSrtSocket::ISrsSrtSocket() { } diff --git a/trunk/src/protocol/srs_protocol_srt.hpp b/trunk/src/protocol/srs_protocol_srt.hpp index 29cc7dcc9c..b45aaccc27 100644 --- a/trunk/src/protocol/srs_protocol_srt.hpp +++ b/trunk/src/protocol/srs_protocol_srt.hpp @@ -119,6 +119,54 @@ class ISrsSrtPoller }; ISrsSrtPoller *srs_srt_poller_new(); +// The SRT socket options, which set the srs_srt_set_xxx options on a SRT fd. +class ISrsSrtOptions +{ +public: + ISrsSrtOptions(); + virtual ~ISrsSrtOptions(); + +public: + virtual srs_error_t set_maxbw(srs_srt_t srt_fd, int64_t maxbw) = 0; + virtual srs_error_t set_mss(srs_srt_t srt_fd, int mss) = 0; + virtual srs_error_t set_payload_size(srs_srt_t srt_fd, int payload_size) = 0; + virtual srs_error_t set_connect_timeout(srs_srt_t srt_fd, int timeout) = 0; + virtual srs_error_t set_peer_idle_timeout(srs_srt_t srt_fd, int timeout) = 0; + virtual srs_error_t set_tsbpdmode(srs_srt_t srt_fd, bool tsbpdmode) = 0; + virtual srs_error_t set_sndbuf(srs_srt_t srt_fd, int sndbuf) = 0; + virtual srs_error_t set_rcvbuf(srs_srt_t srt_fd, int rcvbuf) = 0; + virtual srs_error_t set_tlpktdrop(srs_srt_t srt_fd, bool tlpktdrop) = 0; + virtual srs_error_t set_latency(srs_srt_t srt_fd, int latency) = 0; + virtual srs_error_t set_rcv_latency(srs_srt_t srt_fd, int rcv_latency) = 0; + virtual srs_error_t set_peer_latency(srs_srt_t srt_fd, int peer_latency) = 0; + virtual srs_error_t set_passphrase(srs_srt_t srt_fd, const std::string &passphrase) = 0; + virtual srs_error_t set_pbkeylen(srs_srt_t srt_fd, int pbkeylen) = 0; +}; + +// The SRT socket options, by the srs_srt_set_xxx functions. +class SrsSrtOptions : public ISrsSrtOptions +{ +public: + SrsSrtOptions(); + virtual ~SrsSrtOptions(); + +public: + virtual srs_error_t set_maxbw(srs_srt_t srt_fd, int64_t maxbw); + virtual srs_error_t set_mss(srs_srt_t srt_fd, int mss); + virtual srs_error_t set_payload_size(srs_srt_t srt_fd, int payload_size); + virtual srs_error_t set_connect_timeout(srs_srt_t srt_fd, int timeout); + virtual srs_error_t set_peer_idle_timeout(srs_srt_t srt_fd, int timeout); + virtual srs_error_t set_tsbpdmode(srs_srt_t srt_fd, bool tsbpdmode); + virtual srs_error_t set_sndbuf(srs_srt_t srt_fd, int sndbuf); + virtual srs_error_t set_rcvbuf(srs_srt_t srt_fd, int rcvbuf); + virtual srs_error_t set_tlpktdrop(srs_srt_t srt_fd, bool tlpktdrop); + virtual srs_error_t set_latency(srs_srt_t srt_fd, int latency); + virtual srs_error_t set_rcv_latency(srs_srt_t srt_fd, int rcv_latency); + virtual srs_error_t set_peer_latency(srs_srt_t srt_fd, int peer_latency); + virtual srs_error_t set_passphrase(srs_srt_t srt_fd, const std::string &passphrase); + virtual srs_error_t set_pbkeylen(srs_srt_t srt_fd, int pbkeylen); +}; + // Srt socket interface. class ISrsSrtSocket { diff --git a/trunk/src/utest/srs_utest_ai05.cpp b/trunk/src/utest/srs_utest_ai05.cpp index 961a9571e8..4ab4aaa3a0 100644 --- a/trunk/src/utest/srs_utest_ai05.cpp +++ b/trunk/src/utest/srs_utest_ai05.cpp @@ -434,10 +434,12 @@ VOID TEST(KernelResourceTest, SrsSharedResourceBasic) MockSrsHourGlass::MockSrsHourGlass() { + notify_error_ = srs_success; } MockSrsHourGlass::~MockSrsHourGlass() { + srs_freep(notify_error_); } srs_error_t MockSrsHourGlass::notify(int event, srs_utime_t interval, srs_utime_t tick) @@ -445,7 +447,12 @@ srs_error_t MockSrsHourGlass::notify(int event, srs_utime_t interval, srs_utime_ events_.push_back(event); intervals_.push_back(interval); ticks_.push_back(tick); - return srs_success; + + // Hand the error over to the caller, which owns it from now on. + srs_error_t err = notify_error_; + notify_error_ = srs_success; + + return err; } void MockSrsHourGlass::clear() @@ -457,16 +464,23 @@ void MockSrsHourGlass::clear() MockSrsFastTimer::MockSrsFastTimer() { + on_timer_error_ = srs_success; } MockSrsFastTimer::~MockSrsFastTimer() { + srs_freep(on_timer_error_); } srs_error_t MockSrsFastTimer::on_timer(srs_utime_t interval) { timer_calls_.push_back(interval); - return srs_success; + + // Hand the error over to the caller, which owns it from now on. + srs_error_t err = on_timer_error_; + on_timer_error_ = srs_success; + + return err; } void MockSrsFastTimer::clear() @@ -474,6 +488,156 @@ void MockSrsFastTimer::clear() timer_calls_.clear(); } +MockUnsubscribingFastTimer::MockUnsubscribingFastTimer() +{ + timer_ = NULL; + target_ = NULL; + on_timer_count_ = 0; +} + +MockUnsubscribingFastTimer::~MockUnsubscribingFastTimer() +{ +} + +srs_error_t MockUnsubscribingFastTimer::on_timer(srs_utime_t interval) +{ + on_timer_count_++; + + if (timer_ && target_) { + timer_->unsubscribe(target_); + } + + return srs_success; +} + +MockCoroutineForFastTimer::MockCoroutineForFastTimer() +{ + pull_count_ = 0; + pull_success_ = 0; + start_count_ = 0; + stop_count_ = 0; + start_error_ = srs_success; +} + +MockCoroutineForFastTimer::~MockCoroutineForFastTimer() +{ + srs_freep(start_error_); +} + +srs_error_t MockCoroutineForFastTimer::start() +{ + start_count_++; + + srs_error_t err = start_error_; + start_error_ = srs_success; + + return err; +} + +void MockCoroutineForFastTimer::stop() +{ + stop_count_++; +} + +void MockCoroutineForFastTimer::interrupt() +{ +} + +srs_error_t MockCoroutineForFastTimer::pull() +{ + if (++pull_count_ <= pull_success_) { + return srs_success; + } + return srs_error_new(ERROR_THREAD_INTERRUPED, "interrupted"); +} + +const SrsContextId &MockCoroutineForFastTimer::cid() +{ + return cid_; +} + +void MockCoroutineForFastTimer::set_cid(const SrsContextId &cid) +{ + cid_ = cid; +} + +MockTimeForFastTimer::MockTimeForFastTimer() +{ +} + +MockTimeForFastTimer::~MockTimeForFastTimer() +{ +} + +void MockTimeForFastTimer::usleep(srs_utime_t duration) +{ + usleep_calls_.push_back(duration); +} + +MockContextForFastTimer::MockContextForFastTimer() +{ + get_id_count_ = 0; +} + +MockContextForFastTimer::~MockContextForFastTimer() +{ +} + +SrsContextId MockContextForFastTimer::generate_id() +{ + return id_; +} + +const SrsContextId &MockContextForFastTimer::get_id() +{ + get_id_count_++; + return id_; +} + +const SrsContextId &MockContextForFastTimer::set_id(const SrsContextId &v) +{ + id_ = v; + return id_; +} + +MockKernelFactoryForFastTimer::MockKernelFactoryForFastTimer() +{ + coroutine_ = NULL; + time_ = NULL; + create_coroutine_count_ = 0; + create_time_count_ = 0; + coroutine_handler_ = NULL; +} + +MockKernelFactoryForFastTimer::~MockKernelFactoryForFastTimer() +{ +} + +ISrsCoroutine *MockKernelFactoryForFastTimer::create_coroutine(const std::string &name, ISrsCoroutineHandler *handler, SrsContextId cid) +{ + create_coroutine_count_++; + coroutine_name_ = name; + coroutine_handler_ = handler; + coroutine_cid_ = cid; + return coroutine_; +} + +ISrsTime *MockKernelFactoryForFastTimer::create_time() +{ + create_time_count_++; + return time_; +} + +ISrsConfig *MockKernelFactoryForFastTimer::create_config() +{ + return NULL; +} + +ISrsCond *MockKernelFactoryForFastTimer::create_cond() +{ + return NULL; +} + // Tests for srs_kernel_hourglass.hpp VOID TEST(KernelHourglassTest, ISrsHourGlassHandlerInterface) { @@ -699,6 +863,7 @@ VOID TEST(KernelStreamTest, SimpleStreamAppending) VOID TEST(KernelPithyPrintTest, AlonePithyPrint) { SrsAlonePithyPrint print; + print.assemble(); // The behavior depends on internal timing, just verify it doesn't crash bool can_print_initial = print.can_print(); @@ -4126,6 +4291,503 @@ VOID TEST(KernelHourglassTest, SrsFastTimer_destructor) EXPECT_TRUE(true); } +// The shared fast timer is the heartbeat every subscriber depends on, so its construction must stay +// quiescent and its dispatch loop must be drivable with an injected coroutine and clock. +VOID TEST(KernelHourglassTest, SrsFastTimerAssembleCreatesCoroutineFromInjectedFactory) +{ + MockCoroutineForFastTimer trd; + MockTimeForFastTimer time; + + MockKernelFactoryForFastTimer factory; + factory.coroutine_ = &trd; + factory.time_ = &time; + + MockContextForFastTimer context; + context.set_id(SrsContextId().set_value("timer-cid")); + + SrsFastTimer timer("shared", 20 * SRS_UTIME_MILLISECONDS); + + // Construction reaches no collaborator, so a test can replace them before any work happens. + EXPECT_TRUE(NULL == timer.trd_); + EXPECT_TRUE(NULL == timer.time_); + + timer.factory_ = &factory; + timer.context_ = &context; + timer.assemble(); + + EXPECT_EQ(1, factory.create_coroutine_count_); + EXPECT_EQ(1, factory.create_time_count_); + EXPECT_STREQ("shared", factory.coroutine_name_.c_str()); + EXPECT_TRUE(&timer == factory.coroutine_handler_); + EXPECT_STREQ("timer-cid", factory.coroutine_cid_.c_str()); + EXPECT_TRUE(&trd == timer.trd_); + EXPECT_TRUE(&time == timer.time_); + + // The mocks are borrowed, so the destructor must not free them. + timer.trd_ = NULL; + timer.time_ = NULL; + timer.factory_ = NULL; + timer.context_ = NULL; +} + +VOID TEST(KernelHourglassTest, SrsFastTimerCycleTicksEverySubscriberAndSleeps) +{ + MockCoroutineForFastTimer trd; + MockTimeForFastTimer time; + + MockKernelFactoryForFastTimer factory; + factory.coroutine_ = &trd; + factory.time_ = &time; + + MockContextForFastTimer context; + + SrsFastTimer timer("shared", 20 * SRS_UTIME_MILLISECONDS); + timer.factory_ = &factory; + timer.context_ = &context; + timer.assemble(); + + // Without the injected coroutine and clock, cycle() would run the real timer forever. + ASSERT_TRUE(&trd == timer.trd_); + ASSERT_TRUE(&time == timer.time_); + + MockSrsFastTimer first; + MockSrsFastTimer second; + timer.subscribe(&first); + timer.subscribe(&second); + + // Two rounds, then the coroutine is interrupted. + trd.pull_success_ = 2; + + srs_error_t err = timer.cycle(); + EXPECT_EQ(ERROR_THREAD_INTERRUPED, srs_error_code(err)); + srs_freep(err); + + ASSERT_EQ(2, (int)first.timer_calls_.size()); + EXPECT_EQ(20 * SRS_UTIME_MILLISECONDS, first.timer_calls_[0]); + EXPECT_EQ(20 * SRS_UTIME_MILLISECONDS, first.timer_calls_[1]); + ASSERT_EQ(2, (int)second.timer_calls_.size()); + EXPECT_EQ(20 * SRS_UTIME_MILLISECONDS, second.timer_calls_[0]); + + // One sleep of the configured interval per round, after the handlers ran. + ASSERT_EQ(2, (int)time.usleep_calls_.size()); + EXPECT_EQ(20 * SRS_UTIME_MILLISECONDS, time.usleep_calls_[0]); + EXPECT_EQ(20 * SRS_UTIME_MILLISECONDS, time.usleep_calls_[1]); + + timer.trd_ = NULL; + timer.time_ = NULL; + timer.factory_ = NULL; + timer.context_ = NULL; +} + +VOID TEST(KernelHourglassTest, SrsFastTimerCycleIgnoresHandlerError) +{ + MockCoroutineForFastTimer trd; + MockTimeForFastTimer time; + + MockKernelFactoryForFastTimer factory; + factory.coroutine_ = &trd; + factory.time_ = &time; + + MockContextForFastTimer context; + + SrsFastTimer timer("shared", 100 * SRS_UTIME_MILLISECONDS); + timer.factory_ = &factory; + timer.context_ = &context; + timer.assemble(); + + ASSERT_TRUE(&trd == timer.trd_); + ASSERT_TRUE(&time == timer.time_); + + MockSrsFastTimer failed; + MockSrsFastTimer healthy; + timer.subscribe(&failed); + timer.subscribe(&healthy); + + // The first handler fails on the first round only. + failed.on_timer_error_ = srs_error_new(ERROR_SYSTEM_ASSERT_FAILED, "handler"); + trd.pull_success_ = 2; + + srs_error_t err = timer.cycle(); + EXPECT_EQ(ERROR_THREAD_INTERRUPED, srs_error_code(err)); + srs_freep(err); + + // A failing subscriber neither stops the round nor the timer: the shared timer swallows it. + EXPECT_EQ(2, (int)failed.timer_calls_.size()); + EXPECT_EQ(2, (int)healthy.timer_calls_.size()); + EXPECT_EQ(2, (int)time.usleep_calls_.size()); + + timer.trd_ = NULL; + timer.time_ = NULL; + timer.factory_ = NULL; + timer.context_ = NULL; +} + +VOID TEST(KernelHourglassTest, SrsFastTimerCycleTicksOnlySubscribedHandlers) +{ + MockCoroutineForFastTimer trd; + MockTimeForFastTimer time; + + MockKernelFactoryForFastTimer factory; + factory.coroutine_ = &trd; + factory.time_ = &time; + + MockContextForFastTimer context; + + SrsFastTimer timer("shared", 1 * SRS_UTIME_SECONDS); + timer.factory_ = &factory; + timer.context_ = &context; + timer.assemble(); + + ASSERT_TRUE(&trd == timer.trd_); + ASSERT_TRUE(&time == timer.time_); + + MockSrsFastTimer kept; + MockSrsFastTimer removed; + + // Subscribing twice must not tick the handler twice. + timer.subscribe(&kept); + timer.subscribe(&kept); + timer.subscribe(&removed); + timer.unsubscribe(&removed); + + trd.pull_success_ = 1; + + srs_error_t err = timer.cycle(); + EXPECT_EQ(ERROR_THREAD_INTERRUPED, srs_error_code(err)); + srs_freep(err); + + EXPECT_EQ(1, (int)kept.timer_calls_.size()); + EXPECT_EQ(0, (int)removed.timer_calls_.size()); + + timer.trd_ = NULL; + timer.time_ = NULL; + timer.factory_ = NULL; + timer.context_ = NULL; +} + +VOID TEST(KernelHourglassTest, SrsFastTimerCycleNotifiesEveryHandlerWhenOneUnsubscribesItself) +{ + MockCoroutineForFastTimer trd; + MockTimeForFastTimer time; + + MockKernelFactoryForFastTimer factory; + factory.coroutine_ = &trd; + factory.time_ = &time; + + MockContextForFastTimer context; + + SrsFastTimer timer("shared", 20 * SRS_UTIME_MILLISECONDS); + timer.factory_ = &factory; + timer.context_ = &context; + timer.assemble(); + + ASSERT_TRUE(&trd == timer.trd_); + ASSERT_TRUE(&time == timer.time_); + + // The first handler leaves the timer while the round is still running, as a connection torn + // down inside its own callback does. + MockUnsubscribingFastTimer first; + first.timer_ = &timer; + first.target_ = &first; + MockSrsFastTimer second; + MockSrsFastTimer third; + + timer.subscribe(&first); + timer.subscribe(&second); + timer.subscribe(&third); + + trd.pull_success_ = 1; + + srs_error_t err = timer.cycle(); + EXPECT_EQ(ERROR_THREAD_INTERRUPED, srs_error_code(err)); + srs_freep(err); + + // Every handler still subscribed when the round reaches it must be notified. + EXPECT_EQ(1, first.on_timer_count_); + EXPECT_EQ(1, (int)second.timer_calls_.size()); + EXPECT_EQ(1, (int)third.timer_calls_.size()); + + // The handler that left is gone once the round ends. + EXPECT_EQ(2, (int)timer.handlers_.size()); + + timer.trd_ = NULL; + timer.time_ = NULL; + timer.factory_ = NULL; + timer.context_ = NULL; +} + +VOID TEST(KernelHourglassTest, SrsFastTimerCycleSkipsHandlerUnsubscribedDuringTheRound) +{ + MockCoroutineForFastTimer trd; + MockTimeForFastTimer time; + + MockKernelFactoryForFastTimer factory; + factory.coroutine_ = &trd; + factory.time_ = &time; + + MockContextForFastTimer context; + + SrsFastTimer timer("shared", 20 * SRS_UTIME_MILLISECONDS); + timer.factory_ = &factory; + timer.context_ = &context; + timer.assemble(); + + ASSERT_TRUE(&trd == timer.trd_); + ASSERT_TRUE(&time == timer.time_); + + MockSrsFastTimer second; + MockSrsFastTimer third; + + // The first handler removes a handler the round has not reached yet. + MockUnsubscribingFastTimer first; + first.timer_ = &timer; + first.target_ = &third; + + timer.subscribe(&first); + timer.subscribe(&second); + timer.subscribe(&third); + + trd.pull_success_ = 1; + + srs_error_t err = timer.cycle(); + EXPECT_EQ(ERROR_THREAD_INTERRUPED, srs_error_code(err)); + srs_freep(err); + + // A handler that unsubscribed during the round must not be notified afterwards: it may already + // be destroyed. This holds today too, and the round must keep it that way. + EXPECT_EQ(1, first.on_timer_count_); + EXPECT_EQ(1, (int)second.timer_calls_.size()); + EXPECT_EQ(0, (int)third.timer_calls_.size()); + EXPECT_EQ(2, (int)timer.handlers_.size()); + + timer.trd_ = NULL; + timer.time_ = NULL; + timer.factory_ = NULL; + timer.context_ = NULL; +} + +VOID TEST(KernelHourglassTest, SrsFastTimerCycleNeverNotifiesAFreedHandler) +{ + // A subscriber that unsubscribes from its own destructor, as SrsRtcPublishRtcpTimer, + // SrsRtcPublishTwccTimer and SrsRtcConnectionNackTimer all do. + class SelfRemovingHandler : public ISrsFastTimerHandler + { + public: + ISrsFastTimer *timer_; + int calls_; + SelfRemovingHandler() + { + timer_ = NULL; + calls_ = 0; + } + virtual ~SelfRemovingHandler() + { + if (timer_) { + timer_->unsubscribe(this); + } + } + virtual srs_error_t on_timer(srs_utime_t interval) + { + calls_++; + return srs_success; + } + }; + + // A handler that frees another subscriber during the round, as a connection torn down while + // this callback yields on I/O does. + class DeletingHandler : public ISrsFastTimerHandler + { + public: + SelfRemovingHandler *victim_; + DeletingHandler() + { + victim_ = NULL; + } + virtual srs_error_t on_timer(srs_utime_t interval) + { + srs_freep(victim_); + return srs_success; + } + }; + + MockCoroutineForFastTimer trd; + MockTimeForFastTimer time; + MockKernelFactoryForFastTimer factory; + factory.coroutine_ = &trd; + factory.time_ = &time; + MockContextForFastTimer context; + + SrsFastTimer timer("shared", 20 * SRS_UTIME_MILLISECONDS); + timer.factory_ = &factory; + timer.context_ = &context; + timer.assemble(); + + SelfRemovingHandler *victim = new SelfRemovingHandler(); + victim->timer_ = &timer; + + DeletingHandler deleter; + deleter.victim_ = victim; + + timer.subscribe(&deleter); + timer.subscribe(victim); + + trd.pull_success_ = 1; + + // The freed subscriber must not be notified after the handler that freed it. Walking a copy of + // the subscriber list taken before the round would read that freed object here, which the + // sanitizer build reports as a heap-use-after-free. + srs_error_t err = timer.cycle(); + EXPECT_EQ(ERROR_THREAD_INTERRUPED, srs_error_code(err)); + srs_freep(err); + + // Only the handler that did the freeing is left. + EXPECT_EQ(1, (int)timer.handlers_.size()); + + timer.trd_ = NULL; + timer.time_ = NULL; + timer.factory_ = NULL; + timer.context_ = NULL; +} + +VOID TEST(KernelHourglassTest, SrsHourGlassAssembleCreatesCoroutineFromInjectedFactory) +{ + MockCoroutineForFastTimer trd; + MockTimeForFastTimer time; + + MockKernelFactoryForFastTimer factory; + factory.coroutine_ = &trd; + factory.time_ = &time; + + MockContextForFastTimer context; + context.set_id(SrsContextId().set_value("hourglass-cid")); + + MockSrsHourGlass handler; + SrsHourGlass hourglass("sources", &handler, 1 * SRS_UTIME_SECONDS); + + // Construction reaches no collaborator, so a test can replace them before any work happens. + EXPECT_TRUE(NULL == hourglass.trd_); + EXPECT_TRUE(NULL == hourglass.time_); + + hourglass.factory_ = &factory; + hourglass.context_ = &context; + hourglass.assemble(); + + EXPECT_EQ(1, factory.create_coroutine_count_); + EXPECT_EQ(1, factory.create_time_count_); + // The coroutine is named after the label, which the hourglass now keeps until assemble(). + EXPECT_STREQ("timer-sources", factory.coroutine_name_.c_str()); + EXPECT_TRUE(&hourglass == factory.coroutine_handler_); + EXPECT_STREQ("hourglass-cid", factory.coroutine_cid_.c_str()); + EXPECT_TRUE(&trd == hourglass.trd_); + EXPECT_TRUE(&time == hourglass.time_); + + // The mocks are borrowed, so the destructor must not free them. + hourglass.trd_ = NULL; + hourglass.time_ = NULL; + hourglass.factory_ = NULL; + hourglass.context_ = NULL; +} + +VOID TEST(KernelHourglassTest, SrsHourGlassCycleNotifiesDueTicksAndSleepsResolution) +{ + MockCoroutineForFastTimer trd; + MockTimeForFastTimer time; + + MockKernelFactoryForFastTimer factory; + factory.coroutine_ = &trd; + factory.time_ = &time; + + MockContextForFastTimer context; + + MockSrsHourGlass handler; + SrsHourGlass hourglass("sources", &handler, 100 * SRS_UTIME_MILLISECONDS); + hourglass.factory_ = &factory; + hourglass.context_ = &context; + hourglass.assemble(); + + // Without the injected coroutine and clock, cycle() would run the real timer forever. + ASSERT_TRUE(&trd == hourglass.trd_); + ASSERT_TRUE(&time == hourglass.time_); + + srs_error_t err = srs_success; + HELPER_EXPECT_SUCCESS(hourglass.tick(1, 200 * SRS_UTIME_MILLISECONDS)); + HELPER_EXPECT_SUCCESS(hourglass.tick(2, 300 * SRS_UTIME_MILLISECONDS)); + + // Four rounds, then the coroutine is interrupted. + trd.pull_success_ = 4; + + err = hourglass.cycle(); + EXPECT_EQ(ERROR_THREAD_INTERRUPED, srs_error_code(err)); + srs_freep(err); + + // An event fires when the elapsed time is a multiple of its interval: at 0ms both are due, at + // 100ms neither, at 200ms event 1, at 300ms event 2. + ASSERT_EQ(4, (int)handler.events_.size()); + EXPECT_EQ(1, handler.events_[0]); + EXPECT_EQ(2, handler.events_[1]); + EXPECT_EQ(1, handler.events_[2]); + EXPECT_EQ(2, handler.events_[3]); + + // The handler is told the interval of its own event and the total elapsed time of the round. + EXPECT_EQ(200 * SRS_UTIME_MILLISECONDS, handler.intervals_[0]); + EXPECT_EQ(300 * SRS_UTIME_MILLISECONDS, handler.intervals_[1]); + EXPECT_EQ(0, handler.ticks_[0]); + EXPECT_EQ(0, handler.ticks_[1]); + EXPECT_EQ(200 * SRS_UTIME_MILLISECONDS, handler.ticks_[2]); + EXPECT_EQ(300 * SRS_UTIME_MILLISECONDS, handler.ticks_[3]); + + // One sleep of the resolution per round, whether or not an event was due. + ASSERT_EQ(4, (int)time.usleep_calls_.size()); + EXPECT_EQ(100 * SRS_UTIME_MILLISECONDS, time.usleep_calls_[0]); + EXPECT_EQ(100 * SRS_UTIME_MILLISECONDS, time.usleep_calls_[3]); + + hourglass.trd_ = NULL; + hourglass.time_ = NULL; + hourglass.factory_ = NULL; + hourglass.context_ = NULL; +} + +VOID TEST(KernelHourglassTest, SrsHourGlassCycleStopsWhenHandlerFails) +{ + MockCoroutineForFastTimer trd; + MockTimeForFastTimer time; + + MockKernelFactoryForFastTimer factory; + factory.coroutine_ = &trd; + factory.time_ = &time; + + MockContextForFastTimer context; + + MockSrsHourGlass handler; + SrsHourGlass hourglass("sources", &handler, 100 * SRS_UTIME_MILLISECONDS); + hourglass.factory_ = &factory; + hourglass.context_ = &context; + hourglass.assemble(); + + ASSERT_TRUE(&trd == hourglass.trd_); + ASSERT_TRUE(&time == hourglass.time_); + + srs_error_t err = srs_success; + HELPER_EXPECT_SUCCESS(hourglass.tick(1, 100 * SRS_UTIME_MILLISECONDS)); + + handler.notify_error_ = srs_error_new(ERROR_SYSTEM_ASSERT_FAILED, "notify"); + trd.pull_success_ = 3; + + // Unlike the shared fast timer, which swallows a subscriber error, the hourglass hands it to + // the owner of the timer: the round ends at the failed handler and the timer stops. + err = hourglass.cycle(); + EXPECT_EQ(ERROR_SYSTEM_ASSERT_FAILED, srs_error_code(err)); + srs_freep(err); + + EXPECT_EQ(1, (int)handler.events_.size()); + EXPECT_EQ(0, (int)time.usleep_calls_.size()); + + hourglass.trd_ = NULL; + hourglass.time_ = NULL; + hourglass.factory_ = NULL; + hourglass.context_ = NULL; +} + VOID TEST(KernelHourglassTest, SrsHourGlass_untick) { // Test SrsHourGlass::untick method @@ -4177,6 +4839,8 @@ VOID TEST(KernelHourglassTest, SrsHourGlass_stop) MockHourGlassHandler handler; SrsHourGlass hourglass("test", &handler, 100 * SRS_UTIME_MILLISECONDS); + // The coroutine this test starts and stops is created by assemble(), not by construction. + hourglass.assemble(); // Add a tick srs_error_t err = hourglass.tick(1, 200 * SRS_UTIME_MILLISECONDS); diff --git a/trunk/src/utest/srs_utest_ai05.hpp b/trunk/src/utest/srs_utest_ai05.hpp index c321e50993..a7ba90f488 100644 --- a/trunk/src/utest/srs_utest_ai05.hpp +++ b/trunk/src/utest/srs_utest_ai05.hpp @@ -12,8 +12,10 @@ */ #include +#include #include #include +#include #include #include #include @@ -96,6 +98,8 @@ class MockSrsHourGlass : public ISrsHourGlassHandler std::vector events_; std::vector intervals_; std::vector ticks_; + // The error to return from notify, owned by the hourglass under test. + srs_error_t notify_error_; public: MockSrsHourGlass(); @@ -108,6 +112,8 @@ class MockSrsFastTimer : public ISrsFastTimerHandler { public: std::vector timer_calls_; + // The error to return from on_timer, owned by the timer under test. + srs_error_t on_timer_error_; public: MockSrsFastTimer(); @@ -116,6 +122,105 @@ class MockSrsFastTimer : public ISrsFastTimerHandler void clear(); }; +// Mock ISrsFastTimerHandler that unsubscribes a handler while the timer is dispatching, to cover +// the subscriber list changing under SrsFastTimer::cycle(). +class MockUnsubscribingFastTimer : public ISrsFastTimerHandler +{ +public: + // The timer to unsubscribe from, and the handler to remove, which may be this one. + ISrsFastTimer *timer_; + ISrsFastTimerHandler *target_; + int on_timer_count_; + +public: + MockUnsubscribingFastTimer(); + virtual ~MockUnsubscribingFastTimer(); + +public: + virtual srs_error_t on_timer(srs_utime_t interval); +}; + +// Mock ISrsCoroutine for testing SrsFastTimer::cycle() +class MockCoroutineForFastTimer : public ISrsCoroutine +{ +public: + int pull_count_; + // Return success for the first pull_success_ pulls, then pull_error_. + int pull_success_; + int start_count_; + int stop_count_; + srs_error_t start_error_; + SrsContextId cid_; + +public: + MockCoroutineForFastTimer(); + virtual ~MockCoroutineForFastTimer(); + +public: + virtual srs_error_t start(); + virtual void stop(); + virtual void interrupt(); + virtual srs_error_t pull(); + virtual const SrsContextId &cid(); + virtual void set_cid(const SrsContextId &cid); +}; + +// Mock ISrsTime for testing SrsFastTimer::cycle() +class MockTimeForFastTimer : public ISrsTime +{ +public: + std::vector usleep_calls_; + +public: + MockTimeForFastTimer(); + virtual ~MockTimeForFastTimer(); + +public: + virtual void usleep(srs_utime_t duration); +}; + +// Mock ISrsContext for testing SrsFastTimer::assemble() +class MockContextForFastTimer : public ISrsContext +{ +public: + SrsContextId id_; + int get_id_count_; + +public: + MockContextForFastTimer(); + virtual ~MockContextForFastTimer(); + +public: + virtual SrsContextId generate_id(); + virtual const SrsContextId &get_id(); + virtual const SrsContextId &set_id(const SrsContextId &v); +}; + +// Mock ISrsKernelFactory for testing SrsFastTimer::assemble() +class MockKernelFactoryForFastTimer : public ISrsKernelFactory +{ +public: + // The objects returned by the factory, borrowed and not owned by the mock. + ISrsCoroutine *coroutine_; + ISrsTime *time_; + // What the factory was asked to create. + int create_coroutine_count_; + int create_time_count_; + std::string coroutine_name_; + ISrsCoroutineHandler *coroutine_handler_; + SrsContextId coroutine_cid_; + +public: + MockKernelFactoryForFastTimer(); + virtual ~MockKernelFactoryForFastTimer(); + +public: + virtual ISrsCoroutine *create_coroutine(const std::string &name, ISrsCoroutineHandler *handler, SrsContextId cid); + virtual ISrsTime *create_time(); + virtual ISrsConfig *create_config(); + virtual ISrsCond *create_cond(); +}; + // Mock RTP ring buffer for testing NACK receiver class MockRtpRingBuffer : public SrsRtpRingBuffer { diff --git a/trunk/src/utest/srs_utest_ai15.cpp b/trunk/src/utest/srs_utest_ai15.cpp index 542b1352c2..459d2027b2 100644 --- a/trunk/src/utest/srs_utest_ai15.cpp +++ b/trunk/src/utest/srs_utest_ai15.cpp @@ -1153,8 +1153,57 @@ void MockCoroutineForRtmpConn::set_cid(const SrsContextId &cid) { } +MockContextForRtmpConn::MockContextForRtmpConn() +{ + generate_id_count_ = 0; + set_id_count_ = 0; +} + +MockContextForRtmpConn::~MockContextForRtmpConn() +{ +} + +SrsContextId MockContextForRtmpConn::generate_id() +{ + generate_id_count_++; + return id_; +} + +const SrsContextId &MockContextForRtmpConn::get_id() +{ + return id_; +} + +const SrsContextId &MockContextForRtmpConn::set_id(const SrsContextId &v) +{ + set_id_count_++; + id_ = v; + return id_; +} + +MockAppFactoryForRtmpConn::MockAppFactoryForRtmpConn() +{ + coroutine_ = NULL; + create_coroutine_count_ = 0; + coroutine_handler_ = NULL; +} + +MockAppFactoryForRtmpConn::~MockAppFactoryForRtmpConn() +{ +} + +ISrsCoroutine *MockAppFactoryForRtmpConn::create_coroutine(const std::string &name, ISrsCoroutineHandler *handler, SrsContextId cid) +{ + create_coroutine_count_++; + coroutine_name_ = name; + coroutine_handler_ = handler; + coroutine_cid_ = cid; + return coroutine_; +} + MockRtmpTransportForDoCycle::MockRtmpTransportForDoCycle() { + io_count_ = 0; } MockRtmpTransportForDoCycle::~MockRtmpTransportForDoCycle() @@ -1183,6 +1232,7 @@ int MockRtmpTransportForDoCycle::osfd() ISrsProtocolReadWriter *MockRtmpTransportForDoCycle::io() { + io_count_++; return NULL; } @@ -1268,6 +1318,58 @@ VOID TEST(SrsRtmpConnTest, ConstructorAndAssemble) srs_freep(mock_config); } +// The RTMP connection is the primary publish and play path, so a test has to be able to replace its +// context, its coroutine factory and its transport before any of them is used. Construction must +// therefore stay quiescent, and every collaborator call must happen in assemble(). +VOID TEST(SrsRtmpConnTest, AssembleWiresCollaboratorsFromInjectedDependencies) +{ + MockRtmpTransportForDoCycle *transport = new MockRtmpTransportForDoCycle(); + SrsRtmpConn *conn = new SrsRtmpConn(transport, "192.168.1.100", 1935); + + // GOAL: construction reaches no collaborator, so a test can replace them before any work runs. + EXPECT_TRUE(NULL == conn->trd_); + EXPECT_TRUE(NULL == conn->rtmp_); + EXPECT_EQ(0, conn->create_time_); + EXPECT_EQ(0, transport->io_count_); + + MockCoroutineForRtmpConn trd; + + MockAppFactoryForRtmpConn factory; + factory.coroutine_ = &trd; + + MockContextForRtmpConn context; + context.id_ = SrsContextId().set_value("rtmp-cid"); + + MockAppConfigForRtmpConn config; + + conn->app_factory_ = &factory; + conn->context_ = &context; + conn->config_ = &config; + conn->assemble(); + + // GOAL: the client identity, the coroutine, the transport wiring and the reload subscription + // are all established by assemble(), through the injected dependencies. + EXPECT_EQ(1, context.generate_id_count_); + EXPECT_EQ(1, context.set_id_count_); + EXPECT_EQ(1, factory.create_coroutine_count_); + EXPECT_STREQ("rtmp", factory.coroutine_name_.c_str()); + EXPECT_TRUE(conn == factory.coroutine_handler_); + EXPECT_STREQ("rtmp-cid", factory.coroutine_cid_.c_str()); + EXPECT_TRUE(&trd == conn->trd_); + EXPECT_TRUE(NULL != conn->rtmp_); + EXPECT_TRUE(conn->create_time_ > 0); + EXPECT_TRUE(transport->io_count_ > 0); + EXPECT_EQ(1, config.subscribe_count_); + EXPECT_TRUE(conn == config.last_subscribed_handler_); + + // The coroutine is borrowed from the mock factory, so the destructor must not free it. + conn->trd_ = NULL; + conn->app_factory_ = NULL; + conn->context_ = NULL; + srs_freep(conn); + EXPECT_EQ(1, config.unsubscribe_count_); +} + VOID TEST(SrsServerTest, OnBeforeConnectionExceedLimit) { srs_error_t err = srs_success; @@ -1411,6 +1513,7 @@ VOID TEST(SrsRtmpConnTest, StreamServiceCycleSelection) // Create connection SrsRtmpConn *conn = new SrsRtmpConn(mock_transport, "192.168.1.100", 1935); + conn->assemble(); // Create mock rtmp server MockRtmpServer *mock_rtmp = new MockRtmpServer(); @@ -2526,6 +2629,7 @@ VOID TEST(SrsRtmpConnTest, HttpHooksOnClose) // Inject mocks into connection conn->config_ = mock_config; + conn->assemble(); conn->hooks_ = mock_hooks; // Set up request with valid vhost @@ -2684,6 +2788,7 @@ VOID TEST(SrsRtmpConnTest, HttpHooksOnPublishSuccess) // Inject mocks into connection conn->config_ = mock_config; + conn->assemble(); conn->hooks_ = mock_hooks; // Set up request with valid vhost @@ -2765,6 +2870,7 @@ VOID TEST(SrsRtmpConnTest, HttpHooksOnUnpublishSuccess) // Inject mocks into connection conn->config_ = mock_config; + conn->assemble(); conn->hooks_ = mock_hooks; // Set up request with valid vhost @@ -2846,6 +2952,7 @@ VOID TEST(SrsRtmpConnTest, HttpHooksOnStopSuccess) // Inject mocks into connection conn->config_ = mock_config; + conn->assemble(); conn->hooks_ = mock_hooks; // Set up request with valid vhost @@ -3000,6 +3107,7 @@ VOID TEST(SrsRtmpConnTest, HttpHooksOnPlaySuccess) // Inject mocks into connection conn->config_ = mock_config; + conn->assemble(); conn->hooks_ = mock_hooks; // Set up request with valid vhost diff --git a/trunk/src/utest/srs_utest_ai15.hpp b/trunk/src/utest/srs_utest_ai15.hpp index b1068c6e8b..da4c70a41d 100644 --- a/trunk/src/utest/srs_utest_ai15.hpp +++ b/trunk/src/utest/srs_utest_ai15.hpp @@ -357,9 +357,50 @@ class MockCoroutineForRtmpConn : public ISrsCoroutine virtual void set_cid(const SrsContextId &cid); }; +// Mock ISrsContext for testing SrsRtmpConn::assemble() +class MockContextForRtmpConn : public ISrsContext +{ +public: + SrsContextId id_; + int generate_id_count_; + int set_id_count_; + +public: + MockContextForRtmpConn(); + virtual ~MockContextForRtmpConn(); + +public: + virtual SrsContextId generate_id(); + virtual const SrsContextId &get_id(); + virtual const SrsContextId &set_id(const SrsContextId &v); +}; + +// Mock ISrsAppFactory for testing SrsRtmpConn::assemble() +class MockAppFactoryForRtmpConn : public SrsAppFactory +{ +public: + // The coroutine returned by the factory, borrowed and not owned by the mock. + ISrsCoroutine *coroutine_; + // What the factory was asked to create. + int create_coroutine_count_; + std::string coroutine_name_; + ISrsCoroutineHandler *coroutine_handler_; + SrsContextId coroutine_cid_; + +public: + MockAppFactoryForRtmpConn(); + virtual ~MockAppFactoryForRtmpConn(); + +public: + virtual ISrsCoroutine *create_coroutine(const std::string &name, ISrsCoroutineHandler *handler, SrsContextId cid); +}; + // Mock ISrsRtmpTransport for testing SrsRtmpConn::do_cycle() class MockRtmpTransportForDoCycle : public ISrsRtmpTransport { +public: + int io_count_; + public: MockRtmpTransportForDoCycle(); virtual ~MockRtmpTransportForDoCycle(); diff --git a/trunk/src/utest/srs_utest_ai16.cpp b/trunk/src/utest/srs_utest_ai16.cpp index ea6387f941..9a22bf10eb 100644 --- a/trunk/src/utest/srs_utest_ai16.cpp +++ b/trunk/src/utest/srs_utest_ai16.cpp @@ -10,6 +10,7 @@ using namespace std; #include #include #include +#include #include #include #include @@ -780,6 +781,7 @@ MockStatisticForLiveStream::MockStatisticForLiveStream() { on_client_count_ = 0; on_client_error_ = srs_success; + on_disconnect_count_ = 0; } MockStatisticForLiveStream::~MockStatisticForLiveStream() @@ -788,6 +790,8 @@ MockStatisticForLiveStream::~MockStatisticForLiveStream() void MockStatisticForLiveStream::on_disconnect(std::string id, srs_error_t err) { + on_disconnect_count_++; + on_disconnect_ids_.push_back(id); } srs_error_t MockStatisticForLiveStream::on_client(std::string id, ISrsRequest *req, ISrsExpire *conn, SrsRtmpConnType type) @@ -899,6 +903,7 @@ MockAppConfigForLiveStreamHooks::MockAppConfigForLiveStreamHooks() http_hooks_enabled_ = false; on_play_directive_ = NULL; on_stop_directive_ = NULL; + hls_window_ = 60 * SRS_UTIME_SECONDS; } MockAppConfigForLiveStreamHooks::~MockAppConfigForLiveStreamHooks() @@ -922,6 +927,11 @@ SrsConfDirective *MockAppConfigForLiveStreamHooks::get_vhost_on_stop(std::string return on_stop_directive_; } +srs_utime_t MockAppConfigForLiveStreamHooks::get_hls_window(std::string vhost) +{ + return hls_window_; +} + // Mock HTTP hooks implementation for SrsLiveStream testing MockHttpHooksForLiveStream::MockHttpHooksForLiveStream() { @@ -3536,3 +3546,97 @@ VOID TEST(SrsGoApiMetricsTest, ServeHttpSuccess) api->stat_ = NULL; api->config_ = NULL; } + +// Add a virtual HLS connection to the stream, as SrsHlsStream::alive() does for a new ctx, but with +// a chosen last request time so the expiry decision in on_timer() is deterministic. +static void mock_hls_stream_add_ctx(SrsHlsStream *hls, std::string ctx, srs_utime_t request_time) +{ + SrsHlsVirtualConn *conn = new SrsHlsVirtualConn(); + conn->req_ = new MockRequest("test.vhost", "live", "stream1"); + conn->ctx_ = ctx; + conn->request_time_ = request_time; + hls->map_ctx_info_.insert(std::make_pair(ctx, conn)); +} + +// An HLS session expires when it is idle for more than twice the HLS window of its vhost. The +// window must come from the injected config, and the stop hook and disconnect statistic must go to +// the injected collaborators, so a test controls expiry without touching the process globals. +VOID TEST(HlsStreamTest, TimerExpiresIdleSessionThroughInjectedDependencies) +{ + srs_error_t err = srs_success; + + MockAppConfigForLiveStreamHooks config; + config.hls_window_ = 1 * SRS_UTIME_SECONDS; + config.http_hooks_enabled_ = true; + config.on_stop_directive_ = new SrsConfDirective(); + config.on_stop_directive_->name_ = "on_stop"; + config.on_stop_directive_->args_.push_back("http://127.0.0.1:8085/api/v1/sessions"); + + MockStatisticForLiveStream stat; + MockHttpHooksForLiveStream hooks; + + // The constructor only captures dependencies, and assemble() is not called, so the stream is + // never subscribed to the real shared timer. + SrsUniquePtr hls(new SrsHlsStream()); + hls->config_ = &config; + hls->stat_ = &stat; + hls->hooks_ = &hooks; + hls->shared_timer_ = NULL; + + // Idle for 3s, beyond 2 * 1s, so it expires. The other one was just requested, so it stays. + srs_utime_t now = srs_time_now_cached(); + mock_hls_stream_add_ctx(hls.get(), "idle", now - 3 * SRS_UTIME_SECONDS); + mock_hls_stream_add_ctx(hls.get(), "active", now); + + HELPER_EXPECT_SUCCESS(hls->on_timer(5 * SRS_UTIME_SECONDS)); + + EXPECT_EQ(1, (int)hls->map_ctx_info_.size()); + EXPECT_TRUE(hls->ctx_is_exist("active")); + EXPECT_FALSE(hls->ctx_is_exist("idle")); + + EXPECT_EQ(1, hooks.on_stop_count_); + if (hooks.on_stop_count_ == 1) { + EXPECT_STREQ("http://127.0.0.1:8085/api/v1/sessions", hooks.on_stop_calls_[0].first.c_str()); + } + + EXPECT_EQ(1, stat.on_disconnect_count_); + if (stat.on_disconnect_count_ == 1) { + EXPECT_STREQ("idle", stat.on_disconnect_ids_[0].c_str()); + } + + hls->config_ = NULL; + hls->stat_ = NULL; + hls->hooks_ = NULL; +} + +// With HTTP hooks disabled, an expired HLS session fires no stop hook, but its disconnect must still +// reach the injected statistic, so the viewer does not stay in the client list. +VOID TEST(HlsStreamTest, TimerDisconnectsExpiredSessionWithoutHooks) +{ + srs_error_t err = srs_success; + + MockAppConfigForLiveStreamHooks config; + config.hls_window_ = 1 * SRS_UTIME_SECONDS; + config.http_hooks_enabled_ = false; + + MockStatisticForLiveStream stat; + MockHttpHooksForLiveStream hooks; + + SrsUniquePtr hls(new SrsHlsStream()); + hls->config_ = &config; + hls->stat_ = &stat; + hls->hooks_ = &hooks; + hls->shared_timer_ = NULL; + + mock_hls_stream_add_ctx(hls.get(), "idle", srs_time_now_cached() - 3 * SRS_UTIME_SECONDS); + + HELPER_EXPECT_SUCCESS(hls->on_timer(5 * SRS_UTIME_SECONDS)); + + EXPECT_TRUE(hls->map_ctx_info_.empty()); + EXPECT_EQ(0, hooks.on_stop_count_); + EXPECT_EQ(1, stat.on_disconnect_count_); + + hls->config_ = NULL; + hls->stat_ = NULL; + hls->hooks_ = NULL; +} diff --git a/trunk/src/utest/srs_utest_ai16.hpp b/trunk/src/utest/srs_utest_ai16.hpp index 3fc77dd31b..5c3356302b 100644 --- a/trunk/src/utest/srs_utest_ai16.hpp +++ b/trunk/src/utest/srs_utest_ai16.hpp @@ -71,6 +71,7 @@ class MockAppConfigForLiveStreamHooks : public MockAppConfig bool http_hooks_enabled_; SrsConfDirective *on_play_directive_; SrsConfDirective *on_stop_directive_; + srs_utime_t hls_window_; public: MockAppConfigForLiveStreamHooks(); @@ -80,6 +81,7 @@ class MockAppConfigForLiveStreamHooks : public MockAppConfig virtual bool get_vhost_http_hooks_enabled(std::string vhost); virtual SrsConfDirective *get_vhost_on_play(std::string vhost); virtual SrsConfDirective *get_vhost_on_stop(std::string vhost); + virtual srs_utime_t get_hls_window(std::string vhost); }; // Mock ISrsHttpHooks for testing SrsLiveStream::http_hooks_on_play() and http_hooks_on_stop() @@ -153,6 +155,8 @@ class MockStatisticForLiveStream : public ISrsStatistic public: int on_client_count_; srs_error_t on_client_error_; + int on_disconnect_count_; + std::vector on_disconnect_ids_; public: MockStatisticForLiveStream(); diff --git a/trunk/src/utest/srs_utest_ai22.cpp b/trunk/src/utest/srs_utest_ai22.cpp index 356bd595b5..84d13e15a8 100644 --- a/trunk/src/utest/srs_utest_ai22.cpp +++ b/trunk/src/utest/srs_utest_ai22.cpp @@ -1746,11 +1746,169 @@ void MockRtspPlayStream::reset() srs_freep(start_error_); } +// MockStreamWriterForRtspConn implementation +MockStreamWriterForRtspConn::MockStreamWriterForRtspConn(bool *destroyed) +{ + destroyed_ = destroyed; + write_count_ = 0; +} + +MockStreamWriterForRtspConn::~MockStreamWriterForRtspConn() +{ + if (destroyed_) { + *destroyed_ = true; + } +} + +srs_error_t MockStreamWriterForRtspConn::write(void *buf, size_t size, ssize_t *nwrite) +{ + write_count_++; + + if (nwrite) { + *nwrite = (ssize_t)size; + } + return srs_success; +} + +// MockResourceManagerForRtspConn implementation +MockResourceManagerForRtspConn::MockResourceManagerForRtspConn() +{ + subscribe_count_ = 0; + unsubscribe_count_ = 0; + last_subscribed_handler_ = NULL; + last_unsubscribed_handler_ = NULL; +} + +MockResourceManagerForRtspConn::~MockResourceManagerForRtspConn() +{ +} + +srs_error_t MockResourceManagerForRtspConn::start() +{ + return srs_success; +} + +bool MockResourceManagerForRtspConn::empty() +{ + return true; +} + +size_t MockResourceManagerForRtspConn::size() +{ + return 0; +} + +void MockResourceManagerForRtspConn::add(ISrsResource *conn, bool *exists) +{ +} + +void MockResourceManagerForRtspConn::add_with_id(const std::string &id, ISrsResource *conn) +{ +} + +void MockResourceManagerForRtspConn::add_with_fast_id(uint64_t id, ISrsResource *conn) +{ +} + +void MockResourceManagerForRtspConn::add_with_name(const std::string &name, ISrsResource *conn) +{ +} + +ISrsResource *MockResourceManagerForRtspConn::at(int index) +{ + return NULL; +} + +ISrsResource *MockResourceManagerForRtspConn::find_by_id(std::string id) +{ + return NULL; +} + +ISrsResource *MockResourceManagerForRtspConn::find_by_fast_id(uint64_t id) +{ + return NULL; +} + +ISrsResource *MockResourceManagerForRtspConn::find_by_name(std::string name) +{ + return NULL; +} + +void MockResourceManagerForRtspConn::remove(ISrsResource *c) +{ +} + +void MockResourceManagerForRtspConn::subscribe(ISrsDisposingHandler *h) +{ + subscribe_count_++; + last_subscribed_handler_ = h; +} + +void MockResourceManagerForRtspConn::unsubscribe(ISrsDisposingHandler *h) +{ + unsubscribe_count_++; + last_unsubscribed_handler_ = h; +} + // MockAppFactoryForRtspPlayStream implementation +MockCoroutineForRtsp::MockCoroutineForRtsp() +{ + start_count_ = 0; + stop_count_ = 0; + start_error_ = srs_success; + pull_error_ = srs_success; +} + +MockCoroutineForRtsp::~MockCoroutineForRtsp() +{ + srs_freep(start_error_); + srs_freep(pull_error_); +} + +srs_error_t MockCoroutineForRtsp::start() +{ + start_count_++; + + if (start_error_ != srs_success) { + return srs_error_copy(start_error_); + } + return srs_success; +} + +void MockCoroutineForRtsp::stop() +{ + stop_count_++; +} + +void MockCoroutineForRtsp::interrupt() +{ +} + +srs_error_t MockCoroutineForRtsp::pull() +{ + if (pull_error_ != srs_success) { + return srs_error_copy(pull_error_); + } + return srs_success; +} + +const SrsContextId &MockCoroutineForRtsp::cid() +{ + return cid_; +} + +void MockCoroutineForRtsp::set_cid(const SrsContextId &cid) +{ + cid_ = cid; +} + MockAppFactoryForRtspPlayStream::MockAppFactoryForRtspPlayStream() { create_rtsp_audio_send_track_count_ = 0; create_rtsp_video_send_track_count_ = 0; + coroutine_ = NULL; + create_coroutine_count_ = 0; + coroutine_handler_ = NULL; } MockAppFactoryForRtspPlayStream::~MockAppFactoryForRtspPlayStream() @@ -1769,6 +1927,15 @@ ISrsRtspSendTrack *MockAppFactoryForRtspPlayStream::create_rtsp_video_send_track return new MockRtspSendTrack("video_track", track_desc); } +ISrsCoroutine *MockAppFactoryForRtspPlayStream::create_coroutine(const std::string &name, ISrsCoroutineHandler *handler, SrsContextId cid) +{ + create_coroutine_count_++; + coroutine_name_ = name; + coroutine_handler_ = handler; + coroutine_cid_ = cid; + return coroutine_; +} + void MockAppFactoryForRtspPlayStream::reset() { create_rtsp_audio_send_track_count_ = 0; @@ -1961,6 +2128,83 @@ VOID TEST(RtspPlayStreamTest, OnStreamChange) EXPECT_EQ(1, (int)play_stream->video_tracks_.size()); } +// The track maps and SrsRtspConnection::networks_ sit on opposite sides of an SSRC rewrite: the maps are keyed by the +// publisher SSRC of arriving packets, while SrsRtspSendTrack::on_rtp rewrites each packet to track_desc_->ssrc_ before +// do_send_packet looks it up in networks_, which SETUP keyed from the same track description. A republish must +// therefore re-key the maps to the new publisher SSRC without touching track_desc_->ssrc_, or every packet after it +// would miss in networks_ and the client would also see its negotiated SSRC change mid-session. +// +// This test passes from the start: it locks in the current, intended behavior rather than driving a fix. Without it, +// assigning the new SSRC to track_desc_ inside on_stream_change would leave every existing test green. +VOID TEST(RtspPlayStreamTest, OnStreamChangeKeepsSubscriberSsrc) +{ + srs_error_t err = srs_success; + + MockStatisticForRtspPlayStream mock_stat; + MockRtspSourceManager mock_rtsp_sources; + MockAppFactoryForRtspPlayStream mock_app_factory; + mock_rtsp_sources.mock_source_ = SrsSharedPtr(new SrsRtspSource()); + + SrsUniquePtr mock_req(new MockEdgeRequest("test.vhost", "live", "stream1")); + + SrsContextId cid; + SrsUniquePtr play_stream(new SrsRtspPlayStream(NULL, cid)); + play_stream->stat_ = &mock_stat; + play_stream->rtsp_sources_ = &mock_rtsp_sources; + play_stream->app_factory_ = &mock_app_factory; + + // SETUP negotiated SSRC 1001 for audio and 2001 for video, which is what networks_ is keyed by. + SrsUniquePtr audio_desc(new SrsRtcTrackDescription()); + audio_desc->type_ = "audio"; + audio_desc->id_ = "0"; + audio_desc->ssrc_ = 1001; + audio_desc->media_ = new SrsAudioPayload(111, "opus", 48000, 2); + audio_desc->media_->pt_of_publisher_ = 111; + + SrsUniquePtr video_desc(new SrsRtcTrackDescription()); + video_desc->type_ = "video"; + video_desc->id_ = "1"; + video_desc->ssrc_ = 2001; + video_desc->media_ = new SrsVideoPayload(102, "H264", 90000); + video_desc->media_->pt_of_publisher_ = 102; + + std::map sub_relations; + sub_relations[1001] = audio_desc.get(); + sub_relations[2001] = video_desc.get(); + HELPER_EXPECT_SUCCESS(play_stream->initialize(mock_req.get(), sub_relations)); + + ISrsRtspSendTrack *audio_track = play_stream->audio_tracks_[1001]; + ISrsRtspSendTrack *video_track = play_stream->video_tracks_[2001]; + EXPECT_EQ(1001, (int)audio_track->track_desc()->ssrc_); + EXPECT_EQ(2001, (int)video_track->track_desc()->ssrc_); + + // The publisher republishes and comes back with different SSRCs. + SrsUniquePtr new_desc(new SrsRtcSourceDescription()); + new_desc->audio_track_desc_ = new SrsRtcTrackDescription(); + new_desc->audio_track_desc_->type_ = "audio"; + new_desc->audio_track_desc_->ssrc_ = 1002; + new_desc->audio_track_desc_->media_ = new SrsAudioPayload(112, "opus", 48000, 2); + new_desc->audio_track_desc_->media_->pt_ = 112; + + SrsRtcTrackDescription *new_video_desc = new SrsRtcTrackDescription(); + new_video_desc->type_ = "video"; + new_video_desc->ssrc_ = 2002; + new_video_desc->media_ = new SrsVideoPayload(103, "H264", 90000); + new_video_desc->media_->pt_ = 103; + new_desc->video_track_descs_.push_back(new_video_desc); + + play_stream->on_stream_change(new_desc.get()); + + // The input side follows the publisher, so arriving packets still find their track. + EXPECT_TRUE(play_stream->audio_tracks_.find(1002) != play_stream->audio_tracks_.end()); + EXPECT_TRUE(play_stream->video_tracks_.find(2002) != play_stream->video_tracks_.end()); + + // GOAL: the output side does not move. These SSRCs are what on_rtp stamps onto every outgoing + // packet and what SrsRtspConnection::networks_ is keyed by, so they must survive the republish. + EXPECT_EQ(1001, (int)audio_track->track_desc()->ssrc_); + EXPECT_EQ(2001, (int)video_track->track_desc()->ssrc_); +} + // A republish brings new SSRCs: on_stream_change re-keys the track maps, so it must also clear the fast cache, or the // slots keep the old SSRCs and every packet of the new stream misses the cache for the rest of the session. VOID TEST(RtspPlayStreamTest, OnStreamChangeResetsTrackCache) @@ -2193,6 +2437,78 @@ VOID TEST(RtspPlayStreamTest, SendPacketWithCacheAndTrackLookup) EXPECT_EQ(1, video_track->on_rtp_count_); } +// The sender coroutine is what delivers packets to an RTSP player, so a test must be able to replace +// it before start() runs. Otherwise the started-twice guard and the start failure are only reachable +// by creating a real coroutine. +VOID TEST(RtspPlayStreamTest, StartCreatesSenderCoroutineThroughFactory) +{ + srs_error_t err = srs_success; + + MockRtspConnection mock_session; + MockStatisticForRtspPlayStream mock_stat; + MockAppFactoryForRtspPlayStream mock_factory; + MockCoroutineForRtsp mock_trd; + mock_factory.coroutine_ = &mock_trd; + + SrsContextId cid = SrsContextId().set_value("rtsp-play"); + SrsUniquePtr play_stream(new SrsRtspPlayStream(&mock_session, cid)); + + play_stream->stat_ = &mock_stat; + play_stream->app_factory_ = &mock_factory; + + // GOAL: start() reaches the sender coroutine through the factory and starts it. + HELPER_EXPECT_SUCCESS(play_stream->start()); + EXPECT_EQ(1, mock_factory.create_coroutine_count_); + EXPECT_STREQ("rtsp_sender", mock_factory.coroutine_name_.c_str()); + EXPECT_TRUE(play_stream.get() == mock_factory.coroutine_handler_); + EXPECT_STREQ("rtsp-play", mock_factory.coroutine_cid_.c_str()); + EXPECT_TRUE(&mock_trd == play_stream->trd_); + EXPECT_EQ(1, mock_trd.start_count_); + EXPECT_TRUE(play_stream->is_started); + + // GOAL: a repeated PLAY cannot start a second coroutine for the same stream. + HELPER_EXPECT_SUCCESS(play_stream->start()); + EXPECT_EQ(1, mock_factory.create_coroutine_count_); + EXPECT_EQ(1, mock_trd.start_count_); + + // GOAL: stop() reaches the same coroutine instance the factory handed out. + play_stream->stop(); + EXPECT_EQ(1, mock_trd.stop_count_); + + // The coroutine is borrowed from the mock factory, so the destructor must not free it. The + // statistic stays injected because the destructor reports the disconnect through it. + play_stream->trd_ = NULL; + play_stream->app_factory_ = NULL; +} + +// A failed sender coroutine must leave the play stream unstarted, so that the session is torn down +// instead of being treated as a playing client. +VOID TEST(RtspPlayStreamTest, StartFailsWhenSenderCoroutineFails) +{ + srs_error_t err = srs_success; + + MockRtspConnection mock_session; + MockStatisticForRtspPlayStream mock_stat; + MockAppFactoryForRtspPlayStream mock_factory; + MockCoroutineForRtsp mock_trd; + mock_trd.start_error_ = srs_error_new(ERROR_THREAD_STARTED, "mock start failure"); + mock_factory.coroutine_ = &mock_trd; + + SrsContextId cid = SrsContextId().set_value("rtsp-play"); + SrsUniquePtr play_stream(new SrsRtspPlayStream(&mock_session, cid)); + + play_stream->stat_ = &mock_stat; + play_stream->app_factory_ = &mock_factory; + + // GOAL: the coroutine failure is reported and the stream is not marked started. + HELPER_EXPECT_FAILED(play_stream->start()); + EXPECT_EQ(1, mock_trd.start_count_); + EXPECT_FALSE(play_stream->is_started); + + play_stream->trd_ = NULL; + play_stream->app_factory_ = NULL; +} + VOID TEST(RtspPlayStreamTest, SetAllTracksStatus) { // Create mock dependencies @@ -2271,6 +2587,56 @@ VOID TEST(RtspPlayStreamTest, SetAllTracksStatus) // The destructor will free the tracks in the maps and call stat_->on_disconnect() } +// The RTSP connection is the entry point of every RTSP session, so a test has to be able to replace +// its context, its coroutine factory and its resource manager before any of them is used. +// Construction must therefore stay quiescent, and every collaborator call must happen in assemble(). +// The context, coroutine and factory mocks are the generic recorders already defined for SrsRtmpConn. +VOID TEST(RtspConnectionTest, AssembleWiresCollaboratorsFromInjectedDependencies) +{ + MockResourceManagerForRtspConn manager; + SrsRtspConnection *conn = new SrsRtspConnection(&manager, NULL, "127.0.0.1", 8554); + + // GOAL: construction reaches no collaborator, so a test can replace them before any work runs. + EXPECT_TRUE(NULL == conn->trd_); + + MockCoroutineForRtmpConn trd; + + MockAppFactoryForRtmpConn factory; + factory.coroutine_ = &trd; + + MockContextForRtmpConn context; + context.id_ = SrsContextId().set_value("rtsp-cid"); + + conn->app_factory_ = &factory; + conn->context_ = &context; + conn->rtsp_manager_ = &manager; + conn->assemble(); + + // GOAL: the session identity, the coroutine and the dispose subscription are all established by + // assemble(), through the injected dependencies. + EXPECT_EQ(1, context.generate_id_count_); + EXPECT_EQ(1, context.set_id_count_); + EXPECT_STREQ("rtsp-cid", conn->cid_.c_str()); + EXPECT_EQ(1, factory.create_coroutine_count_); + EXPECT_STREQ("rtsp", factory.coroutine_name_.c_str()); + EXPECT_TRUE(conn == factory.coroutine_handler_); + EXPECT_STREQ("rtsp-cid", factory.coroutine_cid_.c_str()); + EXPECT_TRUE(&trd == conn->trd_); + EXPECT_EQ(1, manager.subscribe_count_); + EXPECT_TRUE(conn == manager.last_subscribed_handler_); + + // The coroutine is borrowed from the mock factory, so the destructor must not free it. + ISrsDisposingHandler *subscribed = conn; + conn->trd_ = NULL; + conn->app_factory_ = NULL; + conn->context_ = NULL; + srs_freep(conn); + + // GOAL: the dispose subscription is symmetric on the same injected manager instance. + EXPECT_EQ(1, manager.unsubscribe_count_); + EXPECT_TRUE(subscribed == manager.last_unsubscribed_handler_); +} + // Test SrsRtspConnection::on_rtsp_request() - major use scenario // This test covers the complete RTSP play flow which is the most common scenario: // 1. Client sends OPTIONS request to query server capabilities @@ -2409,8 +2775,10 @@ VOID TEST(RtspConnectionTest, OnRtspRequestCompletePlayFlow) // through active session management to disposal. VOID TEST(RtspConnectionTest, SessionLifecycleAndDisposal) { - // Create RTSP connection + // Create RTSP connection. This test drives the session identity, so it assembles the connection + // with the production dependencies captured by the constructor. SrsUniquePtr conn(new SrsRtspConnection(NULL, NULL, "127.0.0.1", 8554)); + conn->assemble(); // Test 1: Context management { @@ -2710,6 +3078,210 @@ VOID TEST(RtspConnectionTest, DoSetupWithTcpTransport) conn->networks_.clear(); } +// SETUP carries its parameters in the Transport header, but that header is optional as far as the +// parser is concerned: SrsRtspRequest::transport_ stays NULL when it is absent, and is_setup() only +// looks at the method. A client may therefore reach the SETUP branch with no transport at all, and +// it must be answered as a bad request rather than asserted on, because srs_assert is plain assert() +// and no build defines NDEBUG, so the assertion ends the whole process and every session on it. +VOID TEST(RtspConnectionTest, SetupWithoutTransportIsRejected) +{ + srs_error_t err = srs_success; + + MockRtspStack *mock_rtsp = new MockRtspStack(); + + SrsUniquePtr conn(new SrsRtspConnection(NULL, NULL, "127.0.0.1", 8554)); + conn->rtsp_ = mock_rtsp; + conn->session_id_ = "test_session_123"; + + // A SETUP with no Transport header at all: transport_ is left NULL by the parser. + SrsRtspRequest *req = new SrsRtspRequest(); + req->method_ = "SETUP"; + req->uri_ = "rtsp://127.0.0.1:8554/live/stream/trackID=0"; + req->seq_ = 7; + req->stream_id_ = 0; + EXPECT_TRUE(NULL == req->transport_); + + // GOAL: the request is refused and the connection survives to serve the next one. + HELPER_EXPECT_SUCCESS(conn->on_rtsp_request(req)); + EXPECT_TRUE(mock_rtsp->send_message_called_); + EXPECT_EQ(7, mock_rtsp->last_response_seq_); + EXPECT_EQ(SRS_CONSTS_RTSP_BadRequest, mock_rtsp->last_response_status_); + EXPECT_STREQ("test_session_123", mock_rtsp->last_response_session_.c_str()); + + conn->rtsp_ = NULL; + srs_freep(mock_rtsp); +} + +// RTSP has no session state machine: on_rtsp_request dispatches each request on its own, so a +// client may send DESCRIBE more than once. Each DESCRIBE rebuilds the SDP from the source, so the +// track descriptions it stores must be rebuilt with it, not silently discarded. +VOID TEST(RtspConnectionTest, RepeatedDescribeReplacesTrackDescriptions) +{ + srs_error_t err = srs_success; + + MockEdgeConfig mock_config; + MockSecurity mock_security; + MockHttpHooks mock_hooks; + MockRtspSourceManager mock_rtsp_sources; + + SrsSharedPtr mock_source(new SrsRtspSource()); + + SrsRtcTrackDescription *audio_desc = new SrsRtcTrackDescription(); + audio_desc->type_ = "audio"; + audio_desc->ssrc_ = 1001; + audio_desc->media_ = new SrsAudioPayload(97, "MPEG4-GENERIC", 48000, 2); + mock_source->audio_desc_ = audio_desc; + + mock_rtsp_sources.mock_source_ = mock_source; + + SrsUniquePtr conn(new SrsRtspConnection(NULL, NULL, "127.0.0.1", 8554)); + conn->config_ = &mock_config; + conn->security_ = &mock_security; + conn->hooks_ = &mock_hooks; + conn->rtsp_sources_ = &mock_rtsp_sources; + + SrsUniquePtr req(new SrsRtspRequest()); + req->uri_ = "rtsp://127.0.0.1:8554/live/stream"; + + std::string sdp; + HELPER_EXPECT_SUCCESS(conn->do_describe(req.get(), sdp)); + EXPECT_EQ(1, (int)conn->tracks_.size()); + EXPECT_EQ(97, conn->tracks_[1001]->media_->pt_); + + // The publisher re-negotiates its payload type, so a second DESCRIBE must report the new one. + audio_desc->media_->pt_ = 98; + + sdp = ""; + HELPER_EXPECT_SUCCESS(conn->do_describe(req.get(), sdp)); + + // GOAL: the stored description is the one this DESCRIBE built. std::map::insert keeps the + // existing entry and drops the new copy on the floor, which both leaks it and serves stale + // payload types to the client. + EXPECT_EQ(1, (int)conn->tracks_.size()); + EXPECT_EQ(98, conn->tracks_[1001]->media_->pt_); + + // Clean up injected mocks to avoid double-free + conn->config_ = NULL; + conn->security_ = NULL; + conn->hooks_ = NULL; + conn->rtsp_sources_ = NULL; +} + +// A publisher that republishes between two DESCRIBEs comes back with a new SSRC. The stale track +// must go, because track ids restart at 0 on every DESCRIBE and SETUP resolves a stream id by +// scanning for the first matching id. +VOID TEST(RtspConnectionTest, RepeatedDescribeAfterSsrcChangeDropsStaleTracks) +{ + srs_error_t err = srs_success; + + MockEdgeConfig mock_config; + MockSecurity mock_security; + MockHttpHooks mock_hooks; + MockRtspSourceManager mock_rtsp_sources; + + SrsSharedPtr mock_source(new SrsRtspSource()); + + SrsRtcTrackDescription *audio_desc = new SrsRtcTrackDescription(); + audio_desc->type_ = "audio"; + audio_desc->ssrc_ = 1001; + audio_desc->media_ = new SrsAudioPayload(97, "MPEG4-GENERIC", 48000, 2); + mock_source->audio_desc_ = audio_desc; + + mock_rtsp_sources.mock_source_ = mock_source; + + SrsUniquePtr conn(new SrsRtspConnection(NULL, NULL, "127.0.0.1", 8554)); + conn->config_ = &mock_config; + conn->security_ = &mock_security; + conn->hooks_ = &mock_hooks; + conn->rtsp_sources_ = &mock_rtsp_sources; + + SrsUniquePtr req(new SrsRtspRequest()); + req->uri_ = "rtsp://127.0.0.1:8554/live/stream"; + + std::string sdp; + HELPER_EXPECT_SUCCESS(conn->do_describe(req.get(), sdp)); + + // The publisher restarts and comes back with a different SSRC. + audio_desc->ssrc_ = 1002; + + sdp = ""; + HELPER_EXPECT_SUCCESS(conn->do_describe(req.get(), sdp)); + + // GOAL: only the live track survives, so trackID=0 resolves to the SSRC now being published. + // Keeping both leaves two tracks carrying id_ "0", and the scan returns whichever sorts first. + EXPECT_EQ(1, (int)conn->tracks_.size()); + EXPECT_TRUE(conn->tracks_.find(1002) != conn->tracks_.end()); + + uint32_t ssrc = 0; + HELPER_EXPECT_SUCCESS(conn->get_ssrc_by_stream_id(0, &ssrc)); + EXPECT_EQ(1002, (int)ssrc); + + // Clean up injected mocks to avoid double-free + conn->config_ = NULL; + conn->security_ = NULL; + conn->hooks_ = NULL; + conn->rtsp_sources_ = NULL; +} + +// RTSP allows a client to re-SETUP a track, for instance to move it to another interleaved channel. +// The writer installed by the previous SETUP is owned by the connection, so replacing it must free +// it; otherwise each repeat leaks one until the connection ends. +VOID TEST(RtspConnectionTest, RepeatedSetupFreesPreviousNetwork) +{ + srs_error_t err = srs_success; + + SrsUniquePtr conn(new SrsRtspConnection(NULL, NULL, "127.0.0.1", 8554)); + + SrsRtcTrackDescription *video_desc = new SrsRtcTrackDescription(); + video_desc->type_ = "video"; + video_desc->id_ = "0"; + video_desc->ssrc_ = 12345; + conn->tracks_[12345] = video_desc; + + // Stand in for the writer a previous SETUP installed, so its destruction is observable. + bool destroyed = false; + conn->networks_[12345] = new MockStreamWriterForRtspConn(&destroyed); + + SrsUniquePtr req(new SrsRtspRequest()); + req->method_ = "SETUP"; + req->stream_id_ = 0; + req->transport_ = new SrsRtspTransport(); + req->transport_->transport_ = "RTP"; + req->transport_->profile_ = "AVP"; + req->transport_->lower_transport_ = "TCP"; + req->transport_->interleaved_min_ = 2; + req->transport_->interleaved_max_ = 3; + + uint32_t ssrc = 0; + HELPER_EXPECT_SUCCESS(conn->do_setup(req.get(), &ssrc)); + + // GOAL: the replaced writer is freed, and the slot holds exactly one writer. + EXPECT_TRUE(destroyed); + EXPECT_EQ(1, (int)conn->networks_.size()); + + ISrsStreamWriter *network = conn->networks_[12345]; + srs_freep(network); + conn->networks_.clear(); +} + +// do_send_packet runs for every RTP packet. A lookup miss must not modify the map: std::map's +// operator[] default-inserts a NULL entry, which allocates a node on the send path. +VOID TEST(RtspConnectionTest, SendPacketDoesNotInsertOnLookupMiss) +{ + SrsUniquePtr conn(new SrsRtspConnection(NULL, NULL, "127.0.0.1", 8554)); + + SrsUniquePtr pkt(new SrsRtpPacket()); + pkt->header_.set_ssrc(4242); + + // GOAL: an unknown SSRC is reported, and the map is left alone. + srs_error_t err = conn->do_send_packet(pkt.get()); + EXPECT_TRUE(err != srs_success); + EXPECT_EQ(ERROR_RTSP_NO_TRACK, srs_error_code(err)); + srs_freep(err); + + EXPECT_EQ(0, (int)conn->networks_.size()); +} + // Test SrsRtspConnection::http_hooks_on_play() to verify HTTP hooks are called correctly // when playing RTSP streams. This covers the major use scenario where HTTP hooks are enabled // and multiple hook URLs are configured for on_play events. diff --git a/trunk/src/utest/srs_utest_ai22.hpp b/trunk/src/utest/srs_utest_ai22.hpp index 74acb66bb3..fe9892a5f4 100644 --- a/trunk/src/utest/srs_utest_ai22.hpp +++ b/trunk/src/utest/srs_utest_ai22.hpp @@ -398,12 +398,43 @@ class MockRtspSendTrack : public ISrsRtspSendTrack void reset(); }; +// Mock ISrsCoroutine for testing the RTSP sender coroutine, which must be replaceable so that the +// started-twice guard and the start failure of SrsRtspPlayStream::start() are reachable. +class MockCoroutineForRtsp : public ISrsCoroutine +{ +public: + int start_count_; + int stop_count_; + srs_error_t start_error_; + srs_error_t pull_error_; + SrsContextId cid_; + +public: + MockCoroutineForRtsp(); + virtual ~MockCoroutineForRtsp(); + +public: + virtual srs_error_t start(); + virtual void stop(); + virtual void interrupt(); + virtual srs_error_t pull(); + virtual const SrsContextId &cid(); + virtual void set_cid(const SrsContextId &cid); +}; + // Mock ISrsAppFactory for testing SrsRtspPlayStream class MockAppFactoryForRtspPlayStream : public SrsAppFactory { public: int create_rtsp_audio_send_track_count_; int create_rtsp_video_send_track_count_; + // The coroutine returned by the factory, borrowed and not owned by the mock. + ISrsCoroutine *coroutine_; + // What the factory was asked to create. + int create_coroutine_count_; + std::string coroutine_name_; + ISrsCoroutineHandler *coroutine_handler_; + SrsContextId coroutine_cid_; public: MockAppFactoryForRtspPlayStream(); @@ -412,6 +443,7 @@ class MockAppFactoryForRtspPlayStream : public SrsAppFactory public: virtual ISrsRtspSendTrack *create_rtsp_audio_send_track(ISrsRtspConnection *session, SrsRtcTrackDescription *track_desc); virtual ISrsRtspSendTrack *create_rtsp_video_send_track(ISrsRtspConnection *session, SrsRtcTrackDescription *track_desc); + virtual ISrsCoroutine *create_coroutine(const std::string &name, ISrsCoroutineHandler *handler, SrsContextId cid); void reset(); }; @@ -459,6 +491,55 @@ class MockRtspPlayStream : public ISrsRtspPlayStream virtual void set_all_tracks_status(bool status); void reset(); }; + +// Mock ISrsStreamWriter for testing that SrsRtspConnection::do_setup() frees the writer it replaces. +// It reports its own destruction through a flag the test owns, because the leak is otherwise only +// visible to a leak checker, which macOS does not provide. +class MockStreamWriterForRtspConn : public ISrsStreamWriter +{ +public: + // Set to true by the destructor, so a test can prove the writer was freed. Borrowed, not owned. + bool *destroyed_; + int write_count_; + +public: + MockStreamWriterForRtspConn(bool *destroyed); + virtual ~MockStreamWriterForRtspConn(); + +public: + virtual srs_error_t write(void *buf, size_t size, ssize_t *nwrite); +}; + +// Mock ISrsResourceManager for testing SrsRtspConnection::assemble() and its destructor, which must +// subscribe and unsubscribe the dispose handler on the same injected manager instance. +class MockResourceManagerForRtspConn : public ISrsResourceManager +{ +public: + int subscribe_count_; + int unsubscribe_count_; + ISrsDisposingHandler *last_subscribed_handler_; + ISrsDisposingHandler *last_unsubscribed_handler_; + +public: + MockResourceManagerForRtspConn(); + virtual ~MockResourceManagerForRtspConn(); + +public: + virtual srs_error_t start(); + virtual bool empty(); + virtual size_t size(); + virtual void add(ISrsResource *conn, bool *exists = NULL); + virtual void add_with_id(const std::string &id, ISrsResource *conn); + virtual void add_with_fast_id(uint64_t id, ISrsResource *conn); + virtual void add_with_name(const std::string &name, ISrsResource *conn); + virtual ISrsResource *at(int index); + virtual ISrsResource *find_by_id(std::string id); + virtual ISrsResource *find_by_fast_id(uint64_t id); + virtual ISrsResource *find_by_name(std::string name); + virtual void remove(ISrsResource *c); + virtual void subscribe(ISrsDisposingHandler *h); + virtual void unsubscribe(ISrsDisposingHandler *h); +}; #endif // Mock ISrsDvrPlan for testing SrsDvrSegmenter diff --git a/trunk/src/utest/srs_utest_ai25.cpp b/trunk/src/utest/srs_utest_ai25.cpp index 4513ab8a93..2abc508ac3 100644 --- a/trunk/src/utest/srs_utest_ai25.cpp +++ b/trunk/src/utest/srs_utest_ai25.cpp @@ -23,6 +23,7 @@ using namespace std; #include #include #include +#include #include // Goal tests for hook rejection across every protocol, for both players and publishers. @@ -169,6 +170,146 @@ VOID TEST(HookRejectionTest, HlsRejectedViewerReceivesHookStatus) hls->security_ = NULL; } +MockFileReaderFactoryForHlsStream::MockFileReaderFactoryForHlsStream(std::string content) +{ + content_ = content; + create_count_ = 0; +} + +MockFileReaderFactoryForHlsStream::~MockFileReaderFactoryForHlsStream() +{ +} + +SrsFileReader *MockFileReaderFactoryForHlsStream::create_file_reader() +{ + create_count_++; + return new MockSrsFileReader(content_.data(), (int)content_.length()); +} + +// Request an HLS playlist with the given URL, and return the raw HTTP response in resp. +static srs_error_t mock_hls_serve_m3u8(SrsHlsStream *hls, ISrsFileReaderFactory *factory, std::string url, std::string &resp) +{ + srs_error_t err = srs_success; + + SrsUniquePtr message(new MockHttpMessageForLiveStream()); + if ((err = message->set_url(url, false)) != srs_success) { + return srs_error_wrap(err, "set url"); + } + + SrsUniquePtr request(new MockRequest("test.vhost", "live", "stream1")); + MockResponseWriter writer; + + bool served = false; + err = hls->serve_m3u8_ctx(&writer, message.get(), factory, "./objs/nginx/html/live/stream1.m3u8", request.get(), &served); + + resp = string(writer.io.out_buffer.bytes(), writer.io.out_buffer.length()); + return err; +} + +// HLS: a viewer rejected by the on_play hook must stay rejected when it retries with the same hls_ctx. +// +// The client may choose the hls_ctx of a new session in the query string. SrsHlsStream::serve_m3u8_ctx() +// calls alive() even when serve_new_session() failed, so the rejected ctx is kept as a live session. +// The retry then finds that ctx and takes serve_exists_session(), which serves the playlist without +// consulting the hook, so any client passes on_play by simply asking twice. +VOID TEST(HookRejectionTest, HlsRejectedViewerCannotRetryWithSameCtx) +{ + srs_error_t err = srs_success; + + MockStatisticForLiveStream stat; + MockSecurity security; + MockFileReaderFactoryForHlsStream factory("#EXTM3U\n#EXTINF:10.000,\nstream1-0.ts\n"); + + MockAppConfigForLiveStreamHooks config; + config.http_hooks_enabled_ = true; + config.on_play_directive_ = new SrsConfDirective(); + config.on_play_directive_->name_ = "on_play"; + config.on_play_directive_->args_.push_back("http://127.0.0.1:8085/api/v1/play"); + + // The hook rejects every attempt of this viewer as unauthorized. + MockHttpHooksForLiveStream hooks; + hooks.on_play_error_ = srs_error_new(ERROR_RESPONSE_CODE, "http: response object code %d", MOCK_HOOK_REJECT_STATUS); + + // Only the captured dependencies are replaced. assemble() is not called, so the stream is never + // subscribed to the real shared timer. + SrsUniquePtr hls(new SrsHlsStream()); + hls->config_ = &config; + hls->stat_ = &stat; + hls->hooks_ = &hooks; + srs_freep(hls->security_); + hls->security_ = &security; + hls->shared_timer_ = NULL; + + // The first request with a client-chosen hls_ctx is refused by the hook. + string resp; + HELPER_EXPECT_FAILED(mock_hls_serve_m3u8(hls.get(), &factory, "/live/stream1.m3u8?hls_ctx=chosenbyclient", resp)); + EXPECT_EQ(1, hooks.on_play_count_); + EXPECT_EQ(0, (int)resp.find("HTTP/1.1 401")); + + // The refused viewer is removed from statistic at once, because it has no session to expire. + EXPECT_EQ(1, stat.on_disconnect_count_); + + // GOAL: retrying with the same hls_ctx asks the hook again and is refused again. The playlist of + // the stream must never be read for a viewer the hook refused. + HELPER_EXPECT_FAILED(mock_hls_serve_m3u8(hls.get(), &factory, "/live/stream1.m3u8?hls_ctx=chosenbyclient", resp)); + EXPECT_EQ(2, hooks.on_play_count_); + EXPECT_EQ(0, (int)resp.find("HTTP/1.1 401")); + EXPECT_EQ(string::npos, resp.find("stream1-0.ts")); + EXPECT_EQ(0, factory.create_count_); + + hls->config_ = NULL; + hls->stat_ = NULL; + hls->hooks_ = NULL; + hls->security_ = NULL; +} + +// HLS: a viewer denied by the security rules must stay denied when it retries with the same hls_ctx. +// +// The security check runs in serve_new_session() before the hook, so it is bypassed by the same retry +// as the on_play hook. A denied viewer has no status mapped yet, so it is refused by closing the +// connection with an empty response. +VOID TEST(HookRejectionTest, HlsDeniedViewerCannotRetryWithSameCtx) +{ + srs_error_t err = srs_success; + + MockStatisticForLiveStream stat; + MockAppConfigForLiveStreamHooks config; + MockHttpHooksForLiveStream hooks; + MockFileReaderFactoryForHlsStream factory("#EXTM3U\n#EXTINF:10.000,\nstream1-0.ts\n"); + + // The security rules deny every attempt of this viewer. + MockSecurity security; + security.check_error_ = srs_error_new(ERROR_SYSTEM_SECURITY_DENY, "deny by rule"); + + SrsUniquePtr hls(new SrsHlsStream()); + hls->config_ = &config; + hls->stat_ = &stat; + hls->hooks_ = &hooks; + srs_freep(hls->security_); + hls->security_ = &security; + hls->shared_timer_ = NULL; + + // The first request with a client-chosen hls_ctx is denied. + string resp; + HELPER_EXPECT_FAILED(mock_hls_serve_m3u8(hls.get(), &factory, "/live/stream1.m3u8?hls_ctx=chosenbyclient", resp)); + EXPECT_EQ(1, security.check_count_); + EXPECT_TRUE(resp.empty()); + + // The denied viewer is removed from statistic at once, because it has no session to expire. + EXPECT_EQ(1, stat.on_disconnect_count_); + + // GOAL: retrying with the same hls_ctx runs the security check again and is denied again. + HELPER_EXPECT_FAILED(mock_hls_serve_m3u8(hls.get(), &factory, "/live/stream1.m3u8?hls_ctx=chosenbyclient", resp)); + EXPECT_EQ(2, security.check_count_); + EXPECT_TRUE(resp.empty()); + EXPECT_EQ(0, factory.create_count_); + + hls->config_ = NULL; + hls->stat_ = NULL; + hls->hooks_ = NULL; + hls->security_ = NULL; +} + // RTMP: authorization must be decided before the client is told playback started. // // SrsRtmpConn::stream_service_cycle() calls rtmp_->start_play() first, which sends StreamBegin and diff --git a/trunk/src/utest/srs_utest_ai25.hpp b/trunk/src/utest/srs_utest_ai25.hpp index 19cf8aa0fe..d76164bea5 100644 --- a/trunk/src/utest/srs_utest_ai25.hpp +++ b/trunk/src/utest/srs_utest_ai25.hpp @@ -8,4 +8,23 @@ #include +#include + +#include + +// Mock ISrsFileReaderFactory for testing SrsHlsStream, which reads the m3u8 of an existing session. +class MockFileReaderFactoryForHlsStream : public ISrsFileReaderFactory +{ +public: + std::string content_; + int create_count_; + +public: + MockFileReaderFactoryForHlsStream(std::string content); + virtual ~MockFileReaderFactoryForHlsStream(); + +public: + virtual SrsFileReader *create_file_reader(); +}; + #endif diff --git a/trunk/src/utest/srs_utest_ai28.cpp b/trunk/src/utest/srs_utest_ai28.cpp new file mode 100644 index 0000000000..499f28c0ab --- /dev/null +++ b/trunk/src/utest/srs_utest_ai28.cpp @@ -0,0 +1,399 @@ +// +// Copyright (c) 2013-2026 The SRS Authors +// +// SPDX-License-Identifier: MIT +// +#include + +#include +#include +#include + +#include +using namespace std; + +// The acceptor passes this address and port to the listener it creates. The listener is a mock, so nothing binds it; +// the address is from the TEST-NET-1 documentation range. +static const char *kAcceptorIp = "192.0.2.1"; +static const int kAcceptorPort = 10080; +static const srs_srt_t kListenerFd = 7; + +template +static string to_str(T v) +{ + stringstream ss; + ss << v; + return ss.str(); +} + +MockAppConfigForSrtAcceptor::MockAppConfigForSrtAcceptor() +{ + maxbw_ = 1000000; + mss_ = 1400; + tsbpdmode_ = false; + latency_ = 200; + recv_latency_ = 300; + peer_latency_ = 400; + tlpktdrop_ = false; + conntimeout_ = 5 * SRS_UTIME_SECONDS; + peeridletimeout_ = 7 * SRS_UTIME_SECONDS; + sendbuf_ = 100000; + recvbuf_ = 200000; + payloadsize_ = 1456; + pbkeylen_ = 0; +} + +MockAppConfigForSrtAcceptor::~MockAppConfigForSrtAcceptor() +{ +} + +MockSrtListenerForSrtAcceptor::MockSrtListenerForSrtAcceptor(vector *calls) +{ + calls_ = calls; + fd_ = kListenerFd; + create_socket_error_ = srs_success; + listen_error_ = srs_success; +} + +MockSrtListenerForSrtAcceptor::~MockSrtListenerForSrtAcceptor() +{ + srs_freep(create_socket_error_); + srs_freep(listen_error_); +} + +srs_srt_t MockSrtListenerForSrtAcceptor::fd() +{ + return fd_; +} + +srs_error_t MockSrtListenerForSrtAcceptor::create_socket() +{ + calls_->push_back("create_socket"); + return srs_error_copy(create_socket_error_); +} + +srs_error_t MockSrtListenerForSrtAcceptor::listen() +{ + calls_->push_back("listen"); + return srs_error_copy(listen_error_); +} + +MockSrtOptionsForSrtAcceptor::MockSrtOptionsForSrtAcceptor(vector *calls) +{ + calls_ = calls; +} + +MockSrtOptionsForSrtAcceptor::~MockSrtOptionsForSrtAcceptor() +{ +} + +srs_error_t MockSrtOptionsForSrtAcceptor::record(srs_srt_t srt_fd, string name, string value) +{ + calls_->push_back(name + "=" + value); + fds_.push_back(srt_fd); + + if (name == fail_option_) { + return srs_error_new(ERROR_SOCKET_LISTEN, "mock set %s", name.c_str()); + } + return srs_success; +} + +srs_error_t MockSrtOptionsForSrtAcceptor::set_maxbw(srs_srt_t srt_fd, int64_t maxbw) +{ + return record(srt_fd, "maxbw", to_str(maxbw)); +} + +srs_error_t MockSrtOptionsForSrtAcceptor::set_mss(srs_srt_t srt_fd, int mss) +{ + return record(srt_fd, "mss", to_str(mss)); +} + +srs_error_t MockSrtOptionsForSrtAcceptor::set_payload_size(srs_srt_t srt_fd, int payload_size) +{ + return record(srt_fd, "payload_size", to_str(payload_size)); +} + +srs_error_t MockSrtOptionsForSrtAcceptor::set_connect_timeout(srs_srt_t srt_fd, int timeout) +{ + return record(srt_fd, "connect_timeout", to_str(timeout)); +} + +srs_error_t MockSrtOptionsForSrtAcceptor::set_peer_idle_timeout(srs_srt_t srt_fd, int timeout) +{ + return record(srt_fd, "peer_idle_timeout", to_str(timeout)); +} + +srs_error_t MockSrtOptionsForSrtAcceptor::set_tsbpdmode(srs_srt_t srt_fd, bool tsbpdmode) +{ + return record(srt_fd, "tsbpdmode", to_str(tsbpdmode)); +} + +srs_error_t MockSrtOptionsForSrtAcceptor::set_sndbuf(srs_srt_t srt_fd, int sndbuf) +{ + return record(srt_fd, "sndbuf", to_str(sndbuf)); +} + +srs_error_t MockSrtOptionsForSrtAcceptor::set_rcvbuf(srs_srt_t srt_fd, int rcvbuf) +{ + return record(srt_fd, "rcvbuf", to_str(rcvbuf)); +} + +srs_error_t MockSrtOptionsForSrtAcceptor::set_tlpktdrop(srs_srt_t srt_fd, bool tlpktdrop) +{ + return record(srt_fd, "tlpktdrop", to_str(tlpktdrop)); +} + +srs_error_t MockSrtOptionsForSrtAcceptor::set_latency(srs_srt_t srt_fd, int latency) +{ + return record(srt_fd, "latency", to_str(latency)); +} + +srs_error_t MockSrtOptionsForSrtAcceptor::set_rcv_latency(srs_srt_t srt_fd, int rcv_latency) +{ + return record(srt_fd, "rcv_latency", to_str(rcv_latency)); +} + +srs_error_t MockSrtOptionsForSrtAcceptor::set_peer_latency(srs_srt_t srt_fd, int peer_latency) +{ + return record(srt_fd, "peer_latency", to_str(peer_latency)); +} + +srs_error_t MockSrtOptionsForSrtAcceptor::set_passphrase(srs_srt_t srt_fd, const string &passphrase) +{ + return record(srt_fd, "passphrase", passphrase); +} + +srs_error_t MockSrtOptionsForSrtAcceptor::set_pbkeylen(srs_srt_t srt_fd, int pbkeylen) +{ + return record(srt_fd, "pbkeylen", to_str(pbkeylen)); +} + +MockAppFactoryForSrtAcceptor::MockAppFactoryForSrtAcceptor(MockSrtListenerForSrtAcceptor *listener) +{ + listener_ = listener; + listener_created_ = false; + create_srt_listener_count_ = 0; + handler_ = NULL; + port_ = 0; +} + +MockAppFactoryForSrtAcceptor::~MockAppFactoryForSrtAcceptor() +{ + if (!listener_created_) { + srs_freep(listener_); + } +} + +ISrsSrtListener *MockAppFactoryForSrtAcceptor::create_srt_listener(ISrsSrtHandler *handler, string ip, int port) +{ + create_srt_listener_count_++; + handler_ = handler; + ip_ = ip; + port_ = port; + + listener_created_ = true; + return listener_; +} + +// The options the acceptor must set, in order, for the given config, as the options mock records them. +static vector expected_options(MockAppConfigForSrtAcceptor *config) +{ + vector opts; + opts.push_back("maxbw=" + to_str(config->maxbw_)); + opts.push_back("mss=" + to_str(config->mss_)); + opts.push_back("tsbpdmode=" + to_str(config->tsbpdmode_)); + opts.push_back("latency=" + to_str(config->latency_)); + opts.push_back("rcv_latency=" + to_str(config->recv_latency_)); + opts.push_back("peer_latency=" + to_str(config->peer_latency_)); + opts.push_back("tlpktdrop=" + to_str(config->tlpktdrop_)); + opts.push_back("connect_timeout=" + to_str(srsu2msi(config->conntimeout_))); + opts.push_back("peer_idle_timeout=" + to_str(srsu2msi(config->peeridletimeout_))); + opts.push_back("sndbuf=" + to_str(config->sendbuf_)); + opts.push_back("rcvbuf=" + to_str(config->recvbuf_)); + opts.push_back("payload_size=" + to_str(config->payloadsize_)); + if (!config->passphrase_.empty()) { + opts.push_back("passphrase=" + config->passphrase_); + opts.push_back("pbkeylen=" + to_str(config->pbkeylen_)); + } + return opts; +} + +// Replace every collaborator of the acceptor with a mock. The options object is owned by the acceptor, so free it +// before the replacement; release_mocks() must run before the acceptor is destroyed. +static void inject_mocks(SrsSrtAcceptor *acceptor, ISrsAppConfig *config, ISrsAppFactory *factory, ISrsSrtOptions *options) +{ + acceptor->config_ = config; + acceptor->app_factory_ = factory; + srs_freep(acceptor->srt_options_); + acceptor->srt_options_ = options; +} + +static void release_mocks(SrsSrtAcceptor *acceptor) +{ + acceptor->config_ = NULL; + acceptor->app_factory_ = NULL; + acceptor->srt_options_ = NULL; +} + +// The acceptor creates its listener through the factory, with itself as the handler and the given address. +VOID TEST(SrtAcceptorTest, ListenCreatesListenerThroughFactory) +{ + vector calls; + MockAppConfigForSrtAcceptor config; + MockSrtOptionsForSrtAcceptor options(&calls); + MockAppFactoryForSrtAcceptor factory(new MockSrtListenerForSrtAcceptor(&calls)); + + SrsSrtAcceptor acceptor(NULL); + inject_mocks(&acceptor, &config, &factory, &options); + + srs_error_t err = acceptor.listen(kAcceptorIp, kAcceptorPort); + EXPECT_TRUE(err == srs_success) << srs_error_summary(err); + srs_freep(err); + + EXPECT_EQ(1, factory.create_srt_listener_count_); + EXPECT_TRUE(factory.handler_ == &acceptor); + EXPECT_STREQ(kAcceptorIp, factory.ip_.c_str()); + EXPECT_EQ(kAcceptorPort, factory.port_); + + release_mocks(&acceptor); +} + +// Every option is set from config on the listener's fd, after the socket is created and before it listens. Without a +// passphrase, neither the passphrase nor the key length is set. +VOID TEST(SrtAcceptorTest, ListenSetsEveryOptionBetweenCreateAndListen) +{ + vector calls; + MockAppConfigForSrtAcceptor config; + MockSrtOptionsForSrtAcceptor options(&calls); + MockAppFactoryForSrtAcceptor factory(new MockSrtListenerForSrtAcceptor(&calls)); + + SrsSrtAcceptor acceptor(NULL); + inject_mocks(&acceptor, &config, &factory, &options); + + srs_error_t err = acceptor.listen(kAcceptorIp, kAcceptorPort); + EXPECT_TRUE(err == srs_success) << srs_error_summary(err); + srs_freep(err); + + vector expected; + expected.push_back("create_socket"); + vector opts = expected_options(&config); + expected.insert(expected.end(), opts.begin(), opts.end()); + expected.push_back("listen"); + EXPECT_EQ(expected, calls); + + EXPECT_EQ(opts.size(), options.fds_.size()); + for (int i = 0; i < (int)options.fds_.size(); i++) { + EXPECT_EQ(kListenerFd, options.fds_[i]) << "option " << i; + } + + release_mocks(&acceptor); +} + +// A configured passphrase is set, followed by the key length, as the last options before listening. +VOID TEST(SrtAcceptorTest, ListenSetsPassphraseAndKeyLengthWhenConfigured) +{ + vector calls; + MockAppConfigForSrtAcceptor config; + config.passphrase_ = "0123456789abcdef"; + config.pbkeylen_ = 16; + MockSrtOptionsForSrtAcceptor options(&calls); + MockAppFactoryForSrtAcceptor factory(new MockSrtListenerForSrtAcceptor(&calls)); + + SrsSrtAcceptor acceptor(NULL); + inject_mocks(&acceptor, &config, &factory, &options); + + srs_error_t err = acceptor.listen(kAcceptorIp, kAcceptorPort); + EXPECT_TRUE(err == srs_success) << srs_error_summary(err); + srs_freep(err); + + ASSERT_EQ(16, (int)calls.size()); + EXPECT_STREQ("passphrase=0123456789abcdef", calls[13].c_str()); + EXPECT_STREQ("pbkeylen=16", calls[14].c_str()); + EXPECT_STREQ("listen", calls[15].c_str()); + + release_mocks(&acceptor); +} + +// When the socket cannot be created, the error is returned and no option is set and nothing listens. +VOID TEST(SrtAcceptorTest, ListenFailsWhenCreateSocketFails) +{ + vector calls; + MockAppConfigForSrtAcceptor config; + MockSrtOptionsForSrtAcceptor options(&calls); + MockSrtListenerForSrtAcceptor *listener = new MockSrtListenerForSrtAcceptor(&calls); + listener->create_socket_error_ = srs_error_new(ERROR_SOCKET_CREATE, "mock create"); + MockAppFactoryForSrtAcceptor factory(listener); + + SrsSrtAcceptor acceptor(NULL); + inject_mocks(&acceptor, &config, &factory, &options); + + srs_error_t err = acceptor.listen(kAcceptorIp, kAcceptorPort); + EXPECT_EQ(ERROR_SOCKET_CREATE, srs_error_code(err)); + srs_freep(err); + + vector expected; + expected.push_back("create_socket"); + EXPECT_EQ(expected, calls); + + release_mocks(&acceptor); +} + +// Each option can fail. The acceptor returns that option's error and stops: no later option is set and nothing listens. +VOID TEST(SrtAcceptorTest, ListenStopsAtTheFirstFailedOption) +{ + MockAppConfigForSrtAcceptor config; + config.passphrase_ = "0123456789abcdef"; + config.pbkeylen_ = 16; + vector opts = expected_options(&config); + ASSERT_EQ(14, (int)opts.size()); + + for (int i = 0; i < (int)opts.size(); i++) { + string name = opts[i].substr(0, opts[i].find('=')); + + vector calls; + MockSrtOptionsForSrtAcceptor options(&calls); + options.fail_option_ = name; + MockAppFactoryForSrtAcceptor factory(new MockSrtListenerForSrtAcceptor(&calls)); + + SrsSrtAcceptor acceptor(NULL); + inject_mocks(&acceptor, &config, &factory, &options); + + srs_error_t err = acceptor.listen(kAcceptorIp, kAcceptorPort); + EXPECT_EQ(ERROR_SOCKET_LISTEN, srs_error_code(err)) << "fail at " << name; + srs_freep(err); + + vector expected; + expected.push_back("create_socket"); + expected.insert(expected.end(), opts.begin(), opts.begin() + i + 1); + EXPECT_EQ(expected, calls) << "fail at " << name; + + release_mocks(&acceptor); + } +} + +// When the listener cannot listen, the error is returned after every option was set. +VOID TEST(SrtAcceptorTest, ListenFailsWhenListenerListenFails) +{ + vector calls; + MockAppConfigForSrtAcceptor config; + MockSrtOptionsForSrtAcceptor options(&calls); + MockSrtListenerForSrtAcceptor *listener = new MockSrtListenerForSrtAcceptor(&calls); + listener->listen_error_ = srs_error_new(ERROR_SOCKET_BIND, "mock listen"); + MockAppFactoryForSrtAcceptor factory(listener); + + SrsSrtAcceptor acceptor(NULL); + inject_mocks(&acceptor, &config, &factory, &options); + + srs_error_t err = acceptor.listen(kAcceptorIp, kAcceptorPort); + EXPECT_EQ(ERROR_SOCKET_BIND, srs_error_code(err)); + srs_freep(err); + + vector expected; + expected.push_back("create_socket"); + vector opts = expected_options(&config); + expected.insert(expected.end(), opts.begin(), opts.end()); + expected.push_back("listen"); + EXPECT_EQ(expected, calls); + + release_mocks(&acceptor); +} diff --git a/trunk/src/utest/srs_utest_ai28.hpp b/trunk/src/utest/srs_utest_ai28.hpp new file mode 100644 index 0000000000..4b0ca8d409 --- /dev/null +++ b/trunk/src/utest/srs_utest_ai28.hpp @@ -0,0 +1,135 @@ +// +// Copyright (c) 2013-2026 The SRS Authors +// +// SPDX-License-Identifier: MIT +// + +#ifndef SRS_UTEST_AI28_HPP +#define SRS_UTEST_AI28_HPP + +/* +#include +*/ +#include + +#include +#include +#include +#include + +#include +#include + +// Every SRT option the acceptor reads, set to a value that differs from its default. +class MockAppConfigForSrtAcceptor : public MockAppConfig +{ +public: + int64_t maxbw_; + int mss_; + bool tsbpdmode_; + int latency_; + int recv_latency_; + int peer_latency_; + bool tlpktdrop_; + srs_utime_t conntimeout_; + srs_utime_t peeridletimeout_; + int sendbuf_; + int recvbuf_; + int payloadsize_; + std::string passphrase_; + int pbkeylen_; + +public: + MockAppConfigForSrtAcceptor(); + virtual ~MockAppConfigForSrtAcceptor(); + +public: + virtual int64_t get_srto_maxbw() { return maxbw_; } + virtual int get_srto_mss() { return mss_; } + virtual bool get_srto_tsbpdmode() { return tsbpdmode_; } + virtual int get_srto_latency() { return latency_; } + virtual int get_srto_recv_latency() { return recv_latency_; } + virtual int get_srto_peer_latency() { return peer_latency_; } + virtual bool get_srto_tlpktdrop() { return tlpktdrop_; } + virtual srs_utime_t get_srto_conntimeout() { return conntimeout_; } + virtual srs_utime_t get_srto_peeridletimeout() { return peeridletimeout_; } + virtual int get_srto_sendbuf() { return sendbuf_; } + virtual int get_srto_recvbuf() { return recvbuf_; } + virtual int get_srto_payloadsize() { return payloadsize_; } + virtual std::string get_srto_passphrase() { return passphrase_; } + virtual int get_srto_pbkeylen() { return pbkeylen_; } +}; + +// Records create_socket and listen into a call log shared with the options mock, so a test sees their order. +class MockSrtListenerForSrtAcceptor : public ISrsSrtListener +{ +public: + std::vector *calls_; + srs_srt_t fd_; + srs_error_t create_socket_error_; + srs_error_t listen_error_; + +public: + MockSrtListenerForSrtAcceptor(std::vector *calls); + virtual ~MockSrtListenerForSrtAcceptor(); + +public: + virtual srs_srt_t fd(); + virtual srs_error_t create_socket(); + virtual srs_error_t listen(); +}; + +// Records each option as "name=value" into the shared call log, and fails the option named by fail_option_. +class MockSrtOptionsForSrtAcceptor : public ISrsSrtOptions +{ +public: + std::vector *calls_; + // The fd each option was set on, which must be the listener's fd. + std::vector fds_; + std::string fail_option_; + +public: + MockSrtOptionsForSrtAcceptor(std::vector *calls); + virtual ~MockSrtOptionsForSrtAcceptor(); + +public: + virtual srs_error_t set_maxbw(srs_srt_t srt_fd, int64_t maxbw); + virtual srs_error_t set_mss(srs_srt_t srt_fd, int mss); + virtual srs_error_t set_payload_size(srs_srt_t srt_fd, int payload_size); + virtual srs_error_t set_connect_timeout(srs_srt_t srt_fd, int timeout); + virtual srs_error_t set_peer_idle_timeout(srs_srt_t srt_fd, int timeout); + virtual srs_error_t set_tsbpdmode(srs_srt_t srt_fd, bool tsbpdmode); + virtual srs_error_t set_sndbuf(srs_srt_t srt_fd, int sndbuf); + virtual srs_error_t set_rcvbuf(srs_srt_t srt_fd, int rcvbuf); + virtual srs_error_t set_tlpktdrop(srs_srt_t srt_fd, bool tlpktdrop); + virtual srs_error_t set_latency(srs_srt_t srt_fd, int latency); + virtual srs_error_t set_rcv_latency(srs_srt_t srt_fd, int rcv_latency); + virtual srs_error_t set_peer_latency(srs_srt_t srt_fd, int peer_latency); + virtual srs_error_t set_passphrase(srs_srt_t srt_fd, const std::string &passphrase); + virtual srs_error_t set_pbkeylen(srs_srt_t srt_fd, int pbkeylen); + +private: + srs_error_t record(srs_srt_t srt_fd, std::string name, std::string value); +}; + +// Hands out one mock listener and records the arguments it was created with. The acceptor owns the listener once +// created; the factory frees it only when the acceptor never asked for it. +class MockAppFactoryForSrtAcceptor : public SrsAppFactory +{ +public: + MockSrtListenerForSrtAcceptor *listener_; + bool listener_created_; + int create_srt_listener_count_; + ISrsSrtHandler *handler_; + std::string ip_; + int port_; + +public: + MockAppFactoryForSrtAcceptor(MockSrtListenerForSrtAcceptor *listener); + virtual ~MockAppFactoryForSrtAcceptor(); + +public: + virtual ISrsSrtListener *create_srt_listener(ISrsSrtHandler *handler, std::string ip, int port); +}; + +#endif diff --git a/trunk/src/utest/srs_utest_ai29.cpp b/trunk/src/utest/srs_utest_ai29.cpp new file mode 100644 index 0000000000..613354364d --- /dev/null +++ b/trunk/src/utest/srs_utest_ai29.cpp @@ -0,0 +1,593 @@ +// +// Copyright (c) 2013-2026 The SRS Authors +// +// SPDX-License-Identifier: MIT +// +#include + +#include +#include +#include + +using namespace std; + +// The http static server mounts one directory per vhost, and a root mount for the vhosts that do not claim "/". Every +// mount is read from config, so the tests drive it with a config mock and observe the mounts on a mux mock. + +MockAppConfigForHttpStaticServer::MockAppConfigForHttpStaticServer() +{ + root_ = new SrsConfDirective(); + http_stream_dir_ = "./objs/nginx/html"; +} + +MockAppConfigForHttpStaticServer::~MockAppConfigForHttpStaticServer() +{ + srs_freep(root_); +} + +void MockAppConfigForHttpStaticServer::add_vhost(string vhost, string mount, string dir) +{ + root_->get_or_create("vhost", vhost); + enabled_[vhost] = true; + http_enabled_[vhost] = true; + mounts_[vhost] = mount; + dirs_[vhost] = dir; +} + +void MockAppConfigForHttpStaticServer::add_directive(string name) +{ + root_->get_or_create(name); +} + +SrsConfDirective *MockAppConfigForHttpStaticServer::get_root() +{ + return root_; +} + +string MockAppConfigForHttpStaticServer::get_http_stream_dir() +{ + return http_stream_dir_; +} + +bool MockAppConfigForHttpStaticServer::get_vhost_enabled(string vhost) +{ + return enabled_.count(vhost) ? enabled_[vhost] : false; +} + +bool MockAppConfigForHttpStaticServer::get_vhost_enabled(SrsConfDirective *conf) +{ + return get_vhost_enabled(conf->arg0()); +} + +bool MockAppConfigForHttpStaticServer::get_vhost_http_enabled(string vhost) +{ + return http_enabled_.count(vhost) ? http_enabled_[vhost] : false; +} + +string MockAppConfigForHttpStaticServer::get_vhost_http_mount(string vhost) +{ + return mounts_.count(vhost) ? mounts_[vhost] : ""; +} + +string MockAppConfigForHttpStaticServer::get_vhost_http_dir(string vhost) +{ + return dirs_.count(vhost) ? dirs_[vhost] : ""; +} + +MockHttpServeMuxForHttpStaticServer::MockHttpServeMuxForHttpStaticServer() +{ + handle_error_ = 0; +} + +MockHttpServeMuxForHttpStaticServer::~MockHttpServeMuxForHttpStaticServer() +{ + for (int i = 0; i < (int)handlers_.size(); i++) { + ISrsHttpHandler *handler = handlers_[i]; + srs_freep(handler); + } +} + +srs_error_t MockHttpServeMuxForHttpStaticServer::handle(string pattern, ISrsHttpHandler *handler) +{ + patterns_.push_back(pattern); + handlers_.push_back(handler); + + if (handle_error_) { + return srs_error_new(handle_error_, "mock mux"); + } + + return srs_success; +} + +srs_error_t MockHttpServeMuxForHttpStaticServer::serve_http(ISrsHttpResponseWriter *w, ISrsHttpMessage *r) +{ + return srs_success; +} + +srs_error_t MockHttpServeMuxForHttpStaticServer::find_handler(ISrsHttpMessage *r, ISrsHttpHandler **ph) +{ + return srs_success; +} + +void MockHttpServeMuxForHttpStaticServer::unhandle(string pattern, ISrsHttpHandler *handler) +{ +} + +static void inject_mocks(SrsHttpStaticServer *server, MockAppConfigForHttpStaticServer *config, MockHttpServeMuxForHttpStaticServer *mux) +{ + server->config_ = config; + srs_freep(server->mux_); + server->mux_ = mux; +} + +static void release_mocks(SrsHttpStaticServer *server) +{ + server->config_ = NULL; + server->mux_ = NULL; +} + +// The dir served at a mount, read from the vod stream the static server mounted there. +static string mounted_dir(MockHttpServeMuxForHttpStaticServer *mux, string pattern) +{ + for (int i = 0; i < (int)mux->patterns_.size(); i++) { + if (mux->patterns_[i] != pattern) { + continue; + } + + SrsHttpFileServer *fs = dynamic_cast(mux->handlers_[i]); + return fs ? fs->dir : ""; + } + + return ""; +} + +// A vhost that is disabled is not mounted at all. +VOID TEST(HttpStaticServerTest, MountVhostSkipsDisabledVhost) +{ + srs_error_t err = srs_success; + + MockAppConfigForHttpStaticServer config; + MockHttpServeMuxForHttpStaticServer mux; + config.add_vhost("ossrs.net", "/hls/", "./objs/nginx/html/hls"); + config.enabled_["ossrs.net"] = false; + + SrsHttpStaticServer server; + inject_mocks(&server, &config, &mux); + + string pmount; + HELPER_EXPECT_SUCCESS(server.mount_vhost("ossrs.net", pmount)); + release_mocks(&server); + + EXPECT_STREQ("", pmount.c_str()); + EXPECT_EQ(0, (int)mux.patterns_.size()); +} + +// A vhost with http static disabled is not mounted either. +VOID TEST(HttpStaticServerTest, MountVhostSkipsVhostWithHttpDisabled) +{ + srs_error_t err = srs_success; + + MockAppConfigForHttpStaticServer config; + MockHttpServeMuxForHttpStaticServer mux; + config.add_vhost("ossrs.net", "/hls/", "./objs/nginx/html/hls"); + config.http_enabled_["ossrs.net"] = false; + + SrsHttpStaticServer server; + inject_mocks(&server, &config, &mux); + + string pmount; + HELPER_EXPECT_SUCCESS(server.mount_vhost("ossrs.net", pmount)); + release_mocks(&server); + + EXPECT_STREQ("", pmount.c_str()); + EXPECT_EQ(0, (int)mux.patterns_.size()); +} + +// The configured mount serves the configured dir, and the mount is reported back to the caller. +VOID TEST(HttpStaticServerTest, MountVhostMountsTheConfiguredDir) +{ + srs_error_t err = srs_success; + + MockAppConfigForHttpStaticServer config; + MockHttpServeMuxForHttpStaticServer mux; + config.add_vhost("ossrs.net", "/hls/", "./objs/nginx/html/hls"); + + SrsHttpStaticServer server; + inject_mocks(&server, &config, &mux); + + string pmount; + HELPER_EXPECT_SUCCESS(server.mount_vhost("ossrs.net", pmount)); + release_mocks(&server); + + EXPECT_STREQ("/hls/", pmount.c_str()); + ASSERT_EQ(1, (int)mux.patterns_.size()); + EXPECT_STREQ("/hls/", mux.patterns_[0].c_str()); + EXPECT_STREQ("./objs/nginx/html/hls", mounted_dir(&mux, "/hls/").c_str()); +} + +// The [vhost] variable is replaced by the vhost name, in both the mount and the dir. +VOID TEST(HttpStaticServerTest, MountVhostReplacesTheVhostVariable) +{ + srs_error_t err = srs_success; + + MockAppConfigForHttpStaticServer config; + MockHttpServeMuxForHttpStaticServer mux; + config.add_vhost("ossrs.net", "/[vhost]/hls/", "./objs/nginx/html/[vhost]"); + + SrsHttpStaticServer server; + inject_mocks(&server, &config, &mux); + + string pmount; + HELPER_EXPECT_SUCCESS(server.mount_vhost("ossrs.net", pmount)); + release_mocks(&server); + + EXPECT_STREQ("/ossrs.net/hls/", pmount.c_str()); + ASSERT_EQ(1, (int)mux.patterns_.size()); + EXPECT_STREQ("/ossrs.net/hls/", mux.patterns_[0].c_str()); + EXPECT_STREQ("./objs/nginx/html/ossrs.net", mounted_dir(&mux, "/ossrs.net/hls/").c_str()); +} + +// A mount that does not end with a slash gets one, because it mounts a dir. +VOID TEST(HttpStaticServerTest, MountVhostEndsTheMountWithASlash) +{ + srs_error_t err = srs_success; + + MockAppConfigForHttpStaticServer config; + MockHttpServeMuxForHttpStaticServer mux; + config.add_vhost("ossrs.net", "/hls", "./objs/nginx/html/hls"); + + SrsHttpStaticServer server; + inject_mocks(&server, &config, &mux); + + string pmount; + HELPER_EXPECT_SUCCESS(server.mount_vhost("ossrs.net", pmount)); + release_mocks(&server); + + EXPECT_STREQ("/hls/", pmount.c_str()); + ASSERT_EQ(1, (int)mux.patterns_.size()); + EXPECT_STREQ("/hls/", mux.patterns_[0].c_str()); +} + +// The default vhost is not part of the URL, so its default mount becomes the root mount. +VOID TEST(HttpStaticServerTest, MountVhostStripsTheDefaultVhostFromTheMount) +{ + srs_error_t err = srs_success; + + MockAppConfigForHttpStaticServer config; + MockHttpServeMuxForHttpStaticServer mux; + config.add_vhost(SRS_CONSTS_RTMP_DEFAULT_VHOST, "[vhost]/", "./objs/nginx/html"); + + SrsHttpStaticServer server; + inject_mocks(&server, &config, &mux); + + string pmount; + HELPER_EXPECT_SUCCESS(server.mount_vhost(SRS_CONSTS_RTMP_DEFAULT_VHOST, pmount)); + release_mocks(&server); + + EXPECT_STREQ("/", pmount.c_str()); + ASSERT_EQ(1, (int)mux.patterns_.size()); + EXPECT_STREQ("/", mux.patterns_[0].c_str()); +} + +// A mux that refuses the mount fails the vhost, and nothing is reported as mounted. +VOID TEST(HttpStaticServerTest, MountVhostFailsWhenTheMuxRefusesTheMount) +{ + srs_error_t err = srs_success; + + MockAppConfigForHttpStaticServer config; + MockHttpServeMuxForHttpStaticServer mux; + config.add_vhost("ossrs.net", "/hls/", "./objs/nginx/html/hls"); + mux.handle_error_ = ERROR_HTTP_PATTERN_DUPLICATED; + + SrsHttpStaticServer server; + inject_mocks(&server, &config, &mux); + + string pmount; + HELPER_EXPECT_FAILED(server.mount_vhost("ossrs.net", pmount)); + release_mocks(&server); + + EXPECT_STREQ("", pmount.c_str()); +} + +// Every vhost is mounted, and because none of them claims the root, the http stream dir is mounted at "/". +VOID TEST(HttpStaticServerTest, InitializeMountsEveryVhostAndTheRoot) +{ + srs_error_t err = srs_success; + + MockAppConfigForHttpStaticServer config; + MockHttpServeMuxForHttpStaticServer mux; + config.add_vhost("ossrs.net", "/hls/", "./objs/nginx/html/hls"); + config.add_vhost("srs.io", "/vod/", "./objs/nginx/html/vod"); + config.http_stream_dir_ = "./objs/nginx/html"; + + SrsHttpStaticServer server; + inject_mocks(&server, &config, &mux); + + HELPER_EXPECT_SUCCESS(server.initialize()); + release_mocks(&server); + + ASSERT_EQ(3, (int)mux.patterns_.size()); + EXPECT_STREQ("/hls/", mux.patterns_[0].c_str()); + EXPECT_STREQ("/vod/", mux.patterns_[1].c_str()); + EXPECT_STREQ("/", mux.patterns_[2].c_str()); + EXPECT_STREQ("./objs/nginx/html", mounted_dir(&mux, "/").c_str()); +} + +// A vhost that claims the root owns it, so no second root mount is added. +VOID TEST(HttpStaticServerTest, InitializeKeepsTheVhostThatMountsTheRoot) +{ + srs_error_t err = srs_success; + + MockAppConfigForHttpStaticServer config; + MockHttpServeMuxForHttpStaticServer mux; + config.add_vhost("ossrs.net", "/", "./objs/nginx/html/ossrs.net"); + config.http_stream_dir_ = "./objs/nginx/html"; + + SrsHttpStaticServer server; + inject_mocks(&server, &config, &mux); + + HELPER_EXPECT_SUCCESS(server.initialize()); + release_mocks(&server); + + ASSERT_EQ(1, (int)mux.patterns_.size()); + EXPECT_STREQ("/", mux.patterns_[0].c_str()); + EXPECT_STREQ("./objs/nginx/html/ossrs.net", mounted_dir(&mux, "/").c_str()); +} + +// Directives that are not vhosts are ignored, and the root is still mounted. +VOID TEST(HttpStaticServerTest, InitializeIgnoresDirectivesThatAreNotVhosts) +{ + srs_error_t err = srs_success; + + MockAppConfigForHttpStaticServer config; + MockHttpServeMuxForHttpStaticServer mux; + config.add_directive("http_server"); + config.add_directive("listen"); + + SrsHttpStaticServer server; + inject_mocks(&server, &config, &mux); + + HELPER_EXPECT_SUCCESS(server.initialize()); + release_mocks(&server); + + ASSERT_EQ(1, (int)mux.patterns_.size()); + EXPECT_STREQ("/", mux.patterns_[0].c_str()); +} + +// A vhost that cannot be mounted fails the whole initialization. +VOID TEST(HttpStaticServerTest, InitializeFailsWhenAVhostCannotBeMounted) +{ + srs_error_t err = srs_success; + + MockAppConfigForHttpStaticServer config; + MockHttpServeMuxForHttpStaticServer mux; + config.add_vhost("ossrs.net", "/hls/", "./objs/nginx/html/hls"); + mux.handle_error_ = ERROR_HTTP_PATTERN_DUPLICATED; + + SrsHttpStaticServer server; + inject_mocks(&server, &config, &mux); + + HELPER_EXPECT_FAILED(server.initialize()); + release_mocks(&server); + + ASSERT_EQ(1, (int)mux.patterns_.size()); + EXPECT_STREQ("/hls/", mux.patterns_[0].c_str()); +} + +MockAppConfigForVodStream::MockAppConfigForVodStream() +{ +} + +MockAppConfigForVodStream::~MockAppConfigForVodStream() +{ +} + +void MockAppConfigForVodStream::resolve_vhost_as(string canonical) +{ + srs_freep(default_vhost_); + + default_vhost_ = new SrsConfDirective(); + default_vhost_->name_ = "vhost"; + default_vhost_->args_.push_back(canonical); +} + +SrsConfDirective *MockAppConfigForVodStream::get_vhost(string vhost, bool try_default_vhost) +{ + resolved_vhosts_.push_back(vhost); + return default_vhost_; +} + +bool MockAppConfigForVodStream::get_hls_ctx_enabled(string vhost) +{ + hls_ctx_vhosts_.push_back(vhost); + return false; +} + +MockFileReaderFactoryForVodStream::MockFileReaderFactoryForVodStream(string content) +{ + content_ = content; +} + +MockFileReaderFactoryForVodStream::~MockFileReaderFactoryForVodStream() +{ +} + +SrsFileReader *MockFileReaderFactoryForVodStream::create_file_reader() +{ + return new MockSrsFileReader(content_.data(), (int)content_.length()); +} + +MockResponseWriterForVodStream::MockResponseWriterForVodStream() +{ +} + +MockResponseWriterForVodStream::~MockResponseWriterForVodStream() +{ +} + +srs_error_t MockResponseWriterForVodStream::filter(SrsHttpHeader *h) +{ + h->del("Content-Type"); + h->del("Server"); + h->del("Connection"); + h->del("Access-Control-Allow-Origin"); + h->del("Access-Control-Allow-Methods"); + h->del("Access-Control-Expose-Headers"); + h->del("Access-Control-Allow-Headers"); + return srs_success; +} + +// Serve fullpath as MP4 with the range of url, and return the raw HTTP response in resp. +static srs_error_t mock_vod_serve_mp4(string content, string url, string &resp) +{ + srs_error_t err = srs_success; + + SrsHttpMuxEntry entry; + entry.pattern = "/"; + + SrsVodStream stream("/tmp"); + stream.set_fs_factory(new MockFileReaderFactoryForVodStream(content)); + stream.set_path(new MockSrsPathAlwaysExists()); + stream.entry_ = &entry; + + MockResponseWriterForVodStream w; + SrsHttpMessage r(NULL, NULL); + if ((err = r.set_url(url, false)) != srs_success) { + return srs_error_wrap(err, "set url"); + } + + if ((err = stream.serve_http(&w, &r)) != srs_success) { + return srs_error_wrap(err, "serve http"); + } + + resp = HELPER_BUFFER2STR(&w.io.out_buffer); + + return err; +} + +// The last byte position of a range is inclusive, so an end at the file size is one byte past the last byte. Clamp it +// to the last byte, instead of promising a byte that does not exist and reading past the end of the file. +VOID TEST(VodStreamRangeTest, RangeEndAtFilesizeServesTheWholeFile) +{ + srs_error_t err = srs_success; + + string resp; + HELPER_ASSERT_SUCCESS(mock_vod_serve_mp4("Hello, world!", "/index.mp4?bytes=0-13", resp)); + + EXPECT_PRED2(is_string_contain, "206 Partial Content", resp); + EXPECT_PRED2(is_string_contain, "Content-Length: 13", resp); + EXPECT_PRED2(is_string_contain, "Content-Range: bytes 0-12/13", resp); + EXPECT_PRED2(is_string_contain, "Hello, world!", resp); +} + +// A range end far beyond the file is clamped to the last byte as well. +VOID TEST(VodStreamRangeTest, RangeEndBeyondFilesizeIsClamped) +{ + srs_error_t err = srs_success; + + string resp; + HELPER_ASSERT_SUCCESS(mock_vod_serve_mp4("Hello, world!", "/index.mp4?bytes=7-100", resp)); + + EXPECT_PRED2(is_string_contain, "206 Partial Content", resp); + EXPECT_PRED2(is_string_contain, "Content-Length: 6", resp); + EXPECT_PRED2(is_string_contain, "Content-Range: bytes 7-12/13", resp); + EXPECT_PRED2(is_string_contain, "world!", resp); +} + +// A range inside the file is served as before, with the last byte position it asked for. +VOID TEST(VodStreamRangeTest, RangeInsideTheFileIsUnchanged) +{ + srs_error_t err = srs_success; + + string resp; + HELPER_ASSERT_SUCCESS(mock_vod_serve_mp4("Hello, world!", "/index.mp4?bytes=2-5", resp)); + + EXPECT_PRED2(is_string_contain, "206 Partial Content", resp); + EXPECT_PRED2(is_string_contain, "Content-Length: 4", resp); + EXPECT_PRED2(is_string_contain, "Content-Range: bytes 2-5/13", resp); + EXPECT_PRED2(is_string_contain, "llo,", resp); +} + +// A range that starts past the last byte has nothing to serve, so it still fails. +VOID TEST(VodStreamRangeTest, RangeStartBeyondTheLastByteFails) +{ + srs_error_t err = srs_success; + + string resp; + HELPER_EXPECT_FAILED(mock_vod_serve_mp4("Hello, world!", "/index.mp4?bytes=13-20", resp)); +} + +// Serve the playlist at url, with config deciding the vhost, and return the raw HTTP response in resp. +static srs_error_t mock_vod_serve_m3u8(MockAppConfigForVodStream *config, string url, string &resp) +{ + srs_error_t err = srs_success; + + SrsHttpMuxEntry entry; + entry.pattern = "/"; + + SrsVodStream stream("/tmp"); + stream.set_fs_factory(new MockFileReaderFactoryForVodStream("#EXTM3U\n")); + stream.set_path(new MockSrsPathAlwaysExists()); + stream.entry_ = &entry; + + // The VOD stream resolves the vhost of the request, then the HLS stream it delegates to decides + // whether to serve the request as an HLS session. Both read the same config. + stream.config_ = config; + stream.hls_.config_ = config; + + MockResponseWriterForVodStream w; + SrsHttpMessage r(NULL, NULL); + if ((err = r.set_url(url, false)) == srs_success) { + err = stream.serve_http(&w, &r); + } + + resp = HELPER_BUFFER2STR(&w.io.out_buffer); + + stream.config_ = NULL; + stream.hls_.config_ = NULL; + + return err; +} + +// A vhost the config resolves to another name, such as an alias or a wildcard vhost, must reach the +// HLS stream under the resolved name. Otherwise the HLS settings of the vhost that actually owns the +// stream are never read, and the playlist is served with the settings of a vhost that does not exist. +VOID TEST(VodStreamVhostTest, M3u8CtxServesTheVhostResolvedByConfig) +{ + srs_error_t err = srs_success; + + MockAppConfigForVodStream config; + config.resolve_vhost_as("srs.io"); + + string resp; + HELPER_ASSERT_SUCCESS(mock_vod_serve_m3u8(&config, "/live/stream.m3u8?vhost=ossrs.net", resp)); + + // The vhost of the request is the one asked about. + ASSERT_EQ(1, (int)config.resolved_vhosts_.size()); + EXPECT_STREQ("ossrs.net", config.resolved_vhosts_[0].c_str()); + + // GOAL: the HLS stream is asked about the resolved vhost, not the one from the URL. + ASSERT_EQ(1, (int)config.hls_ctx_vhosts_.size()); + EXPECT_STREQ("srs.io", config.hls_ctx_vhosts_[0].c_str()); +} + +// A vhost the config does not resolve keeps the name from the URL, and is still served as a plain +// file by the default HLS handler, because this config disables the HLS ctx sessions. +VOID TEST(VodStreamVhostTest, M3u8CtxKeepsTheRequestVhostWhenConfigResolvesNone) +{ + srs_error_t err = srs_success; + + MockAppConfigForVodStream config; + + string resp; + HELPER_ASSERT_SUCCESS(mock_vod_serve_m3u8(&config, "/live/stream.m3u8?vhost=ossrs.net", resp)); + + // GOAL: the vhost is resolved through the injected config, not through the process global. + ASSERT_EQ(1, (int)config.resolved_vhosts_.size()); + EXPECT_STREQ("ossrs.net", config.resolved_vhosts_[0].c_str()); + + ASSERT_EQ(1, (int)config.hls_ctx_vhosts_.size()); + EXPECT_STREQ("ossrs.net", config.hls_ctx_vhosts_[0].c_str()); + + EXPECT_PRED2(is_string_contain, "#EXTM3U", resp); +} diff --git a/trunk/src/utest/srs_utest_ai29.hpp b/trunk/src/utest/srs_utest_ai29.hpp new file mode 100644 index 0000000000..cb149c6e7c --- /dev/null +++ b/trunk/src/utest/srs_utest_ai29.hpp @@ -0,0 +1,123 @@ +// +// Copyright (c) 2013-2026 The SRS Authors +// +// SPDX-License-Identifier: MIT +// + +#ifndef SRS_UTEST_AI29_HPP +#define SRS_UTEST_AI29_HPP + +/* +#include +*/ +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +// The vhosts of a config file, and the http static settings the static server reads for each of them. +class MockAppConfigForHttpStaticServer : public MockAppConfig +{ +public: + SrsConfDirective *root_; + std::string http_stream_dir_; + std::map enabled_; + std::map http_enabled_; + std::map mounts_; + std::map dirs_; + +public: + MockAppConfigForHttpStaticServer(); + virtual ~MockAppConfigForHttpStaticServer(); + +public: + // Add an enabled vhost to the root, serving dir at mount. + void add_vhost(std::string vhost, std::string mount, std::string dir); + // Add a directive to the root that is not a vhost. + void add_directive(std::string name); + +public: + virtual SrsConfDirective *get_root(); + virtual std::string get_http_stream_dir(); + virtual bool get_vhost_enabled(std::string vhost); + virtual bool get_vhost_enabled(SrsConfDirective *conf); + virtual bool get_vhost_http_enabled(std::string vhost); + virtual std::string get_vhost_http_mount(std::string vhost); + virtual std::string get_vhost_http_dir(std::string vhost); +}; + +// Records every mount and owns the handlers it is given, as SrsHttpServeMux does. +class MockHttpServeMuxForHttpStaticServer : public ISrsHttpServeMux +{ +public: + std::vector patterns_; + std::vector handlers_; + // When set, handle() fails with this error code, after taking the handler. + int handle_error_; + +public: + MockHttpServeMuxForHttpStaticServer(); + virtual ~MockHttpServeMuxForHttpStaticServer(); + +public: + virtual srs_error_t handle(std::string pattern, ISrsHttpHandler *handler); + virtual srs_error_t serve_http(ISrsHttpResponseWriter *w, ISrsHttpMessage *r); + virtual srs_error_t find_handler(ISrsHttpMessage *r, ISrsHttpHandler **ph); + virtual void unhandle(std::string pattern, ISrsHttpHandler *handler); +}; + +// Resolves the vhost of a VOD request, and records what the VOD and HLS streams asked about. +class MockAppConfigForVodStream : public MockAppConfig +{ +public: + // The vhosts SrsVodStream asked to resolve, in order. + std::vector resolved_vhosts_; + // The vhosts SrsHlsStream was asked about, in order, after the resolution. + std::vector hls_ctx_vhosts_; + +public: + MockAppConfigForVodStream(); + virtual ~MockAppConfigForVodStream(); + +public: + // Resolve every vhost to a directive named canonical, as an alias or wildcard vhost does. + void resolve_vhost_as(std::string canonical); + +public: + virtual SrsConfDirective *get_vhost(std::string vhost, bool try_default_vhost = true); + virtual bool get_hls_ctx_enabled(std::string vhost); +}; + +// Serves the VOD file from memory, for the range tests of SrsVodStream. +class MockFileReaderFactoryForVodStream : public ISrsFileReaderFactory +{ +public: + std::string content_; + +public: + MockFileReaderFactoryForVodStream(std::string content); + virtual ~MockFileReaderFactoryForVodStream(); + +public: + virtual SrsFileReader *create_file_reader(); +}; + +// Keeps the Content-Range header, which MockResponseWriter drops, so the range tests can assert it. +class MockResponseWriterForVodStream : public MockResponseWriter +{ +public: + MockResponseWriterForVodStream(); + virtual ~MockResponseWriterForVodStream(); + +public: + virtual srs_error_t filter(SrsHttpHeader *h); +}; + +#endif diff --git a/trunk/src/utest/srs_utest_ai30.cpp b/trunk/src/utest/srs_utest_ai30.cpp new file mode 100644 index 0000000000..dc0dc88ee7 --- /dev/null +++ b/trunk/src/utest/srs_utest_ai30.cpp @@ -0,0 +1,537 @@ +// +// Copyright (c) 2013-2026 The SRS Authors +// +// SPDX-License-Identifier: MIT +// +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +using namespace std; + +MockAppConfigForFileLog::MockAppConfigForFileLog() +{ + get_log_file_count_ = 0; +} + +MockAppConfigForFileLog::~MockAppConfigForFileLog() +{ +} + +string MockAppConfigForFileLog::get_log_file() +{ + get_log_file_count_++; + return log_file_; +} + +MockLogWriterForFileLog::MockLogWriterForFileLog() +{ + open_file_count_ = 0; + open_fd_ = -1; + closed_fd_ = -1; + close_file_count_ = 0; + closed_fd_out_ = NULL; + written_fd_ = -1; + write_file_count_ = 0; + write_console_count_ = 0; +} + +MockLogWriterForFileLog::~MockLogWriterForFileLog() +{ +} + +int MockLogWriterForFileLog::open_file(const string &path) +{ + open_file_count_++; + opened_path_ = path; + return open_fd_; +} + +void MockLogWriterForFileLog::close_file(int fd) +{ + close_file_count_++; + closed_fd_ = fd; + + if (closed_fd_out_) { + *closed_fd_out_ = fd; + } +} + +void MockLogWriterForFileLog::write_file(int fd, const char *str_log, int size) +{ + write_file_count_++; + written_fd_ = fd; + written_ = string(str_log, size); +} + +void MockLogWriterForFileLog::write_console(const char *color, const char *str_log, int size) +{ + write_console_count_++; + console_color_ = color; + console_ = string(str_log, size); +} + +// Whether fd is still an open descriptor of this process. +static bool is_fd_open(int fd) +{ + return ::fcntl(fd, F_GETFD) != -1; +} + +// The file logger reopens its log file on SIGUSR1, for log rotation. Every path out of reopen() must leave the +// descriptor member consistent with what the process actually holds, because write_log() opens a new file only when +// the member is negative. A closed descriptor left in the member is worse than a leaked one: the number is free for +// the next socket or file the process opens, so the next log write, and the close of the next reopen(), land on +// whatever took it over. + +// Reopening while logging to the console has no file to reopen, so the descriptor must be released and cleared. +VOID TEST(FileLogTest, ReopenClearsTheDescriptorWhenTheTankIsConsole) +{ + SrsFileLog log; + + // The logger holds an open log file, as it does after a start with the file tank. + log.fd_ = ::open("/dev/null", O_RDWR); + ASSERT_TRUE(log.fd_ > 0); + int previous = log.fd_; + + // A reload moved the logs to the console, then SIGUSR1 asked for a reopen. + log.log_to_file_tank_ = false; + log.reopen(); + + // GOAL: the descriptor is closed and the member no longer names it. + EXPECT_FALSE(is_fd_open(previous)); + EXPECT_EQ(-1, log.fd_); +} + +// Reopening with no log file configured cannot open one, so the descriptor must be released and cleared. +VOID TEST(FileLogTest, ReopenClearsTheDescriptorWhenNoLogFileIsConfigured) +{ + MockAppConfigForFileLog config; + SrsFileLog log; + + // The logger holds an open log file, and keeps the file tank. + log.fd_ = ::open("/dev/null", O_RDWR); + ASSERT_TRUE(log.fd_ > 0); + int previous = log.fd_; + + log.config_ = &config; + log.log_to_file_tank_ = true; + config.log_file_ = ""; + + log.reopen(); + + // GOAL: the file name comes from the injected config, and finding none leaves no descriptor behind. + EXPECT_EQ(1, config.get_log_file_count_); + EXPECT_FALSE(is_fd_open(previous)); + EXPECT_EQ(-1, log.fd_); + + log.config_ = NULL; +} + +// Reopening with the file tank must open the configured file, which is what log rotation depends on. +VOID TEST(FileLogTest, ReopenOpensTheConfiguredLogFile) +{ + MockAppConfigForFileLog config; + SrsFileLog log; + + string path = "./objs/srs_utest_ai30.log"; + ::unlink(path.c_str()); + + log.fd_ = ::open("/dev/null", O_RDWR); + ASSERT_TRUE(log.fd_ > 0); + int previous = log.fd_; + + log.config_ = &config; + log.log_to_file_tank_ = true; + config.log_file_ = path; + + log.reopen(); + + // GOAL: the previous descriptor is released, and the file the injected config names is open. + EXPECT_EQ(1, config.get_log_file_count_); + EXPECT_TRUE(log.fd_ > 0); + EXPECT_TRUE(is_fd_open(log.fd_)); + EXPECT_EQ(0, ::access(path.c_str(), F_OK)); + + // The reopened descriptor may reuse the released number, so only assert the release when it was not reused. + if (log.fd_ != previous) { + EXPECT_FALSE(is_fd_open(previous)); + } + + log.config_ = NULL; + ::unlink(path.c_str()); +} + +// The logger writes through ISrsLogWriter, so these tests read back the exact bytes and the exact descriptor the +// logger handed to the system, without creating a log file or printing to the terminal. + +// Call the logger the way the SRS log macros do, through a va_list. +static void mock_log(SrsFileLog &log, SrsLogLevel level, const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + log.log(level, NULL, SrsContextId(), fmt, args); + va_end(args); +} + +// Every log line is terminated, and the console tank prints the line with no color below the warn level. +VOID TEST(FileLogTest, WriteLogEndsTheConsoleLineWithTheTail) +{ + MockLogWriterForFileLog writer; + SrsFileLog log; + + srs_freep(log.writer_); + log.writer_ = &writer; + log.log_to_file_tank_ = false; + + char str_log[16] = "hello"; + log.write_log(log.fd_, str_log, 5, SrsLogLevelTrace); + + // GOAL: the console receives the line with its tail appended, and no color wraps it. + EXPECT_EQ(1, writer.write_console_count_); + EXPECT_STREQ("", writer.console_color_.c_str()); + EXPECT_STREQ("hello\n", writer.console_.c_str()); + EXPECT_EQ(0, writer.write_file_count_); + + log.writer_ = NULL; +} + +// A warning is yellow and an error is red, which is how an operator spots them in a terminal. +VOID TEST(FileLogTest, WriteLogColorsTheConsoleByLevel) +{ + MockLogWriterForFileLog writer; + SrsFileLog log; + + srs_freep(log.writer_); + log.writer_ = &writer; + log.log_to_file_tank_ = false; + + char warn_log[16] = "warn"; + log.write_log(log.fd_, warn_log, 4, SrsLogLevelWarn); + + // GOAL: the warn line is wrapped in the yellow code. + EXPECT_STREQ("\033[33m", writer.console_color_.c_str()); + EXPECT_STREQ("warn\n", writer.console_.c_str()); + + char error_log[16] = "error"; + log.write_log(log.fd_, error_log, 5, SrsLogLevelError); + + // GOAL: the error line is wrapped in the red code. + EXPECT_STREQ("\033[31m", writer.console_color_.c_str()); + EXPECT_STREQ("error\n", writer.console_.c_str()); + + EXPECT_EQ(2, writer.write_console_count_); + + log.writer_ = NULL; +} + +// A line longer than the log buffer is truncated, leaving room for the tail inside the buffer. +VOID TEST(FileLogTest, WriteLogTruncatesAnOversizedLine) +{ + MockLogWriterForFileLog writer; + SrsFileLog log; + + srs_freep(log.writer_); + log.writer_ = &writer; + log.log_to_file_tank_ = false; + + // LOG_MAX_SIZE is 64KB, and one byte each is reserved for the tail and the terminating zero. + int capacity = 65536; + char *str_log = new char[capacity]; + memset(str_log, 'a', capacity); + + log.write_log(log.fd_, str_log, capacity, SrsLogLevelTrace); + + // GOAL: the console receives 65534 bytes of payload plus the tail, never more than the buffer holds. + EXPECT_EQ(65535, (int)writer.console_.length()); + EXPECT_EQ('\n', writer.console_[writer.console_.length() - 1]); + EXPECT_EQ('a', writer.console_[writer.console_.length() - 2]); + + srs_freepa(str_log); + log.writer_ = NULL; +} + +// With the file tank and an open descriptor, the line goes to that descriptor and no file is opened again. +VOID TEST(FileLogTest, WriteLogWritesToTheOpenDescriptor) +{ + MockAppConfigForFileLog config; + MockLogWriterForFileLog writer; + SrsFileLog log; + + srs_freep(log.writer_); + log.writer_ = &writer; + log.config_ = &config; + log.log_to_file_tank_ = true; + log.fd_ = 7; + + char str_log[16] = "hello"; + log.write_log(log.fd_, str_log, 5, SrsLogLevelTrace); + + // GOAL: the bytes reach the descriptor the logger already holds. + EXPECT_EQ(1, writer.write_file_count_); + EXPECT_EQ(7, writer.written_fd_); + EXPECT_STREQ("hello\n", writer.written_.c_str()); + EXPECT_EQ(0, writer.open_file_count_); + EXPECT_EQ(0, writer.write_console_count_); + + log.fd_ = -1; + log.config_ = NULL; + log.writer_ = NULL; +} + +// With the file tank and no descriptor, as after a reopen, the configured file is opened and then written. +VOID TEST(FileLogTest, WriteLogOpensTheLogFileWhenNoDescriptorIsHeld) +{ + MockAppConfigForFileLog config; + MockLogWriterForFileLog writer; + SrsFileLog log; + + srs_freep(log.writer_); + log.writer_ = &writer; + log.config_ = &config; + log.log_to_file_tank_ = true; + log.fd_ = -1; + config.log_file_ = "./objs/srs_utest_ai30_write.log"; + writer.open_fd_ = 9; + + char str_log[16] = "hello"; + log.write_log(log.fd_, str_log, 5, SrsLogLevelTrace); + + // GOAL: the file the injected config names is opened once, and the line goes to the descriptor it returned. + EXPECT_EQ(1, writer.open_file_count_); + EXPECT_STREQ("./objs/srs_utest_ai30_write.log", writer.opened_path_.c_str()); + EXPECT_EQ(9, log.fd_); + EXPECT_EQ(1, writer.write_file_count_); + EXPECT_EQ(9, writer.written_fd_); + EXPECT_STREQ("hello\n", writer.written_.c_str()); + + log.fd_ = -1; + log.config_ = NULL; + log.writer_ = NULL; +} + +// A log file that cannot be opened, such as a path that is not writable, must drop the line rather than write to a +// negative descriptor. +VOID TEST(FileLogTest, WriteLogDropsTheLineWhenTheLogFileCannotBeOpened) +{ + MockAppConfigForFileLog config; + MockLogWriterForFileLog writer; + SrsFileLog log; + + srs_freep(log.writer_); + log.writer_ = &writer; + log.config_ = &config; + log.log_to_file_tank_ = true; + log.fd_ = -1; + config.log_file_ = "./objs/srs_utest_ai30_write.log"; + writer.open_fd_ = -1; + + char str_log[16] = "hello"; + log.write_log(log.fd_, str_log, 5, SrsLogLevelTrace); + + // GOAL: the open is attempted, and nothing is written. + EXPECT_EQ(1, writer.open_file_count_); + EXPECT_EQ(-1, log.fd_); + EXPECT_EQ(0, writer.write_file_count_); + + log.fd_ = -1; + log.config_ = NULL; + log.writer_ = NULL; +} + +// A level below the configured one is dropped, which is what the log level setting buys. +VOID TEST(FileLogTest, LogDropsALevelBelowTheConfiguredOne) +{ + MockLogWriterForFileLog writer; + SrsFileLog log; + + srs_freep(log.writer_); + log.writer_ = &writer; + log.log_to_file_tank_ = false; + log.level_ = SrsLogLevelTrace; + + mock_log(log, SrsLogLevelInfo, "dropped"); + + // GOAL: nothing reaches the output. + EXPECT_EQ(0, writer.write_console_count_); + EXPECT_EQ(0, writer.write_file_count_); + + // GOAL: the configured level itself still passes. + mock_log(log, SrsLogLevelTrace, "kept"); + EXPECT_EQ(1, writer.write_console_count_); + + log.writer_ = NULL; +} + +// The disabled level turns the logger off, whatever the configured level is. +VOID TEST(FileLogTest, LogDropsTheDisabledLevel) +{ + MockLogWriterForFileLog writer; + SrsFileLog log; + + srs_freep(log.writer_); + log.writer_ = &writer; + log.log_to_file_tank_ = false; + log.level_ = SrsLogLevelVerbose; + + mock_log(log, SrsLogLevelDisabled, "dropped"); + + // GOAL: nothing reaches the output, even though the level is above the configured one. + EXPECT_EQ(0, writer.write_console_count_); + EXPECT_EQ(0, writer.write_file_count_); + + // GOAL: the highest level that is not disabled still passes. + mock_log(log, SrsLogLevelError, "kept"); + EXPECT_EQ(1, writer.write_console_count_); + + log.writer_ = NULL; +} + +// The message the caller formatted reaches the output, after the header the logger builds. +VOID TEST(FileLogTest, LogWritesTheFormattedMessage) +{ + MockLogWriterForFileLog writer; + SrsFileLog log; + + srs_freep(log.writer_); + log.writer_ = &writer; + log.log_to_file_tank_ = false; + log.level_ = SrsLogLevelTrace; + + mock_log(log, SrsLogLevelTrace, "stream=%s, clients=%d", "live/livestream", 7); + + // GOAL: the formatted arguments arrive, behind a header and in front of the tail. The header spelling is not + // asserted, because the level strings differ between the v1 and v2 log level builds. + EXPECT_EQ(1, writer.write_console_count_); + size_t at = writer.console_.find("stream=live/livestream, clients=7\n"); + ASSERT_TRUE(at != string::npos); + EXPECT_TRUE(at > 0); + EXPECT_EQ('[', writer.console_[0]); + + log.writer_ = NULL; +} + +// An error line carries the system error text, so the reason is in the log without the caller passing it. +VOID TEST(FileLogTest, LogAppendsTheSystemErrorToAnErrorLine) +{ + MockLogWriterForFileLog writer; + SrsFileLog log; + + srs_freep(log.writer_); + log.writer_ = &writer; + log.log_to_file_tank_ = false; + log.level_ = SrsLogLevelTrace; + + errno = EACCES; + mock_log(log, SrsLogLevelError, "open failed"); + errno = 0; + + // GOAL: strerror() of the pending errno is appended in parentheses. + string expect = string("open failed(") + strerror(EACCES) + ")\n"; + EXPECT_TRUE(writer.console_.find(expect) != string::npos); + + // GOAL: a trace line carries no system error. + errno = EACCES; + mock_log(log, SrsLogLevelTrace, "no error here"); + errno = 0; + EXPECT_TRUE(writer.console_.find("no error here\n") != string::npos); + + log.writer_ = NULL; +} + +// Descriptor 0 is a valid descriptor, and the logger opens its log file with whatever number the system hands out. +// A process started with its standard input closed gets 0 for the log file, so the logger must treat 0 as a file it +// holds. The sentinel for "no file" is -1, which is what every guard has to test against. + +// A log file that landed on descriptor 0 must still be written to, rather than dropping every line for the life of +// the process while the file sits there empty. +VOID TEST(FileLogTest, WriteLogWritesToDescriptorZero) +{ + MockAppConfigForFileLog config; + MockLogWriterForFileLog writer; + SrsFileLog log; + + srs_freep(log.writer_); + log.writer_ = &writer; + log.config_ = &config; + log.log_to_file_tank_ = true; + log.fd_ = -1; + config.log_file_ = "./objs/srs_utest_ai30_write.log"; + + // The process was started with its standard input closed, so the log file is the lowest free descriptor. + writer.open_fd_ = 0; + + char str_log[16] = "hello"; + log.write_log(log.fd_, str_log, 5, SrsLogLevelTrace); + + // GOAL: the line reaches descriptor 0. + EXPECT_EQ(1, writer.open_file_count_); + EXPECT_EQ(0, log.fd_); + EXPECT_EQ(1, writer.write_file_count_); + EXPECT_EQ(0, writer.written_fd_); + EXPECT_STREQ("hello\n", writer.written_.c_str()); + + // GOAL: the descriptor is kept, so the next line does not open the file again. + char second[16] = "again"; + log.write_log(log.fd_, second, 5, SrsLogLevelTrace); + EXPECT_EQ(1, writer.open_file_count_); + EXPECT_EQ(2, writer.write_file_count_); + + log.fd_ = -1; + log.config_ = NULL; + log.writer_ = NULL; +} + +// Rotating a log file that landed on descriptor 0 must release it, or the process leaks it and the number is gone +// for good. +VOID TEST(FileLogTest, ReopenClosesDescriptorZero) +{ + MockAppConfigForFileLog config; + MockLogWriterForFileLog writer; + SrsFileLog log; + + srs_freep(log.writer_); + log.writer_ = &writer; + log.config_ = &config; + log.log_to_file_tank_ = true; + log.fd_ = 0; + config.log_file_ = "./objs/srs_utest_ai30_write.log"; + writer.open_fd_ = 5; + + log.reopen(); + + // GOAL: descriptor 0 is closed, and the reopened file replaces it. + EXPECT_EQ(1, writer.close_file_count_); + EXPECT_EQ(0, writer.closed_fd_); + EXPECT_EQ(5, log.fd_); + + log.fd_ = -1; + log.config_ = NULL; + log.writer_ = NULL; +} + +// Destroying the logger must release a log file that landed on descriptor 0, for the same reason. +VOID TEST(FileLogTest, DestructorClosesDescriptorZero) +{ + // The logger owns this writer, so the close is recorded outside it. + int closed = -2; + MockLogWriterForFileLog *writer = new MockLogWriterForFileLog(); + writer->closed_fd_out_ = &closed; + + SrsFileLog *log = new SrsFileLog(); + srs_freep(log->writer_); + log->writer_ = writer; + log->fd_ = 0; + + srs_freep(log); + + // GOAL: descriptor 0 was closed on the way out. + EXPECT_EQ(0, closed); +} diff --git a/trunk/src/utest/srs_utest_ai30.hpp b/trunk/src/utest/srs_utest_ai30.hpp new file mode 100644 index 0000000000..59c6ca1f56 --- /dev/null +++ b/trunk/src/utest/srs_utest_ai30.hpp @@ -0,0 +1,72 @@ +// +// Copyright (c) 2013-2026 The SRS Authors +// +// SPDX-License-Identifier: MIT +// + +#ifndef SRS_UTEST_AI30_HPP +#define SRS_UTEST_AI30_HPP + +/* +#include +*/ +#include + +#include +#include + +#include + +// The log file the file logger opens, and whether it was asked for one. +class MockAppConfigForFileLog : public MockAppConfig +{ +public: + // The log file to open. Empty means no log file is configured. + std::string log_file_; + // How many times the logger asked for the log file. + int get_log_file_count_; + +public: + MockAppConfigForFileLog(); + virtual ~MockAppConfigForFileLog(); + +public: + virtual std::string get_log_file(); +}; + +// What the file logger wrote, captured instead of a log file and a console. +class MockLogWriterForFileLog : public ISrsLogWriter +{ +public: + // The path of the last open_file(), and how many times it was asked to open one. + std::string opened_path_; + int open_file_count_; + // The descriptor open_file() returns. Negative means the open failed. + int open_fd_; + // The descriptor of the last close_file(), and how many times it was asked to close one. + int closed_fd_; + int close_file_count_; + // When set, close_file() also records the descriptor here, so a test can observe a close that happens while the + // logger is being destroyed and owns this writer. + int *closed_fd_out_; + // The descriptor and bytes of the last write_file(), and how many times it was called. + int written_fd_; + std::string written_; + int write_file_count_; + // The color and bytes of the last write_console(), and how many times it was called. + std::string console_color_; + std::string console_; + int write_console_count_; + +public: + MockLogWriterForFileLog(); + virtual ~MockLogWriterForFileLog(); + // Interface ISrsLogWriter +public: + virtual int open_file(const std::string &path); + virtual void close_file(int fd); + virtual void write_file(int fd, const char *str_log, int size); + virtual void write_console(const char *color, const char *str_log, int size); +}; + +#endif diff --git a/trunk/src/utest/srs_utest_ai31.cpp b/trunk/src/utest/srs_utest_ai31.cpp new file mode 100644 index 0000000000..3bbb7df721 --- /dev/null +++ b/trunk/src/utest/srs_utest_ai31.cpp @@ -0,0 +1,470 @@ +// +// Copyright (c) 2013-2026 The SRS Authors +// +// SPDX-License-Identifier: MIT +// +#include + +#include +#include +#include +#include + +using namespace std; + +MockHttpHandlerForHttpServer::MockHttpHandlerForHttpServer() +{ +} + +MockHttpHandlerForHttpServer::~MockHttpHandlerForHttpServer() +{ +} + +srs_error_t MockHttpHandlerForHttpServer::serve_http(ISrsHttpResponseWriter *w, ISrsHttpMessage *r) +{ + return srs_success; +} + +MockHttpServeMuxForHttpServer::MockHttpServeMuxForHttpServer() +{ + handle_error_ = 0; + serve_http_count_ = 0; + find_handler_count_ = 0; + not_found_ = new SrsHttpNotFoundHandler(); + handler_ = not_found_; + find_handler_error_ = 0; +} + +MockHttpServeMuxForHttpServer::~MockHttpServeMuxForHttpServer() +{ + for (int i = 0; i < (int)handlers_.size(); i++) { + ISrsHttpHandler *handler = handlers_[i]; + srs_freep(handler); + } + + srs_freep(not_found_); +} + +srs_error_t MockHttpServeMuxForHttpServer::handle(string pattern, ISrsHttpHandler *handler) +{ + patterns_.push_back(pattern); + handlers_.push_back(handler); + + if (handle_error_) { + return srs_error_new(handle_error_, "mock mux"); + } + + return srs_success; +} + +srs_error_t MockHttpServeMuxForHttpServer::serve_http(ISrsHttpResponseWriter *w, ISrsHttpMessage *r) +{ + serve_http_count_++; + return srs_success; +} + +srs_error_t MockHttpServeMuxForHttpServer::find_handler(ISrsHttpMessage *r, ISrsHttpHandler **ph) +{ + find_handler_count_++; + + if (find_handler_error_) { + return srs_error_new(find_handler_error_, "mock mux"); + } + + *ph = handler_; + return srs_success; +} + +void MockHttpServeMuxForHttpServer::unhandle(string pattern, ISrsHttpHandler *handler) +{ +} + +MockHttpStreamServerForHttpServer::MockHttpStreamServerForHttpServer() +{ + mux_ = new MockHttpServeMuxForHttpServer(); + assemble_count_ = 0; + initialize_count_ = 0; + initialize_error_ = 0; +} + +MockHttpStreamServerForHttpServer::~MockHttpStreamServerForHttpServer() +{ + srs_freep(mux_); +} + +void MockHttpStreamServerForHttpServer::assemble() +{ + assemble_count_++; +} + +srs_error_t MockHttpStreamServerForHttpServer::initialize() +{ + initialize_count_++; + + if (initialize_error_) { + return srs_error_new(initialize_error_, "mock stream"); + } + + return srs_success; +} + +srs_error_t MockHttpStreamServerForHttpServer::http_mount(ISrsRequest *r) +{ + mounted_.push_back(r); + return srs_success; +} + +void MockHttpStreamServerForHttpServer::http_unmount(ISrsRequest *r) +{ + unmounted_.push_back(r); +} + +ISrsHttpServeMux *MockHttpStreamServerForHttpServer::mux() +{ + return mux_; +} + +srs_error_t MockHttpStreamServerForHttpServer::dynamic_match(ISrsHttpMessage *request, ISrsHttpHandler **ph) +{ + return srs_success; +} + +MockHttpStaticServerForHttpServer::MockHttpStaticServerForHttpServer() +{ + mux_ = new MockHttpServeMuxForHttpServer(); + initialize_count_ = 0; + initialize_error_ = 0; +} + +MockHttpStaticServerForHttpServer::~MockHttpStaticServerForHttpServer() +{ + srs_freep(mux_); +} + +srs_error_t MockHttpStaticServerForHttpServer::initialize() +{ + initialize_count_++; + + if (initialize_error_) { + return srs_error_new(initialize_error_, "mock static"); + } + + return srs_success; +} + +ISrsHttpServeMux *MockHttpStaticServerForHttpServer::mux() +{ + return mux_; +} + +srs_error_t MockHttpStaticServerForHttpServer::serve_http(ISrsHttpResponseWriter *w, ISrsHttpMessage *r) +{ + return srs_success; +} + +// Replace the two servers the http server allocated for itself with mocks the test drives. +static void inject_mocks(SrsHttpServer *server, MockHttpStreamServerForHttpServer *stream, MockHttpStaticServerForHttpServer *statics) +{ + srs_freep(server->http_stream_); + server->http_stream_ = stream; + + srs_freep(server->http_static_); + server->http_static_ = statics; +} + +// The mocks are borrowed, so drop them before the http server destructor frees what it holds. +static void release_mocks(SrsHttpServer *server) +{ + server->http_stream_ = NULL; + server->http_static_ = NULL; +} + +// SrsHttpServer is the composition of the two servers behind one HTTP port: the live stream server and the static +// file server. It owns both, initializes both, and decides which of them answers each request. Nothing it does +// needs a socket, a file or a config, so all of it is testable once an owner can put its own two servers in place. + +// GOAL: the stream server is assembled by the owner calling assemble(), not by the constructor, so a test can put +// its own stream server in place first. Assembling from the constructor registers the dynamic matcher on the +// stream server the constructor made, which an owner then throws away. +VOID TEST(HttpServerTest, AssembleAssemblesTheStreamServer) +{ + MockHttpStreamServerForHttpServer stream; + MockHttpStaticServerForHttpServer statics; + + SrsHttpServer server; + inject_mocks(&server, &stream, &statics); + + EXPECT_EQ(0, stream.assemble_count_); + + server.assemble(); + release_mocks(&server); + + EXPECT_EQ(1, stream.assemble_count_); +} + +// The versions API is mounted on the static server, so SRS go-sharp can detect an HTTP-FLV cluster node. +VOID TEST(HttpServerTest, InitializeMountsTheVersionsApiOnTheStaticServer) +{ + srs_error_t err = srs_success; + + MockHttpStreamServerForHttpServer stream; + MockHttpStaticServerForHttpServer statics; + + SrsHttpServer server; + inject_mocks(&server, &stream, &statics); + + HELPER_EXPECT_SUCCESS(server.initialize()); + release_mocks(&server); + + ASSERT_EQ(1, (int)statics.mux_->patterns_.size()); + EXPECT_STREQ("/api/v1/versions", statics.mux_->patterns_[0].c_str()); + EXPECT_EQ(0, (int)stream.mux_->patterns_.size()); +} + +// Both halves are initialized. +VOID TEST(HttpServerTest, InitializeInitializesBothServers) +{ + srs_error_t err = srs_success; + + MockHttpStreamServerForHttpServer stream; + MockHttpStaticServerForHttpServer statics; + + SrsHttpServer server; + inject_mocks(&server, &stream, &statics); + + HELPER_EXPECT_SUCCESS(server.initialize()); + release_mocks(&server); + + EXPECT_EQ(1, stream.initialize_count_); + EXPECT_EQ(1, statics.initialize_count_); +} + +// A static server that refuses the versions API fails the whole initialization, before either half is initialized. +VOID TEST(HttpServerTest, InitializeFailsWhenTheVersionsApiIsRefused) +{ + srs_error_t err = srs_success; + + MockHttpStreamServerForHttpServer stream; + MockHttpStaticServerForHttpServer statics; + statics.mux_->handle_error_ = ERROR_HTTP_PATTERN_DUPLICATED; + + SrsHttpServer server; + inject_mocks(&server, &stream, &statics); + + HELPER_EXPECT_FAILED(server.initialize()); + release_mocks(&server); + + EXPECT_EQ(0, stream.initialize_count_); + EXPECT_EQ(0, statics.initialize_count_); +} + +// The stream server is initialized first, so its failure stops the static server from being initialized at all. +VOID TEST(HttpServerTest, InitializeFailsBeforeTheStaticServerWhenTheStreamServerFails) +{ + srs_error_t err = srs_success; + + MockHttpStreamServerForHttpServer stream; + MockHttpStaticServerForHttpServer statics; + stream.initialize_error_ = ERROR_HTTP_HANDLER_INVALID; + + SrsHttpServer server; + inject_mocks(&server, &stream, &statics); + + HELPER_EXPECT_FAILED(server.initialize()); + release_mocks(&server); + + EXPECT_EQ(1, stream.initialize_count_); + EXPECT_EQ(0, statics.initialize_count_); +} + +// A static server that cannot initialize fails the whole initialization too. +VOID TEST(HttpServerTest, InitializeFailsWhenTheStaticServerFails) +{ + srs_error_t err = srs_success; + + MockHttpStreamServerForHttpServer stream; + MockHttpStaticServerForHttpServer statics; + statics.initialize_error_ = ERROR_HTTP_HANDLER_INVALID; + + SrsHttpServer server; + inject_mocks(&server, &stream, &statics); + + HELPER_EXPECT_FAILED(server.initialize()); + release_mocks(&server); + + EXPECT_EQ(1, stream.initialize_count_); + EXPECT_EQ(1, statics.initialize_count_); +} + +// A handler registered on the http server lands on the static server, which is where the API and console live. +VOID TEST(HttpServerTest, HandleRegistersThePatternOnTheStaticServer) +{ + srs_error_t err = srs_success; + + MockHttpStreamServerForHttpServer stream; + MockHttpStaticServerForHttpServer statics; + + SrsHttpServer server; + inject_mocks(&server, &stream, &statics); + + HELPER_EXPECT_SUCCESS(server.handle("/api/v1/clients", new SrsHttpNotFoundHandler())); + release_mocks(&server); + + ASSERT_EQ(1, (int)statics.mux_->patterns_.size()); + EXPECT_STREQ("/api/v1/clients", statics.mux_->patterns_[0].c_str()); + EXPECT_EQ(0, (int)stream.mux_->patterns_.size()); +} + +// An API request goes to the static server without ever asking the stream server, because a stream can never be +// mounted under /api/. +VOID TEST(HttpServerTest, ServeHttpRoutesTheApiToTheStaticServer) +{ + srs_error_t err = srs_success; + + MockHttpStreamServerForHttpServer stream; + MockHttpStaticServerForHttpServer statics; + MockHttpMessageForDynamicMatch msg; + msg.path_ = "/api/v1/versions"; + + SrsHttpServer server; + inject_mocks(&server, &stream, &statics); + + HELPER_EXPECT_SUCCESS(server.serve_http(NULL, &msg)); + release_mocks(&server); + + EXPECT_EQ(1, statics.mux_->serve_http_count_); + EXPECT_EQ(0, stream.mux_->find_handler_count_); + EXPECT_EQ(0, stream.mux_->serve_http_count_); +} + +// The console is served the same way, from the static server only. +VOID TEST(HttpServerTest, ServeHttpRoutesTheConsoleToTheStaticServer) +{ + srs_error_t err = srs_success; + + MockHttpStreamServerForHttpServer stream; + MockHttpStaticServerForHttpServer statics; + MockHttpMessageForDynamicMatch msg; + msg.path_ = "/console/index.html"; + + SrsHttpServer server; + inject_mocks(&server, &stream, &statics); + + HELPER_EXPECT_SUCCESS(server.serve_http(NULL, &msg)); + release_mocks(&server); + + EXPECT_EQ(1, statics.mux_->serve_http_count_); + EXPECT_EQ(0, stream.mux_->find_handler_count_); +} + +// The API shortcut needs the whole "/api/" prefix. A path that is only as long as "/api" is a normal path, so the +// stream server is asked about it like any other. +VOID TEST(HttpServerTest, ServeHttpDoesNotTakeAShortPathForTheApi) +{ + srs_error_t err = srs_success; + + MockHttpStreamServerForHttpServer stream; + MockHttpStaticServerForHttpServer statics; + MockHttpMessageForDynamicMatch msg; + msg.path_ = "/api"; + + SrsHttpServer server; + inject_mocks(&server, &stream, &statics); + + HELPER_EXPECT_SUCCESS(server.serve_http(NULL, &msg)); + release_mocks(&server); + + EXPECT_EQ(1, stream.mux_->find_handler_count_); + EXPECT_EQ(1, statics.mux_->serve_http_count_); +} + +// A path a stream is mounted at is served by the stream server. +VOID TEST(HttpServerTest, ServeHttpRoutesAMountedStreamToTheStreamServer) +{ + srs_error_t err = srs_success; + + MockHttpStreamServerForHttpServer stream; + MockHttpStaticServerForHttpServer statics; + MockHttpHandlerForHttpServer found; + stream.mux_->handler_ = &found; + + MockHttpMessageForDynamicMatch msg; + msg.path_ = "/live/livestream.flv"; + + SrsHttpServer server; + inject_mocks(&server, &stream, &statics); + + // The handler the stream mux found is not the not found handler, so the stream server serves the request. + HELPER_EXPECT_SUCCESS(server.serve_http(NULL, &msg)); + release_mocks(&server); + + EXPECT_EQ(1, stream.mux_->find_handler_count_); + EXPECT_EQ(1, stream.mux_->serve_http_count_); + EXPECT_EQ(0, statics.mux_->serve_http_count_); +} + +// A path no stream is mounted at falls back to the static server, which serves the files. +VOID TEST(HttpServerTest, ServeHttpFallsBackToTheStaticServerWhenNoStreamMatches) +{ + srs_error_t err = srs_success; + + MockHttpStreamServerForHttpServer stream; + MockHttpStaticServerForHttpServer statics; + MockHttpMessageForDynamicMatch msg; + msg.path_ = "/live/livestream.m3u8"; + + SrsHttpServer server; + inject_mocks(&server, &stream, &statics); + + HELPER_EXPECT_SUCCESS(server.serve_http(NULL, &msg)); + release_mocks(&server); + + EXPECT_EQ(1, stream.mux_->find_handler_count_); + EXPECT_EQ(0, stream.mux_->serve_http_count_); + EXPECT_EQ(1, statics.mux_->serve_http_count_); +} + +// A failed lookup fails the request instead of falling back, so a broken stream mux is not hidden by the static +// server answering 404. +VOID TEST(HttpServerTest, ServeHttpFailsWhenTheStreamLookupFails) +{ + srs_error_t err = srs_success; + + MockHttpStreamServerForHttpServer stream; + MockHttpStaticServerForHttpServer statics; + stream.mux_->find_handler_error_ = ERROR_HTTP_HANDLER_MATCH_URL; + + MockHttpMessageForDynamicMatch msg; + msg.path_ = "/live/livestream.flv"; + + SrsHttpServer server; + inject_mocks(&server, &stream, &statics); + + HELPER_EXPECT_FAILED(server.serve_http(NULL, &msg)); + release_mocks(&server); + + EXPECT_EQ(0, stream.mux_->serve_http_count_); + EXPECT_EQ(0, statics.mux_->serve_http_count_); +} + +// Mounting and unmounting a stream reaches the stream server, which is the only half that serves live streams. +VOID TEST(HttpServerTest, MountAndUnmountReachTheStreamServer) +{ + srs_error_t err = srs_success; + + MockHttpStreamServerForHttpServer stream; + MockHttpStaticServerForHttpServer statics; + SrsRequest req; + + SrsHttpServer server; + inject_mocks(&server, &stream, &statics); + + HELPER_EXPECT_SUCCESS(server.http_mount(&req)); + server.http_unmount(&req); + release_mocks(&server); + + ASSERT_EQ(1, (int)stream.mounted_.size()); + EXPECT_TRUE(&req == stream.mounted_[0]); + ASSERT_EQ(1, (int)stream.unmounted_.size()); + EXPECT_TRUE(&req == stream.unmounted_[0]); +} diff --git a/trunk/src/utest/srs_utest_ai31.hpp b/trunk/src/utest/srs_utest_ai31.hpp new file mode 100644 index 0000000000..ee980480a4 --- /dev/null +++ b/trunk/src/utest/srs_utest_ai31.hpp @@ -0,0 +1,110 @@ +// +// Copyright (c) 2013-2026 The SRS Authors +// +// SPDX-License-Identifier: MIT +// + +#ifndef SRS_UTEST_AI31_HPP +#define SRS_UTEST_AI31_HPP + +/* +#include +*/ +#include + +#include +#include +#include + +#include +#include + +// A handler that matched a request, as opposed to the not found handler a mux answers with when nothing matched. +class MockHttpHandlerForHttpServer : public ISrsHttpHandler +{ +public: + MockHttpHandlerForHttpServer(); + virtual ~MockHttpHandlerForHttpServer(); + +public: + virtual srs_error_t serve_http(ISrsHttpResponseWriter *w, ISrsHttpMessage *r); +}; + +// Records every mount and lookup one of the two muxes of the http server received. +class MockHttpServeMuxForHttpServer : public ISrsHttpServeMux +{ +public: + // The patterns handled, in order, and the handlers this mux took ownership of. + std::vector patterns_; + std::vector handlers_; + // When set, handle() fails with this error code, after taking the handler. + int handle_error_; + // How many requests this mux was asked to serve. + int serve_http_count_; + // How many handler lookups this mux answered. + int find_handler_count_; + // The not found handler this mux answers with when nothing matches, as SrsHttpServeMux does. + SrsHttpNotFoundHandler *not_found_; + // The handler find_handler() returns. Borrowed, and defaults to not_found_. + ISrsHttpHandler *handler_; + // When set, find_handler() fails with this error code. + int find_handler_error_; + +public: + MockHttpServeMuxForHttpServer(); + virtual ~MockHttpServeMuxForHttpServer(); + +public: + virtual srs_error_t handle(std::string pattern, ISrsHttpHandler *handler); + virtual srs_error_t serve_http(ISrsHttpResponseWriter *w, ISrsHttpMessage *r); + virtual srs_error_t find_handler(ISrsHttpMessage *r, ISrsHttpHandler **ph); + virtual void unhandle(std::string pattern, ISrsHttpHandler *handler); +}; + +// The live stream half of the http server: records the assembly, the initialization and every mount. +class MockHttpStreamServerForHttpServer : public ISrsHttpStreamServer +{ +public: + MockHttpServeMuxForHttpServer *mux_; + // How many times the owner assembled this server. + int assemble_count_; + // How many times the owner initialized it, and the error initialize() fails with. + int initialize_count_; + int initialize_error_; + // The requests mounted and unmounted, in order. + std::vector mounted_; + std::vector unmounted_; + +public: + MockHttpStreamServerForHttpServer(); + virtual ~MockHttpStreamServerForHttpServer(); + +public: + virtual void assemble(); + virtual srs_error_t initialize(); + virtual srs_error_t http_mount(ISrsRequest *r); + virtual void http_unmount(ISrsRequest *r); + virtual ISrsHttpServeMux *mux(); + virtual srs_error_t dynamic_match(ISrsHttpMessage *request, ISrsHttpHandler **ph); +}; + +// The static file half of the http server: records the initialization and serves through its mux. +class MockHttpStaticServerForHttpServer : public ISrsHttpStaticServer +{ +public: + MockHttpServeMuxForHttpServer *mux_; + // How many times the owner initialized it, and the error initialize() fails with. + int initialize_count_; + int initialize_error_; + +public: + MockHttpStaticServerForHttpServer(); + virtual ~MockHttpStaticServerForHttpServer(); + +public: + virtual srs_error_t initialize(); + virtual ISrsHttpServeMux *mux(); + virtual srs_error_t serve_http(ISrsHttpResponseWriter *w, ISrsHttpMessage *r); +}; + +#endif diff --git a/trunk/src/utest/srs_utest_ai32.cpp b/trunk/src/utest/srs_utest_ai32.cpp new file mode 100644 index 0000000000..80d24602e2 --- /dev/null +++ b/trunk/src/utest/srs_utest_ai32.cpp @@ -0,0 +1,458 @@ +// +// Copyright (c) 2013-2026 The SRS Authors +// +// SPDX-License-Identifier: MIT +// +#include + +#include +#include + +using namespace std; + +MockStageManagerForPithyPrint::MockStageManagerForPithyPrint() +{ +} + +MockStageManagerForPithyPrint::~MockStageManagerForPithyPrint() +{ + map::iterator it; + for (it = stages_.begin(); it != stages_.end(); ++it) { + SrsStageInfo *stage = it->second; + srs_freep(stage); + } +} + +SrsStageInfo *MockStageManagerForPithyPrint::fetch_or_create(int stage_id, bool *pnew) +{ + fetch_ids_.push_back(stage_id); + + map::iterator it = stages_.find(stage_id); + if (it != stages_.end()) { + if (pnew) { + *pnew = false; + } + return it->second; + } + + SrsStageInfo *stage = new SrsStageInfo(stage_id); + stages_[stage_id] = stage; + + if (pnew) { + *pnew = true; + } + + return stage; +} + +MockConfigForStageInfo::MockConfigForStageInfo() +{ + pithy_print_ = 0; +} + +MockConfigForStageInfo::~MockConfigForStageInfo() +{ +} + +srs_utime_t MockConfigForStageInfo::get_pithy_print() +{ + return pithy_print_; +} + +string MockConfigForStageInfo::get_default_app_name() +{ + return "live"; +} + +string MockConfigForStageInfo::get_srt_default_mode() +{ + return "request"; +} + +MockKernelFactoryForStageInfo::MockKernelFactoryForStageInfo() +{ + pithy_print_ = 0; + create_config_count_ = 0; +} + +MockKernelFactoryForStageInfo::~MockKernelFactoryForStageInfo() +{ +} + +ISrsCoroutine *MockKernelFactoryForStageInfo::create_coroutine(const string &name, ISrsCoroutineHandler *handler, + SrsContextId cid) +{ + return NULL; +} + +ISrsTime *MockKernelFactoryForStageInfo::create_time() +{ + return NULL; +} + +ISrsConfig *MockKernelFactoryForStageInfo::create_config() +{ + create_config_count_++; + + MockConfigForStageInfo *config = new MockConfigForStageInfo(); + config->pithy_print_ = pithy_print_; + return config; +} + +ISrsCond *MockKernelFactoryForStageInfo::create_cond() +{ + return NULL; +} + +MockClockForPithyPrint::MockClockForPithyPrint() +{ + now_ = 0; +} + +MockClockForPithyPrint::~MockClockForPithyPrint() +{ +} + +srs_utime_t MockClockForPithyPrint::now() +{ + return now_; +} + +// The printer enters its stage from assemble(), through the injected manager, not from the constructor. +VOID TEST(PithyPrintStageTest, AssembleEntersStageThroughInjectedManager) +{ + MockStageManagerForPithyPrint stages; + MockClockForPithyPrint clk; + + SrsPithyPrint pprint(9001); + pprint.stages_ = &stages; + pprint.clk_ = &clk; + + clk.now_ = 30 * SRS_UTIME_SECONDS; + pprint.assemble(); + + ASSERT_EQ(1, (int)stages.fetch_ids_.size()); + EXPECT_EQ(9001, stages.fetch_ids_[0]); + EXPECT_EQ(1, stages.stages_[9001]->nb_clients_); + EXPECT_EQ(0, pprint.client_id_); + EXPECT_EQ(30 * SRS_UTIME_SECONDS, pprint.previous_tick_); +} + +// Constructing a printer has no observable effect on its stage, so a test can inject before anything happens. +VOID TEST(PithyPrintStageTest, ConstructorEntersNoStage) +{ + SrsStageInfo *stage = _srs_stages->fetch_or_create(9002); + int nb_clients = stage->nb_clients_; + + if (true) { + SrsPithyPrint pprint(9002); + EXPECT_EQ(nb_clients, stage->nb_clients_); + + // Never entered, so do not leave: the printer must not decrement a stage it did not enter. + pprint.stages_ = NULL; + } + + EXPECT_EQ(nb_clients, stage->nb_clients_); +} + +// Each printer leaves the stage it entered, through the same manager it entered by. +VOID TEST(PithyPrintStageTest, DestructorLeavesStageThroughInjectedManager) +{ + MockStageManagerForPithyPrint stages; + MockClockForPithyPrint clk; + SrsStageInfo *stage = NULL; + + if (true) { + SrsPithyPrint first(9003); + first.stages_ = &stages; + first.clk_ = &clk; + first.assemble(); + + SrsPithyPrint second(9003); + second.stages_ = &stages; + second.clk_ = &clk; + second.assemble(); + + stage = stages.stages_[9003]; + ASSERT_TRUE(stage != NULL); + EXPECT_EQ(2, stage->nb_clients_); + EXPECT_EQ(0, first.client_id_); + EXPECT_EQ(1, second.client_id_); + } + + EXPECT_EQ(0, stage->nb_clients_); +} + +// The elapsed time comes from the injected clock, and feeds both the client age and the shared stage. +VOID TEST(PithyPrintStageTest, ElapseAccumulatesInjectedClockDelta) +{ + MockStageManagerForPithyPrint stages; + MockClockForPithyPrint clk; + + clk.now_ = 10 * SRS_UTIME_SECONDS; + + SrsPithyPrint pprint(9004); + pprint.stages_ = &stages; + pprint.clk_ = &clk; + pprint.assemble(); + + SrsStageInfo *stage = stages.stages_[9004]; + ASSERT_TRUE(stage != NULL); + EXPECT_EQ(0, pprint.age()); + EXPECT_EQ(0, stage->age_); + + clk.now_ = 13 * SRS_UTIME_SECONDS; + pprint.elapse(); + EXPECT_EQ(3 * SRS_UTIME_SECONDS, pprint.age()); + EXPECT_EQ(3 * SRS_UTIME_SECONDS, stage->age_); + + clk.now_ = 14 * SRS_UTIME_SECONDS; + pprint.elapse(); + EXPECT_EQ(4 * SRS_UTIME_SECONDS, pprint.age()); + EXPECT_EQ(4 * SRS_UTIME_SECONDS, stage->age_); + + // The stage is looked up once by assemble() and once by the first elapse(), then cached. + EXPECT_EQ(2, (int)stages.fetch_ids_.size()); +} + +// A clock that moves backwards contributes nothing, rather than a negative age. +VOID TEST(PithyPrintStageTest, ElapseClampsBackwardClock) +{ + MockStageManagerForPithyPrint stages; + MockClockForPithyPrint clk; + + clk.now_ = 10 * SRS_UTIME_SECONDS; + + SrsPithyPrint pprint(9005); + pprint.stages_ = &stages; + pprint.clk_ = &clk; + pprint.assemble(); + + clk.now_ = 5 * SRS_UTIME_SECONDS; + pprint.elapse(); + + SrsStageInfo *stage = stages.stages_[9005]; + ASSERT_TRUE(stage != NULL); + EXPECT_EQ(0, pprint.age()); + EXPECT_EQ(0, stage->age_); + EXPECT_EQ(5 * SRS_UTIME_SECONDS, pprint.previous_tick_); +} + +// The clients of a stage share its print interval: with two clients, one prints every two intervals. +VOID TEST(PithyPrintStageTest, CanPrintSharesIntervalAmongClients) +{ + MockStageManagerForPithyPrint stages; + MockClockForPithyPrint clk; + + SrsPithyPrint first(9006); + first.stages_ = &stages; + first.clk_ = &clk; + first.assemble(); + + SrsPithyPrint second(9006); + second.stages_ = &stages; + second.clk_ = &clk; + second.assemble(); + + SrsStageInfo *stage = stages.stages_[9006]; + ASSERT_TRUE(stage != NULL); + stage->interval_ = 3 * SRS_UTIME_SECONDS; + stage->age_ = 0; + EXPECT_EQ(2, stage->nb_clients_); + + clk.now_ = 5 * SRS_UTIME_SECONDS; + first.elapse(); + EXPECT_FALSE(first.can_print()); + + clk.now_ = 6 * SRS_UTIME_SECONDS; + first.elapse(); + EXPECT_TRUE(first.can_print()); + + // Printing resets the age of the shared stage, but not the age of the client. + EXPECT_EQ(0, stage->age_); + EXPECT_EQ(6 * SRS_UTIME_SECONDS, first.age()); + EXPECT_FALSE(second.can_print()); +} + +// Constructing a stage creates nothing, so a test can replace the factory before it is used. +VOID TEST(PithyPrintStageTest, StageInfoConstructorCreatesNoConfig) +{ + SrsStageInfo stage(9007, 1.0); + + EXPECT_TRUE(stage.config_ == NULL); + EXPECT_EQ(0, stage.interval_); +} + +// The print interval comes from a config the stage creates through the injected factory. +VOID TEST(PithyPrintStageTest, StageInfoAssembleReadsIntervalThroughFactory) +{ + MockKernelFactoryForStageInfo factory; + factory.pithy_print_ = 7 * SRS_UTIME_SECONDS; + + SrsStageInfo stage(9008, 1.0); + stage.factory_ = &factory; + stage.assemble(); + + EXPECT_EQ(1, factory.create_config_count_); + EXPECT_TRUE(stage.config_ != NULL); + EXPECT_EQ(7 * SRS_UTIME_SECONDS, stage.interval_); +} + +// update_print_time() re-reads the interval from the config the stage already holds. +VOID TEST(PithyPrintStageTest, StageInfoUpdatePrintTimeRereadsTheConfig) +{ + MockConfigForStageInfo *config = new MockConfigForStageInfo(); + config->pithy_print_ = 9 * SRS_UTIME_SECONDS; + + SrsStageInfo stage(9012, 1.0); + srs_freep(stage.config_); // whatever the stage holds is owned by it: free, then replace + stage.config_ = config; + + stage.update_print_time(); + EXPECT_EQ(9 * SRS_UTIME_SECONDS, stage.interval_); +} + +// A manager assembles the stages it creates, so every stage it hands out has its interval. +// Like FactoriesAssembleThePrinter below, this passes before and after the refactor: it exists to +// prove the move of the config read out of the constructor changed nothing a caller can see. +VOID TEST(PithyPrintStageTest, StageManagerAssemblesTheStagesItCreates) +{ + SrsStageManager manager; + + SrsStageInfo *stage = manager.fetch_or_create(9009); + ASSERT_TRUE(stage != NULL); + EXPECT_TRUE(stage->config_ != NULL); + EXPECT_EQ(stage->config_->get_pithy_print(), stage->interval_); +} + +// A standalone printer creates nothing either, and it is one client of its own stage. +VOID TEST(PithyPrintStageTest, AlonePithyPrintConstructorIsQuiescent) +{ + SrsAlonePithyPrint pprint; + + EXPECT_TRUE(pprint.info_.config_ == NULL); + EXPECT_EQ(0, pprint.previous_tick_); + EXPECT_EQ(1, pprint.info_.nb_clients_); +} + +// The standalone printer ages on the injected clock and prints once its own interval passes. +VOID TEST(PithyPrintStageTest, AlonePithyPrintElapsesOnInjectedClock) +{ + MockKernelFactoryForStageInfo factory; + factory.pithy_print_ = 7 * SRS_UTIME_SECONDS; + MockClockForPithyPrint clk; + + SrsAlonePithyPrint pprint; + pprint.info_.factory_ = &factory; + pprint.clk_ = &clk; + + clk.now_ = 20 * SRS_UTIME_SECONDS; + pprint.assemble(); + + EXPECT_EQ(7 * SRS_UTIME_SECONDS, pprint.info_.interval_); + EXPECT_EQ(20 * SRS_UTIME_SECONDS, pprint.previous_tick_); + + clk.now_ = 26 * SRS_UTIME_SECONDS; + pprint.elapse(); + EXPECT_EQ(6 * SRS_UTIME_SECONDS, pprint.info_.age_); + EXPECT_FALSE(pprint.can_print()); + + clk.now_ = 27 * SRS_UTIME_SECONDS; + pprint.elapse(); + EXPECT_EQ(7 * SRS_UTIME_SECONDS, pprint.info_.age_); + EXPECT_TRUE(pprint.can_print()); + EXPECT_EQ(0, pprint.info_.age_); + + // A clock that moves backwards contributes nothing. + clk.now_ = 1 * SRS_UTIME_SECONDS; + pprint.elapse(); + EXPECT_EQ(0, pprint.info_.age_); +} + +// The error printer rate-limits one error code on the injected clock. +VOID TEST(PithyPrintStageTest, ErrorPithyPrintRateLimitsOnInjectedClock) +{ + MockClockForPithyPrint clk; + // The clock starts at a wall-clock value rather than zero, because can_print() reads a stored + // tick of zero as "this code has no tick yet"; srs_time_now_cached() is never zero in production. + clk.now_ = 1000 * SRS_UTIME_SECONDS; + + SrsErrorPithyPrint epp(1.0); + epp.clk_ = &clk; + + // The first error of a code is always printed, and opens its stage. + uint32_t nn = 0; + EXPECT_TRUE(epp.can_print(9010, &nn)); + EXPECT_EQ(1, nn); + + SrsStageInfo *stage = epp.stages_.fetch_or_create(9010); + ASSERT_TRUE(stage != NULL); + stage->interval_ = 5 * SRS_UTIME_SECONDS; + EXPECT_EQ(1, stage->nb_clients_); + + clk.now_ = 1003 * SRS_UTIME_SECONDS; + EXPECT_FALSE(epp.can_print(9010, &nn)); + EXPECT_EQ(2, nn); + EXPECT_EQ(3 * SRS_UTIME_SECONDS, stage->age_); + + clk.now_ = 1005 * SRS_UTIME_SECONDS; + EXPECT_TRUE(epp.can_print(9010, &nn)); + EXPECT_EQ(3, nn); + EXPECT_EQ(0, stage->age_); + + // A clock that moves backwards contributes nothing. + clk.now_ = 1001 * SRS_UTIME_SECONDS; + EXPECT_FALSE(epp.can_print(9010, &nn)); + EXPECT_EQ(0, stage->age_); + + // Each error code is rate limited on its own, and the total count covers them all. + EXPECT_TRUE(epp.can_print(9011, &nn)); + EXPECT_EQ(1, nn); + EXPECT_EQ(5, epp.nn_count_); +} + +// Every factory hands back an assembled printer, which enters and leaves the stage of its kind. +VOID TEST(PithyPrintStageTest, FactoriesAssembleThePrinter) +{ + // The stage ids of srs_kernel_pithy_print.cpp, which the factories share by kind. + struct { + SrsPithyPrint *(*create)(); + int stage_id; + } factories[] = { + {SrsPithyPrint::create_rtmp_play, 1}, {SrsPithyPrint::create_rtmp_publish, 2}, + {SrsPithyPrint::create_forwarder, 3}, {SrsPithyPrint::create_encoder, 4}, + {SrsPithyPrint::create_hls, 5}, {SrsPithyPrint::create_ingester, 6}, + {SrsPithyPrint::create_edge, 7}, {SrsPithyPrint::create_caster, 8}, + {SrsPithyPrint::create_http_stream, 9}, {SrsPithyPrint::create_http_stream_cache, 10}, + {SrsPithyPrint::create_exec, 11}, {SrsPithyPrint::create_rtc_play, 12}, + {SrsPithyPrint::create_srt_play, 15}, {SrsPithyPrint::create_srt_publish, 16}, + }; + + for (int i = 0; i < (int)(sizeof(factories) / sizeof(factories[0])); i++) { + SrsStageInfo *stage = _srs_stages->fetch_or_create(factories[i].stage_id); + int nb_clients = stage->nb_clients_; + + if (true) { + SrsUniquePtr pprint(factories[i].create()); + EXPECT_EQ(nb_clients + 1, stage->nb_clients_) << "stage " << factories[i].stage_id; + } + + EXPECT_EQ(nb_clients, stage->nb_clients_) << "stage " << factories[i].stage_id; + } + + // The RTC sender and receiver get a stage of their own for each fd. + SrsStageInfo *send = _srs_stages->fetch_or_create(7 << 16 | 13); + int nb_send = send->nb_clients_; + SrsStageInfo *recv = _srs_stages->fetch_or_create(7 << 16 | 14); + int nb_recv = recv->nb_clients_; + + if (true) { + SrsUniquePtr sender(SrsPithyPrint::create_rtc_send(7)); + SrsUniquePtr receiver(SrsPithyPrint::create_rtc_recv(7)); + EXPECT_EQ(nb_send + 1, send->nb_clients_); + EXPECT_EQ(nb_recv + 1, recv->nb_clients_); + } + + EXPECT_EQ(nb_send, send->nb_clients_); + EXPECT_EQ(nb_recv, recv->nb_clients_); +} diff --git a/trunk/src/utest/srs_utest_ai32.hpp b/trunk/src/utest/srs_utest_ai32.hpp new file mode 100644 index 0000000000..6923c8181f --- /dev/null +++ b/trunk/src/utest/srs_utest_ai32.hpp @@ -0,0 +1,91 @@ +// +// Copyright (c) 2013-2026 The SRS Authors +// +// SPDX-License-Identifier: MIT +// + +#ifndef SRS_UTEST_AI32_HPP +#define SRS_UTEST_AI32_HPP + +/* +#include +*/ +#include + +#include +#include +#include + +#include +#include +#include + +// Records every stage lookup a pithy print asks for, and owns the stages it hands back. +class MockStageManagerForPithyPrint : public ISrsStageManager +{ +public: + // The stage ids fetch_or_create() was asked for, in order. + std::vector fetch_ids_; + // The stages handed out, keyed by stage id, owned by this mock. + std::map stages_; + +public: + MockStageManagerForPithyPrint(); + virtual ~MockStageManagerForPithyPrint(); + +public: + virtual SrsStageInfo *fetch_or_create(int stage_id, bool *pnew = NULL); +}; + +// A config carrying the print interval the test chose. +class MockConfigForStageInfo : public ISrsConfig +{ +public: + srs_utime_t pithy_print_; + +public: + MockConfigForStageInfo(); + virtual ~MockConfigForStageInfo(); + +public: + virtual srs_utime_t get_pithy_print(); + virtual std::string get_default_app_name(); + virtual std::string get_srt_default_mode(); +}; + +// Hands out MockConfigForStageInfo, and counts how many a stage asked for. +// MockKernelFactoryForFastTimer cannot serve here, because its create_config() returns NULL. +class MockKernelFactoryForStageInfo : public ISrsKernelFactory +{ +public: + // The interval every config this factory creates reports. + srs_utime_t pithy_print_; + // How many configs this factory was asked for. + int create_config_count_; + +public: + MockKernelFactoryForStageInfo(); + virtual ~MockKernelFactoryForStageInfo(); + +public: + virtual ISrsCoroutine *create_coroutine(const std::string &name, ISrsCoroutineHandler *handler, SrsContextId cid); + virtual ISrsTime *create_time(); + virtual ISrsConfig *create_config(); + virtual ISrsCond *create_cond(); +}; + +// A clock the test moves by hand. +class MockClockForPithyPrint : public ISrsClock +{ +public: + srs_utime_t now_; + +public: + MockClockForPithyPrint(); + virtual ~MockClockForPithyPrint(); + +public: + virtual srs_utime_t now(); +}; + +#endif diff --git a/trunk/src/utest/srs_utest_manual_mock.hpp b/trunk/src/utest/srs_utest_manual_mock.hpp index 2ad48fc01f..5f52aad553 100644 --- a/trunk/src/utest/srs_utest_manual_mock.hpp +++ b/trunk/src/utest/srs_utest_manual_mock.hpp @@ -465,6 +465,11 @@ class MockAppConfig : public ISrsAppConfig std::string get_default_app_name() { return "live"; } void subscribe(ISrsReloadHandler *handler) {} void unsubscribe(ISrsReloadHandler *handler) {} + virtual bool get_log_tank_file() { return false; } + virtual std::string get_log_file() { return ""; } + virtual std::string get_log_level() { return "trace"; } + virtual std::string get_log_level_v2() { return ""; } + virtual bool get_utc_time() { return false; } virtual srs_error_t reload(SrsReloadState *pstate) { return srs_success; } virtual srs_error_t persistence() { return srs_success; } virtual std::string config() { return ""; } @@ -507,6 +512,9 @@ class MockAppConfig : public ISrsAppConfig virtual std::string get_https_stream_ssl_cert() { return ""; } virtual std::string get_http_stream_dir() { return ""; } virtual bool get_http_stream_crossdomain() { return false; } + virtual bool get_vhost_http_enabled(std::string vhost) { return false; } + virtual std::string get_vhost_http_mount(std::string vhost) { return ""; } + virtual std::string get_vhost_http_dir(std::string vhost) { return ""; } virtual bool get_rtc_server_enabled() { return rtc_server_enabled_; } virtual bool get_rtc_server_tcp_enabled() { return false; } virtual std::vector get_rtc_server_tcp_listens() @@ -614,7 +622,20 @@ class MockAppConfig : public ISrsAppConfig virtual std::string get_srt_default_streamid() { return "#!::r=live/livestream,m=request"; } virtual std::string get_srt_default_mode() { return srt_default_mode_; } virtual bool get_srt_to_rtmp(std::string vhost) { return srt_to_rtmp_; } + virtual int64_t get_srto_maxbw() { return -1; } + virtual int get_srto_mss() { return 1500; } + virtual bool get_srto_tsbpdmode() { return true; } + virtual int get_srto_latency() { return 120; } + virtual int get_srto_recv_latency() { return 120; } + virtual int get_srto_peer_latency() { return 0; } + virtual bool get_srto_tlpktdrop() { return true; } + virtual srs_utime_t get_srto_conntimeout() { return 3 * SRS_UTIME_SECONDS; } virtual srs_utime_t get_srto_peeridletimeout() { return 10 * SRS_UTIME_SECONDS; } + virtual int get_srto_sendbuf() { return 8192 * (1500 - 28); } + virtual int get_srto_recvbuf() { return 8192 * (1500 - 28); } + virtual int get_srto_payloadsize() { return 1316; } + virtual std::string get_srto_passphrase() { return ""; } + virtual int get_srto_pbkeylen() { return 0; } virtual bool get_rtc_to_rtmp(std::string vhost) { return rtc_to_rtmp_; } virtual srs_utime_t get_rtc_stun_timeout(std::string vhost) { return 30 * SRS_UTIME_SECONDS; } virtual bool get_rtc_stun_strict_check(std::string vhost) { return false; } diff --git a/trunk/src/utest/srs_utest_manual_pithy_print.cpp b/trunk/src/utest/srs_utest_manual_pithy_print.cpp index dcca538838..3b4f0db2ca 100644 --- a/trunk/src/utest/srs_utest_manual_pithy_print.cpp +++ b/trunk/src/utest/srs_utest_manual_pithy_print.cpp @@ -161,6 +161,7 @@ VOID TEST(PithyPrintTest, SrsAlonePithyPrintBasicFunctionality) // Test basic initialization if (true) { SrsAlonePithyPrint app; + app.assemble(); // Should be initialized with nb_clients_ = 1 EXPECT_EQ(1, app.info_.nb_clients_); } @@ -168,6 +169,7 @@ VOID TEST(PithyPrintTest, SrsAlonePithyPrintBasicFunctionality) // Test elapse and can_print if (true) { SrsAlonePithyPrint app; + app.assemble(); // Initially should not be able to print EXPECT_FALSE(app.can_print()); diff --git a/trunk/src/utest/srs_utest_workflow_forward.cpp b/trunk/src/utest/srs_utest_workflow_forward.cpp index 42cde10ddd..cafc268796 100644 --- a/trunk/src/utest/srs_utest_workflow_forward.cpp +++ b/trunk/src/utest/srs_utest_workflow_forward.cpp @@ -101,6 +101,8 @@ VOID TEST(BasicWorkflowForwardTest, ForwardBackendFailureRollsBackPublishState) conn->config_ = mock_config.get(); conn->stat_ = mock_stat.get(); conn->hooks_ = mock_hooks.get(); + conn->assemble(); + conn->info_->req_->vhost_ = req->vhost_; conn->info_->req_->app_ = req->app_; conn->info_->req_->stream_ = req->stream_; diff --git a/trunk/src/utest/srs_utest_workflow_rtmp2rtc.cpp b/trunk/src/utest/srs_utest_workflow_rtmp2rtc.cpp index cebd38787f..d9c7804b38 100644 --- a/trunk/src/utest/srs_utest_workflow_rtmp2rtc.cpp +++ b/trunk/src/utest/srs_utest_workflow_rtmp2rtc.cpp @@ -121,6 +121,8 @@ VOID TEST(BasicWorkflowRtmp2RtcTest, ManuallyVerifyTypicalScenario) #ifdef SRS_RTSP conn->rtsp_sources_ = mock_rtsp_sources.get(); #endif + conn->assemble(); + srs_freep(conn->rtmp_); conn->rtmp_ = mock_rtmp_server; srs_freep(conn->security_); diff --git a/trunk/src/utest/srs_utest_workflow_rtmp_conn.cpp b/trunk/src/utest/srs_utest_workflow_rtmp_conn.cpp index bcaa893a0c..518b5a01c3 100644 --- a/trunk/src/utest/srs_utest_workflow_rtmp_conn.cpp +++ b/trunk/src/utest/srs_utest_workflow_rtmp_conn.cpp @@ -84,6 +84,8 @@ VOID TEST(BasicWorkflowRtmpConnTest, ManuallyVerifyForPublisher) #ifdef SRS_RTSP conn->rtsp_sources_ = mock_rtsp_sources.get(); #endif + conn->assemble(); + srs_freep(conn->rtmp_); conn->rtmp_ = mock_rtmp_server; srs_freep(conn->security_); @@ -255,6 +257,8 @@ VOID TEST(BasicWorkflowRtmpConnTest, ManuallyVerifyForPlayer) #ifdef SRS_RTSP conn->rtsp_sources_ = mock_rtsp_sources.get(); #endif + conn->assemble(); + srs_freep(conn->rtmp_); conn->rtmp_ = mock_rtmp_server; srs_freep(conn->security_);