multiparty-meeting/app/lib/edge/ortcUtils.js

459 lines
13 KiB
JavaScript

/* global RTCRtpReceiver */
import sdpTransform from 'sdp-transform';
/**
* Extract RTP capabilities from remote description.
* @param {Object} sdpObject - Remote SDP object generated by sdp-transform.
* @return {RTCRtpCapabilities}
*/
export function extractCapabilities(sdpObject) {
// Map of RtpCodecParameters indexed by payload type.
const codecsMap = new Map();
// Array of RtpHeaderExtensions.
const headerExtensions = [];
for (const m of sdpObject.media) {
// Media kind.
const kind = m.type;
if (kind !== 'audio' && kind !== 'video') {
continue; // eslint-disable-line no-continue
}
// Get codecs.
for (const rtp of m.rtp) {
const codec = {
clockRate: rtp.rate,
kind,
mimeType: `${kind}/${rtp.codec}`,
name: rtp.codec,
numChannels: rtp.encoding || 1,
parameters: {},
preferredPayloadType: rtp.payload,
rtcpFeedback: []
};
codecsMap.set(codec.preferredPayloadType, codec);
}
// Get codec parameters.
for (const fmtp of m.fmtp || []) {
const parameters = sdpTransform.parseFmtpConfig(fmtp.config);
const codec = codecsMap.get(fmtp.payload);
if (!codec) {
continue; // eslint-disable-line no-continue
}
codec.parameters = parameters;
}
// Get RTCP feedback for each codec.
for (const fb of m.rtcpFb || []) {
const codec = codecsMap.get(fb.payload);
if (!codec) {
continue; // eslint-disable-line no-continue
}
codec.rtcpFeedback.push({
parameter: fb.subtype || '',
type: fb.type
});
}
// Get RTP header extensions.
for (const ext of m.ext || []) {
const preferredId = ext.value;
const uri = ext.uri;
const headerExtension = {
kind,
uri,
preferredId
};
// Check if already present.
const duplicated = headerExtensions.find(savedHeaderExtension =>
headerExtension.kind === savedHeaderExtension.kind
&& headerExtension.uri === savedHeaderExtension.uri
);
if (!duplicated) {
headerExtensions.push(headerExtension);
}
}
}
return {
codecs: Array.from(codecsMap.values()),
fecMechanisms: [], // TODO
headerExtensions
};
}
/**
* Extract DTLS parameters from remote description.
* @param {Object} sdpObject - Remote SDP object generated by sdp-transform.
* @return {RTCDtlsParameters}
*/
export function extractDtlsParameters(sdpObject) {
const media = getFirstActiveMediaSection(sdpObject);
const fingerprint = media.fingerprint || sdpObject.fingerprint;
let role;
switch (media.setup) {
case 'active':
role = 'client';
break;
case 'passive':
role = 'server';
break;
case 'actpass':
role = 'auto';
break;
}
return {
role,
fingerprints: [
{
algorithm: fingerprint.type,
value: fingerprint.hash
}
]
};
}
/**
* Extract ICE candidates from remote description.
* NOTE: This implementation assumes a single BUNDLEd transport and rtcp-mux.
* @param {Object} sdpObject - Remote SDP object generated by sdp-transform.
* @return {sequence<RTCIceCandidate>}
*/
export function extractIceCandidates(sdpObject) {
const media = getFirstActiveMediaSection(sdpObject);
const candidates = [];
for (const c of media.candidates) {
// Ignore RTCP candidates (we assume rtcp-mux).
if (c.component !== 1) {
continue; // eslint-disable-line no-continue
}
const candidate = {
foundation: c.foundation,
ip: c.ip,
port: c.port,
priority: c.priority,
protocol: c.transport.toLowerCase(),
type: c.type
};
candidates.push(candidate);
}
return candidates;
}
/**
* Extract ICE parameters from remote description.
* NOTE: This implementation assumes a single BUNDLEd transport.
* @param {Object} sdpObject - Remote SDP object generated by sdp-transform.
* @return {RTCIceParameters}
*/
export function extractIceParameters(sdpObject) {
const media = getFirstActiveMediaSection(sdpObject);
const usernameFragment = media.iceUfrag;
const password = media.icePwd;
const icelite = sdpObject.icelite === 'ice-lite';
return {
icelite,
password,
usernameFragment
};
}
/**
* Extract MID values from remote description.
* @param {Object} sdpObject - Remote SDP object generated by sdp-transform.
* @return {map<String, String>} Ordered Map with MID as key and kind as value.
*/
export function extractMids(sdpObject) {
const midToKind = new Map();
// Ignore disabled media sections.
for (const m of sdpObject.media) {
midToKind.set(m.mid, m.type);
}
return midToKind;
}
/**
* Extract tracks information.
* @param {Object} sdpObject - Remote SDP object generated by sdp-transform.
* @return {Map}
*/
export function extractTrackInfos(sdpObject) {
// Map with info about receiving media.
// - index: Media SSRC
// - value: Object
// - kind: 'audio' / 'video'
// - ssrc: Media SSRC
// - rtxSsrc: RTX SSRC (may be unset)
// - streamId: MediaStream.jitsiRemoteId
// - trackId: MediaStreamTrack.jitsiRemoteId
// - cname: CNAME
// @type {map<Number, Object>}
const infos = new Map();
// Map with stream SSRC as index and associated RTX SSRC as value.
// @type {map<Number, Number>}
const rtxMap = new Map();
// Set of RTX SSRC values.
const rtxSet = new Set();
for (const m of sdpObject.media) {
const kind = m.type;
if (kind !== 'audio' && kind !== 'video') {
continue; // eslint-disable-line no-continue
}
// Get RTX information.
for (const ssrcGroup of m.ssrcGroups || []) {
// Just consider FID.
if (ssrcGroup.semantics !== 'FID') {
continue; // eslint-disable-line no-continue
}
const ssrcs
= ssrcGroup.ssrcs.split(' ').map(ssrc => Number(ssrc));
const ssrc = ssrcs[0];
const rtxSsrc = ssrcs[1];
rtxMap.set(ssrc, rtxSsrc);
rtxSet.add(rtxSsrc);
}
for (const ssrcObject of m.ssrcs || []) {
const ssrc = ssrcObject.id;
// Ignore RTX.
if (rtxSet.has(ssrc)) {
continue; // eslint-disable-line no-continue
}
let info = infos.get(ssrc);
if (!info) {
info = {
kind,
rtxSsrc: rtxMap.get(ssrc),
ssrc
};
infos.set(ssrc, info);
}
switch (ssrcObject.attribute) {
case 'cname': {
info.cname = ssrcObject.value;
break;
}
case 'msid': {
const values = ssrcObject.value.split(' ');
const streamId = values[0];
const trackId = values[1];
info.streamId = streamId;
info.trackId = trackId;
break;
}
case 'mslabel': {
const streamId = ssrcObject.value;
info.streamId = streamId;
break;
}
case 'label': {
const trackId = ssrcObject.value;
info.trackId = trackId;
break;
}
}
}
}
return infos;
}
/**
* Get local ORTC RTP capabilities filtered and adapted to the given remote RTP
* capabilities.
* @param {RTCRtpCapabilities} filterWithCapabilities - RTP capabilities to
* filter with.
* @return {RTCRtpCapabilities}
*/
export function getLocalCapabilities(filterWithCapabilities) {
const localFullCapabilities = RTCRtpReceiver.getCapabilities();
const localCapabilities = {
codecs: [],
fecMechanisms: [],
headerExtensions: []
};
// Map of RTX and codec payloads.
// - index: Codec payloadType
// - value: Associated RTX payloadType
// @type {map<Number, Number>}
const remoteRtxMap = new Map();
// Set codecs.
for (const remoteCodec of filterWithCapabilities.codecs) {
const remoteCodecName = remoteCodec.name.toLowerCase();
if (remoteCodecName === 'rtx') {
remoteRtxMap.set(
remoteCodec.parameters.apt, remoteCodec.preferredPayloadType);
continue; // eslint-disable-line no-continue
}
const localCodec = localFullCapabilities.codecs.find(codec =>
codec.name.toLowerCase() === remoteCodecName
&& codec.kind === remoteCodec.kind
&& codec.clockRate === remoteCodec.clockRate
);
if (!localCodec) {
continue; // eslint-disable-line no-continue
}
const codec = {
clockRate: localCodec.clockRate,
kind: localCodec.kind,
mimeType: `${localCodec.kind}/${localCodec.name}`,
name: localCodec.name,
numChannels: localCodec.numChannels || 1,
parameters: {},
preferredPayloadType: remoteCodec.preferredPayloadType,
rtcpFeedback: []
};
for (const remoteParamName of Object.keys(remoteCodec.parameters)) {
const remoteParamValue
= remoteCodec.parameters[remoteParamName];
for (const localParamName of Object.keys(localCodec.parameters)) {
const localParamValue
= localCodec.parameters[localParamName];
if (localParamName !== remoteParamName) {
continue; // eslint-disable-line no-continue
}
// TODO: We should consider much more cases here, but Edge
// does not support many codec parameters.
if (localParamValue === remoteParamValue) {
// Use this RTP parameter.
codec.parameters[localParamName] = localParamValue;
break;
}
}
}
for (const remoteFb of remoteCodec.rtcpFeedback) {
const localFb = localCodec.rtcpFeedback.find(fb =>
fb.type === remoteFb.type
&& fb.parameter === remoteFb.parameter
);
if (localFb) {
// Use this RTCP feedback.
codec.rtcpFeedback.push(localFb);
}
}
// Use this codec.
localCapabilities.codecs.push(codec);
}
// Add RTX for video codecs.
for (const codec of localCapabilities.codecs) {
const payloadType = codec.preferredPayloadType;
if (!remoteRtxMap.has(payloadType)) {
continue; // eslint-disable-line no-continue
}
const rtxCodec = {
clockRate: codec.clockRate,
kind: codec.kind,
mimeType: `${codec.kind}/rtx`,
name: 'rtx',
parameters: {
apt: payloadType
},
preferredPayloadType: remoteRtxMap.get(payloadType),
rtcpFeedback: []
};
// Add RTX codec.
localCapabilities.codecs.push(rtxCodec);
}
// Add RTP header extensions.
for (const remoteExtension of filterWithCapabilities.headerExtensions) {
const localExtension
= localFullCapabilities.headerExtensions.find(extension =>
extension.kind === remoteExtension.kind
&& extension.uri === remoteExtension.uri
);
if (localExtension) {
const extension = {
kind: localExtension.kind,
preferredEncrypt: Boolean(remoteExtension.preferredEncrypt),
preferredId: remoteExtension.preferredId,
uri: localExtension.uri
};
// Use this RTP header extension.
localCapabilities.headerExtensions.push(extension);
}
}
// Add FEC mechanisms.
// NOTE: We don't support FEC yet and, in fact, neither does Edge.
for (const remoteFecMechanism of filterWithCapabilities.fecMechanisms) {
const localFecMechanism
= localFullCapabilities.fecMechanisms.find(fec =>
fec === remoteFecMechanism
);
if (localFecMechanism) {
// Use this FEC mechanism.
localCapabilities.fecMechanisms.push(localFecMechanism);
}
}
return localCapabilities;
}
/**
* Get the first acive media section.
* @param {Object} sdpObject - SDP object generated by sdp-transform.
* @return {Object} SDP media section as parsed by sdp-transform.
*/
function getFirstActiveMediaSection(sdpObject) {
return sdpObject.media.find(m =>
m.iceUfrag && m.port !== 0
);
}