From bfb91f9b82bc1d5d328c851d842ece2f4fb41808 Mon Sep 17 00:00:00 2001 From: OSSRS-AI Date: Sun, 9 Nov 2025 12:08:03 -0500 Subject: [PATCH] AI: WebRTC: Support G.711 (PCMU/PCMA) audio codec for WebRTC. v7.0.124 (#4075) (#4568) This PR adds G.711 (PCMU/PCMA) audio codec support for WebRTC in SRS, enabling relay-only streaming of G.711 audio between WebRTC clients via WHIP/WHEP. G.711 is a widely-used, royalty-free audio codec with excellent compatibility across VoIP systems, IP cameras, and legacy telephony equipment. Fixes #4075 Many IP cameras, VoIP systems, and IoT devices use G.711 (PCMU/PCMA) as their default audio codec. Previously, SRS only supported Opus for WebRTC audio, requiring transcoding or rejecting G.711 streams entirely. This PR enables direct relay of G.711 audio streams in WebRTC, similar to how VP9/AV1 video codecs are supported. Enhanced WHIP/WHEP players with URL-based codec selection: ``` # Audio codec only http://localhost:8080/players/whip.html?acodec=pcmu http://localhost:8080/players/whip.html?acodec=pcma # Video + audio codecs http://localhost:8080/players/whip.html?vcodec=vp9&acodec=pcmu http://localhost:8080/players/whep.html?vcodec=h264&acodec=pcma # Backward compatible (codec = vcodec) http://localhost:8080/players/whip.html?codec=vp9 ``` Testing ```bash # Build and run unit tests cd trunk make utest -j && ./objs/srs_utest # Test with WHIP player # 1. Start SRS server ./objs/srs -c conf/rtc.conf # 2. Open WHIP publisher with PCMU audio http://localhost:8080/players/whip.html?acodec=pcmu # 3. Open WHEP player to receive stream http://localhost:8080/players/whep.html ``` ## Related Issues - Fixes #4075 - WebRTC G.711A Audio Codec Support - Related to #4548 - VP9 codec support (similar relay-only pattern) --- trunk/3rdparty/srs-docs/doc/webrtc.md | 2 +- trunk/doc/CHANGELOG.md | 1 + trunk/research/players/js/srs.page.js | 5 +- trunk/research/players/js/srs.sdk.js | 59 +++++ trunk/research/players/whep.html | 8 + trunk/research/players/whip.html | 10 +- trunk/src/app/srs_app_log.cpp | 2 +- trunk/src/app/srs_app_rtc_api.cpp | 42 +++- trunk/src/app/srs_app_rtc_conn.cpp | 61 ++++- trunk/src/app/srs_app_rtc_server.hpp | 3 +- trunk/src/app/srs_app_rtc_source.cpp | 2 +- trunk/src/app/srs_app_statistic.cpp | 8 +- trunk/src/core/srs_core_version7.hpp | 2 +- trunk/src/kernel/srs_kernel_codec.cpp | 10 +- trunk/src/kernel/srs_kernel_codec.hpp | 6 +- trunk/src/kernel/srs_kernel_ts.cpp | 4 +- trunk/src/utest/srs_utest_ai17.cpp | 2 +- trunk/src/utest/srs_utest_manual_mock.cpp | 54 +++++ trunk/src/utest/srs_utest_manual_mock.hpp | 2 + .../src/utest/srs_utest_workflow_rtc_conn.cpp | 223 +++++++++++++++++- 20 files changed, 463 insertions(+), 43 deletions(-) diff --git a/trunk/3rdparty/srs-docs/doc/webrtc.md b/trunk/3rdparty/srs-docs/doc/webrtc.md index 318bb3075..c72f40bd1 100644 --- a/trunk/3rdparty/srs-docs/doc/webrtc.md +++ b/trunk/3rdparty/srs-docs/doc/webrtc.md @@ -456,7 +456,7 @@ DVR recording, or maximum compatibility. ## VP9 Codec Support -SRS supports VP9 codec for WebRTC-to-WebRTC streaming since v7.0.0 ([#4548](https://github.com/ossrs/srs/issues/4548)). +SRS supports VP9 codec for WebRTC-to-WebRTC streaming since v7.0.123 ([#4548](https://github.com/ossrs/srs/issues/4548)). VP9 is a royalty-free codec that saves 20-40% bandwidth compared to H.264. VP9 works better than H.264/H.265 with congestion control in WebRTC, making it ideal for keeping streams live under network fluctuations. SRS implements VP9 as relay-only (SFU mode), accepting VP9 streams via WHIP and forwarding to WHEP players without transcoding. VP9 streams cannot be converted to diff --git a/trunk/doc/CHANGELOG.md b/trunk/doc/CHANGELOG.md index 27375b5d3..e0b11ffab 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-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) * v7.0, 2025-11-08, AI: API: Add audio_frames and video_frames to HTTP API. v7.0.122 (#4559) * v7.0, 2025-11-07, AI: WHIP: Return detailed HTTP error responses with proper status codes. v7.0.121 (#4502) diff --git a/trunk/research/players/js/srs.page.js b/trunk/research/players/js/srs.page.js index 520491e48..1da1b099b 100755 --- a/trunk/research/players/js/srs.page.js +++ b/trunk/research/players/js/srs.page.js @@ -24,7 +24,7 @@ function update_nav() { $("#nav_vlc").attr("href", "vlc.html" + window.location.search); } -// Special extra params, such as auth_key. +// Special extra params, such as auth_key, codec, vcodec, acodec. function user_extra_params(query, params, rtc) { var queries = params || []; @@ -124,6 +124,9 @@ function build_default_whip_whep_url(query, apiPath) { console.log('?api=x to overwrite WebRTC API(1985).'); console.log('?schema=http|https to overwrite WebRTC API protocol.'); console.log(`?path=xxx to overwrite default ${apiPath}`); + console.log('?codec=xxx to specify video codec (alias for vcodec, e.g., h264, vp9, av1)'); + console.log('?vcodec=xxx to specify video codec (e.g., h264, vp9, av1)'); + console.log('?acodec=xxx to specify audio codec (e.g., opus, pcmu, pcma)'); var server = (!query.server)? window.location.hostname:query.server; var vhost = (!query.vhost)? window.location.hostname:query.vhost; diff --git a/trunk/research/players/js/srs.sdk.js b/trunk/research/players/js/srs.sdk.js index 32fc69de9..d007ea297 100644 --- a/trunk/research/players/js/srs.sdk.js +++ b/trunk/research/players/js/srs.sdk.js @@ -41,11 +41,15 @@ function SrsRtcWhipWhepAsync() { // camera: boolean, whether capture video from camera, default to true. // screen: boolean, whether capture video from screen, default to false. // audio: boolean, whether play audio, default to true. + // vcodec: string, video codec to use (e.g., 'h264', 'vp9', 'av1'), default to undefined. + // acodec: string, audio codec to use (e.g., 'opus', 'pcmu', 'pcma'), default to undefined. self.publish = async function (url, options) { if (url.indexOf('/whip/') === -1) throw new Error(`invalid WHIP url ${url}`); const hasAudio = options?.audio ?? true; const useCamera = options?.camera ?? true; const useScreen = options?.screen ?? false; + const vcodec = options?.vcodec; + const acodec = options?.acodec; if (!hasAudio && !useCamera && !useScreen) throw new Error(`The camera, screen and audio can't be false at the same time`); @@ -91,6 +95,13 @@ function SrsRtcWhipWhepAsync() { var offer = await self.pc.createOffer(); await self.pc.setLocalDescription(offer); + + // Filter codecs if specified + if (vcodec || acodec) { + offer.sdp = self.__internal.filterCodec(offer.sdp, vcodec, acodec); + console.log(`Filtered codecs (vcodec=${vcodec}, acodec=${acodec}): ${offer.sdp}`); + } + const answer = await new Promise(function (resolve, reject) { console.log(`Generated offer: ${offer.sdp}`); @@ -119,15 +130,26 @@ function SrsRtcWhipWhepAsync() { // @options The options to control playing, supports: // videoOnly: boolean, whether only play video, default to false. // audioOnly: boolean, whether only play audio, default to false. + // vcodec: string, video codec to use (e.g., 'h264', 'vp9', 'av1'), default to undefined. + // acodec: string, audio codec to use (e.g., 'opus', 'pcmu', 'pcma'), default to undefined. self.play = async function(url, options) { if (url.indexOf('/whip-play/') === -1 && url.indexOf('/whep/') === -1) throw new Error(`invalid WHEP url ${url}`); if (options?.videoOnly && options?.audioOnly) throw new Error(`The videoOnly and audioOnly in options can't be true at the same time`); + const vcodec = options?.vcodec; + const acodec = options?.acodec; if (!options?.videoOnly) self.pc.addTransceiver("audio", {direction: "recvonly"}); if (!options?.audioOnly) self.pc.addTransceiver("video", {direction: "recvonly"}); var offer = await self.pc.createOffer(); await self.pc.setLocalDescription(offer); + + // Filter codecs if specified + if (vcodec || acodec) { + offer.sdp = self.__internal.filterCodec(offer.sdp, vcodec, acodec); + console.log(`Filtered codecs (vcodec=${vcodec}, acodec=${acodec}): ${offer.sdp}`); + } + const answer = await new Promise(function(resolve, reject) { console.log(`Generated offer: ${offer.sdp}`); @@ -199,6 +221,43 @@ function SrsRtcWhipWhepAsync() { simulator: a.protocol + '//' + a.host + '/rtc/v1/nack/', }; }, + filterCodec: (sdp, vcodec, acodec) => { + // Filter video codec if specified + if (vcodec) { + const vcodecUpper = vcodec.toUpperCase(); + sdp = sdp.split('\n').filter(line => { + // Keep all non-video lines + if (!line.startsWith('a=rtpmap:') && !line.startsWith('a=rtcp-fb:') && + !line.startsWith('a=fmtp:')) { + return true; + } + // For video codec lines, only keep the specified codec + if (line.includes('video/')) { + return line.toUpperCase().includes(vcodecUpper); + } + return true; + }).join('\n'); + } + + // Filter audio codec if specified + if (acodec) { + const acodecUpper = acodec.toUpperCase(); + sdp = sdp.split('\n').filter(line => { + // Keep all non-audio lines + if (!line.startsWith('a=rtpmap:') && !line.startsWith('a=rtcp-fb:') && + !line.startsWith('a=fmtp:')) { + return true; + } + // For audio codec lines, only keep the specified codec + if (line.includes('audio/')) { + return line.toUpperCase().includes(acodecUpper); + } + return true; + }).join('\n'); + } + + return sdp; + }, }; // https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/ontrack diff --git a/trunk/research/players/whep.html b/trunk/research/players/whep.html index 103412e59..a02fd698f 100644 --- a/trunk/research/players/whep.html +++ b/trunk/research/players/whep.html @@ -125,9 +125,17 @@ $(function(){ // For example: webrtc://r.ossrs.net/live/livestream var url = $("#txt_url").val(); + var query = parse_query_string(); + + // Support codec parameters: codec (alias for vcodec), vcodec, acodec + var vcodec = query.vcodec || query.codec; + var acodec = query.acodec; + sdk.play(url, { videoOnly: $('#ch_videoonly').prop('checked'), audioOnly: $('#ch_audioonly').prop('checked'), + vcodec: vcodec, + acodec: acodec }).then(function(session){ $('#sessionid').html(session.sessionid); $('#simulator-drop').attr('href', session.simulator + '?drop=1&username=' + session.sessionid); diff --git a/trunk/research/players/whip.html b/trunk/research/players/whip.html index 407d3af84..0cffedf7b 100644 --- a/trunk/research/players/whip.html +++ b/trunk/research/players/whip.html @@ -132,10 +132,18 @@ $(function(){ // For example: webrtc://r.ossrs.net/live/livestream var url = $("#txt_url").val(); + var query = parse_query_string(); + + // Support codec parameters: codec (alias for vcodec), vcodec, acodec + var vcodec = query.vcodec || query.codec; + var acodec = query.acodec; + sdk.publish(url, { camera: $('#ra_camera').prop('checked'), screen: $('#ra_screen').prop('checked'), - audio: $('#ch_audio').prop('checked') + audio: $('#ch_audio').prop('checked'), + vcodec: vcodec, + acodec: acodec }).then(function(session){ $('#sessionid').html(session.sessionid); $('#simulator-drop').attr('href', session.simulator + '?drop=1&username=' + session.sessionid); diff --git a/trunk/src/app/srs_app_log.cpp b/trunk/src/app/srs_app_log.cpp index fda1e7c6d..eee32e15f 100644 --- a/trunk/src/app/srs_app_log.cpp +++ b/trunk/src/app/srs_app_log.cpp @@ -20,7 +20,7 @@ #include // the max size of a line of log. -#define LOG_MAX_SIZE 8192 +#define LOG_MAX_SIZE 65536 // 64 KB // the tail append to each log. #define LOG_TAIL '\n' diff --git a/trunk/src/app/srs_app_rtc_api.cpp b/trunk/src/app/srs_app_rtc_api.cpp index 17d9f00d3..2d6410eb9 100644 --- a/trunk/src/app/srs_app_rtc_api.cpp +++ b/trunk/src/app/srs_app_rtc_api.cpp @@ -158,19 +158,25 @@ srs_error_t SrsGoApiRtcPlay::do_serve_http(ISrsHttpResponseWriter *w, ISrsHttpMe if (eip.empty()) { eip = r->query_get("candidate"); } - string codec = r->query_get("codec"); + // Support vcodec/codec (alias for vcodec) and acodec parameters + string vcodec = r->query_get("vcodec"); + if (vcodec.empty()) { + vcodec = r->query_get("codec"); + } + string acodec = r->query_get("acodec"); // For client to specifies whether encrypt by SRTP. string srtp = r->query_get("encrypt"); string dtls = r->query_get("dtls"); srs_trace( - "RTC play %s, api=%s, tid=%s, clientip=%s, app=%s, stream=%s, offer=%dB, eip=%s, codec=%s, srtp=%s, dtls=%s", + "RTC play %s, api=%s, tid=%s, clientip=%s, app=%s, stream=%s, offer=%dB, eip=%s, vcodec=%s, acodec=%s, srtp=%s, dtls=%s", streamurl.c_str(), api.c_str(), tid.c_str(), clientip.c_str(), ruc.req_->app_.c_str(), ruc.req_->stream_.c_str(), remote_sdp_str.length(), - eip.c_str(), codec.c_str(), srtp.c_str(), dtls.c_str()); + eip.c_str(), vcodec.c_str(), acodec.c_str(), srtp.c_str(), dtls.c_str()); ruc.eip_ = eip; - ruc.codec_ = codec; + ruc.vcodec_ = vcodec; + ruc.acodec_ = acodec; ruc.publish_ = false; ruc.dtls_ = (dtls != "false"); @@ -479,14 +485,20 @@ srs_error_t SrsGoApiRtcPublish::do_serve_http(ISrsHttpResponseWriter *w, ISrsHtt if (eip.empty()) { eip = r->query_get("candidate"); } - string codec = r->query_get("codec"); + // Support vcodec/codec (alias for vcodec) and acodec parameters + string vcodec = r->query_get("vcodec"); + if (vcodec.empty()) { + vcodec = r->query_get("codec"); + } + string acodec = r->query_get("acodec"); - srs_trace("RTC publish %s, api=%s, tid=%s, clientip=%s, app=%s, stream=%s, offer=%dB, eip=%s, codec=%s", + srs_trace("RTC publish %s, api=%s, tid=%s, clientip=%s, app=%s, stream=%s, offer=%dB, eip=%s, vcodec=%s, acodec=%s", streamurl.c_str(), api.c_str(), tid.c_str(), clientip.c_str(), ruc.req_->app_.c_str(), ruc.req_->stream_.c_str(), - remote_sdp_str.length(), eip.c_str(), codec.c_str()); + remote_sdp_str.length(), eip.c_str(), vcodec.c_str(), acodec.c_str()); ruc.eip_ = eip; - ruc.codec_ = codec; + ruc.vcodec_ = vcodec; + ruc.acodec_ = acodec; ruc.publish_ = true; ruc.dtls_ = ruc.srtp_ = true; @@ -776,7 +788,12 @@ srs_error_t SrsGoApiRtcWhip::do_serve_http_with(ISrsHttpResponseWriter *w, ISrsH if (eip.empty()) { eip = r->query_get("candidate"); } - string codec = r->query_get("codec"); + // Support vcodec/codec (alias for vcodec) and acodec parameters + string vcodec = r->query_get("vcodec"); + if (vcodec.empty()) { + vcodec = r->query_get("codec"); + } + string acodec = r->query_get("acodec"); string app = r->query_get("app"); string stream = r->query_get("stream"); string action = r->query_get("action"); @@ -815,13 +832,14 @@ srs_error_t SrsGoApiRtcWhip::do_serve_http_with(ISrsHttpResponseWriter *w, ISrsH string srtp = r->query_get("encrypt"); string dtls = r->query_get("dtls"); - srs_trace("RTC whip %s %s, clientip=%s, app=%s, stream=%s, offer=%dB, eip=%s, codec=%s, srtp=%s, dtls=%s, ufrag=%s, pwd=%s, param=%s", + srs_trace("RTC whip %s %s, clientip=%s, app=%s, stream=%s, offer=%dB, eip=%s, vcodec=%s, acodec=%s, srtp=%s, dtls=%s, ufrag=%s, pwd=%s, param=%s", action.c_str(), ruc->req_->get_stream_url().c_str(), clientip.c_str(), ruc->req_->app_.c_str(), ruc->req_->stream_.c_str(), - remote_sdp_str.length(), eip.c_str(), codec.c_str(), srtp.c_str(), dtls.c_str(), ruc->req_->ice_ufrag_.c_str(), + remote_sdp_str.length(), eip.c_str(), vcodec.c_str(), acodec.c_str(), srtp.c_str(), dtls.c_str(), ruc->req_->ice_ufrag_.c_str(), ruc->req_->ice_pwd_.c_str(), ruc->req_->param_.c_str()); ruc->eip_ = eip; - ruc->codec_ = codec; + ruc->vcodec_ = vcodec; + ruc->acodec_ = acodec; ruc->publish_ = (action == "publish"); // For client to specifies whether encrypt by SRTP. diff --git a/trunk/src/app/srs_app_rtc_conn.cpp b/trunk/src/app/srs_app_rtc_conn.cpp index 74d7b6f31..22eb39252 100644 --- a/trunk/src/app/srs_app_rtc_conn.cpp +++ b/trunk/src/app/srs_app_rtc_conn.cpp @@ -3335,10 +3335,30 @@ srs_error_t SrsRtcPublisherNegotiator::negotiate_publish_capability(SrsRtcUserCo // Update the ruc, which is about user specified configuration. ruc->audio_before_video_ = !nn_any_video_parsed; - // TODO: check opus format specific param - std::vector payloads = remote_media_desc.find_media_with_encoding_name("opus"); + // Try to find audio codec based on user preference or default order + std::vector payloads; + + // If user specified audio codec, try that first + if (!ruc->acodec_.empty()) { + payloads = remote_media_desc.find_media_with_encoding_name(ruc->acodec_); + if (payloads.empty()) { + return srs_error_new(ERROR_RTC_SDP_EXCHANGE, "no valid found %s audio payload type", ruc->acodec_.c_str()); + } + } else { + // Default order: Opus, PCMU (G.711 μ-law), PCMA (G.711 A-law) + // Prioritize PCMU over PCMA as per Chrome SDP order + payloads = remote_media_desc.find_media_with_encoding_name("opus"); + if (payloads.empty()) { + // Then try PCMU (G.711 μ-law) + payloads = remote_media_desc.find_media_with_encoding_name("PCMU"); + } + if (payloads.empty()) { + // Finally try PCMA (G.711 A-law) + payloads = remote_media_desc.find_media_with_encoding_name("PCMA"); + } + } if (payloads.empty()) { - return srs_error_new(ERROR_RTC_SDP_EXCHANGE, "no valid found opus payload type"); + return srs_error_new(ERROR_RTC_SDP_EXCHANGE, "no valid found audio payload type (opus/PCMU/PCMA)"); } for (int j = 0; j < (int)payloads.size(); j++) { @@ -3366,10 +3386,10 @@ srs_error_t SrsRtcPublisherNegotiator::negotiate_publish_capability(SrsRtcUserCo track_desc->type_ = "audio"; track_desc->set_codec_payload((SrsCodecPayload *)audio_payload); - // Only choose one match opus codec. + // Only choose one match audio codec. break; } - } else if (remote_media_desc.is_video() && srs_video_codec_str2id(ruc->codec_) == SrsVideoCodecIdAV1) { + } else if (remote_media_desc.is_video() && srs_video_codec_str2id(ruc->vcodec_) == SrsVideoCodecIdAV1) { std::vector payloads = remote_media_desc.find_media_with_encoding_name("AV1"); if (payloads.empty()) { // Be compatible with the Chrome M96, still check the AV1X encoding name @@ -3406,7 +3426,7 @@ srs_error_t SrsRtcPublisherNegotiator::negotiate_publish_capability(SrsRtcUserCo track_desc->set_codec_payload((SrsCodecPayload *)video_payload); break; } - } else if (remote_media_desc.is_video() && srs_video_codec_str2id(ruc->codec_) == SrsVideoCodecIdVP9) { + } else if (remote_media_desc.is_video() && srs_video_codec_str2id(ruc->vcodec_) == SrsVideoCodecIdVP9) { std::vector payloads = remote_media_desc.find_media_with_encoding_name("VP9"); if (payloads.empty()) { return srs_error_new(ERROR_RTC_SDP_EXCHANGE, "no found valid VP9 payload type"); @@ -3438,7 +3458,7 @@ srs_error_t SrsRtcPublisherNegotiator::negotiate_publish_capability(SrsRtcUserCo track_desc->set_codec_payload((SrsCodecPayload *)video_payload); break; } - } else if (remote_media_desc.is_video() && srs_video_codec_str2id(ruc->codec_) == SrsVideoCodecIdHEVC) { + } else if (remote_media_desc.is_video() && srs_video_codec_str2id(ruc->vcodec_) == SrsVideoCodecIdHEVC) { std::vector payloads = remote_media_desc.find_media_with_encoding_name("H265"); if (payloads.empty()) { return srs_error_new(ERROR_RTC_SDP_EXCHANGE, "no found valid H.265 payload type"); @@ -3797,16 +3817,33 @@ srs_error_t SrsRtcPlayerNegotiator::negotiate_play_capability(SrsRtcUserConfig * // Update the ruc, which is about user specified configuration. ruc->audio_before_video_ = !nn_any_video_parsed; - // TODO: check opus format specific param - vector payloads = remote_media_desc.find_media_with_encoding_name("opus"); + // Try to find audio tracks in source with different codec names + // Try Opus first (most common), then PCMU, then PCMA + std::vector source_audio_tracks = source->get_track_desc("audio", "opus"); + std::string source_audio_codec = "opus"; + + if (source_audio_tracks.empty()) { + source_audio_tracks = source->get_track_desc("audio", "PCMU"); + source_audio_codec = "PCMU"; + } + if (source_audio_tracks.empty()) { + source_audio_tracks = source->get_track_desc("audio", "PCMA"); + source_audio_codec = "PCMA"; + } + if (source_audio_tracks.empty()) { + return srs_error_new(ERROR_RTC_SDP_EXCHANGE, "no audio track in source (tried opus/PCMU/PCMA)"); + } + + // Try to find matching codec in remote SDP + vector payloads = remote_media_desc.find_media_with_encoding_name(source_audio_codec); if (payloads.empty()) { - return srs_error_new(ERROR_RTC_SDP_EXCHANGE, "no valid found opus payload type"); + return srs_error_new(ERROR_RTC_SDP_EXCHANGE, "no valid found %s payload type", source_audio_codec.c_str()); } remote_payload = payloads.at(0); - track_descs = source->get_track_desc("audio", "opus"); + track_descs = source_audio_tracks; } else if (remote_media_desc.is_video()) { - SrsVideoCodecId prefer_codec = srs_video_codec_str2id(ruc->codec_); + SrsVideoCodecId prefer_codec = srs_video_codec_str2id(ruc->vcodec_); if (prefer_codec == SrsVideoCodecIdReserved) { // Get the source codec if not specified. std::vector source_track_descs = source->get_track_desc("video", ""); diff --git a/trunk/src/app/srs_app_rtc_server.hpp b/trunk/src/app/srs_app_rtc_server.hpp index 58f83f4a9..93330271d 100644 --- a/trunk/src/app/srs_app_rtc_server.hpp +++ b/trunk/src/app/srs_app_rtc_server.hpp @@ -70,7 +70,8 @@ public: std::string remote_sdp_str_; SrsSdp remote_sdp_; std::string eip_; - std::string codec_; + std::string vcodec_; // Video codec + std::string acodec_; // Audio codec std::string api_; // Session data. diff --git a/trunk/src/app/srs_app_rtc_source.cpp b/trunk/src/app/srs_app_rtc_source.cpp index 6fc5b1968..1b7d07f06 100644 --- a/trunk/src/app/srs_app_rtc_source.cpp +++ b/trunk/src/app/srs_app_rtc_source.cpp @@ -3838,7 +3838,7 @@ srs_error_t SrsRtcFormat::on_rtp_packet(SrsRtcRecvTrack *track, bool is_audio) SrsAudioCodecId codec_id = (SrsAudioCodecId)media->codec(false); // Parse channels and sample rate from track description - if (codec_id == SrsAudioCodecIdOpus) { + if (codec_id == SrsAudioCodecIdOpus || codec_id == SrsAudioCodecIdPCMA || codec_id == SrsAudioCodecIdPCMU) { SrsAudioPayload *audio_media = dynamic_cast(media); if (!audio_media) { return err; diff --git a/trunk/src/app/srs_app_statistic.cpp b/trunk/src/app/srs_app_statistic.cpp index b23f8430e..b2b39c28a 100644 --- a/trunk/src/app/srs_app_statistic.cpp +++ b/trunk/src/app/srs_app_statistic.cpp @@ -178,7 +178,13 @@ srs_error_t SrsStatisticStream::dumps(SrsJsonObject *obj) audio->set("codec", SrsJsonAny::str(srs_audio_codec_id2str(acodec_).c_str())); audio->set("sample_rate", SrsJsonAny::integer(srs_audio_sample_rate2number(asample_rate_))); audio->set("channel", SrsJsonAny::integer(asound_type_ + 1)); - audio->set("profile", SrsJsonAny::str(srs_aac_object2str(aac_object_).c_str())); + + // G.711 codecs don't have profiles, similar to VP9/AV1 + if (acodec_ == SrsAudioCodecIdPCMA || acodec_ == SrsAudioCodecIdPCMU) { + audio->set("profile", SrsJsonAny::null()); + } else { + audio->set("profile", SrsJsonAny::str(srs_aac_object2str(aac_object_).c_str())); + } } return err; diff --git a/trunk/src/core/srs_core_version7.hpp b/trunk/src/core/srs_core_version7.hpp index f2c2670e1..c7220953f 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 123 +#define VERSION_REVISION 124 #endif \ No newline at end of file diff --git a/trunk/src/kernel/srs_kernel_codec.cpp b/trunk/src/kernel/srs_kernel_codec.cpp index cd84f81a3..2250ed9b8 100644 --- a/trunk/src/kernel/srs_kernel_codec.cpp +++ b/trunk/src/kernel/srs_kernel_codec.cpp @@ -127,6 +127,10 @@ string srs_audio_codec_id2str(SrsAudioCodecId codec) return "MP3"; case SrsAudioCodecIdOpus: return "Opus"; + case SrsAudioCodecIdPCMA: + return "PCMA"; + case SrsAudioCodecIdPCMU: + return "PCMU"; case SrsAudioCodecIdReserved1: case SrsAudioCodecIdLinearPCMPlatformEndian: case SrsAudioCodecIdADPCM: @@ -134,8 +138,6 @@ string srs_audio_codec_id2str(SrsAudioCodecId codec) case SrsAudioCodecIdNellymoser16kHzMono: case SrsAudioCodecIdNellymoser8kHzMono: case SrsAudioCodecIdNellymoser: - case SrsAudioCodecIdReservedG711AlawLogarithmicPCM: - case SrsAudioCodecIdReservedG711MuLawLogarithmicPCM: case SrsAudioCodecIdReserved: case SrsAudioCodecIdSpeex: case SrsAudioCodecIdReservedMP3_8kHz: @@ -157,6 +159,10 @@ SrsAudioCodecId srs_audio_codec_str2id(const std::string &codec) return SrsAudioCodecIdMP3; } else if (upper_codec == "OPUS") { return SrsAudioCodecIdOpus; + } else if (upper_codec == "PCMA") { + return SrsAudioCodecIdPCMA; + } else if (upper_codec == "PCMU") { + return SrsAudioCodecIdPCMU; } else if (upper_codec == "SPEEX") { return SrsAudioCodecIdSpeex; } diff --git a/trunk/src/kernel/srs_kernel_codec.hpp b/trunk/src/kernel/srs_kernel_codec.hpp index ffdb97455..a180df5f0 100644 --- a/trunk/src/kernel/srs_kernel_codec.hpp +++ b/trunk/src/kernel/srs_kernel_codec.hpp @@ -170,8 +170,10 @@ enum SrsAudioCodecId { SrsAudioCodecIdNellymoser16kHzMono = 4, SrsAudioCodecIdNellymoser8kHzMono = 5, SrsAudioCodecIdNellymoser = 6, - SrsAudioCodecIdReservedG711AlawLogarithmicPCM = 7, - SrsAudioCodecIdReservedG711MuLawLogarithmicPCM = 8, + // G.711 A-law codec for WebRTC, https://github.com/ossrs/srs/issues/4075 + SrsAudioCodecIdPCMA = 7, + // G.711 μ-law codec for WebRTC, https://github.com/ossrs/srs/issues/4075 + SrsAudioCodecIdPCMU = 8, SrsAudioCodecIdReserved = 9, SrsAudioCodecIdAAC = 10, SrsAudioCodecIdSpeex = 11, diff --git a/trunk/src/kernel/srs_kernel_ts.cpp b/trunk/src/kernel/srs_kernel_ts.cpp index 77020b4c1..077209dca 100644 --- a/trunk/src/kernel/srs_kernel_ts.cpp +++ b/trunk/src/kernel/srs_kernel_ts.cpp @@ -466,8 +466,8 @@ srs_error_t SrsTsContext::encode(ISrsStreamWriter *writer, SrsTsMessage *msg, Sr case SrsAudioCodecIdNellymoser16kHzMono: case SrsAudioCodecIdNellymoser8kHzMono: case SrsAudioCodecIdNellymoser: - case SrsAudioCodecIdReservedG711AlawLogarithmicPCM: - case SrsAudioCodecIdReservedG711MuLawLogarithmicPCM: + case SrsAudioCodecIdPCMA: + case SrsAudioCodecIdPCMU: case SrsAudioCodecIdReserved: case SrsAudioCodecIdSpeex: case SrsAudioCodecIdReservedMP3_8kHz: diff --git a/trunk/src/utest/srs_utest_ai17.cpp b/trunk/src/utest/srs_utest_ai17.cpp index fd0f96112..7072bfe7e 100644 --- a/trunk/src/utest/srs_utest_ai17.cpp +++ b/trunk/src/utest/srs_utest_ai17.cpp @@ -2453,7 +2453,7 @@ VOID TEST(SrsGoApiRtcWhipTest, DoServeHttpPublishSuccess) // Verify RTC user config fields were set correctly EXPECT_STREQ("203.0.113.10", ruc->eip_.c_str()); - EXPECT_STREQ("h264", ruc->codec_.c_str()); + EXPECT_STREQ("h264", ruc->vcodec_.c_str()); EXPECT_TRUE(ruc->publish_); // action=publish EXPECT_TRUE(ruc->dtls_); // dtls=true EXPECT_TRUE(ruc->srtp_); // encrypt=true diff --git a/trunk/src/utest/srs_utest_manual_mock.cpp b/trunk/src/utest/srs_utest_manual_mock.cpp index 39cfe15f1..a8f339d9b 100644 --- a/trunk/src/utest/srs_utest_manual_mock.cpp +++ b/trunk/src/utest/srs_utest_manual_mock.cpp @@ -262,6 +262,60 @@ std::string MockSdpFactory::create_chrome_publisher_offer_with_vp9() return ss.str(); } +std::string MockSdpFactory::create_chrome_publisher_offer_with_g711_pcmu() +{ + // Create a real Chrome-like WebRTC SDP offer with H.264 video and G.711 PCMU audio + // Use member variables for SSRC and payload type values + // PCMU payload type is 0 (standard) + uint8_t pcmu_pt = 0; + std::stringstream ss; + ss << "v=0\r\n" + << "o=- 4611731400430051339 2 IN IP4 127.0.0.1\r\n" + << "s=-\r\n" + << "t=0 0\r\n" + << "a=group:BUNDLE 0 1\r\n" + << "a=msid-semantic: WMS stream\r\n" + // Audio media description (PCMU) + << "m=audio 9 UDP/TLS/RTP/SAVPF " << (int)pcmu_pt << "\r\n" + << "c=IN IP4 0.0.0.0\r\n" + << "a=rtcp:9 IN IP4 0.0.0.0\r\n" + << "a=ice-ufrag:test1234\r\n" + << "a=ice-pwd:testpassword1234567890\r\n" + << "a=ice-options:trickle\r\n" + << "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" + << "a=setup:actpass\r\n" + << "a=mid:0\r\n" + << "a=sendonly\r\n" + << "a=rtcp-mux\r\n" + << "a=rtpmap:" << (int)pcmu_pt << " PCMU/8000\r\n" + << "a=ssrc:" << audio_ssrc_ << " cname:test-audio-cname\r\n" + << "a=ssrc:" << audio_ssrc_ << " msid:stream audio\r\n" + // Video media description (H.264) + << "m=video 9 UDP/TLS/RTP/SAVPF " << (int)video_pt_ << "\r\n" + << "c=IN IP4 0.0.0.0\r\n" + << "a=rtcp:9 IN IP4 0.0.0.0\r\n" + << "a=ice-ufrag:test1234\r\n" + << "a=ice-pwd:testpassword1234567890\r\n" + << "a=ice-options:trickle\r\n" + << "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" + << "a=setup:actpass\r\n" + << "a=mid:1\r\n" + << "a=sendonly\r\n" + << "a=rtcp-mux\r\n" + << "a=rtcp-rsize\r\n" + << "a=rtpmap:" << (int)video_pt_ << " H264/90000\r\n" + << "a=rtcp-fb:" << (int)video_pt_ << " goog-remb\r\n" + << "a=rtcp-fb:" << (int)video_pt_ << " transport-cc\r\n" + << "a=rtcp-fb:" << (int)video_pt_ << " ccm fir\r\n" + << "a=rtcp-fb:" << (int)video_pt_ << " nack\r\n" + << "a=rtcp-fb:" << (int)video_pt_ << " nack pli\r\n" + << "a=fmtp:" << (int)video_pt_ << " level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r\n" + << "a=ssrc:" << video_ssrc_ << " cname:test-video-cname\r\n" + << "a=ssrc:" << video_ssrc_ << " msid:stream video\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 32a6087f0..d6ff4f6dd 100644 --- a/trunk/src/utest/srs_utest_manual_mock.hpp +++ b/trunk/src/utest/srs_utest_manual_mock.hpp @@ -85,6 +85,8 @@ public: std::string create_chrome_publisher_offer_with_av1(); // Create a Chrome-like WebRTC publisher offer SDP with VP9 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(); }; // 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 f20afba53..414a7f9e2 100644 --- a/trunk/src/utest/srs_utest_workflow_rtc_conn.cpp +++ b/trunk/src/utest/srs_utest_workflow_rtc_conn.cpp @@ -450,7 +450,7 @@ VOID TEST(BasicWorkflowRtcConnTest, ManuallyVerifyForPublisherWithAV1) ruc->dtls_ = true; ruc->srtp_ = true; ruc->audio_before_video_ = false; - ruc->codec_ = "av1"; // Specify AV1 codec + ruc->vcodec_ = "av1"; // Specify AV1 codec ruc->remote_sdp_str_ = mock_sdp_factory->create_chrome_publisher_offer_with_av1(); HELPER_EXPECT_SUCCESS(ruc->remote_sdp_.parse(ruc->remote_sdp_str_)); @@ -593,7 +593,7 @@ VOID TEST(BasicWorkflowRtcConnTest, ManuallyVerifyForPublisherWithAV1) pkt.header_.set_ssrc(mock_sdp_factory->video_ssrc_); pkt.header_.set_sequence(100); pkt.header_.set_timestamp(1000); - pkt.header_.set_payload_type(45); // AV1 payload type + pkt.header_.set_payload_type(45); // AV1 payload type SrsUniquePtr data(new char[1500]); SrsBuffer buf(data.get(), 1500); @@ -665,7 +665,7 @@ VOID TEST(BasicWorkflowRtcConnTest, ManuallyVerifyForPublisherWithVP9) ruc->dtls_ = true; ruc->srtp_ = true; ruc->audio_before_video_ = false; - ruc->codec_ = "vp9"; // Specify VP9 codec + ruc->vcodec_ = "vp9"; // Specify VP9 codec ruc->remote_sdp_str_ = mock_sdp_factory->create_chrome_publisher_offer_with_vp9(); HELPER_EXPECT_SUCCESS(ruc->remote_sdp_.parse(ruc->remote_sdp_str_)); @@ -808,7 +808,222 @@ VOID TEST(BasicWorkflowRtcConnTest, ManuallyVerifyForPublisherWithVP9) pkt.header_.set_ssrc(mock_sdp_factory->video_ssrc_); pkt.header_.set_sequence(100); pkt.header_.set_timestamp(1000); - pkt.header_.set_payload_type(98); // VP9 payload type + pkt.header_.set_payload_type(98); // VP9 payload type + + SrsUniquePtr data(new char[1500]); + SrsBuffer buf(data.get(), 1500); + HELPER_EXPECT_SUCCESS(pkt.encode(&buf)); + + HELPER_EXPECT_SUCCESS(conn->on_rtp_cipher(data.get(), buf.pos())); + HELPER_EXPECT_SUCCESS(conn->on_rtp_plaintext(data.get(), buf.pos())); + + EXPECT_EQ(mock_rtc_source->rtp_video_count_, i + 1); + } + + // Stop the publisher + publisher->stop(); +} + +// 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) +{ + 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-publisher-g711-pcmu-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 G.711 PCMU codec + 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->acodec_ = "pcmu"; // Specify PCMU codec + + 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_)); + } + + // 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->audio_ssrc_) != conn->publishers_ssrc_map_.end()); + EXPECT_TRUE(conn->publishers_ssrc_map_.find(mock_sdp_factory->video_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_); + // PCMU uses payload type 0 + EXPECT_TRUE(stream_desc->audio_track_desc_->media_->pt_ == 0); + + // Verify the codec is PCMU + EXPECT_TRUE(stream_desc->audio_track_desc_->media_->name_ == "PCMU"); + + // 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 audio media desc with PCMU + SrsMediaDesc *audio_desc = &local_sdp.media_descs_[0]; + 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_ == 0); + EXPECT_TRUE(audio_desc->payload_types_[0].encoding_name_ == "PCMU"); + EXPECT_TRUE(audio_desc->payload_types_[0].clock_rate_ == 8000); + + // Second should be video media desc + SrsMediaDesc *video_desc = &local_sdp.media_descs_[1]; + 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); + } + + // 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 *audio_desc = &local_sdp.media_descs_[0]; + 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"); + + SrsMediaDesc *video_desc = &local_sdp.media_descs_[1]; + 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"); + + 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_); + } + + // Got a RTP audio packet with PCMU payload type. + for (int i = 0; i < 3; i++) { + SrsRtpPacket pkt; + pkt.header_.set_ssrc(mock_sdp_factory->audio_ssrc_); + pkt.header_.set_sequence(100); + pkt.header_.set_timestamp(1000); + pkt.header_.set_payload_type(0); // PCMU payload type + + SrsUniquePtr data(new char[1500]); + SrsBuffer buf(data.get(), 1500); + HELPER_EXPECT_SUCCESS(pkt.encode(&buf)); + + HELPER_EXPECT_SUCCESS(conn->on_rtp_cipher(data.get(), buf.pos())); + HELPER_EXPECT_SUCCESS(conn->on_rtp_plaintext(data.get(), buf.pos())); + + EXPECT_EQ(mock_rtc_source->rtp_audio_count_, i + 1); + } + + // Got a RTP video packet. + for (int i = 0; i < 3; i++) { + SrsRtpPacket pkt; + pkt.header_.set_ssrc(mock_sdp_factory->video_ssrc_); + pkt.header_.set_sequence(100); + pkt.header_.set_timestamp(1000); + pkt.header_.set_payload_type(mock_sdp_factory->video_pt_); SrsUniquePtr data(new char[1500]); SrsBuffer buf(data.get(), 1500);