Skip to content

Commit 9ba6434

Browse files
ossrs-aiwinlinvip
authored andcommitted
AI: WebRTC: Support G.711 (PCMU/PCMA) audio codec for WebRTC. v7.0.124 (ossrs#4075)
1 parent 206a5cb commit 9ba6434

File tree

4 files changed

+272
-0
lines changed

4 files changed

+272
-0
lines changed

trunk/doc/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ The changelog for SRS.
77
<a name="v7-changes"></a>
88

99
## SRS 7.0 Changelog
10+
* v7.0, 2025-11-09, AI: WebRTC: Support G.711 (PCMU/PCMA) audio codec for WebRTC. v7.0.124 (#4075)
1011
* v7.0, 2025-11-08, AI: WebRTC: Support VP9 codec for WebRTC-to-WebRTC streaming. v7.0.123 (#4548)
1112
* v7.0, 2025-11-08, AI: API: Add audio_frames and video_frames to HTTP API. v7.0.122 (#4559)
1213
* v7.0, 2025-11-07, AI: WHIP: Return detailed HTTP error responses with proper status codes. v7.0.121 (#4502)

trunk/src/utest/srs_utest_manual_mock.cpp

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,60 @@ std::string MockSdpFactory::create_chrome_publisher_offer_with_vp9()
262262
return ss.str();
263263
}
264264

265+
std::string MockSdpFactory::create_chrome_publisher_offer_with_g711_pcmu()
266+
{
267+
// Create a real Chrome-like WebRTC SDP offer with H.264 video and G.711 PCMU audio
268+
// Use member variables for SSRC and payload type values
269+
// PCMU payload type is 0 (standard)
270+
uint8_t pcmu_pt = 0;
271+
std::stringstream ss;
272+
ss << "v=0\r\n"
273+
<< "o=- 4611731400430051339 2 IN IP4 127.0.0.1\r\n"
274+
<< "s=-\r\n"
275+
<< "t=0 0\r\n"
276+
<< "a=group:BUNDLE 0 1\r\n"
277+
<< "a=msid-semantic: WMS stream\r\n"
278+
// Audio media description (PCMU)
279+
<< "m=audio 9 UDP/TLS/RTP/SAVPF " << (int)pcmu_pt << "\r\n"
280+
<< "c=IN IP4 0.0.0.0\r\n"
281+
<< "a=rtcp:9 IN IP4 0.0.0.0\r\n"
282+
<< "a=ice-ufrag:test1234\r\n"
283+
<< "a=ice-pwd:testpassword1234567890\r\n"
284+
<< "a=ice-options:trickle\r\n"
285+
<< "a=fingerprint:sha-256 AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99\r\n"
286+
<< "a=setup:actpass\r\n"
287+
<< "a=mid:0\r\n"
288+
<< "a=sendonly\r\n"
289+
<< "a=rtcp-mux\r\n"
290+
<< "a=rtpmap:" << (int)pcmu_pt << " PCMU/8000\r\n"
291+
<< "a=ssrc:" << audio_ssrc_ << " cname:test-audio-cname\r\n"
292+
<< "a=ssrc:" << audio_ssrc_ << " msid:stream audio\r\n"
293+
// Video media description (H.264)
294+
<< "m=video 9 UDP/TLS/RTP/SAVPF " << (int)video_pt_ << "\r\n"
295+
<< "c=IN IP4 0.0.0.0\r\n"
296+
<< "a=rtcp:9 IN IP4 0.0.0.0\r\n"
297+
<< "a=ice-ufrag:test1234\r\n"
298+
<< "a=ice-pwd:testpassword1234567890\r\n"
299+
<< "a=ice-options:trickle\r\n"
300+
<< "a=fingerprint:sha-256 AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99\r\n"
301+
<< "a=setup:actpass\r\n"
302+
<< "a=mid:1\r\n"
303+
<< "a=sendonly\r\n"
304+
<< "a=rtcp-mux\r\n"
305+
<< "a=rtcp-rsize\r\n"
306+
<< "a=rtpmap:" << (int)video_pt_ << " H264/90000\r\n"
307+
<< "a=rtcp-fb:" << (int)video_pt_ << " goog-remb\r\n"
308+
<< "a=rtcp-fb:" << (int)video_pt_ << " transport-cc\r\n"
309+
<< "a=rtcp-fb:" << (int)video_pt_ << " ccm fir\r\n"
310+
<< "a=rtcp-fb:" << (int)video_pt_ << " nack\r\n"
311+
<< "a=rtcp-fb:" << (int)video_pt_ << " nack pli\r\n"
312+
<< "a=fmtp:" << (int)video_pt_ << " level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r\n"
313+
<< "a=ssrc:" << video_ssrc_ << " cname:test-video-cname\r\n"
314+
<< "a=ssrc:" << video_ssrc_ << " msid:stream video\r\n";
315+
316+
return ss.str();
317+
}
318+
265319
MockDtlsCertificate::MockDtlsCertificate()
266320
{
267321
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";

trunk/src/utest/srs_utest_manual_mock.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ class MockSdpFactory
8585
std::string create_chrome_publisher_offer_with_av1();
8686
// Create a Chrome-like WebRTC publisher offer SDP with VP9
8787
std::string create_chrome_publisher_offer_with_vp9();
88+
// Create a Chrome-like WebRTC publisher offer SDP with G.711 PCMU audio
89+
std::string create_chrome_publisher_offer_with_g711_pcmu();
8890
};
8991

9092
// Mock DTLS certificate for testing

trunk/src/utest/srs_utest_workflow_rtc_conn.cpp

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -823,3 +823,218 @@ VOID TEST(BasicWorkflowRtcConnTest, ManuallyVerifyForPublisherWithVP9)
823823
// Stop the publisher
824824
publisher->stop();
825825
}
826+
827+
// This test is used to verify the basic workflow of the RTC connection with G.711 PCMU codec.
828+
// It's finished with the help of AI, but each step is manually designed
829+
// and verified. So this is not dominated by AI, but by humanbeing.
830+
VOID TEST(BasicWorkflowRtcConnTest, ManuallyVerifyForPublisherWithG711Pcmu)
831+
{
832+
srs_error_t err;
833+
834+
// Create mock dependencies FIRST (they must outlive the connection)
835+
SrsUniquePtr<MockCircuitBreaker> mock_circuit_breaker(new MockCircuitBreaker());
836+
SrsUniquePtr<MockConnectionManager> mock_conn_manager(new MockConnectionManager());
837+
SrsUniquePtr<MockRtcSourceManager> mock_rtc_sources(new MockRtcSourceManager());
838+
SrsUniquePtr<MockAppConfig> mock_config(new MockAppConfig());
839+
SrsUniquePtr<MockDtlsCertificate> mock_dtls_certificate(new MockDtlsCertificate());
840+
SrsUniquePtr<MockSdpFactory> mock_sdp_factory(new MockSdpFactory());
841+
SrsUniquePtr<MockAppFactoryForRtcConn> mock_app_factory(new MockAppFactoryForRtcConn());
842+
SrsStreamPublishTokenManager token_manager;
843+
844+
mock_config->rtc_dtls_role_ = "passive";
845+
mock_dtls_certificate->fingerprint_ = "test-fingerprint";
846+
mock_app_factory->rtc_sources_ = mock_rtc_sources.get();
847+
mock_app_factory->mock_protocol_utility_ = new MockProtocolUtility("192.168.1.100");
848+
MockRtcSource *mock_rtc_source = new MockRtcSource();
849+
mock_rtc_sources->mock_source_ = SrsSharedPtr<SrsRtcSource>(mock_rtc_source);
850+
851+
// Create a real ISrsRtcConnection using _srs_app_factory_
852+
MockRtcAsyncTaskExecutor mock_exec;
853+
SrsContextId cid;
854+
cid.set_value("test-rtc-conn-publisher-g711-pcmu-workflow");
855+
856+
SrsUniquePtr<ISrsRtcConnection> conn_ptr(_srs_app_factory->create_rtc_connection(&mock_exec, cid));
857+
SrsRtcConnection *conn = dynamic_cast<SrsRtcConnection *>(conn_ptr.get());
858+
EXPECT_TRUE(conn != NULL);
859+
860+
// Mock the RTC conn, also mock the config in publisher_negotiator_ and player_negotiator_
861+
conn->circuit_breaker_ = mock_circuit_breaker.get();
862+
conn->conn_manager_ = mock_conn_manager.get();
863+
conn->rtc_sources_ = mock_rtc_sources.get();
864+
conn->config_ = mock_config.get();
865+
conn->dtls_certificate_ = mock_dtls_certificate.get();
866+
conn->app_factory_ = mock_app_factory.get();
867+
868+
SrsRtcPublisherNegotiator *pub_neg = dynamic_cast<SrsRtcPublisherNegotiator *>(conn->publisher_negotiator_);
869+
pub_neg->config_ = mock_config.get();
870+
SrsRtcPlayerNegotiator *play_neg = dynamic_cast<SrsRtcPlayerNegotiator *>(conn->player_negotiator_);
871+
play_neg->config_ = mock_config.get();
872+
play_neg->rtc_sources_ = mock_rtc_sources.get();
873+
874+
// Create RTC user config for add_publisher with G.711 PCMU codec
875+
SrsUniquePtr<SrsRtcUserConfig> ruc(new SrsRtcUserConfig());
876+
if (true) {
877+
srs_freep(ruc->req_);
878+
ruc->req_ = new MockRtcAsyncCallRequest("test.vhost", "live", "stream1");
879+
ruc->publish_ = true;
880+
ruc->dtls_ = true;
881+
ruc->srtp_ = true;
882+
ruc->audio_before_video_ = false;
883+
ruc->acodec_ = "pcmu"; // Specify PCMU codec
884+
885+
ruc->remote_sdp_str_ = mock_sdp_factory->create_chrome_publisher_offer_with_g711_pcmu();
886+
HELPER_EXPECT_SUCCESS(ruc->remote_sdp_.parse(ruc->remote_sdp_str_));
887+
}
888+
889+
// Add publisher, which negotiate the SDP and generate local SDP
890+
SrsSdp local_sdp;
891+
local_sdp.session_config_.dtls_role_ = mock_config->get_rtc_dtls_role(ruc->req_->vhost_);
892+
893+
if (true) {
894+
HELPER_EXPECT_SUCCESS(conn->add_publisher(ruc.get(), local_sdp));
895+
896+
// Verify publishers and SSRC mappings
897+
EXPECT_TRUE(conn->publishers_.size() == 1);
898+
EXPECT_TRUE(conn->publishers_ssrc_map_.size() == 2);
899+
EXPECT_TRUE(conn->publishers_ssrc_map_.find(mock_sdp_factory->audio_ssrc_) != conn->publishers_ssrc_map_.end());
900+
EXPECT_TRUE(conn->publishers_ssrc_map_.find(mock_sdp_factory->video_ssrc_) != conn->publishers_ssrc_map_.end());
901+
902+
// Verify the source stream desription, should have two tracks.
903+
SrsRtcSourceDescription *stream_desc = mock_rtc_sources->mock_source_->stream_desc_;
904+
EXPECT_TRUE(stream_desc->audio_track_desc_ != NULL);
905+
EXPECT_TRUE(stream_desc->video_track_descs_.size() == 1);
906+
907+
// Verify the audio track ssrc and payload type.
908+
EXPECT_TRUE(stream_desc->audio_track_desc_->ssrc_ == mock_sdp_factory->audio_ssrc_);
909+
// PCMU uses payload type 0
910+
EXPECT_TRUE(stream_desc->audio_track_desc_->media_->pt_ == 0);
911+
912+
// Verify the codec is PCMU
913+
EXPECT_TRUE(stream_desc->audio_track_desc_->media_->name_ == "PCMU");
914+
915+
// Verify the video track ssrc and payload type.
916+
EXPECT_TRUE(stream_desc->video_track_descs_[0]->ssrc_ == mock_sdp_factory->video_ssrc_);
917+
EXPECT_TRUE(stream_desc->video_track_descs_[0]->media_->pt_ == mock_sdp_factory->video_pt_);
918+
919+
// Verify the local SDP was generated with media information
920+
EXPECT_TRUE(local_sdp.version_ == "0");
921+
EXPECT_TRUE(local_sdp.group_policy_ == "BUNDLE");
922+
EXPECT_TRUE(local_sdp.msids_.size() == 1);
923+
EXPECT_TRUE(local_sdp.msids_[0] == "live/stream1");
924+
EXPECT_TRUE(local_sdp.media_descs_.size() == 2);
925+
926+
// First should be audio media desc with PCMU
927+
SrsMediaDesc *audio_desc = &local_sdp.media_descs_[0];
928+
EXPECT_TRUE(audio_desc->type_ == "audio");
929+
EXPECT_TRUE(audio_desc->recvonly_);
930+
EXPECT_TRUE(audio_desc->payload_types_.size() == 1);
931+
EXPECT_TRUE(audio_desc->payload_types_[0].payload_type_ == 0);
932+
EXPECT_TRUE(audio_desc->payload_types_[0].encoding_name_ == "PCMU");
933+
EXPECT_TRUE(audio_desc->payload_types_[0].clock_rate_ == 8000);
934+
935+
// Second should be video media desc
936+
SrsMediaDesc *video_desc = &local_sdp.media_descs_[1];
937+
EXPECT_TRUE(video_desc->type_ == "video");
938+
EXPECT_TRUE(video_desc->recvonly_);
939+
EXPECT_TRUE(video_desc->payload_types_.size() == 1);
940+
EXPECT_TRUE(video_desc->payload_types_[0].payload_type_ == mock_sdp_factory->video_pt_);
941+
EXPECT_TRUE(video_desc->payload_types_[0].encoding_name_ == "H264");
942+
EXPECT_TRUE(video_desc->payload_types_[0].clock_rate_ == 90000);
943+
}
944+
945+
// Generate local SDP and setup SDP.
946+
std::string username;
947+
if (true) {
948+
bool status = true;
949+
conn->set_all_tracks_status(ruc->req_->get_stream_url(), ruc->publish_, status);
950+
951+
HELPER_EXPECT_SUCCESS(conn->generate_local_sdp(ruc.get(), local_sdp, username));
952+
conn->set_remote_sdp(ruc->remote_sdp_);
953+
conn->set_local_sdp(local_sdp);
954+
conn->set_state_as_waiting_stun();
955+
956+
// Verify the local SDP was generated ice pwd
957+
SrsMediaDesc *audio_desc = &local_sdp.media_descs_[0];
958+
EXPECT_TRUE(!audio_desc->session_info_.ice_pwd_.empty());
959+
EXPECT_TRUE(!audio_desc->session_info_.fingerprint_.empty());
960+
EXPECT_TRUE(audio_desc->candidates_.size() == 1);
961+
EXPECT_TRUE(audio_desc->candidates_[0].ip_ == "192.168.1.100");
962+
EXPECT_TRUE(audio_desc->session_info_.setup_ == "passive");
963+
964+
SrsMediaDesc *video_desc = &local_sdp.media_descs_[1];
965+
EXPECT_TRUE(!video_desc->session_info_.ice_pwd_.empty());
966+
EXPECT_TRUE(!video_desc->session_info_.fingerprint_.empty());
967+
EXPECT_TRUE(video_desc->candidates_.size() == 1);
968+
EXPECT_TRUE(video_desc->candidates_[0].ip_ == "192.168.1.100");
969+
EXPECT_TRUE(video_desc->session_info_.setup_ == "passive");
970+
971+
EXPECT_TRUE(local_sdp.session_negotiate_.dtls_role_ == "passive");
972+
}
973+
974+
// Initialize the connection
975+
if (true) {
976+
HELPER_EXPECT_SUCCESS(conn->initialize(ruc->req_, ruc->dtls_, ruc->srtp_, username));
977+
EXPECT_TRUE(conn->nack_enabled_);
978+
979+
// Create and set publish token
980+
SrsStreamPublishToken *publish_token_raw = NULL;
981+
HELPER_EXPECT_SUCCESS(token_manager.acquire_token(ruc->req_, publish_token_raw));
982+
SrsSharedPtr<ISrsStreamPublishToken> publish_token(publish_token_raw);
983+
984+
conn->set_publish_token(publish_token);
985+
EXPECT_TRUE(conn->publish_token_->is_acquired());
986+
}
987+
988+
// DTLS done, start publisher
989+
SrsRtcPublishStream *publisher = NULL;
990+
if (true) {
991+
HELPER_EXPECT_SUCCESS(conn->on_dtls_handshake_done());
992+
993+
// Wait for coroutine to start. Normally it should be ready wait for PLI requests.
994+
srs_usleep(1 * SRS_UTIME_MILLISECONDS);
995+
996+
// Verify the publisher is created and started
997+
EXPECT_TRUE(conn->publishers_.size() == 1);
998+
publisher = dynamic_cast<SrsRtcPublishStream *>(conn->publishers_.begin()->second);
999+
EXPECT_TRUE(publisher->is_sender_started_);
1000+
}
1001+
1002+
// Got a RTP audio packet with PCMU payload type.
1003+
for (int i = 0; i < 3; i++) {
1004+
SrsRtpPacket pkt;
1005+
pkt.header_.set_ssrc(mock_sdp_factory->audio_ssrc_);
1006+
pkt.header_.set_sequence(100);
1007+
pkt.header_.set_timestamp(1000);
1008+
pkt.header_.set_payload_type(0); // PCMU payload type
1009+
1010+
SrsUniquePtr<char[]> data(new char[1500]);
1011+
SrsBuffer buf(data.get(), 1500);
1012+
HELPER_EXPECT_SUCCESS(pkt.encode(&buf));
1013+
1014+
HELPER_EXPECT_SUCCESS(conn->on_rtp_cipher(data.get(), buf.pos()));
1015+
HELPER_EXPECT_SUCCESS(conn->on_rtp_plaintext(data.get(), buf.pos()));
1016+
1017+
EXPECT_EQ(mock_rtc_source->rtp_audio_count_, i + 1);
1018+
}
1019+
1020+
// Got a RTP video packet.
1021+
for (int i = 0; i < 3; i++) {
1022+
SrsRtpPacket pkt;
1023+
pkt.header_.set_ssrc(mock_sdp_factory->video_ssrc_);
1024+
pkt.header_.set_sequence(100);
1025+
pkt.header_.set_timestamp(1000);
1026+
pkt.header_.set_payload_type(mock_sdp_factory->video_pt_);
1027+
1028+
SrsUniquePtr<char[]> data(new char[1500]);
1029+
SrsBuffer buf(data.get(), 1500);
1030+
HELPER_EXPECT_SUCCESS(pkt.encode(&buf));
1031+
1032+
HELPER_EXPECT_SUCCESS(conn->on_rtp_cipher(data.get(), buf.pos()));
1033+
HELPER_EXPECT_SUCCESS(conn->on_rtp_plaintext(data.get(), buf.pos()));
1034+
1035+
EXPECT_EQ(mock_rtc_source->rtp_video_count_, i + 1);
1036+
}
1037+
1038+
// Stop the publisher
1039+
publisher->stop();
1040+
}

0 commit comments

Comments
 (0)