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)
This commit is contained in:
parent
7fcd406a63
commit
bfb91f9b82
2
trunk/3rdparty/srs-docs/doc/webrtc.md
vendored
2
trunk/3rdparty/srs-docs/doc/webrtc.md
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ The changelog for SRS.
|
|||
<a name="v7-changes"></a>
|
||||
|
||||
## 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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
#include <srs_kernel_utility.hpp>
|
||||
|
||||
// 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'
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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<SrsMediaPayloadType> payloads = remote_media_desc.find_media_with_encoding_name("opus");
|
||||
// Try to find audio codec based on user preference or default order
|
||||
std::vector<SrsMediaPayloadType> 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<SrsMediaPayloadType> 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<SrsMediaPayloadType> 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<SrsMediaPayloadType> 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<SrsMediaPayloadType> 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<SrsRtcTrackDescription *> 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<SrsMediaPayloadType> 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<SrsRtcTrackDescription *> source_track_descs = source->get_track_desc("video", "");
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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<SrsAudioPayload *>(media);
|
||||
if (!audio_media) {
|
||||
return err;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,6 @@
|
|||
|
||||
#define VERSION_MAJOR 7
|
||||
#define VERSION_MINOR 0
|
||||
#define VERSION_REVISION 123
|
||||
#define VERSION_REVISION 124
|
||||
|
||||
#endif
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<char[]> 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<char[]> 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<MockCircuitBreaker> mock_circuit_breaker(new MockCircuitBreaker());
|
||||
SrsUniquePtr<MockConnectionManager> mock_conn_manager(new MockConnectionManager());
|
||||
SrsUniquePtr<MockRtcSourceManager> mock_rtc_sources(new MockRtcSourceManager());
|
||||
SrsUniquePtr<MockAppConfig> mock_config(new MockAppConfig());
|
||||
SrsUniquePtr<MockDtlsCertificate> mock_dtls_certificate(new MockDtlsCertificate());
|
||||
SrsUniquePtr<MockSdpFactory> mock_sdp_factory(new MockSdpFactory());
|
||||
SrsUniquePtr<MockAppFactoryForRtcConn> 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<SrsRtcSource>(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<ISrsRtcConnection> conn_ptr(_srs_app_factory->create_rtc_connection(&mock_exec, cid));
|
||||
SrsRtcConnection *conn = dynamic_cast<SrsRtcConnection *>(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<SrsRtcPublisherNegotiator *>(conn->publisher_negotiator_);
|
||||
pub_neg->config_ = mock_config.get();
|
||||
SrsRtcPlayerNegotiator *play_neg = dynamic_cast<SrsRtcPlayerNegotiator *>(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<SrsRtcUserConfig> 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<ISrsStreamPublishToken> 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<SrsRtcPublishStream *>(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<char[]> 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<char[]> data(new char[1500]);
|
||||
SrsBuffer buf(data.get(), 1500);
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user