diff --git a/trunk/3rdparty/srs-docs/doc/webrtc.md b/trunk/3rdparty/srs-docs/doc/webrtc.md index 221489f49..318bb3075 100644 --- a/trunk/3rdparty/srs-docs/doc/webrtc.md +++ b/trunk/3rdparty/srs-docs/doc/webrtc.md @@ -439,6 +439,37 @@ The streams: * HTTP-FLV:[http://localhost:8080/live/show.flv](http://localhost:8080/players/srs_player.html?autostart=true&stream=show.flv) * RTMP by VLC:rtmp://localhost/live/show +## AV1 Codec Support + +SRS supports AV1 codec for WebRTC-to-WebRTC streaming since v4.0.91 ([#2324](https://github.com/ossrs/srs/pull/2324)). +AV1 is a royalty-free codec that saves 30-50% bandwidth compared to H.264. SRS implements AV1 as relay-only (SFU mode), +accepting AV1 streams via WHIP and forwarding to WHEP players without transcoding. AV1 streams cannot be converted to +RTMP/HLS or recorded to DVR. + +To use AV1, add the `codec=av1` query parameter to WHIP/WHEP URLs: + +* Publish: `http://localhost:1985/rtc/v1/whip/?app=live&stream=livestream&codec=av1` +* Play: `http://localhost:1985/rtc/v1/whep/?app=live&stream=livestream&codec=av1` + +Browser support: Chrome/Edge M90+, Firefox (full support), Safari (decode only). Use H.264 if you need RTMP/HLS conversion, +DVR recording, or maximum compatibility. + +## VP9 Codec Support + +SRS supports VP9 codec for WebRTC-to-WebRTC streaming since v7.0.0 ([#4548](https://github.com/ossrs/srs/issues/4548)). +VP9 is a royalty-free codec that saves 20-40% bandwidth compared to H.264. VP9 works better than H.264/H.265 with congestion control +in WebRTC, making it ideal for keeping streams live under network fluctuations. SRS implements VP9 as relay-only (SFU mode), +accepting VP9 streams via WHIP and forwarding to WHEP players without transcoding. VP9 streams cannot be converted to +RTMP/HLS or recorded to DVR. + +To use VP9, add the `codec=vp9` query parameter to WHIP/WHEP URLs: + +* Publish: `http://localhost:1985/rtc/v1/whip/?app=live&stream=livestream&codec=vp9` +* Play: `http://localhost:1985/rtc/v1/whep/?app=live&stream=livestream&codec=vp9` + +Browser support: Chrome/Edge M29+, Firefox M28+, Opera M16+. Safari does not support VP9. Use H.264 if you need RTMP/HLS conversion, +DVR recording, or Safari compatibility. + ## SFU: One to One Please use `conf/rtc.conf` as config. diff --git a/trunk/doc/CHANGELOG.md b/trunk/doc/CHANGELOG.md index 4d32a25a5..27375b5d3 100644 --- a/trunk/doc/CHANGELOG.md +++ b/trunk/doc/CHANGELOG.md @@ -7,6 +7,7 @@ The changelog for SRS. ## SRS 7.0 Changelog +* v7.0, 2025-11-08, AI: WebRTC: Support VP9 codec for WebRTC-to-WebRTC streaming. v7.0.123 (#4548) * v7.0, 2025-11-08, AI: API: Add audio_frames and video_frames to HTTP API. v7.0.122 (#4559) * v7.0, 2025-11-07, AI: WHIP: Return detailed HTTP error responses with proper status codes. v7.0.121 (#4502) * v7.0, 2025-11-07, AI: HLS: Support query string in hls_key_url for JWT tokens. v7.0.120 (#4426) diff --git a/trunk/src/app/srs_app_rtc_api.cpp b/trunk/src/app/srs_app_rtc_api.cpp index 23aa75c69..17d9f00d3 100644 --- a/trunk/src/app/srs_app_rtc_api.cpp +++ b/trunk/src/app/srs_app_rtc_api.cpp @@ -666,7 +666,7 @@ srs_error_t SrsGoApiRtcWhip::serve_http(ISrsHttpResponseWriter *w, ISrsHttpMessa string code_str; if (true) { srs_error_t err = srs_success; - + err = serve_http_with(w, r); if (err == srs_success) { return err; @@ -674,8 +674,8 @@ srs_error_t SrsGoApiRtcWhip::serve_http(ISrsHttpResponseWriter *w, ISrsHttpMessa code = srs_error_code(err); code_str = srs_error_code_str(err); - srs_warn("WHIP: serve http for %s with err %d:%s, %s", - r->url().c_str(), code, code_str.c_str(), srs_error_desc(err).c_str()); + srs_warn("WHIP: serve http for %s with err %d:%s, %s", + r->url().c_str(), code, code_str.c_str(), srs_error_desc(err).c_str()); srs_freep(err); } diff --git a/trunk/src/app/srs_app_rtc_conn.cpp b/trunk/src/app/srs_app_rtc_conn.cpp index e68c373b7..74d7b6f31 100644 --- a/trunk/src/app/srs_app_rtc_conn.cpp +++ b/trunk/src/app/srs_app_rtc_conn.cpp @@ -3402,6 +3402,38 @@ srs_error_t SrsRtcPublisherNegotiator::negotiate_publish_capability(SrsRtcUserCo } } + track_desc->type_ = "video"; + track_desc->set_codec_payload((SrsCodecPayload *)video_payload); + break; + } + } else if (remote_media_desc.is_video() && srs_video_codec_str2id(ruc->codec_) == SrsVideoCodecIdVP9) { + std::vector payloads = remote_media_desc.find_media_with_encoding_name("VP9"); + if (payloads.empty()) { + return srs_error_new(ERROR_RTC_SDP_EXCHANGE, "no found valid VP9 payload type"); + } + + for (int j = 0; j < (int)payloads.size(); j++) { + const SrsMediaPayloadType &payload = payloads.at(j); + + // Generate video payload for vp9. + SrsVideoPayload *video_payload = new SrsVideoPayload(payload.payload_type_, payload.encoding_name_, payload.clock_rate_); + + // TODO: FIXME: Only support some transport algorithms. + for (int k = 0; k < (int)payload.rtcp_fb_.size(); ++k) { + const string &rtcp_fb = payload.rtcp_fb_.at(k); + + if (nack_enabled) { + if (rtcp_fb == "nack" || rtcp_fb == "nack pli") { + video_payload->rtcp_fbs_.push_back(rtcp_fb); + } + } + if (twcc_enabled && remote_twcc_id) { + if (rtcp_fb == "transport-cc") { + video_payload->rtcp_fbs_.push_back(rtcp_fb); + } + } + } + track_desc->type_ = "video"; track_desc->set_codec_payload((SrsCodecPayload *)video_payload); break; @@ -3804,6 +3836,14 @@ srs_error_t SrsRtcPlayerNegotiator::negotiate_play_capability(SrsRtcUserConfig * // @see https://bugs.chromium.org/p/webrtc/issues/detail?id=13166 track_descs = source->get_track_desc("video", "AV1X"); } + } else if (prefer_codec == SrsVideoCodecIdVP9) { + std::vector payloads = remote_media_desc.find_media_with_encoding_name("VP9"); + if (payloads.empty()) { + return srs_error_new(ERROR_RTC_SDP_EXCHANGE, "no found valid VP9 payload type"); + } + + remote_payload = payloads.at(0); + track_descs = source->get_track_desc("video", "VP9"); } else if (prefer_codec == SrsVideoCodecIdHEVC) { std::vector payloads = remote_media_desc.find_media_with_encoding_name("H265"); if (payloads.empty()) { diff --git a/trunk/src/app/srs_app_rtc_source.cpp b/trunk/src/app/srs_app_rtc_source.cpp index 635fb2262..6fc5b1968 100644 --- a/trunk/src/app/srs_app_rtc_source.cpp +++ b/trunk/src/app/srs_app_rtc_source.cpp @@ -3851,7 +3851,7 @@ srs_error_t SrsRtcFormat::on_rtp_packet(SrsRtcRecvTrack *track, bool is_audio) SrsAudioChannels channels = (SrsAudioChannels)audio_media->channel_; if ((err = stat_->on_audio_info(req_, codec_id, sample_rate, channels, - SrsAacObjectTypeReserved)) != srs_success) { + SrsAacObjectTypeReserved)) != srs_success) { return srs_error_wrap(err, "stat audio info"); } srs_trace("RTC: parsed %s codec, sample_rate=%dHz, channels=%d", @@ -3926,6 +3926,13 @@ srs_error_t SrsRtcFormat::on_rtp_packet(SrsRtcRecvTrack *track, bool is_audio) srs_video_codec_id2str(codec_id).c_str(), srs_hevc_profile2str(profile).c_str(), level_id); + } else if (codec_id == SrsVideoCodecIdAV1 || codec_id == SrsVideoCodecIdVP9) { + // AV1 and VP9 are relay-only codecs for WebRTC-to-WebRTC + // No detailed profile/level parsing needed for relay mode + if ((err = stat_->on_video_info(req_, codec_id, 0, 0, 0, 0)) != srs_success) { + return srs_error_wrap(err, "stat video info"); + } + srs_trace("RTC: parsed %s codec", srs_video_codec_id2str(codec_id).c_str()); } } diff --git a/trunk/src/app/srs_app_statistic.cpp b/trunk/src/app/srs_app_statistic.cpp index 1bbaae113..b23f8430e 100644 --- a/trunk/src/app/srs_app_statistic.cpp +++ b/trunk/src/app/srs_app_statistic.cpp @@ -154,6 +154,12 @@ srs_error_t SrsStatisticStream::dumps(SrsJsonObject *obj) } else if (vcodec_ == SrsVideoCodecIdHEVC) { video->set("profile", SrsJsonAny::str(srs_hevc_profile2str(hevc_profile_).c_str())); video->set("level", SrsJsonAny::str(srs_hevc_level2str(hevc_level_).c_str())); + } else if (vcodec_ == SrsVideoCodecIdAV1) { + video->set("profile", SrsJsonAny::null()); + video->set("level", SrsJsonAny::null()); + } else if (vcodec_ == SrsVideoCodecIdVP9) { + video->set("profile", SrsJsonAny::null()); + video->set("level", SrsJsonAny::null()); } else { video->set("profile", SrsJsonAny::str("Other")); video->set("level", SrsJsonAny::str("Other")); diff --git a/trunk/src/core/srs_core_version7.hpp b/trunk/src/core/srs_core_version7.hpp index 4685600de..f2c2670e1 100644 --- a/trunk/src/core/srs_core_version7.hpp +++ b/trunk/src/core/srs_core_version7.hpp @@ -9,6 +9,6 @@ #define VERSION_MAJOR 7 #define VERSION_MINOR 0 -#define VERSION_REVISION 122 +#define VERSION_REVISION 123 #endif \ No newline at end of file diff --git a/trunk/src/kernel/srs_kernel_codec.cpp b/trunk/src/kernel/srs_kernel_codec.cpp index 7d575efd9..cd84f81a3 100644 --- a/trunk/src/kernel/srs_kernel_codec.cpp +++ b/trunk/src/kernel/srs_kernel_codec.cpp @@ -82,6 +82,8 @@ string srs_video_codec_id2str(SrsVideoCodecId codec) return "HEVC"; case SrsVideoCodecIdAV1: return "AV1"; + case SrsVideoCodecIdVP9: + return "VP9"; case SrsVideoCodecIdReserved: case SrsVideoCodecIdReserved1: case SrsVideoCodecIdReserved2: @@ -105,6 +107,8 @@ SrsVideoCodecId srs_video_codec_str2id(const std::string &codec) return SrsVideoCodecIdHEVC; } else if (upper_codec == "AV1") { return SrsVideoCodecIdAV1; + } else if (upper_codec == "VP9") { + return SrsVideoCodecIdVP9; } else if (upper_codec == "VP6") { return SrsVideoCodecIdOn2VP6; } else if (upper_codec == "VP6A") { diff --git a/trunk/src/kernel/srs_kernel_codec.hpp b/trunk/src/kernel/srs_kernel_codec.hpp index ab838c786..ffdb97455 100644 --- a/trunk/src/kernel/srs_kernel_codec.hpp +++ b/trunk/src/kernel/srs_kernel_codec.hpp @@ -61,8 +61,10 @@ enum SrsVideoCodecId { SrsVideoCodecIdAVC = 7, // See page 79 at @doc https://github.com/CDN-Union/H265/blob/master/Document/video_file_format_spec_v10_1_ksyun_20170615.doc SrsVideoCodecIdHEVC = 12, - // https://mp.weixin.qq.com/s/H3qI7zsON5sdf4oDJ9qlkg + // AV1 codec for WebRTC, https://github.com/ossrs/srs/pull/2324 SrsVideoCodecIdAV1 = 13, + // VP9 codec for WebRTC, https://github.com/ossrs/srs/pull/4565 + SrsVideoCodecIdVP9 = 14, }; std::string srs_video_codec_id2str(SrsVideoCodecId codec); SrsVideoCodecId srs_video_codec_str2id(const std::string &codec); diff --git a/trunk/src/kernel/srs_kernel_error.hpp b/trunk/src/kernel/srs_kernel_error.hpp index 0fe936f32..329e15fb5 100644 --- a/trunk/src/kernel/srs_kernel_error.hpp +++ b/trunk/src/kernel/srs_kernel_error.hpp @@ -111,10 +111,9 @@ XX(ERROR_STREAM_DISPOSING, 1098, "StreamDisposing", "Stream is disposing") \ XX(ERROR_NOT_IMPLEMENTED, 1099, "NotImplemented", "Feature is not implemented") \ XX(ERROR_NOT_SUPPORTED, 1100, "NotSupported", "Feature is not supported") \ - XX(ERROR_SYSTEM_FILE_UNLINK, 1101, "FileUnlink", "Failed to unlink file") \ + XX(ERROR_SYSTEM_FILE_UNLINK, 1101, "FileUnlink", "Failed to unlink file") \ XX(ERROR_SYSTEM_AUTH, 1102, "SystemAuth", "Failed to authenticate stream") - /**************************************************/ /* RTMP protocol error. */ #define SRS_ERRNO_MAP_RTMP(XX) \ @@ -382,7 +381,7 @@ XX(ERROR_RTSP_NO_TRACK, 5039, "RtspNoTrack", "Drop RTSP packet for track not found") \ XX(ERROR_RTSP_TOKEN_NOT_NORMAL, 5040, "RtspToken", "Invalid RTSP token state not normal") \ XX(ERROR_RTSP_REQUEST_HEADER_EOF, 5041, "RtspHeaderEof", "Invalid RTSP request for header EOF") \ - XX(ERROR_RTSP_NEED_MORE_DATA, 5042, "RtspNeedMoreData", "Need more data to complete RTCP frame parsing") \ + XX(ERROR_RTSP_NEED_MORE_DATA, 5042, "RtspNeedMoreData", "Need more data to complete RTCP frame parsing") \ XX(ERROR_RTC_INVALID_SDP, 5043, "RtcInvalidSdp", "Invalid SDP for RTC") /**************************************************/ @@ -492,7 +491,7 @@ public: // } #define srs_error_new(code, fmt, ...) SrsCplxError::create(__FUNCTION__, __FILE__, __LINE__, code, fmt, ##__VA_ARGS__) -// Wrap an existing error with additional context. The error code is +// Wrap an existing error with additional context. The error code is // preserved from the wrapped error. // // Example: @@ -501,8 +500,8 @@ public: // } #define srs_error_wrap(err, fmt, ...) SrsCplxError::wrap(__FUNCTION__, __FILE__, __LINE__, err, fmt, ##__VA_ARGS__) -// Transform an error by wrapping it and changing its error code. Useful -// for converting internal errors to protocol-specific error codes (e.g., +// Transform an error by wrapping it and changing its error code. Useful +// for converting internal errors to protocol-specific error codes (e.g., // HTTP, RTMP). The wrapped error chain is preserved. // // Example: diff --git a/trunk/src/kernel/srs_kernel_ts.cpp b/trunk/src/kernel/srs_kernel_ts.cpp index e84405d63..77020b4c1 100644 --- a/trunk/src/kernel/srs_kernel_ts.cpp +++ b/trunk/src/kernel/srs_kernel_ts.cpp @@ -443,6 +443,7 @@ srs_error_t SrsTsContext::encode(ISrsStreamWriter *writer, SrsTsMessage *msg, Sr case SrsVideoCodecIdOn2VP6WithAlphaChannel: case SrsVideoCodecIdScreenVideoVersion2: case SrsVideoCodecIdAV1: + case SrsVideoCodecIdVP9: vs = SrsTsStreamReserved; break; } diff --git a/trunk/src/utest/srs_utest_ai18.cpp b/trunk/src/utest/srs_utest_ai18.cpp index 5a6c5a679..99cbd812d 100644 --- a/trunk/src/utest/srs_utest_ai18.cpp +++ b/trunk/src/utest/srs_utest_ai18.cpp @@ -21,6 +21,7 @@ using namespace std; #include #include #include +#include #include // Mock ISrsSrtSocket implementation @@ -193,10 +194,11 @@ VOID TEST(UdpListenerTest, ListenAndReceivePacket) dest_addr.sin_port = htons(port); dest_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); - for (int i = 0; i < 3; i++) { + for (int i = 0; i < 10; i++) { int sent = srs_sendto(client_fd, (void *)test_data.c_str(), test_data.size(), (sockaddr *)&dest_addr, sizeof(dest_addr), SRS_UTIME_NO_TIMEOUT); EXPECT_EQ(sent, (int)test_data.size()); + srs_usleep(1 * SRS_UTIME_MILLISECONDS); } // Wait a bit for the listener to receive and process the packet @@ -2186,39 +2188,8 @@ VOID TEST(ApiServerAsCandidatesTest, MajorUseScenario) EXPECT_TRUE(candidate_ips.find("192.168.1.100") != candidate_ips.end()); } -// Mock SrsProtocolUtility implementation -MockProtocolUtility::MockProtocolUtility() -{ -} - -MockProtocolUtility::~MockProtocolUtility() -{ - clear_ips(); -} - -vector &MockProtocolUtility::local_ips() -{ - return mock_ips_; -} - -void MockProtocolUtility::add_ip(string ip, string ifname, bool is_ipv4, bool is_loopback, bool is_internet) -{ - SrsIPAddress *addr = new SrsIPAddress(); - addr->ip_ = ip; - addr->ifname_ = ifname; - addr->is_ipv4_ = is_ipv4; - addr->is_loopback_ = is_loopback; - addr->is_internet_ = is_internet; - mock_ips_.push_back(addr); -} - -void MockProtocolUtility::clear_ips() -{ - for (size_t i = 0; i < mock_ips_.size(); i++) { - srs_freep(mock_ips_[i]); - } - mock_ips_.clear(); -} +// Note: MockProtocolUtility has been merged into MockProtocolUtility +// in srs_utest_manual_mock.hpp/cpp. All usages below now use MockProtocolUtility. // Mock ISrsAppConfig for discover_candidates implementation MockAppConfigForDiscoverCandidates::MockAppConfigForDiscoverCandidates() diff --git a/trunk/src/utest/srs_utest_ai18.hpp b/trunk/src/utest/srs_utest_ai18.hpp index d5261df02..5885b3ab0 100644 --- a/trunk/src/utest/srs_utest_ai18.hpp +++ b/trunk/src/utest/srs_utest_ai18.hpp @@ -196,19 +196,8 @@ public: void clear_calls(); }; -// Mock SrsProtocolUtility for testing discover_candidates -class MockProtocolUtility : public SrsProtocolUtility -{ -public: - std::vector mock_ips_; - -public: - MockProtocolUtility(); - virtual ~MockProtocolUtility(); - virtual std::vector &local_ips(); - void add_ip(std::string ip, std::string ifname, bool is_ipv4, bool is_loopback, bool is_internet); - void clear_ips(); -}; +// Note: MockProtocolUtility has been merged into MockProtocolUtility +// in srs_utest_manual_mock.hpp. Use MockProtocolUtility instead. // Mock ISrsAppConfig for testing discover_candidates class MockAppConfigForDiscoverCandidates : public MockAppConfig diff --git a/trunk/src/utest/srs_utest_manual_mock.cpp b/trunk/src/utest/srs_utest_manual_mock.cpp index 664794879..39cfe15f1 100644 --- a/trunk/src/utest/srs_utest_manual_mock.cpp +++ b/trunk/src/utest/srs_utest_manual_mock.cpp @@ -56,7 +56,7 @@ MockSdpFactory::~MockSdpFactory() { } -std::string MockSdpFactory::create_chrome_player_offer() +std::string MockSdpFactory::create_chrome_player_offer_with_h264() { // Create a real Chrome-like WebRTC SDP offer for a player (subscriber) with H.264 video and Opus audio // Use member variables for SSRC and payload type values @@ -104,7 +104,7 @@ std::string MockSdpFactory::create_chrome_player_offer() return ss.str(); } -std::string MockSdpFactory::create_chrome_publisher_offer() +std::string MockSdpFactory::create_chrome_publisher_offer_with_h264() { // Create a real Chrome-like WebRTC SDP offer with H.264 video and Opus audio // Use member variables for SSRC and payload type values @@ -155,6 +155,113 @@ std::string MockSdpFactory::create_chrome_publisher_offer() return ss.str(); } +std::string MockSdpFactory::create_chrome_publisher_offer_with_av1() +{ + // Create a real Chrome-like WebRTC SDP offer with AV1 video and Opus audio + // Use member variables for SSRC and payload type values + // AV1 payload type is typically 45 or 96+ + uint8_t av1_pt = 45; + std::stringstream ss; + ss << "v=0\r\n" + << "o=- 4611731400430051338 2 IN IP4 127.0.0.1\r\n" + << "s=-\r\n" + << "t=0 0\r\n" + << "a=group:BUNDLE 0 1\r\n" + << "a=msid-semantic: WMS stream\r\n" + // Audio media description (Opus) + << "m=audio 9 UDP/TLS/RTP/SAVPF " << (int)audio_pt_ << "\r\n" + << "c=IN IP4 0.0.0.0\r\n" + << "a=rtcp:9 IN IP4 0.0.0.0\r\n" + << "a=ice-ufrag:test1234\r\n" + << "a=ice-pwd:testpassword1234567890\r\n" + << "a=ice-options:trickle\r\n" + << "a=fingerprint:sha-256 AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99\r\n" + << "a=setup:actpass\r\n" + << "a=mid:0\r\n" + << "a=sendonly\r\n" + << "a=rtcp-mux\r\n" + << "a=rtpmap:" << (int)audio_pt_ << " opus/48000/2\r\n" + << "a=fmtp:" << (int)audio_pt_ << " minptime=10;useinbandfec=1\r\n" + << "a=ssrc:" << audio_ssrc_ << " cname:test-audio-cname\r\n" + << "a=ssrc:" << audio_ssrc_ << " msid:stream audio\r\n" + // Video media description (AV1) + << "m=video 9 UDP/TLS/RTP/SAVPF " << (int)av1_pt << "\r\n" + << "c=IN IP4 0.0.0.0\r\n" + << "a=rtcp:9 IN IP4 0.0.0.0\r\n" + << "a=ice-ufrag:test1234\r\n" + << "a=ice-pwd:testpassword1234567890\r\n" + << "a=ice-options:trickle\r\n" + << "a=fingerprint:sha-256 AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99\r\n" + << "a=setup:actpass\r\n" + << "a=mid:1\r\n" + << "a=sendonly\r\n" + << "a=rtcp-mux\r\n" + << "a=rtcp-rsize\r\n" + << "a=rtpmap:" << (int)av1_pt << " AV1/90000\r\n" + << "a=rtcp-fb:" << (int)av1_pt << " nack\r\n" + << "a=rtcp-fb:" << (int)av1_pt << " nack pli\r\n" + << "a=rtcp-fb:" << (int)av1_pt << " transport-cc\r\n" + << "a=ssrc:" << video_ssrc_ << " cname:test-video-cname\r\n" + << "a=ssrc:" << video_ssrc_ << " msid:stream video\r\n"; + + return ss.str(); +} + +std::string MockSdpFactory::create_chrome_publisher_offer_with_vp9() +{ + // Create a real Chrome-like WebRTC SDP offer with VP9 video and Opus audio + // Use member variables for SSRC and payload type values + // VP9 payload type is typically 98 (Profile 0) or 100 (Profile 2) + uint8_t vp9_pt = 98; + std::stringstream ss; + ss << "v=0\r\n" + << "o=- 4611731400430051338 2 IN IP4 127.0.0.1\r\n" + << "s=-\r\n" + << "t=0 0\r\n" + << "a=group:BUNDLE 0 1\r\n" + << "a=msid-semantic: WMS stream\r\n" + // Audio media description (Opus) + << "m=audio 9 UDP/TLS/RTP/SAVPF " << (int)audio_pt_ << "\r\n" + << "c=IN IP4 0.0.0.0\r\n" + << "a=rtcp:9 IN IP4 0.0.0.0\r\n" + << "a=ice-ufrag:test1234\r\n" + << "a=ice-pwd:testpassword1234567890\r\n" + << "a=ice-options:trickle\r\n" + << "a=fingerprint:sha-256 AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99\r\n" + << "a=setup:actpass\r\n" + << "a=mid:0\r\n" + << "a=sendonly\r\n" + << "a=rtcp-mux\r\n" + << "a=rtpmap:" << (int)audio_pt_ << " opus/48000/2\r\n" + << "a=fmtp:" << (int)audio_pt_ << " minptime=10;useinbandfec=1\r\n" + << "a=ssrc:" << audio_ssrc_ << " cname:test-audio-cname\r\n" + << "a=ssrc:" << audio_ssrc_ << " msid:stream audio\r\n" + // Video media description (VP9) + << "m=video 9 UDP/TLS/RTP/SAVPF " << (int)vp9_pt << "\r\n" + << "c=IN IP4 0.0.0.0\r\n" + << "a=rtcp:9 IN IP4 0.0.0.0\r\n" + << "a=ice-ufrag:test1234\r\n" + << "a=ice-pwd:testpassword1234567890\r\n" + << "a=ice-options:trickle\r\n" + << "a=fingerprint:sha-256 AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99\r\n" + << "a=setup:actpass\r\n" + << "a=mid:1\r\n" + << "a=sendonly\r\n" + << "a=rtcp-mux\r\n" + << "a=rtcp-rsize\r\n" + << "a=rtpmap:" << (int)vp9_pt << " VP9/90000\r\n" + << "a=rtcp-fb:" << (int)vp9_pt << " goog-remb\r\n" + << "a=rtcp-fb:" << (int)vp9_pt << " transport-cc\r\n" + << "a=rtcp-fb:" << (int)vp9_pt << " ccm fir\r\n" + << "a=rtcp-fb:" << (int)vp9_pt << " nack\r\n" + << "a=rtcp-fb:" << (int)vp9_pt << " nack pli\r\n" + << "a=fmtp:" << (int)vp9_pt << " profile-id=0\r\n" + << "a=ssrc:" << video_ssrc_ << " cname:test-video-cname\r\n" + << "a=ssrc:" << video_ssrc_ << " msid:stream video\r\n"; + + return ss.str(); +} + MockDtlsCertificate::MockDtlsCertificate() { fingerprint_ = "AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99"; @@ -306,6 +413,8 @@ ISrsRequest *MockRtcAsyncCallRequest::as_http() MockRtcSource::MockRtcSource() { on_rtp_count_ = 0; + rtp_audio_count_ = 0; + rtp_video_count_ = 0; } MockRtcSource::~MockRtcSource() @@ -315,6 +424,14 @@ MockRtcSource::~MockRtcSource() srs_error_t MockRtcSource::on_rtp(SrsRtpPacket *pkt) { on_rtp_count_++; + + // Count audio and video packets separately + if (pkt->frame_type_ == SrsFrameTypeAudio) { + rtp_audio_count_++; + } else if (pkt->frame_type_ == SrsFrameTypeVideo) { + rtp_video_count_++; + } + return SrsRtcSource::on_rtp(pkt); } @@ -2337,3 +2454,53 @@ void MockAudioTranscoder::aac_codec_header(uint8_t **data, int *len) *data = copy; *len = size; } + +// Mock ISrsProtocolUtility implementation +MockProtocolUtility::MockProtocolUtility(std::string ip) +{ + mock_ip_ = ip; +} + +MockProtocolUtility::~MockProtocolUtility() +{ + clear_ips(); +} + +std::vector &MockProtocolUtility::local_ips() +{ + if (!ips_.empty()) { + return ips_; + } + + // If mock_ip_ is set (via constructor), create a default IP address + if (!mock_ip_.empty()) { + SrsIPAddress *addr = new SrsIPAddress(); + addr->ip_ = mock_ip_; + addr->is_ipv4_ = true; + addr->is_loopback_ = false; // Not loopback + addr->is_internet_ = true; // Public IP + addr->ifname_ = "eth0"; // Interface name + ips_.push_back(addr); + } + + return ips_; +} + +void MockProtocolUtility::add_ip(std::string ip, std::string ifname, bool is_ipv4, bool is_loopback, bool is_internet) +{ + SrsIPAddress *addr = new SrsIPAddress(); + addr->ip_ = ip; + addr->ifname_ = ifname; + addr->is_ipv4_ = is_ipv4; + addr->is_loopback_ = is_loopback; + addr->is_internet_ = is_internet; + ips_.push_back(addr); +} + +void MockProtocolUtility::clear_ips() +{ + for (size_t i = 0; i < ips_.size(); i++) { + srs_freep(ips_[i]); + } + ips_.clear(); +} diff --git a/trunk/src/utest/srs_utest_manual_mock.hpp b/trunk/src/utest/srs_utest_manual_mock.hpp index ea53599da..32a6087f0 100644 --- a/trunk/src/utest/srs_utest_manual_mock.hpp +++ b/trunk/src/utest/srs_utest_manual_mock.hpp @@ -78,9 +78,13 @@ public: public: // Create a Chrome-like WebRTC publisher offer SDP - std::string create_chrome_publisher_offer(); + std::string create_chrome_publisher_offer_with_h264(); // Create a Chrome-like WebRTC player offer SDP - std::string create_chrome_player_offer(); + std::string create_chrome_player_offer_with_h264(); + // Create a Chrome-like WebRTC publisher offer SDP with AV1 + std::string create_chrome_publisher_offer_with_av1(); + // Create a Chrome-like WebRTC publisher offer SDP with VP9 + std::string create_chrome_publisher_offer_with_vp9(); }; // Mock DTLS certificate for testing @@ -162,6 +166,8 @@ class MockRtcSource : public SrsRtcSource { public: int on_rtp_count_; + int rtp_audio_count_; + int rtp_video_count_; public: MockRtcSource(); @@ -1332,4 +1338,23 @@ public: virtual void aac_codec_header(uint8_t **data, int *len); }; +// Mock ISrsProtocolUtility for testing RTC connections +// This class merges functionality from MockProtocolUtility in srs_utest_ai18.hpp +// It supports both simple single-IP usage (via constructor) and complex multi-IP usage (via add_ip) +class MockProtocolUtility : public ISrsProtocolUtility +{ +public: + std::vector ips_; + std::string mock_ip_; + +public: + MockProtocolUtility(std::string ip = ""); + virtual ~MockProtocolUtility(); + +public: + virtual std::vector &local_ips(); + void add_ip(std::string ip, std::string ifname, bool is_ipv4, bool is_loopback, bool is_internet); + void clear_ips(); +}; + #endif diff --git a/trunk/src/utest/srs_utest_workflow_rtc_conn.cpp b/trunk/src/utest/srs_utest_workflow_rtc_conn.cpp index 8829f0335..f20afba53 100644 --- a/trunk/src/utest/srs_utest_workflow_rtc_conn.cpp +++ b/trunk/src/utest/srs_utest_workflow_rtc_conn.cpp @@ -34,73 +34,6 @@ #include #include -MockProtocolUtilityForRtcConn::MockProtocolUtilityForRtcConn(std::string ip) -{ - mock_ip_ = ip; -} - -MockProtocolUtilityForRtcConn::~MockProtocolUtilityForRtcConn() -{ -} - -std::vector &MockProtocolUtilityForRtcConn::local_ips() -{ - if (!ips_.empty()) { - return ips_; - } - - SrsIPAddress *addr = new SrsIPAddress(); - addr->ip_ = mock_ip_; - addr->is_ipv4_ = true; - addr->is_loopback_ = false; // Not loopback - addr->is_internet_ = true; // Public IP - addr->ifname_ = "eth0"; // Interface name - ips_.push_back(addr); - - return ips_; -} - -MockAppFactoryForRtcConn::MockAppFactoryForRtcConn() -{ - mock_protocol_utility_ = NULL; -} - -MockAppFactoryForRtcConn::~MockAppFactoryForRtcConn() -{ -} - -ISrsProtocolUtility *MockAppFactoryForRtcConn::create_protocol_utility() -{ - return mock_protocol_utility_; -} - -ISrsRtcPublishStream *MockAppFactoryForRtcConn::create_rtc_publish_stream(ISrsExecRtcAsyncTask *exec, ISrsExpire *expire, ISrsRtcPacketReceiver *receiver, const SrsContextId &cid) -{ - SrsRtcPublishStream *publisher = new SrsRtcPublishStream(exec, expire, receiver, cid); - publisher->rtc_sources_ = rtc_sources_; - return publisher; -} - -MockRtcSourceForRtcConn::MockRtcSourceForRtcConn() -{ - rtp_audio_count_ = 0; - rtp_video_count_ = 0; -} - -MockRtcSourceForRtcConn::~MockRtcSourceForRtcConn() -{ -} - -srs_error_t MockRtcSourceForRtcConn::on_rtp(SrsRtpPacket *pkt) -{ - if (pkt->frame_type_ == SrsFrameTypeAudio) { - rtp_audio_count_++; - } else if (pkt->frame_type_ == SrsFrameTypeVideo) { - rtp_video_count_++; - } - return srs_success; -} - // This test is used to verify the basic workflow of the RTC connection. // It's finished with the help of AI, but each step is manually designed // and verified. So this is not dominated by AI, but by humanbeing. @@ -120,8 +53,8 @@ VOID TEST(BasicWorkflowRtcConnTest, ManuallyVerifyForPlayer) mock_config->rtc_dtls_role_ = "passive"; mock_dtls_certificate->fingerprint_ = "test-fingerprint"; mock_app_factory->rtc_sources_ = mock_rtc_sources.get(); - mock_app_factory->mock_protocol_utility_ = new MockProtocolUtilityForRtcConn("192.168.1.100"); - MockRtcSourceForRtcConn *mock_rtc_source = new MockRtcSourceForRtcConn(); + mock_app_factory->mock_protocol_utility_ = new MockProtocolUtility("192.168.1.100"); + MockRtcSource *mock_rtc_source = new MockRtcSource(); mock_rtc_sources->mock_source_ = SrsSharedPtr(mock_rtc_source); // Create a real ISrsRtcConnection using _srs_app_factory_ @@ -157,7 +90,7 @@ VOID TEST(BasicWorkflowRtcConnTest, ManuallyVerifyForPlayer) ruc->srtp_ = true; ruc->audio_before_video_ = false; - ruc->remote_sdp_str_ = mock_sdp_factory->create_chrome_player_offer(); + ruc->remote_sdp_str_ = mock_sdp_factory->create_chrome_player_offer_with_h264(); HELPER_EXPECT_SUCCESS(ruc->remote_sdp_.parse(ruc->remote_sdp_str_)); } @@ -271,8 +204,8 @@ VOID TEST(BasicWorkflowRtcConnTest, ManuallyVerifyForPublisher) mock_config->rtc_dtls_role_ = "passive"; mock_dtls_certificate->fingerprint_ = "test-fingerprint"; mock_app_factory->rtc_sources_ = mock_rtc_sources.get(); - mock_app_factory->mock_protocol_utility_ = new MockProtocolUtilityForRtcConn("192.168.1.100"); - MockRtcSourceForRtcConn *mock_rtc_source = new MockRtcSourceForRtcConn(); + mock_app_factory->mock_protocol_utility_ = new MockProtocolUtility("192.168.1.100"); + MockRtcSource *mock_rtc_source = new MockRtcSource(); mock_rtc_sources->mock_source_ = SrsSharedPtr(mock_rtc_source); // Create a real ISrsRtcConnection using _srs_app_factory_ @@ -308,7 +241,7 @@ VOID TEST(BasicWorkflowRtcConnTest, ManuallyVerifyForPublisher) ruc->srtp_ = true; ruc->audio_before_video_ = false; - ruc->remote_sdp_str_ = mock_sdp_factory->create_chrome_publisher_offer(); + ruc->remote_sdp_str_ = mock_sdp_factory->create_chrome_publisher_offer_with_h264(); HELPER_EXPECT_SUCCESS(ruc->remote_sdp_.parse(ruc->remote_sdp_str_)); } @@ -460,3 +393,433 @@ VOID TEST(BasicWorkflowRtcConnTest, ManuallyVerifyForPublisher) // Stop the publisher publisher->stop(); } + +// This test is used to verify the basic workflow of the RTC connection with AV1 codec. +// It's finished with the help of AI, but each step is manually designed +// and verified. So this is not dominated by AI, but by humanbeing. +VOID TEST(BasicWorkflowRtcConnTest, ManuallyVerifyForPublisherWithAV1) +{ + srs_error_t err; + + // Create mock dependencies FIRST (they must outlive the connection) + SrsUniquePtr mock_circuit_breaker(new MockCircuitBreaker()); + SrsUniquePtr mock_conn_manager(new MockConnectionManager()); + SrsUniquePtr mock_rtc_sources(new MockRtcSourceManager()); + SrsUniquePtr mock_config(new MockAppConfig()); + SrsUniquePtr mock_dtls_certificate(new MockDtlsCertificate()); + SrsUniquePtr mock_sdp_factory(new MockSdpFactory()); + SrsUniquePtr mock_app_factory(new MockAppFactoryForRtcConn()); + SrsStreamPublishTokenManager token_manager; + + mock_config->rtc_dtls_role_ = "passive"; + mock_dtls_certificate->fingerprint_ = "test-fingerprint"; + mock_app_factory->rtc_sources_ = mock_rtc_sources.get(); + mock_app_factory->mock_protocol_utility_ = new MockProtocolUtility("192.168.1.100"); + MockRtcSource *mock_rtc_source = new MockRtcSource(); + mock_rtc_sources->mock_source_ = SrsSharedPtr(mock_rtc_source); + + // Create a real ISrsRtcConnection using _srs_app_factory_ + MockRtcAsyncTaskExecutor mock_exec; + SrsContextId cid; + cid.set_value("test-rtc-conn-publisher-av1-workflow"); + + SrsUniquePtr conn_ptr(_srs_app_factory->create_rtc_connection(&mock_exec, cid)); + SrsRtcConnection *conn = dynamic_cast(conn_ptr.get()); + EXPECT_TRUE(conn != NULL); + + // Mock the RTC conn, also mock the config in publisher_negotiator_ and player_negotiator_ + conn->circuit_breaker_ = mock_circuit_breaker.get(); + conn->conn_manager_ = mock_conn_manager.get(); + conn->rtc_sources_ = mock_rtc_sources.get(); + conn->config_ = mock_config.get(); + conn->dtls_certificate_ = mock_dtls_certificate.get(); + conn->app_factory_ = mock_app_factory.get(); + + SrsRtcPublisherNegotiator *pub_neg = dynamic_cast(conn->publisher_negotiator_); + pub_neg->config_ = mock_config.get(); + SrsRtcPlayerNegotiator *play_neg = dynamic_cast(conn->player_negotiator_); + play_neg->config_ = mock_config.get(); + play_neg->rtc_sources_ = mock_rtc_sources.get(); + + // Create RTC user config for add_publisher with AV1 codec + SrsUniquePtr ruc(new SrsRtcUserConfig()); + if (true) { + srs_freep(ruc->req_); + ruc->req_ = new MockRtcAsyncCallRequest("test.vhost", "live", "stream1"); + ruc->publish_ = true; + ruc->dtls_ = true; + ruc->srtp_ = true; + ruc->audio_before_video_ = false; + ruc->codec_ = "av1"; // Specify AV1 codec + + ruc->remote_sdp_str_ = mock_sdp_factory->create_chrome_publisher_offer_with_av1(); + HELPER_EXPECT_SUCCESS(ruc->remote_sdp_.parse(ruc->remote_sdp_str_)); + } + + // Add publisher, which negotiate the SDP and generate local SDP + SrsSdp local_sdp; + local_sdp.session_config_.dtls_role_ = mock_config->get_rtc_dtls_role(ruc->req_->vhost_); + + if (true) { + HELPER_EXPECT_SUCCESS(conn->add_publisher(ruc.get(), local_sdp)); + + // Verify publishers and SSRC mappings + EXPECT_TRUE(conn->publishers_.size() == 1); + EXPECT_TRUE(conn->publishers_ssrc_map_.size() == 2); + EXPECT_TRUE(conn->publishers_ssrc_map_.find(mock_sdp_factory->audio_ssrc_) != conn->publishers_ssrc_map_.end()); + EXPECT_TRUE(conn->publishers_ssrc_map_.find(mock_sdp_factory->video_ssrc_) != conn->publishers_ssrc_map_.end()); + + // Verify the source stream desription, should have two tracks. + SrsRtcSourceDescription *stream_desc = mock_rtc_sources->mock_source_->stream_desc_; + EXPECT_TRUE(stream_desc->audio_track_desc_ != NULL); + EXPECT_TRUE(stream_desc->video_track_descs_.size() == 1); + + // Verify the audio track ssrc and payload type. + EXPECT_TRUE(stream_desc->audio_track_desc_->ssrc_ == mock_sdp_factory->audio_ssrc_); + EXPECT_TRUE(stream_desc->audio_track_desc_->media_->pt_ == mock_sdp_factory->audio_pt_); + + // Verify the video track ssrc and payload type. + EXPECT_TRUE(stream_desc->video_track_descs_[0]->ssrc_ == mock_sdp_factory->video_ssrc_); + // AV1 uses payload type 45 + EXPECT_TRUE(stream_desc->video_track_descs_[0]->media_->pt_ == 45); + + // Verify the codec is AV1 + EXPECT_TRUE(stream_desc->video_track_descs_[0]->media_->name_ == "AV1"); + + // Verify the local SDP was generated with media information + EXPECT_TRUE(local_sdp.version_ == "0"); + EXPECT_TRUE(local_sdp.group_policy_ == "BUNDLE"); + EXPECT_TRUE(local_sdp.msids_.size() == 1); + EXPECT_TRUE(local_sdp.msids_[0] == "live/stream1"); + EXPECT_TRUE(local_sdp.media_descs_.size() == 2); + + // First should be audio media desc + SrsMediaDesc *audio_desc = &local_sdp.media_descs_[0]; + EXPECT_TRUE(audio_desc->type_ == "audio"); + EXPECT_TRUE(audio_desc->recvonly_); + EXPECT_TRUE(audio_desc->payload_types_.size() == 1); + EXPECT_TRUE(audio_desc->payload_types_[0].payload_type_ == mock_sdp_factory->audio_pt_); + EXPECT_TRUE(audio_desc->payload_types_[0].encoding_name_ == "opus"); + EXPECT_TRUE(audio_desc->payload_types_[0].clock_rate_ == 48000); + + // Second should be video media desc with AV1 + SrsMediaDesc *video_desc = &local_sdp.media_descs_[1]; + EXPECT_TRUE(video_desc->type_ == "video"); + EXPECT_TRUE(video_desc->recvonly_); + EXPECT_TRUE(video_desc->payload_types_.size() == 1); + EXPECT_TRUE(video_desc->payload_types_[0].payload_type_ == 45); + EXPECT_TRUE(video_desc->payload_types_[0].encoding_name_ == "AV1"); + EXPECT_TRUE(video_desc->payload_types_[0].clock_rate_ == 90000); + } + + // Generate local SDP and setup SDP. + std::string username; + if (true) { + bool status = true; + conn->set_all_tracks_status(ruc->req_->get_stream_url(), ruc->publish_, status); + + HELPER_EXPECT_SUCCESS(conn->generate_local_sdp(ruc.get(), local_sdp, username)); + conn->set_remote_sdp(ruc->remote_sdp_); + conn->set_local_sdp(local_sdp); + conn->set_state_as_waiting_stun(); + + // Verify the local SDP was generated ice pwd + SrsMediaDesc *audio_desc = &local_sdp.media_descs_[0]; + EXPECT_TRUE(!audio_desc->session_info_.ice_pwd_.empty()); + EXPECT_TRUE(!audio_desc->session_info_.fingerprint_.empty()); + EXPECT_TRUE(audio_desc->candidates_.size() == 1); + EXPECT_TRUE(audio_desc->candidates_[0].ip_ == "192.168.1.100"); + EXPECT_TRUE(audio_desc->session_info_.setup_ == "passive"); + + SrsMediaDesc *video_desc = &local_sdp.media_descs_[1]; + EXPECT_TRUE(!video_desc->session_info_.ice_pwd_.empty()); + EXPECT_TRUE(!video_desc->session_info_.fingerprint_.empty()); + EXPECT_TRUE(video_desc->candidates_.size() == 1); + EXPECT_TRUE(video_desc->candidates_[0].ip_ == "192.168.1.100"); + EXPECT_TRUE(video_desc->session_info_.setup_ == "passive"); + + EXPECT_TRUE(local_sdp.session_negotiate_.dtls_role_ == "passive"); + } + + // Initialize the connection + if (true) { + HELPER_EXPECT_SUCCESS(conn->initialize(ruc->req_, ruc->dtls_, ruc->srtp_, username)); + EXPECT_TRUE(conn->nack_enabled_); + + // Create and set publish token + SrsStreamPublishToken *publish_token_raw = NULL; + HELPER_EXPECT_SUCCESS(token_manager.acquire_token(ruc->req_, publish_token_raw)); + SrsSharedPtr publish_token(publish_token_raw); + + conn->set_publish_token(publish_token); + EXPECT_TRUE(conn->publish_token_->is_acquired()); + } + + // DTLS done, start publisher + SrsRtcPublishStream *publisher = NULL; + if (true) { + HELPER_EXPECT_SUCCESS(conn->on_dtls_handshake_done()); + + // Wait for coroutine to start. Normally it should be ready wait for PLI requests. + srs_usleep(1 * SRS_UTIME_MILLISECONDS); + + // Verify the publisher is created and started + EXPECT_TRUE(conn->publishers_.size() == 1); + publisher = dynamic_cast(conn->publishers_.begin()->second); + EXPECT_TRUE(publisher->is_sender_started_); + } + + // Got a RTP audio packet. + for (int i = 0; i < 3; i++) { + SrsRtpPacket pkt; + pkt.header_.set_ssrc(mock_sdp_factory->audio_ssrc_); + pkt.header_.set_sequence(100); + pkt.header_.set_timestamp(1000); + pkt.header_.set_payload_type(mock_sdp_factory->audio_pt_); + + SrsUniquePtr data(new char[1500]); + SrsBuffer buf(data.get(), 1500); + HELPER_EXPECT_SUCCESS(pkt.encode(&buf)); + + HELPER_EXPECT_SUCCESS(conn->on_rtp_cipher(data.get(), buf.pos())); + HELPER_EXPECT_SUCCESS(conn->on_rtp_plaintext(data.get(), buf.pos())); + + EXPECT_EQ(mock_rtc_source->rtp_audio_count_, i + 1); + } + + // Got a RTP video packet with AV1 payload type. + for (int i = 0; i < 3; i++) { + SrsRtpPacket pkt; + pkt.header_.set_ssrc(mock_sdp_factory->video_ssrc_); + pkt.header_.set_sequence(100); + pkt.header_.set_timestamp(1000); + pkt.header_.set_payload_type(45); // AV1 payload type + + SrsUniquePtr data(new char[1500]); + SrsBuffer buf(data.get(), 1500); + HELPER_EXPECT_SUCCESS(pkt.encode(&buf)); + + HELPER_EXPECT_SUCCESS(conn->on_rtp_cipher(data.get(), buf.pos())); + HELPER_EXPECT_SUCCESS(conn->on_rtp_plaintext(data.get(), buf.pos())); + + EXPECT_EQ(mock_rtc_source->rtp_video_count_, i + 1); + } + + // Stop the publisher + publisher->stop(); +} + +// This test is used to verify the basic workflow of the RTC connection with VP9 codec. +// It's finished with the help of AI, but each step is manually designed +// and verified. So this is not dominated by AI, but by humanbeing. +VOID TEST(BasicWorkflowRtcConnTest, ManuallyVerifyForPublisherWithVP9) +{ + srs_error_t err; + + // Create mock dependencies FIRST (they must outlive the connection) + SrsUniquePtr mock_circuit_breaker(new MockCircuitBreaker()); + SrsUniquePtr mock_conn_manager(new MockConnectionManager()); + SrsUniquePtr mock_rtc_sources(new MockRtcSourceManager()); + SrsUniquePtr mock_config(new MockAppConfig()); + SrsUniquePtr mock_dtls_certificate(new MockDtlsCertificate()); + SrsUniquePtr mock_sdp_factory(new MockSdpFactory()); + SrsUniquePtr mock_app_factory(new MockAppFactoryForRtcConn()); + SrsStreamPublishTokenManager token_manager; + + mock_config->rtc_dtls_role_ = "passive"; + mock_dtls_certificate->fingerprint_ = "test-fingerprint"; + mock_app_factory->rtc_sources_ = mock_rtc_sources.get(); + mock_app_factory->mock_protocol_utility_ = new MockProtocolUtility("192.168.1.100"); + MockRtcSource *mock_rtc_source = new MockRtcSource(); + mock_rtc_sources->mock_source_ = SrsSharedPtr(mock_rtc_source); + + // Create a real ISrsRtcConnection using _srs_app_factory_ + MockRtcAsyncTaskExecutor mock_exec; + SrsContextId cid; + cid.set_value("test-rtc-conn-publisher-vp9-workflow"); + + SrsUniquePtr conn_ptr(_srs_app_factory->create_rtc_connection(&mock_exec, cid)); + SrsRtcConnection *conn = dynamic_cast(conn_ptr.get()); + EXPECT_TRUE(conn != NULL); + + // Mock the RTC conn, also mock the config in publisher_negotiator_ and player_negotiator_ + conn->circuit_breaker_ = mock_circuit_breaker.get(); + conn->conn_manager_ = mock_conn_manager.get(); + conn->rtc_sources_ = mock_rtc_sources.get(); + conn->config_ = mock_config.get(); + conn->dtls_certificate_ = mock_dtls_certificate.get(); + conn->app_factory_ = mock_app_factory.get(); + + SrsRtcPublisherNegotiator *pub_neg = dynamic_cast(conn->publisher_negotiator_); + pub_neg->config_ = mock_config.get(); + SrsRtcPlayerNegotiator *play_neg = dynamic_cast(conn->player_negotiator_); + play_neg->config_ = mock_config.get(); + play_neg->rtc_sources_ = mock_rtc_sources.get(); + + // Create RTC user config for add_publisher with VP9 codec + SrsUniquePtr ruc(new SrsRtcUserConfig()); + if (true) { + srs_freep(ruc->req_); + ruc->req_ = new MockRtcAsyncCallRequest("test.vhost", "live", "stream1"); + ruc->publish_ = true; + ruc->dtls_ = true; + ruc->srtp_ = true; + ruc->audio_before_video_ = false; + ruc->codec_ = "vp9"; // Specify VP9 codec + + ruc->remote_sdp_str_ = mock_sdp_factory->create_chrome_publisher_offer_with_vp9(); + HELPER_EXPECT_SUCCESS(ruc->remote_sdp_.parse(ruc->remote_sdp_str_)); + } + + // Add publisher, which negotiate the SDP and generate local SDP + SrsSdp local_sdp; + local_sdp.session_config_.dtls_role_ = mock_config->get_rtc_dtls_role(ruc->req_->vhost_); + + if (true) { + HELPER_EXPECT_SUCCESS(conn->add_publisher(ruc.get(), local_sdp)); + + // Verify publishers and SSRC mappings + EXPECT_TRUE(conn->publishers_.size() == 1); + EXPECT_TRUE(conn->publishers_ssrc_map_.size() == 2); + EXPECT_TRUE(conn->publishers_ssrc_map_.find(mock_sdp_factory->audio_ssrc_) != conn->publishers_ssrc_map_.end()); + EXPECT_TRUE(conn->publishers_ssrc_map_.find(mock_sdp_factory->video_ssrc_) != conn->publishers_ssrc_map_.end()); + + // Verify the source stream desription, should have two tracks. + SrsRtcSourceDescription *stream_desc = mock_rtc_sources->mock_source_->stream_desc_; + EXPECT_TRUE(stream_desc->audio_track_desc_ != NULL); + EXPECT_TRUE(stream_desc->video_track_descs_.size() == 1); + + // Verify the audio track ssrc and payload type. + EXPECT_TRUE(stream_desc->audio_track_desc_->ssrc_ == mock_sdp_factory->audio_ssrc_); + EXPECT_TRUE(stream_desc->audio_track_desc_->media_->pt_ == mock_sdp_factory->audio_pt_); + + // Verify the video track ssrc and payload type. + EXPECT_TRUE(stream_desc->video_track_descs_[0]->ssrc_ == mock_sdp_factory->video_ssrc_); + // VP9 uses payload type 98 + EXPECT_TRUE(stream_desc->video_track_descs_[0]->media_->pt_ == 98); + + // Verify the codec is VP9 + EXPECT_TRUE(stream_desc->video_track_descs_[0]->media_->name_ == "VP9"); + + // Verify the local SDP was generated with media information + EXPECT_TRUE(local_sdp.version_ == "0"); + EXPECT_TRUE(local_sdp.group_policy_ == "BUNDLE"); + EXPECT_TRUE(local_sdp.msids_.size() == 1); + EXPECT_TRUE(local_sdp.msids_[0] == "live/stream1"); + EXPECT_TRUE(local_sdp.media_descs_.size() == 2); + + // First should be audio media desc + SrsMediaDesc *audio_desc = &local_sdp.media_descs_[0]; + EXPECT_TRUE(audio_desc->type_ == "audio"); + EXPECT_TRUE(audio_desc->recvonly_); + EXPECT_TRUE(audio_desc->payload_types_.size() == 1); + EXPECT_TRUE(audio_desc->payload_types_[0].payload_type_ == mock_sdp_factory->audio_pt_); + EXPECT_TRUE(audio_desc->payload_types_[0].encoding_name_ == "opus"); + EXPECT_TRUE(audio_desc->payload_types_[0].clock_rate_ == 48000); + + // Second should be video media desc with VP9 + SrsMediaDesc *video_desc = &local_sdp.media_descs_[1]; + EXPECT_TRUE(video_desc->type_ == "video"); + EXPECT_TRUE(video_desc->recvonly_); + EXPECT_TRUE(video_desc->payload_types_.size() == 1); + EXPECT_TRUE(video_desc->payload_types_[0].payload_type_ == 98); + EXPECT_TRUE(video_desc->payload_types_[0].encoding_name_ == "VP9"); + EXPECT_TRUE(video_desc->payload_types_[0].clock_rate_ == 90000); + } + + // Generate local SDP and setup SDP. + std::string username; + if (true) { + bool status = true; + conn->set_all_tracks_status(ruc->req_->get_stream_url(), ruc->publish_, status); + + HELPER_EXPECT_SUCCESS(conn->generate_local_sdp(ruc.get(), local_sdp, username)); + conn->set_remote_sdp(ruc->remote_sdp_); + conn->set_local_sdp(local_sdp); + conn->set_state_as_waiting_stun(); + + // Verify the local SDP was generated ice pwd + SrsMediaDesc *audio_desc = &local_sdp.media_descs_[0]; + EXPECT_TRUE(!audio_desc->session_info_.ice_pwd_.empty()); + EXPECT_TRUE(!audio_desc->session_info_.fingerprint_.empty()); + EXPECT_TRUE(audio_desc->candidates_.size() == 1); + EXPECT_TRUE(audio_desc->candidates_[0].ip_ == "192.168.1.100"); + EXPECT_TRUE(audio_desc->session_info_.setup_ == "passive"); + + SrsMediaDesc *video_desc = &local_sdp.media_descs_[1]; + EXPECT_TRUE(!video_desc->session_info_.ice_pwd_.empty()); + EXPECT_TRUE(!video_desc->session_info_.fingerprint_.empty()); + EXPECT_TRUE(video_desc->candidates_.size() == 1); + EXPECT_TRUE(video_desc->candidates_[0].ip_ == "192.168.1.100"); + EXPECT_TRUE(video_desc->session_info_.setup_ == "passive"); + + EXPECT_TRUE(local_sdp.session_negotiate_.dtls_role_ == "passive"); + } + + // Initialize the connection + if (true) { + HELPER_EXPECT_SUCCESS(conn->initialize(ruc->req_, ruc->dtls_, ruc->srtp_, username)); + EXPECT_TRUE(conn->nack_enabled_); + + // Create and set publish token + SrsStreamPublishToken *publish_token_raw = NULL; + HELPER_EXPECT_SUCCESS(token_manager.acquire_token(ruc->req_, publish_token_raw)); + SrsSharedPtr publish_token(publish_token_raw); + + conn->set_publish_token(publish_token); + EXPECT_TRUE(conn->publish_token_->is_acquired()); + } + + // DTLS done, start publisher + SrsRtcPublishStream *publisher = NULL; + if (true) { + HELPER_EXPECT_SUCCESS(conn->on_dtls_handshake_done()); + + // Wait for coroutine to start. Normally it should be ready wait for PLI requests. + srs_usleep(1 * SRS_UTIME_MILLISECONDS); + + // Verify the publisher is created and started + EXPECT_TRUE(conn->publishers_.size() == 1); + publisher = dynamic_cast(conn->publishers_.begin()->second); + EXPECT_TRUE(publisher->is_sender_started_); + } + + // Got a RTP audio packet. + for (int i = 0; i < 3; i++) { + SrsRtpPacket pkt; + pkt.header_.set_ssrc(mock_sdp_factory->audio_ssrc_); + pkt.header_.set_sequence(100); + pkt.header_.set_timestamp(1000); + pkt.header_.set_payload_type(mock_sdp_factory->audio_pt_); + + SrsUniquePtr data(new char[1500]); + SrsBuffer buf(data.get(), 1500); + HELPER_EXPECT_SUCCESS(pkt.encode(&buf)); + + HELPER_EXPECT_SUCCESS(conn->on_rtp_cipher(data.get(), buf.pos())); + HELPER_EXPECT_SUCCESS(conn->on_rtp_plaintext(data.get(), buf.pos())); + + EXPECT_EQ(mock_rtc_source->rtp_audio_count_, i + 1); + } + + // Got a RTP video packet with VP9 payload type. + for (int i = 0; i < 3; i++) { + SrsRtpPacket pkt; + pkt.header_.set_ssrc(mock_sdp_factory->video_ssrc_); + pkt.header_.set_sequence(100); + pkt.header_.set_timestamp(1000); + pkt.header_.set_payload_type(98); // VP9 payload type + + SrsUniquePtr data(new char[1500]); + SrsBuffer buf(data.get(), 1500); + HELPER_EXPECT_SUCCESS(pkt.encode(&buf)); + + HELPER_EXPECT_SUCCESS(conn->on_rtp_cipher(data.get(), buf.pos())); + HELPER_EXPECT_SUCCESS(conn->on_rtp_plaintext(data.get(), buf.pos())); + + EXPECT_EQ(mock_rtc_source->rtp_video_count_, i + 1); + } + + // Stop the publisher + publisher->stop(); +} diff --git a/trunk/src/utest/srs_utest_workflow_rtc_conn.hpp b/trunk/src/utest/srs_utest_workflow_rtc_conn.hpp index 6ee035a6e..14dfae20a 100644 --- a/trunk/src/utest/srs_utest_workflow_rtc_conn.hpp +++ b/trunk/src/utest/srs_utest_workflow_rtc_conn.hpp @@ -30,54 +30,34 @@ #include #include -#include - -#include -#include - -class MockAppFactoryForRtcConn; - -class MockProtocolUtilityForRtcConn : public ISrsProtocolUtility -{ -public: - std::vector ips_; - std::string mock_ip_; - -public: - MockProtocolUtilityForRtcConn(std::string ip); - virtual ~MockProtocolUtilityForRtcConn(); - -public: - virtual std::vector &local_ips(); -}; +#include class MockAppFactoryForRtcConn : public SrsAppFactory { public: ISrsRtcSourceManager *rtc_sources_; - MockProtocolUtilityForRtcConn *mock_protocol_utility_; + MockProtocolUtility *mock_protocol_utility_; public: - MockAppFactoryForRtcConn(); - virtual ~MockAppFactoryForRtcConn(); + MockAppFactoryForRtcConn() + { + mock_protocol_utility_ = NULL; + } + virtual ~MockAppFactoryForRtcConn() + { + } public: - virtual ISrsProtocolUtility *create_protocol_utility(); - virtual ISrsRtcPublishStream *create_rtc_publish_stream(ISrsExecRtcAsyncTask *exec, ISrsExpire *expire, ISrsRtcPacketReceiver *receiver, const SrsContextId &cid); -}; - -class MockRtcSourceForRtcConn : public SrsRtcSource -{ -public: - int rtp_audio_count_; - int rtp_video_count_; - -public: - MockRtcSourceForRtcConn(); - virtual ~MockRtcSourceForRtcConn(); - -public: - virtual srs_error_t on_rtp(SrsRtpPacket *pkt); + virtual ISrsProtocolUtility *create_protocol_utility() + { + return mock_protocol_utility_; + } + virtual ISrsRtcPublishStream *create_rtc_publish_stream(ISrsExecRtcAsyncTask *exec, ISrsExpire *expire, ISrsRtcPacketReceiver *receiver, const SrsContextId &cid) + { + SrsRtcPublishStream *publisher = new SrsRtcPublishStream(exec, expire, receiver, cid); + publisher->rtc_sources_ = rtc_sources_; + return publisher; + } }; #endif