From 6e93dd73b59b524dabf3174f58f908d363ff812a Mon Sep 17 00:00:00 2001 From: OSSRS-AI Date: Tue, 11 Nov 2025 10:22:31 -0500 Subject: [PATCH] AI: WebRTC: Support optional msid attribute per RFC 8830. v7.0.126 (#4570) (#4572) Fix issue #4570 by supporting optional `msid` attribute in WebRTC SDP negotiation, enabling compatibility with libdatachannel and other clients that don't include msid information. SRS failed to negotiate WebRTC connections from libdatachannel clients because: - libdatachannel SDP lacks `a=ssrc:XX msid:stream_id track_id` attributes - SRS required msid information to create track descriptions - According to RFC 8830, the msid attribute and its appdata (track_id) are **optional** If diligently look at the SDP generated by libdatachannel: ``` a=ssrc:42 cname:video-send a=ssrc:43 cname:audio-send ``` It's deliberately missing the `a=ssrc:XX msid:stream_id track_id` line, comparing that with this one: ``` a=ssrc:42 cname:video-send a=ssrc:42 msid:stream_id video_track_id a=ssrc:43 cname:audio-send a=ssrc:43 msid:stream_id audio_track_id ``` In such a situation, to keep compatible with libdatachannel, if no msid line in sdp, SRS comprehensively and consistently uses: * app/stream as stream_id, such as live/livestream * type=video|audio, cname, and ssrc as track_id, such as track-video-video-send-43 --- .vscode/README.md | 5 + .vscode/launch.json | 65 +++--- trunk/doc/CHANGELOG.md | 1 + trunk/src/app/srs_app_rtc_conn.cpp | 21 +- trunk/src/core/srs_core_version7.hpp | 2 +- trunk/src/protocol/srs_protocol_sdp.cpp | 7 +- trunk/src/utest/srs_utest_ai12.cpp | 110 ++++++++++ trunk/src/utest/srs_utest_ai24.cpp | 136 +++++++++++++ trunk/src/utest/srs_utest_manual_mock.cpp | 52 +++++ trunk/src/utest/srs_utest_manual_mock.hpp | 3 + .../src/utest/srs_utest_workflow_rtc_conn.cpp | 190 +++++++++++++++++- 11 files changed, 547 insertions(+), 45 deletions(-) diff --git a/.vscode/README.md b/.vscode/README.md index 829cddbd7..27d82f053 100644 --- a/.vscode/README.md +++ b/.vscode/README.md @@ -40,6 +40,11 @@ cmake --build $HOME/git/srs/trunk/cmake/build ## macOS: SRS UTest +The most straightforward way is to select a test name like `WorkflowRtcManuallyVerifyForPublisher`, +then select `Debug gtest (macOS CodeLLDB)` and run the debug. + +Or you can use the following way to run specified test from the test panel. + Install the following extensions: - C++ TestMate diff --git a/.vscode/launch.json b/.vscode/launch.json index c3f1eee3e..6e4777112 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,7 +2,7 @@ "version": "0.2.0", "configurations": [ { - "name": "Launch SRS with conf/console.conf", + "name": "Debug SRS with conf/console.conf", "type": "cppdbg", "request": "launch", "program": "${workspaceFolder}/trunk/cmake/build/srs", @@ -30,7 +30,7 @@ } }, { - "name": "Launch SRS with conf/rtc.conf", + "name": "Debug SRS with conf/rtc.conf", "type": "cppdbg", "request": "launch", "program": "${workspaceFolder}/trunk/cmake/build/srs", @@ -58,40 +58,41 @@ } }, { - "name": "Launch SRS with console.conf", - "type": "cppdbg", - "request": "launch", - "program": "${workspaceFolder}/trunk/cmake/build/srs", - "args": ["-c", "console.conf"], - "stopAtEntry": false, - "cwd": "${workspaceFolder}/trunk", - "environment": [], - "externalConsole": false, - "linux": { - "MIMode": "gdb" - }, - "osx": { - "MIMode": "lldb" - }, - "setupCommands": [ - { - "description": "Enable pretty-printing for gdb", - "text": "-enable-pretty-printing", - "ignoreFailures": true - } - ], - "preLaunchTask": "build", - "logging": { - "engineLogging": true - } - }, - { - "name": "Launch srs-proxy", + "name": "Debug srs-proxy", "type": "go", "request": "launch", "mode": "auto", "cwd": "${workspaceFolder}/proxy", "program": "${workspaceFolder}/proxy" - } + }, + { + "name": "Debug SRS (macOS, CodeLLDB) console.conf", + "type": "lldb", + "request": "launch", + "program": "${workspaceFolder}/trunk/cmake/build/srs", + "args": ["-c", "console.conf"], + "cwd": "${workspaceFolder}/trunk", + "stopOnEntry": false, + "terminal": "integrated", + "initCommands": [ + "command script import lldb.formatters.cpp.libcxx" + ], + "preLaunchTask": "build", + "env": {}, + "sourceLanguages": ["cpp"] + }, + { + "name": "Debug gtest (macOS CodeLLDB)", + "type": "lldb", + "request": "launch", + "program": "${workspaceFolder}/trunk/cmake/build/utest", + "args": ["--gtest_filter=*${selectedText}*"], + "cwd": "${workspaceFolder}/trunk", + "terminal": "integrated", + "initCommands": [ + "command script import lldb.formatters.cpp.libcxx" + ], + "sourceLanguages": ["cpp"] + } ] } \ No newline at end of file diff --git a/trunk/doc/CHANGELOG.md b/trunk/doc/CHANGELOG.md index 87e2ec0f1..93904fe92 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-11, AI: WebRTC: Support optional msid attribute per RFC 8830. v7.0.126 (#4570) * v7.0, 2025-11-11, AI: SRT: Stop TS parsing after codec detection. v7.0.125 (#4569) * v7.0, 2025-11-09, AI: WebRTC: Support G.711 (PCMU/PCMA) audio codec for WebRTC. v7.0.124 (#4075) * v7.0, 2025-11-08, AI: WebRTC: Support VP9 codec for WebRTC-to-WebRTC streaming. v7.0.123 (#4548) diff --git a/trunk/src/app/srs_app_rtc_conn.cpp b/trunk/src/app/srs_app_rtc_conn.cpp index 22eb39252..648575b11 100644 --- a/trunk/src/app/srs_app_rtc_conn.cpp +++ b/trunk/src/app/srs_app_rtc_conn.cpp @@ -3601,12 +3601,25 @@ srs_error_t SrsRtcPublisherNegotiator::negotiate_publish_capability(SrsRtcUserCo for (int j = 0; j < (int)remote_media_desc.ssrc_infos_.size(); ++j) { const SrsSSRCInfo &ssrc_info = remote_media_desc.ssrc_infos_.at(j); + // Generate msid because it's optional in sdp. + string msid_tracker = ssrc_info.msid_tracker_; + if (msid_tracker.empty()) { + msid_tracker = srs_fmt_sprintf("track-%s-%s-%d", + track_desc->type_.c_str(), ssrc_info.cname_.c_str(), ssrc_info.ssrc_); + } + + // Generate msid because it's optional in sdp. + string msid = ssrc_info.msid_; + if (msid.empty()) { + msid = req->app_ + "/" + req->stream_; + } + // ssrc have same track id, will be description in the same track description. - if (track_id != ssrc_info.msid_tracker_) { + if (track_id != msid_tracker) { SrsRtcTrackDescription *track_desc_copy = track_desc->copy(); track_desc_copy->ssrc_ = ssrc_info.ssrc_; - track_desc_copy->id_ = ssrc_info.msid_tracker_; - track_desc_copy->msid_ = ssrc_info.msid_; + track_desc_copy->id_ = msid_tracker; + track_desc_copy->msid_ = msid; if (remote_media_desc.is_audio() && !stream_desc->audio_track_desc_) { stream_desc->audio_track_desc_ = track_desc_copy; @@ -3616,7 +3629,7 @@ srs_error_t SrsRtcPublisherNegotiator::negotiate_publish_capability(SrsRtcUserCo srs_freep(track_desc_copy); } } - track_id = ssrc_info.msid_tracker_; + track_id = msid_tracker; } // set track fec_ssrc and rtx_ssrc diff --git a/trunk/src/core/srs_core_version7.hpp b/trunk/src/core/srs_core_version7.hpp index 67a256300..5abca4f16 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 125 +#define VERSION_REVISION 126 #endif \ No newline at end of file diff --git a/trunk/src/protocol/srs_protocol_sdp.cpp b/trunk/src/protocol/srs_protocol_sdp.cpp index 61242f76d..5e70b95f8 100644 --- a/trunk/src/protocol/srs_protocol_sdp.cpp +++ b/trunk/src/protocol/srs_protocol_sdp.cpp @@ -375,9 +375,10 @@ vector SrsMediaDesc::find_media_with_encoding_name(const st transform(encoding_name.begin(), encoding_name.end(), upper_name.begin(), ::toupper); for (size_t i = 0; i < payload_types_.size(); ++i) { - if (payload_types_[i].encoding_name_ == std::string(lower_name.c_str()) || - payload_types_[i].encoding_name_ == std::string(upper_name.c_str())) { - payloads.push_back(payload_types_[i]); + SrsMediaPayloadType payload = payload_types_[i]; + if (payload.encoding_name_ == std::string(lower_name.c_str()) || + payload.encoding_name_ == std::string(upper_name.c_str())) { + payloads.push_back(payload); } } diff --git a/trunk/src/utest/srs_utest_ai12.cpp b/trunk/src/utest/srs_utest_ai12.cpp index e97d0f3ec..a5b26e953 100644 --- a/trunk/src/utest/srs_utest_ai12.cpp +++ b/trunk/src/utest/srs_utest_ai12.cpp @@ -2039,6 +2039,116 @@ VOID TEST(SrsRtcPublisherNegotiatorTest, TypicalUseScenario) EXPECT_EQ("video", video_sdp.media_descs_[0].type_); } +VOID TEST(SrsRtcPublisherNegotiatorTest, LibdatachannelUseScenario) +{ + srs_error_t err; + + // Create SrsRtcPublisherNegotiator + SrsUniquePtr negotiator(new SrsRtcPublisherNegotiator()); + + // Create mock request for initialization + SrsUniquePtr mock_request(new MockRtcConnectionRequest("test.vhost", "live", "stream1")); + + // Create mock RTC user config with remote SDP + SrsUniquePtr ruc(new SrsRtcUserConfig()); + ruc->req_ = mock_request->copy(); + ruc->publish_ = true; + ruc->dtls_ = true; + ruc->srtp_ = true; + ruc->audio_before_video_ = true; + + // SDP from issue 4570 - libdatachannel format with video first, then audio + ruc->remote_sdp_str_ = + "v=0\r\n" + "o=- rtc 4158491451 0 IN IP4 127.0.0.1\r\n" + "s=-\r\n" + "t=0 0\r\n" + "a=group:BUNDLE video audio\r\n" + "a=group:LS video audio\r\n" + "a=msid-semantic:WMS *\r\n" + "a=ice-options:ice2,trickle\r\n" + "a=fingerprint:sha-256 28:37:F7:18:77:FC:46:33:6F:B2:0F:12:83:C2:BF:5C:61:5E:96:EB:4B:B9:97:81:92:7C:82:10:97:B8:8E:60\r\n" + "m=video 56144 UDP/TLS/RTP/SAVPF 96 97\r\n" + "c=IN IP4 172.24.64.1\r\n" + "a=mid:video\r\n" + "a=sendonly\r\n" + "a=ssrc:42 cname:video-send\r\n" + "a=rtcp-mux\r\n" + "a=rtpmap:96 H264/90000\r\n" + "a=rtcp-fb:96 nack\r\n" + "a=rtcp-fb:96 nack pli\r\n" + "a=rtcp-fb:96 goog-remb\r\n" + "a=fmtp:96 profile-level-id=42e01f;packetization-mode=1;level-asymmetry-allowed=1\r\n" + "a=rtpmap:97 RTX/90000\r\n" + "a=fmtp:97 apt=96\r\n" + "a=setup:actpass\r\n" + "a=ice-ufrag:fEw/\r\n" + "a=ice-pwd:jBua8YGWQKc/Vn6Y9EZ9+0\r\n" + "a=candidate:1 1 UDP 2122317823 172.24.64.1 56144 typ host\r\n" + "a=candidate:2 1 UDP 2122315767 10.0.0.94 56144 typ host\r\n" + "a=candidate:3 1 UDP 1686189695 111.43.134.137 56144 typ srflx raddr 0.0.0.0 rport 0\r\n" + "a=end-of-candidates\r\n" + "m=audio 56144 UDP/TLS/RTP/SAVPF 111\r\n" + "c=IN IP4 172.24.64.1\r\n" + "a=mid:audio\r\n" + "a=sendonly\r\n" + "a=ssrc:43 cname:audio-send\r\n" + "a=rtcp-mux\r\n" + "a=rtpmap:111 opus/48000/2\r\n" + "a=fmtp:111 minptime=10;maxaveragebitrate=98000;stereo=1;sprop-stereo=1;useinbandfec=1\r\n" + "a=setup:actpass\r\n" + "a=ice-ufrag:fEw/\r\n" + "a=ice-pwd:jBua8YGWQKc/Vn6Y9EZ9+0\r\n"; + + // Parse the remote SDP + HELPER_EXPECT_SUCCESS(ruc->remote_sdp_.parse(ruc->remote_sdp_str_)); + + // Create stream description for negotiation output + SrsUniquePtr stream_desc(new SrsRtcSourceDescription()); + + // Test negotiate_publish_capability - typical WebRTC publisher negotiation + HELPER_EXPECT_SUCCESS(negotiator->negotiate_publish_capability(ruc.get(), stream_desc.get())); + + // Verify that stream description was populated with audio and video tracks + EXPECT_TRUE(stream_desc->audio_track_desc_ != NULL); + EXPECT_FALSE(stream_desc->video_track_descs_.empty()); + EXPECT_EQ("audio", stream_desc->audio_track_desc_->type_); + EXPECT_EQ("video", stream_desc->video_track_descs_[0]->type_); + + // Test generate_publish_local_sdp - create answer SDP + SrsSdp local_sdp; + HELPER_EXPECT_SUCCESS(negotiator->generate_publish_local_sdp( + ruc->req_, local_sdp, stream_desc.get(), + ruc->remote_sdp_.is_unified(), ruc->audio_before_video_)); + + // Verify that local SDP was generated with media descriptions + EXPECT_FALSE(local_sdp.media_descs_.empty()); + + // Find audio and video media descriptions + bool has_audio = false, has_video = false; + for (size_t i = 0; i < local_sdp.media_descs_.size(); i++) { + if (local_sdp.media_descs_[i].type_ == "audio") + has_audio = true; + if (local_sdp.media_descs_[i].type_ == "video") + has_video = true; + } + EXPECT_TRUE(has_audio); + EXPECT_TRUE(has_video); + + // Test individual SDP generation methods + SrsSdp audio_sdp, video_sdp; + HELPER_EXPECT_SUCCESS(negotiator->generate_publish_local_sdp_for_audio(audio_sdp, stream_desc.get())); + HELPER_EXPECT_SUCCESS(negotiator->generate_publish_local_sdp_for_video(video_sdp, stream_desc.get(), true)); + + // Verify audio SDP generation + EXPECT_FALSE(audio_sdp.media_descs_.empty()); + EXPECT_EQ("audio", audio_sdp.media_descs_[0].type_); + + // Verify video SDP generation + EXPECT_FALSE(video_sdp.media_descs_.empty()); + EXPECT_EQ("video", video_sdp.media_descs_[0].type_); +} + VOID TEST(SrsRtcConnectionTest, InitializeTypicalScenario) { srs_error_t err; diff --git a/trunk/src/utest/srs_utest_ai24.cpp b/trunk/src/utest/srs_utest_ai24.cpp index 12b6db543..6f03e2614 100644 --- a/trunk/src/utest/srs_utest_ai24.cpp +++ b/trunk/src/utest/srs_utest_ai24.cpp @@ -1175,3 +1175,139 @@ VOID TEST(AppUtilityTest, IsBoolean) EXPECT_FALSE(srs_is_boolean("")); EXPECT_FALSE(srs_is_boolean("random")); } + +// Test: Parse libdatachannel SDP from issue 4570 and verify fields +VOID TEST(SdpTest, ParseLibdatachannelSdpFromIssue4570) +{ + srs_error_t err; + + // SDP from issue 4570 - libdatachannel format with video first, then audio + std::string sdp_str = + "v=0\r\n" + "o=- rtc 4158491451 0 IN IP4 127.0.0.1\r\n" + "s=-\r\n" + "t=0 0\r\n" + "a=group:BUNDLE video audio\r\n" + "a=group:LS video audio\r\n" + "a=msid-semantic:WMS *\r\n" + "a=ice-options:ice2,trickle\r\n" + "a=fingerprint:sha-256 28:37:F7:18:77:FC:46:33:6F:B2:0F:12:83:C2:BF:5C:61:5E:96:EB:4B:B9:97:81:92:7C:82:10:97:B8:8E:60\r\n" + "m=video 56144 UDP/TLS/RTP/SAVPF 96 97\r\n" + "c=IN IP4 172.24.64.1\r\n" + "a=mid:video\r\n" + "a=sendonly\r\n" + "a=ssrc:42 cname:video-send\r\n" + "a=rtcp-mux\r\n" + "a=rtpmap:96 H264/90000\r\n" + "a=rtcp-fb:96 nack\r\n" + "a=rtcp-fb:96 nack pli\r\n" + "a=rtcp-fb:96 goog-remb\r\n" + "a=fmtp:96 profile-level-id=42e01f;packetization-mode=1;level-asymmetry-allowed=1\r\n" + "a=rtpmap:97 RTX/90000\r\n" + "a=fmtp:97 apt=96\r\n" + "a=setup:actpass\r\n" + "a=ice-ufrag:fEw/\r\n" + "a=ice-pwd:jBua8YGWQKc/Vn6Y9EZ9+0\r\n" + "a=candidate:1 1 UDP 2122317823 172.24.64.1 56144 typ host\r\n" + "a=candidate:2 1 UDP 2122315767 10.0.0.94 56144 typ host\r\n" + "a=candidate:3 1 UDP 1686189695 111.43.134.137 56144 typ srflx raddr 0.0.0.0 rport 0\r\n" + "a=end-of-candidates\r\n" + "m=audio 56144 UDP/TLS/RTP/SAVPF 111\r\n" + "c=IN IP4 172.24.64.1\r\n" + "a=mid:audio\r\n" + "a=sendonly\r\n" + "a=ssrc:43 cname:audio-send\r\n" + "a=rtcp-mux\r\n" + "a=rtpmap:111 opus/48000/2\r\n" + "a=fmtp:111 minptime=10;maxaveragebitrate=98000;stereo=1;sprop-stereo=1;useinbandfec=1\r\n" + "a=setup:actpass\r\n" + "a=ice-ufrag:fEw/\r\n" + "a=ice-pwd:jBua8YGWQKc/Vn6Y9EZ9+0\r\n"; + + // Parse the SDP + SrsSdp sdp; + HELPER_EXPECT_SUCCESS(sdp.parse(sdp_str)); + + // Verify session-level fields + EXPECT_TRUE(sdp.version_ == "0"); + EXPECT_TRUE(sdp.group_policy_ == "BUNDLE"); + EXPECT_TRUE(sdp.groups_.size() == 2); + EXPECT_TRUE(sdp.groups_[0] == "video"); + EXPECT_TRUE(sdp.groups_[1] == "audio"); + + // Verify we have 2 media descriptions (video and audio) + EXPECT_TRUE(sdp.media_descs_.size() == 2); + + // Verify first media description is video + SrsMediaDesc* video_desc = &sdp.media_descs_[0]; + EXPECT_TRUE(video_desc->type_ == "video"); + EXPECT_TRUE(video_desc->mid_ == "video"); + EXPECT_TRUE(video_desc->sendonly_); + EXPECT_FALSE(video_desc->recvonly_); + EXPECT_TRUE(video_desc->port_ == 56144); + EXPECT_TRUE(video_desc->protos_ == "UDP/TLS/RTP/SAVPF"); + + // Verify video payload types + EXPECT_TRUE(video_desc->payload_types_.size() >= 1); + + // Find H264 payload (PT 96) + SrsMediaPayloadType* h264_payload = NULL; + for (size_t i = 0; i < video_desc->payload_types_.size(); i++) { + if (video_desc->payload_types_[i].payload_type_ == 96) { + h264_payload = &video_desc->payload_types_[i]; + break; + } + } + EXPECT_TRUE(h264_payload != NULL); + EXPECT_TRUE(h264_payload->encoding_name_ == "H264"); + EXPECT_TRUE(h264_payload->clock_rate_ == 90000); + + // Verify video SSRC + EXPECT_TRUE(video_desc->ssrc_infos_.size() >= 1); + bool found_video_ssrc = false; + for (size_t i = 0; i < video_desc->ssrc_infos_.size(); i++) { + if (video_desc->ssrc_infos_[i].ssrc_ == 42) { + found_video_ssrc = true; + EXPECT_TRUE(video_desc->ssrc_infos_[i].cname_ == "video-send"); + break; + } + } + EXPECT_TRUE(found_video_ssrc); + + // Verify second media description is audio + SrsMediaDesc* audio_desc = &sdp.media_descs_[1]; + EXPECT_TRUE(audio_desc->type_ == "audio"); + EXPECT_TRUE(audio_desc->mid_ == "audio"); + EXPECT_TRUE(audio_desc->sendonly_); + EXPECT_FALSE(audio_desc->recvonly_); + EXPECT_TRUE(audio_desc->port_ == 56144); + EXPECT_TRUE(audio_desc->protos_ == "UDP/TLS/RTP/SAVPF"); + + // Verify audio payload types + EXPECT_TRUE(audio_desc->payload_types_.size() >= 1); + + // Find Opus payload (PT 111) + SrsMediaPayloadType* opus_payload = NULL; + for (size_t i = 0; i < audio_desc->payload_types_.size(); i++) { + if (audio_desc->payload_types_[i].payload_type_ == 111) { + opus_payload = &audio_desc->payload_types_[i]; + break; + } + } + EXPECT_TRUE(opus_payload != NULL); + EXPECT_TRUE(opus_payload->encoding_name_ == "opus"); + EXPECT_TRUE(opus_payload->clock_rate_ == 48000); + EXPECT_TRUE(opus_payload->encoding_param_ == "2"); + + // Verify audio SSRC + EXPECT_TRUE(audio_desc->ssrc_infos_.size() >= 1); + bool found_audio_ssrc = false; + for (size_t i = 0; i < audio_desc->ssrc_infos_.size(); i++) { + if (audio_desc->ssrc_infos_[i].ssrc_ == 43) { + found_audio_ssrc = true; + EXPECT_TRUE(audio_desc->ssrc_infos_[i].cname_ == "audio-send"); + break; + } + } + EXPECT_TRUE(found_audio_ssrc); +} diff --git a/trunk/src/utest/srs_utest_manual_mock.cpp b/trunk/src/utest/srs_utest_manual_mock.cpp index a8f339d9b..330c64516 100644 --- a/trunk/src/utest/srs_utest_manual_mock.cpp +++ b/trunk/src/utest/srs_utest_manual_mock.cpp @@ -316,6 +316,58 @@ std::string MockSdpFactory::create_chrome_publisher_offer_with_g711_pcmu() return ss.str(); } +std::string MockSdpFactory::create_libdatachannel_publisher_offer_with_h264() +{ + // Create a libdatachannel-like WebRTC SDP offer with H.264 video and Opus audio + // Key difference from Chrome: video comes first, then audio (libdatachannel order) + // This is the actual SDP format from issue 4570 + std::stringstream ss; + ss << "v=0\r\n" + << "o=- rtc 4158491451 0 IN IP4 127.0.0.1\r\n" + << "s=-\r\n" + << "t=0 0\r\n" + << "a=group:BUNDLE video audio\r\n" + << "a=group:LS video audio\r\n" + << "a=msid-semantic:WMS *\r\n" + << "a=ice-options:ice2,trickle\r\n" + << "a=fingerprint:sha-256 28:37:F7:18:77:FC:46:33:6F:B2:0F:12:83:C2:BF:5C:61:5E:96:EB:4B:B9:97:81:92:7C:82:10:97:B8:8E:60\r\n" + // Video media description (H.264) - comes first in libdatachannel + << "m=video 56144 UDP/TLS/RTP/SAVPF " << (int)video_pt_ << " 97\r\n" + << "c=IN IP4 172.24.64.1\r\n" + << "a=mid:video\r\n" + << "a=sendonly\r\n" + << "a=ssrc:" << video_ssrc_ << " cname:video-send\r\n" + << "a=rtcp-mux\r\n" + << "a=rtpmap:" << (int)video_pt_ << " H264/90000\r\n" + << "a=rtcp-fb:" << (int)video_pt_ << " nack\r\n" + << "a=rtcp-fb:" << (int)video_pt_ << " nack pli\r\n" + << "a=rtcp-fb:" << (int)video_pt_ << " goog-remb\r\n" + << "a=fmtp:" << (int)video_pt_ << " profile-level-id=42e01f;packetization-mode=1;level-asymmetry-allowed=1\r\n" + << "a=rtpmap:97 RTX/90000\r\n" + << "a=fmtp:97 apt=" << (int)video_pt_ << "\r\n" + << "a=setup:actpass\r\n" + << "a=ice-ufrag:fEw/\r\n" + << "a=ice-pwd:jBua8YGWQKc/Vn6Y9EZ9+0\r\n" + << "a=candidate:1 1 UDP 2122317823 172.24.64.1 56144 typ host\r\n" + << "a=candidate:2 1 UDP 2122315767 10.0.0.94 56144 typ host\r\n" + << "a=candidate:3 1 UDP 1686189695 111.43.134.137 56144 typ srflx raddr 0.0.0.0 rport 0\r\n" + << "a=end-of-candidates\r\n" + // Audio media description (Opus) - comes second in libdatachannel + << "m=audio 56144 UDP/TLS/RTP/SAVPF " << (int)audio_pt_ << "\r\n" + << "c=IN IP4 172.24.64.1\r\n" + << "a=mid:audio\r\n" + << "a=sendonly\r\n" + << "a=ssrc:" << audio_ssrc_ << " cname:audio-send\r\n" + << "a=rtcp-mux\r\n" + << "a=rtpmap:" << (int)audio_pt_ << " opus/48000/2\r\n" + << "a=fmtp:" << (int)audio_pt_ << " minptime=10;maxaveragebitrate=98000;stereo=1;sprop-stereo=1;useinbandfec=1\r\n" + << "a=setup:actpass\r\n" + << "a=ice-ufrag:fEw/\r\n" + << "a=ice-pwd:jBua8YGWQKc/Vn6Y9EZ9+0\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"; diff --git a/trunk/src/utest/srs_utest_manual_mock.hpp b/trunk/src/utest/srs_utest_manual_mock.hpp index d6ff4f6dd..e98e7fd4b 100644 --- a/trunk/src/utest/srs_utest_manual_mock.hpp +++ b/trunk/src/utest/srs_utest_manual_mock.hpp @@ -87,6 +87,9 @@ public: std::string create_chrome_publisher_offer_with_vp9(); // Create a Chrome-like WebRTC publisher offer SDP with G.711 PCMU audio std::string create_chrome_publisher_offer_with_g711_pcmu(); + // Create a libdatachannel-like WebRTC publisher offer SDP with H.264 video and Opus audio + // This mimics the SDP format from libdatachannel library (video first, then audio) + std::string create_libdatachannel_publisher_offer_with_h264(); }; // Mock DTLS certificate for testing diff --git a/trunk/src/utest/srs_utest_workflow_rtc_conn.cpp b/trunk/src/utest/srs_utest_workflow_rtc_conn.cpp index 414a7f9e2..e41955511 100644 --- a/trunk/src/utest/srs_utest_workflow_rtc_conn.cpp +++ b/trunk/src/utest/srs_utest_workflow_rtc_conn.cpp @@ -37,7 +37,7 @@ // 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. -VOID TEST(BasicWorkflowRtcConnTest, ManuallyVerifyForPlayer) +VOID TEST(BasicWorkflowRtcConnTest, WorkflowRtcManuallyVerifyForPlayer) { srs_error_t err; @@ -92,6 +92,7 @@ VOID TEST(BasicWorkflowRtcConnTest, ManuallyVerifyForPlayer) ruc->remote_sdp_str_ = mock_sdp_factory->create_chrome_player_offer_with_h264(); HELPER_EXPECT_SUCCESS(ruc->remote_sdp_.parse(ruc->remote_sdp_str_)); + EXPECT_TRUE(ruc->remote_sdp_.media_descs_.size() == 2); } // Add player, which negotiate the SDP and generate local SDP @@ -187,7 +188,7 @@ VOID TEST(BasicWorkflowRtcConnTest, ManuallyVerifyForPlayer) // 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. -VOID TEST(BasicWorkflowRtcConnTest, ManuallyVerifyForPublisher) +VOID TEST(BasicWorkflowRtcConnTest, WorkflowRtcManuallyVerifyForPublisher) { srs_error_t err; @@ -243,6 +244,7 @@ VOID TEST(BasicWorkflowRtcConnTest, ManuallyVerifyForPublisher) ruc->remote_sdp_str_ = mock_sdp_factory->create_chrome_publisher_offer_with_h264(); HELPER_EXPECT_SUCCESS(ruc->remote_sdp_.parse(ruc->remote_sdp_str_)); + EXPECT_TRUE(ruc->remote_sdp_.media_descs_.size() == 2); } // Add publisher, which negotiate the SDP and generate local SDP @@ -394,10 +396,185 @@ VOID TEST(BasicWorkflowRtcConnTest, ManuallyVerifyForPublisher) publisher->stop(); } +// This test is used to verify the libdatachannel SDP offer from issue 4570. +// The issue reports that SRS returns an incomplete SDP answer when receiving +// an offer from libdatachannel library. +VOID TEST(BasicWorkflowRtcConnTest, WorkflowRtcManuallyVerifyForLibdatachannel) +{ + 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-libdatachannel-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 libdatachannel SDP offer from issue #4570 + 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->remote_sdp_str_ = mock_sdp_factory->create_libdatachannel_publisher_offer_with_h264(); + HELPER_EXPECT_SUCCESS(ruc->remote_sdp_.parse(ruc->remote_sdp_str_)); + EXPECT_TRUE(ruc->remote_sdp_.media_descs_.size() == 2); + } + + // 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->video_ssrc_) != conn->publishers_ssrc_map_.end()); + EXPECT_TRUE(conn->publishers_ssrc_map_.find(mock_sdp_factory->audio_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_); + EXPECT_TRUE(stream_desc->video_track_descs_[0]->media_->pt_ == mock_sdp_factory->video_pt_); + + // 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 video media desc (libdatachannel puts video first) + SrsMediaDesc *video_desc = &local_sdp.media_descs_[0]; + 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_ == mock_sdp_factory->video_pt_); + EXPECT_TRUE(video_desc->payload_types_[0].encoding_name_ == "H264"); + EXPECT_TRUE(video_desc->payload_types_[0].clock_rate_ == 90000); + + // Second should be audio media desc + SrsMediaDesc *audio_desc = &local_sdp.media_descs_[1]; + 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); + } + + // 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 *video_desc = &local_sdp.media_descs_[0]; + 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"); + + SrsMediaDesc *audio_desc = &local_sdp.media_descs_[1]; + 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"); + + 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_); + } + + // 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) +VOID TEST(BasicWorkflowRtcConnTest, WorkflowRtcManuallyVerifyForPublisherWithAV1) { srs_error_t err; @@ -454,6 +631,7 @@ VOID TEST(BasicWorkflowRtcConnTest, ManuallyVerifyForPublisherWithAV1) ruc->remote_sdp_str_ = mock_sdp_factory->create_chrome_publisher_offer_with_av1(); HELPER_EXPECT_SUCCESS(ruc->remote_sdp_.parse(ruc->remote_sdp_str_)); + EXPECT_TRUE(ruc->remote_sdp_.media_descs_.size() == 2); } // Add publisher, which negotiate the SDP and generate local SDP @@ -612,7 +790,7 @@ VOID TEST(BasicWorkflowRtcConnTest, ManuallyVerifyForPublisherWithAV1) // 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) +VOID TEST(BasicWorkflowRtcConnTest, WorkflowRtcManuallyVerifyForPublisherWithVP9) { srs_error_t err; @@ -669,6 +847,7 @@ VOID TEST(BasicWorkflowRtcConnTest, ManuallyVerifyForPublisherWithVP9) ruc->remote_sdp_str_ = mock_sdp_factory->create_chrome_publisher_offer_with_vp9(); HELPER_EXPECT_SUCCESS(ruc->remote_sdp_.parse(ruc->remote_sdp_str_)); + EXPECT_TRUE(ruc->remote_sdp_.media_descs_.size() == 2); } // Add publisher, which negotiate the SDP and generate local SDP @@ -827,7 +1006,7 @@ VOID TEST(BasicWorkflowRtcConnTest, ManuallyVerifyForPublisherWithVP9) // This test is used to verify the basic workflow of the RTC connection with G.711 PCMU 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, ManuallyVerifyForPublisherWithG711Pcmu) +VOID TEST(BasicWorkflowRtcConnTest, WorkflowRtcManuallyVerifyForPublisherWithG711Pcmu) { srs_error_t err; @@ -884,6 +1063,7 @@ VOID TEST(BasicWorkflowRtcConnTest, ManuallyVerifyForPublisherWithG711Pcmu) ruc->remote_sdp_str_ = mock_sdp_factory->create_chrome_publisher_offer_with_g711_pcmu(); HELPER_EXPECT_SUCCESS(ruc->remote_sdp_.parse(ruc->remote_sdp_str_)); + EXPECT_TRUE(ruc->remote_sdp_.media_descs_.size() == 2); } // Add publisher, which negotiate the SDP and generate local SDP