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
This commit is contained in:
OSSRS-AI 2025-11-11 10:22:31 -05:00 committed by GitHub
parent 3f2539d8fb
commit 6e93dd73b5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 547 additions and 45 deletions

5
.vscode/README.md vendored
View File

@ -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

65
.vscode/launch.json vendored
View File

@ -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"]
}
]
}

View File

@ -7,6 +7,7 @@ The changelog for SRS.
<a name="v7-changes"></a>
## 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)

View File

@ -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

View File

@ -9,6 +9,6 @@
#define VERSION_MAJOR 7
#define VERSION_MINOR 0
#define VERSION_REVISION 125
#define VERSION_REVISION 126
#endif

View File

@ -375,9 +375,10 @@ vector<SrsMediaPayloadType> 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);
}
}

View File

@ -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<SrsRtcPublisherNegotiator> negotiator(new SrsRtcPublisherNegotiator());
// Create mock request for initialization
SrsUniquePtr<MockRtcConnectionRequest> mock_request(new MockRtcConnectionRequest("test.vhost", "live", "stream1"));
// Create mock RTC user config with remote SDP
SrsUniquePtr<SrsRtcUserConfig> 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<SrsRtcSourceDescription> 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;

View File

@ -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);
}

View File

@ -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";

View File

@ -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

View File

@ -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<MockCircuitBreaker> mock_circuit_breaker(new MockCircuitBreaker());
SrsUniquePtr<MockConnectionManager> mock_conn_manager(new MockConnectionManager());
SrsUniquePtr<MockRtcSourceManager> mock_rtc_sources(new MockRtcSourceManager());
SrsUniquePtr<MockAppConfig> mock_config(new MockAppConfig());
SrsUniquePtr<MockDtlsCertificate> mock_dtls_certificate(new MockDtlsCertificate());
SrsUniquePtr<MockSdpFactory> mock_sdp_factory(new MockSdpFactory());
SrsUniquePtr<MockAppFactoryForRtcConn> 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<SrsRtcSource>(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<ISrsRtcConnection> conn_ptr(_srs_app_factory->create_rtc_connection(&mock_exec, cid));
SrsRtcConnection *conn = dynamic_cast<SrsRtcConnection *>(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<SrsRtcPublisherNegotiator *>(conn->publisher_negotiator_);
pub_neg->config_ = mock_config.get();
SrsRtcPlayerNegotiator *play_neg = dynamic_cast<SrsRtcPlayerNegotiator *>(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<SrsRtcUserConfig> 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<ISrsStreamPublishToken> 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<SrsRtcPublishStream *>(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