From 00494d5f87fdd9c753b3abfc092a5f67d4ee3032 Mon Sep 17 00:00:00 2001 From: OSSRS-AI Date: Wed, 3 Dec 2025 09:00:16 -0500 Subject: [PATCH] AI: WebRTC: Fix audio-only WHIP publish without SSRC. v7.0.132 (#4570) (#4599) --- trunk/doc/CHANGELOG.md | 1 + trunk/src/app/srs_app_rtc_conn.cpp | 20 +++++++ trunk/src/core/srs_core_version7.hpp | 2 +- trunk/src/utest/srs_utest_ai12.cpp | 85 ++++++++++++++++++++++++++++ 4 files changed, 107 insertions(+), 1 deletion(-) diff --git a/trunk/doc/CHANGELOG.md b/trunk/doc/CHANGELOG.md index 7c0ad9f30..a7bcb2ed1 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-12-03, AI: WebRTC: Fix audio-only WHIP publish without SSRC. v7.0.132 (#4570) * v7.0, 2025-11-30, SRT: Support default_mode config for short streamid format. v7.0.131 * v7.0, 2025-11-28, SRT: Fix player not exiting when publisher disconnects. v7.0.130 (#4591) * v7.0, 2025-11-27, Merge [#4588](https://github.com/ossrs/srs/pull/4588): RTMP: Ignore FMLE start packet after flash publish. v7.0.129 (#4588) diff --git a/trunk/src/app/srs_app_rtc_conn.cpp b/trunk/src/app/srs_app_rtc_conn.cpp index 6e0ff2fb6..659d2de0d 100644 --- a/trunk/src/app/srs_app_rtc_conn.cpp +++ b/trunk/src/app/srs_app_rtc_conn.cpp @@ -3632,6 +3632,26 @@ srs_error_t SrsRtcPublisherNegotiator::negotiate_publish_capability(SrsRtcUserCo track_id = msid_tracker; } + // Handle case where no SSRC info is present in the offer (e.g., libdatachannel audio-only). + // We still need to create track description to generate proper SDP answer. + // See https://github.com/paullouisageneau/libdatachannel which may not include SSRC. + // See https://github.com/ossrs/srs/issues/4570#issuecomment-3604598513 + if (remote_media_desc.ssrc_infos_.empty()) { + SrsRtcTrackDescription *track_desc_copy = track_desc->copy(); + // Generate synthetic values since no SSRC info provided. + track_desc_copy->ssrc_ = 0; + track_desc_copy->id_ = srs_fmt_sprintf("track-%s-%s", track_desc->type_.c_str(), remote_media_desc.mid_.c_str()); + track_desc_copy->msid_ = req->app_ + "/" + req->stream_; + + if (remote_media_desc.is_audio() && !stream_desc->audio_track_desc_) { + stream_desc->audio_track_desc_ = track_desc_copy; + } else if (remote_media_desc.is_video()) { + stream_desc->video_track_descs_.push_back(track_desc_copy); + } else { + srs_freep(track_desc_copy); + } + } + // set track fec_ssrc and rtx_ssrc for (int j = 0; j < (int)remote_media_desc.ssrc_groups_.size(); ++j) { const SrsSSRCGroup &ssrc_group = remote_media_desc.ssrc_groups_.at(j); diff --git a/trunk/src/core/srs_core_version7.hpp b/trunk/src/core/srs_core_version7.hpp index 33b21501d..a47da7a15 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 131 +#define VERSION_REVISION 132 #endif \ No newline at end of file diff --git a/trunk/src/utest/srs_utest_ai12.cpp b/trunk/src/utest/srs_utest_ai12.cpp index a5b26e953..107ae19ab 100644 --- a/trunk/src/utest/srs_utest_ai12.cpp +++ b/trunk/src/utest/srs_utest_ai12.cpp @@ -2149,6 +2149,91 @@ VOID TEST(SrsRtcPublisherNegotiatorTest, LibdatachannelUseScenario) EXPECT_EQ("video", video_sdp.media_descs_[0].type_); } +// Test audio-only libdatachannel scenario WITHOUT SSRC info. +// This test demonstrates the bug where libdatachannel fails with: +// "Remote description has no ICE user fragment" +// Root cause: When the offer SDP has no a=ssrc: line, stream_desc->audio_track_desc_ +// is never set, so generate_publish_local_sdp_for_audio() doesn't add the m=audio +// section to the answer SDP. +VOID TEST(SrsRtcPublisherNegotiatorTest, LibdatachannelAudioOnlyWithoutSsrc) +{ + srs_error_t err; + + // Create SrsRtcPublisherNegotiator + SrsUniquePtr negotiator(new SrsRtcPublisherNegotiator()); + + // Create mock request for initialization + SrsUniquePtr mock_request(new MockRtcConnectionRequest("test.vhost", "live", "voice_stream")); + + // 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; + + // Audio-only SDP from libdatachannel - NO SSRC LINE (this is the key difference!) + // This matches the actual user-reported SDP that causes the bug + ruc->remote_sdp_str_ = + "v=0\r\n" + "o=rtc 4107523824 0 IN IP4 127.0.0.1\r\n" + "s=-\r\n" + "t=0 0\r\n" + "a=group:BUNDLE audio\r\n" + "a=group:LS audio\r\n" + "a=msid-semantic:WMS *\r\n" + "a=ice-options:ice2,trickle\r\n" + "a=fingerprint:sha-256 C3:22:A4:0D:46:6C:8C:3E:3B:05:59:63:C3:8A:43:97:30:4C:3E:5F:01:BA:C9:77:AC:10:89:A7:83:BA:21:08\r\n" + "m=audio 36954 UDP/TLS/RTP/SAVPF 111\r\n" + "c=IN IP4 192.168.1.100\r\n" + "a=mid:audio\r\n" + "a=sendonly\r\n" + "a=rtcp-mux\r\n" + "a=rtpmap:111 opus/48000/2\r\n" + "a=fmtp:111 minptime=10;maxaveragebitrate=96000;stereo=1;sprop-stereo=1;useinbandfec=1\r\n" + "a=setup:actpass\r\n" + "a=ice-ufrag:rUic\r\n" + "a=ice-pwd:76ZWO/4FkRx6r2nMUF8yeH\r\n" + // NOTE: No a=ssrc: line here - this is the bug trigger! + "a=candidate:1 1 UDP 2114977791 192.168.1.100 36954 typ host\r\n" + "a=end-of-candidates\r\n"; + + // Parse the remote SDP + HELPER_EXPECT_SUCCESS(ruc->remote_sdp_.parse(ruc->remote_sdp_str_)); + + // Verify only audio media description is present + EXPECT_EQ(1u, ruc->remote_sdp_.media_descs_.size()); + EXPECT_EQ("audio", ruc->remote_sdp_.media_descs_[0].type_); + + // Verify NO SSRC info in the parsed SDP (this is the bug condition) + EXPECT_TRUE(ruc->remote_sdp_.media_descs_[0].ssrc_infos_.empty()); + + // Create stream description for negotiation output + SrsUniquePtr stream_desc(new SrsRtcSourceDescription()); + + // Test negotiate_publish_capability - this should work but audio_track_desc_ will be NULL + HELPER_EXPECT_SUCCESS(negotiator->negotiate_publish_capability(ruc.get(), stream_desc.get())); + + // BUG: audio_track_desc_ is NULL because there's no SSRC info in the offer + // This causes generate_publish_local_sdp_for_audio() to not add m=audio section + EXPECT_TRUE(stream_desc->audio_track_desc_ != NULL) << "BUG: audio_track_desc_ should not be NULL for audio-only SDP without SSRC"; + EXPECT_TRUE(stream_desc->video_track_descs_.empty()); + + // 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_)); + + // BUG: local_sdp.media_descs_ is empty because audio_track_desc_ was NULL + // This causes the answer SDP to have no m=audio line, which makes libdatachannel fail + EXPECT_EQ(1u, local_sdp.media_descs_.size()) << "BUG: Answer SDP should have m=audio section"; + if (!local_sdp.media_descs_.empty()) { + EXPECT_EQ("audio", local_sdp.media_descs_[0].type_); + } +} + VOID TEST(SrsRtcConnectionTest, InitializeTypicalScenario) { srs_error_t err;