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