UNPKG

mediasoup-client

Version:

mediasoup client side TypeScript library

460 lines (459 loc) 20.4 kB
"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]; }