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