diff --git a/.vscode/README.md b/.vscode/README.md
index 829cddbd7d..27d82f0534 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 c3f1eee3e6..6e4777112a 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",
+ "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"],
- "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
- }
+ "stopOnEntry": false,
+ "terminal": "integrated",
+ "initCommands": [
+ "command script import lldb.formatters.cpp.libcxx"
],
"preLaunchTask": "build",
- "logging": {
- "engineLogging": true
- }
+ "env": {},
+ "sourceLanguages": ["cpp"]
},
{
- "name": "Launch srs-proxy",
- "type": "go",
+ "name": "Debug gtest (macOS CodeLLDB)",
+ "type": "lldb",
"request": "launch",
- "mode": "auto",
- "cwd": "${workspaceFolder}/proxy",
- "program": "${workspaceFolder}/proxy"
- }
+ "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 87e2ec0f1c..93904fe926 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 22eb392529..648575b116 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 67a2563002..5abca4f165 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 61242f76df..5e70b95f8b 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 e97d0f3ec2..a5b26e9531 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 12b6db5436..6f03e26143 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 a8f339d9bd..330c645168 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 d6ff4f6ddb..e98e7fd4b9 100644
--- a/trunk/src/utest/srs_utest_manual_mock.hpp
+++ b/trunk/src/utest/srs_utest_manual_mock.hpp
@@ -87,6 +87,9 @@ class MockSdpFactory
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 414a7f9e2d..e419555119 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