mediasoup-client
Version:
mediasoup client side TypeScript library
460 lines (459 loc) • 20.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.OfferMediaSection = exports.AnswerMediaSection = exports.MediaSection = void 0;
const sdpTransform = require("sdp-transform");
const utils = require("../../utils");
class MediaSection {
// SDP media object.
_mediaObject;
constructor({ iceParameters, iceCandidates, dtlsParameters, }) {
this._mediaObject = {
type: '',
port: 0,
protocol: '',
payloads: '',
rtp: [],
fmtp: [],
};
if (iceParameters) {
this.setIceParameters(iceParameters);
}
if (iceCandidates) {
this._mediaObject.candidates = [];
for (const candidate of iceCandidates) {
const candidateObject = {
foundation: candidate.foundation,
// mediasoup does mandates rtcp-mux so candidates component is always
// RTP (1).
component: 1,
// Be ready for new candidate.address field in mediasoup server side
// field and keep backward compatibility with deprecated candidate.ip.
ip: candidate.address ?? candidate.ip,
port: candidate.port,
priority: candidate.priority,
transport: candidate.protocol,
type: candidate.type,
};
if (candidate.tcpType) {
candidateObject.tcptype = candidate.tcpType;
}
this._mediaObject.candidates.push(candidateObject);
}
this._mediaObject.endOfCandidates = 'end-of-candidates';
this._mediaObject.iceOptions = 'renomination';
}
if (dtlsParameters) {
this.setDtlsRole(dtlsParameters.role);
}
}
get mid() {
return String(this._mediaObject.mid);
}
get closed() {
return this._mediaObject.port === 0;
}
getObject() {
return this._mediaObject;
}
setIceParameters(iceParameters) {
this._mediaObject.iceUfrag = iceParameters.usernameFragment;
this._mediaObject.icePwd = iceParameters.password;
}
pause() {
this._mediaObject.direction = 'inactive';
}
disable() {
this.pause();
}
close() {
this.disable();
// Set port in m= line to 0, which means that the media sction is closed.
this._mediaObject.port = 0;
// NOTE: Do not remove header extensions since it's controversial in the spec.
delete this._mediaObject.candidates;
delete this._mediaObject.endOfCandidates;
delete this._mediaObject.iceUfrag;
delete this._mediaObject.icePwd;
delete this._mediaObject.iceOptions;
this._mediaObject.rtp = [];
this._mediaObject.fmtp = [];
delete this._mediaObject.rtcp;
delete this._mediaObject.rtcpFb;
delete this._mediaObject.ssrcs;
delete this._mediaObject.ssrcGroups;
delete this._mediaObject.simulcast;
delete this._mediaObject.simulcast_03;
delete this._mediaObject.rids;
delete this._mediaObject.extmapAllowMixed;
}
}
exports.MediaSection = MediaSection;
class AnswerMediaSection extends MediaSection {
constructor({ iceParameters, iceCandidates, dtlsParameters, sctpParameters, plainRtpParameters, offerMediaObject, offerRtpParameters, answerRtpParameters, codecOptions, }) {
super({ iceParameters, iceCandidates, dtlsParameters });
this._mediaObject.mid = String(offerMediaObject.mid);
this._mediaObject.type = offerMediaObject.type;
this._mediaObject.protocol = offerMediaObject.protocol;
if (!plainRtpParameters) {
this._mediaObject.connection = { ip: '127.0.0.1', version: 4 };
this._mediaObject.port = 7;
}
else {
this._mediaObject.connection = {
ip: plainRtpParameters.ip,
version: plainRtpParameters.ipVersion,
};
this._mediaObject.port = plainRtpParameters.port;
}
switch (offerMediaObject.type) {
case 'audio':
case 'video': {
this._mediaObject.direction = 'recvonly';
this._mediaObject.rtp = [];
this._mediaObject.rtcpFb = [];
this._mediaObject.fmtp = [];
for (const codec of answerRtpParameters.codecs) {
const rtp = {
payload: codec.payloadType,
codec: getCodecName(codec),
rate: codec.clockRate,
};
if (codec.channels > 1) {
rtp.encoding = codec.channels;
}
this._mediaObject.rtp.push(rtp);
const codecParameters = utils.clone(codec.parameters) ?? {};
let codecRtcpFeedback = utils.clone(codec.rtcpFeedback) ?? [];
if (codecOptions) {
const { opusStereo, opusFec, opusDtx, opusMaxPlaybackRate, opusMaxAverageBitrate, opusPtime, opusNack, videoGoogleStartBitrate, videoGoogleMaxBitrate, videoGoogleMinBitrate, } = codecOptions;
const offerCodec = offerRtpParameters.codecs.find((c) => c.payloadType === codec.payloadType);
switch (codec.mimeType.toLowerCase()) {
case 'audio/opus':
case 'audio/multiopus': {
if (opusStereo !== undefined) {
offerCodec.parameters['sprop-stereo'] = opusStereo ? 1 : 0;
codecParameters['stereo'] = opusStereo ? 1 : 0;
}
if (opusFec !== undefined) {
offerCodec.parameters['useinbandfec'] = opusFec ? 1 : 0;
codecParameters['useinbandfec'] = opusFec ? 1 : 0;
}
if (opusDtx !== undefined) {
offerCodec.parameters['usedtx'] = opusDtx ? 1 : 0;
codecParameters['usedtx'] = opusDtx ? 1 : 0;
}
if (opusMaxPlaybackRate !== undefined) {
codecParameters['maxplaybackrate'] = opusMaxPlaybackRate;
}
if (opusMaxAverageBitrate !== undefined) {
codecParameters['maxaveragebitrate'] = opusMaxAverageBitrate;
}
if (opusPtime !== undefined) {
offerCodec.parameters['ptime'] = opusPtime;
codecParameters['ptime'] = opusPtime;
}
// If opusNack is not set, we must remove NACK support for OPUS.
// Otherwise it would be enabled for those handlers that artificially
// announce it in their RTP capabilities.
if (!opusNack) {
offerCodec.rtcpFeedback = offerCodec.rtcpFeedback.filter(fb => fb.type !== 'nack' || fb.parameter);
codecRtcpFeedback = codecRtcpFeedback.filter(fb => fb.type !== 'nack' || fb.parameter);
}
break;
}
case 'video/vp8':
case 'video/vp9':
case 'video/h264':
case 'video/h265':
case 'video/av1': {
if (videoGoogleStartBitrate !== undefined) {
codecParameters['x-google-start-bitrate'] =
videoGoogleStartBitrate;
}
if (videoGoogleMaxBitrate !== undefined) {
codecParameters['x-google-max-bitrate'] =
videoGoogleMaxBitrate;
}
if (videoGoogleMinBitrate !== undefined) {
codecParameters['x-google-min-bitrate'] =
videoGoogleMinBitrate;
}
break;
}
}
}
const fmtp = {
payload: codec.payloadType,
config: '',
};
for (const key of Object.keys(codecParameters)) {
if (fmtp.config) {
fmtp.config += ';';
}
fmtp.config += `${key}=${codecParameters[key]}`;
}
if (fmtp.config) {
this._mediaObject.fmtp.push(fmtp);
}
for (const fb of codecRtcpFeedback) {
this._mediaObject.rtcpFb.push({
payload: codec.payloadType,
type: fb.type,
subtype: fb.parameter,
});
}
}
this._mediaObject.payloads = answerRtpParameters.codecs
.map((codec) => codec.payloadType)
.join(' ');
this._mediaObject.ext = [];
for (const ext of answerRtpParameters.headerExtensions) {
// Don't add a header extension if not present in the offer.
const found = (offerMediaObject.ext ?? []).some((localExt) => localExt.uri === ext.uri);
if (!found) {
continue;
}
this._mediaObject.ext.push({
uri: ext.uri,
value: ext.id,
});
}
// Allow both 1 byte and 2 bytes length header extensions since
// mediasoup can receive both at any time.
if (offerMediaObject.extmapAllowMixed === 'extmap-allow-mixed') {
this._mediaObject.extmapAllowMixed = 'extmap-allow-mixed';
}
// Simulcast.
if (offerMediaObject.simulcast) {
this._mediaObject.simulcast = {
dir1: 'recv',
list1: offerMediaObject.simulcast.list1,
};
this._mediaObject.rids = [];
for (const rid of offerMediaObject.rids ?? []) {
if (rid.direction !== 'send') {
continue;
}
this._mediaObject.rids.push({
id: rid.id,
direction: 'recv',
});
}
}
// Simulcast (draft version 03).
else if (offerMediaObject.simulcast_03) {
this._mediaObject.simulcast_03 = {
value: offerMediaObject.simulcast_03.value.replace(/send/g, 'recv'),
};
this._mediaObject.rids = [];
for (const rid of offerMediaObject.rids ?? []) {
if (rid.direction !== 'send') {
continue;
}
this._mediaObject.rids.push({
id: rid.id,
direction: 'recv',
});
}
}
this._mediaObject.rtcpMux = 'rtcp-mux';
this._mediaObject.rtcpRsize = 'rtcp-rsize';
break;
}
case 'application': {
// New spec.
if (typeof offerMediaObject.sctpPort === 'number') {
this._mediaObject.payloads = 'webrtc-datachannel';
this._mediaObject.sctpPort = sctpParameters.port;
this._mediaObject.maxMessageSize = sctpParameters.maxMessageSize;
}
// Old spec.
else if (offerMediaObject.sctpmap) {
this._mediaObject.payloads = String(sctpParameters.port);
this._mediaObject.sctpmap = {
app: 'webrtc-datachannel',
sctpmapNumber: sctpParameters.port,
maxMessageSize: sctpParameters.maxMessageSize,
};
}
break;
}
}
}
setDtlsRole(role) {
switch (role) {
case 'client': {
this._mediaObject.setup = 'active';
break;
}
case 'server': {
this._mediaObject.setup = 'passive';
break;
}
case 'auto': {
this._mediaObject.setup = 'actpass';
break;
}
}
}
resume() {
this._mediaObject.direction = 'recvonly';
}
muxSimulcastStreams(encodings) {
if (!this._mediaObject.simulcast?.list1) {
return;
}
const layers = {};
for (const encoding of encodings) {
if (encoding.rid) {
layers[encoding.rid] = encoding;
}
}
const raw = this._mediaObject.simulcast.list1;
const simulcastStreams = sdpTransform.parseSimulcastStreamList(raw);
for (const simulcastStream of simulcastStreams) {
for (const simulcastFormat of simulcastStream) {
simulcastFormat.paused = !layers[simulcastFormat.scid]?.active;
}
}
this._mediaObject.simulcast.list1 = simulcastStreams
.map(simulcastFormats => simulcastFormats.map(f => `${f.paused ? '~' : ''}${f.scid}`).join(','))
.join(';');
}
}
exports.AnswerMediaSection = AnswerMediaSection;
class OfferMediaSection extends MediaSection {
constructor({ iceParameters, iceCandidates, dtlsParameters, sctpParameters, plainRtpParameters, mid, kind, offerRtpParameters, streamId, trackId, }) {
super({ iceParameters, iceCandidates, dtlsParameters });
this._mediaObject.mid = String(mid);
this._mediaObject.type = kind;
if (!plainRtpParameters) {
this._mediaObject.connection = { ip: '127.0.0.1', version: 4 };
if (!sctpParameters) {
this._mediaObject.protocol = 'UDP/TLS/RTP/SAVPF';
}
else {
this._mediaObject.protocol = 'UDP/DTLS/SCTP';
}
this._mediaObject.port = 7;
}
else {
this._mediaObject.connection = {
ip: plainRtpParameters.ip,
version: plainRtpParameters.ipVersion,
};
this._mediaObject.protocol = 'RTP/AVP';
this._mediaObject.port = plainRtpParameters.port;
}
// Allow both 1 byte and 2 bytes length header extensions since
// mediasoup can send both at any time.
this._mediaObject.extmapAllowMixed = 'extmap-allow-mixed';
switch (kind) {
case 'audio':
case 'video': {
this._mediaObject.direction = 'sendonly';
this._mediaObject.rtp = [];
this._mediaObject.rtcpFb = [];
this._mediaObject.fmtp = [];
// @ts-expect-error --- @types/sdp-transform 2.15.0 is not ready for
// sdp-transform 3.0.0.
this._mediaObject.msid = [{ id: streamId, appdata: trackId }];
for (const codec of offerRtpParameters.codecs) {
const rtp = {
payload: codec.payloadType,
codec: getCodecName(codec),
rate: codec.clockRate,
};
if (codec.channels > 1) {
rtp.encoding = codec.channels;
}
this._mediaObject.rtp.push(rtp);
const fmtp = {
payload: codec.payloadType,
config: '',
};
for (const key of Object.keys(codec.parameters ?? {})) {
if (fmtp.config) {
fmtp.config += ';';
}
fmtp.config += `${key}=${codec.parameters[key]}`;
}
if (fmtp.config) {
this._mediaObject.fmtp.push(fmtp);
}
for (const fb of codec.rtcpFeedback) {
this._mediaObject.rtcpFb.push({
payload: codec.payloadType,
type: fb.type,
subtype: fb.parameter,
});
}
}
this._mediaObject.payloads = offerRtpParameters.codecs
.map((codec) => codec.payloadType)
.join(' ');
this._mediaObject.ext = [];
for (const ext of offerRtpParameters.headerExtensions) {
this._mediaObject.ext.push({
uri: ext.uri,
value: ext.id,
});
}
this._mediaObject.rtcpMux = 'rtcp-mux';
this._mediaObject.rtcpRsize = 'rtcp-rsize';
const encoding = offerRtpParameters.encodings[0];
const ssrc = encoding.ssrc;
const rtxSsrc = encoding.rtx?.ssrc;
this._mediaObject.ssrcs = [];
this._mediaObject.ssrcGroups = [];
if (ssrc && offerRtpParameters.rtcp.cname) {
this._mediaObject.ssrcs.push({
id: ssrc,
attribute: 'cname',
value: offerRtpParameters.rtcp.cname,
});
}
if (rtxSsrc) {
if (offerRtpParameters.rtcp.cname) {
this._mediaObject.ssrcs.push({
id: rtxSsrc,
attribute: 'cname',
value: offerRtpParameters.rtcp.cname,
});
}
// Associate original and retransmission SSRCs.
if (ssrc) {
this._mediaObject.ssrcGroups.push({
semantics: 'FID',
ssrcs: `${ssrc} ${rtxSsrc}`,
});
}
}
break;
}
case 'application': {
this._mediaObject.payloads = 'webrtc-datachannel';
this._mediaObject.sctpPort = sctpParameters.port;
this._mediaObject.maxMessageSize = sctpParameters.maxMessageSize;
break;
}
}
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
setDtlsRole(role) {
// Always 'actpass'.
this._mediaObject.setup = 'actpass';
}
resume() {
this._mediaObject.direction = 'sendonly';
}
}
exports.OfferMediaSection = OfferMediaSection;
function getCodecName(codec) {
const MimeTypeRegex = new RegExp('^(audio|video)/(.+)', 'i');
const mimeTypeMatch = MimeTypeRegex.exec(codec.mimeType);
if (!mimeTypeMatch) {
throw new TypeError('invalid codec.mimeType');
}
return mimeTypeMatch[2];
}