UNPKG

mediasoup-client

Version:

mediasoup client side TypeScript library

296 lines (295 loc) 12 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.RemoteSdp = void 0; const sdpTransform = require("sdp-transform"); const Logger_1 = require("../../Logger"); const MediaSection_1 = require("./MediaSection"); const __1 = require("../../"); const DependencyDescriptorCodecs = ['av1', 'h264']; const logger = new Logger_1.Logger('RemoteSdp'); class RemoteSdp { // Remote ICE parameters. _iceParameters; // Remote ICE candidates. _iceCandidates; // Remote DTLS parameters. _dtlsParameters; // Remote SCTP parameters. _sctpParameters; // Parameters for plain RTP (no SRTP nor DTLS no BUNDLE). _plainRtpParameters; // MediaSection instances with same order as in the SDP. _mediaSections = []; // MediaSection indices indexed by MID. _midToIndex = new Map(); // First MID. _firstMid; // SDP object. _sdpObject; constructor({ iceParameters, iceCandidates, dtlsParameters, sctpParameters, plainRtpParameters, }) { this._iceParameters = iceParameters; this._iceCandidates = iceCandidates; this._dtlsParameters = dtlsParameters; this._sctpParameters = sctpParameters; this._plainRtpParameters = plainRtpParameters; this._sdpObject = { version: 0, origin: { address: '0.0.0.0', ipVer: 4, netType: 'IN', sessionId: '10000', sessionVersion: 0, username: `mediasoup-client-v${__1.version}`, }, name: '-', timing: { start: 0, stop: 0 }, media: [], }; // Indicate support of RFC 8445 (ICE bis / ice2). this._sdpObject.iceOptions = 'ice2'; // If ICE parameters are given, add ICE-Lite indicator. if (iceParameters?.iceLite) { this._sdpObject.icelite = 'ice-lite'; } // If DTLS parameters are given, assume WebRTC and BUNDLE. if (dtlsParameters) { // NOTE: This is not standard anymore (it was removed in RFC 8830), // however some WebRTC clients still rely on it. this._sdpObject.msidSemantic = { semantic: 'WMS', token: '*' }; // NOTE: We take the latest fingerprint. const numFingerprints = this._dtlsParameters.fingerprints.length; this._sdpObject.fingerprint = { type: dtlsParameters.fingerprints[numFingerprints - 1].algorithm, hash: dtlsParameters.fingerprints[numFingerprints - 1].value, }; this._sdpObject.groups = [{ type: 'BUNDLE', mids: '' }]; } // If there are plain RPT parameters, override SDP origin. if (plainRtpParameters) { this._sdpObject.origin.address = plainRtpParameters.ip; this._sdpObject.origin.ipVer = plainRtpParameters.ipVersion; } } updateIceParameters(iceParameters) { logger.debug('updateIceParameters() [iceParameters:%o]', iceParameters); this._iceParameters = iceParameters; this._sdpObject.icelite = iceParameters.iceLite ? 'ice-lite' : undefined; for (const mediaSection of this._mediaSections) { mediaSection.setIceParameters(iceParameters); } } updateDtlsRole(role) { logger.debug('updateDtlsRole() [role:%s]', role); this._dtlsParameters.role = role; for (const mediaSection of this._mediaSections) { mediaSection.setDtlsRole(role); } } /** * Set session level a=extmap-allow-mixed attibute. */ setSessionExtmapAllowMixed() { logger.debug('setSessionExtmapAllowMixed()'); this._sdpObject.extmapAllowMixed = 'extmap-allow-mixed'; } getNextMediaSectionIdx() { // If a closed media section is found, return its index. for (let idx = 0; idx < this._mediaSections.length; ++idx) { const mediaSection = this._mediaSections[idx]; if (mediaSection.closed) { return { idx, reuseMid: mediaSection.mid }; } } // If no closed media section is found, return next one. return { idx: this._mediaSections.length }; } send({ offerMediaObject, reuseMid, offerRtpParameters, answerRtpParameters, codecOptions, }) { const mediaSection = new MediaSection_1.AnswerMediaSection({ iceParameters: this._iceParameters, iceCandidates: this._iceCandidates, dtlsParameters: this._dtlsParameters, plainRtpParameters: this._plainRtpParameters, offerMediaObject, offerRtpParameters, answerRtpParameters, codecOptions, }); const mediaObject = mediaSection.getObject(); // Remove Dependency Descriptor extension unless there is support for // the codec in mediasoup. const ddCodec = mediaObject.rtp.find(rtp => DependencyDescriptorCodecs.includes(rtp.codec.toLowerCase())); if (!ddCodec) { mediaObject.ext = mediaObject.ext?.filter(extmap => extmap.uri !== 'https://aomediacodec.github.io/av1-rtp-spec/#dependency-descriptor-rtp-header-extension'); } // Unified-Plan with closed media section replacement. if (reuseMid) { this.replaceMediaSection(mediaSection, reuseMid); } // Unified-Plan or Plan-B with different media kind. else if (!this._midToIndex.has(mediaSection.mid)) { this.addMediaSection(mediaSection); } // Plan-B with same media kind. else { this.replaceMediaSection(mediaSection); } } receive({ mid, kind, offerRtpParameters, streamId, trackId, }) { // Allow both 1 byte and 2 bytes length header extensions since // mediasoup can send both at any time. this.setSessionExtmapAllowMixed(); const mediaSection = new MediaSection_1.OfferMediaSection({ iceParameters: this._iceParameters, iceCandidates: this._iceCandidates, dtlsParameters: this._dtlsParameters, plainRtpParameters: this._plainRtpParameters, mid, kind, offerRtpParameters, streamId, trackId, }); // Let's try to recycle a closed media section (if any). // NOTE: We cannot recycle a closed m=audio section as m=video (or vice // versa). Firefox rejects the SDP when the media type of a recycled m-line // changes. const oldMediaSection = this._mediaSections.find(m => m.closed && m.getObject().type === kind); if (oldMediaSection) { this.replaceMediaSection(mediaSection, oldMediaSection.mid); } else { this.addMediaSection(mediaSection); } } pauseMediaSection(mid) { const mediaSection = this.findMediaSection(mid); mediaSection.pause(); } resumeSendingMediaSection(mid) { const mediaSection = this.findMediaSection(mid); mediaSection.resume(); } resumeReceivingMediaSection(mid) { const mediaSection = this.findMediaSection(mid); mediaSection.resume(); } disableMediaSection(mid) { const mediaSection = this.findMediaSection(mid); mediaSection.disable(); } /** * Closes media section. Returns true if the given MID corresponds to a m * section that has been indeed closed. False otherwise. * * NOTE: Closing the first m section is a pain since it invalidates the bundled * transport, so instead closing it we just disable it. */ closeMediaSection(mid) { const mediaSection = this.findMediaSection(mid); // NOTE: Closing the first m section is a pain since it invalidates the // bundled transport, so let's avoid it. if (mid === this._firstMid) { logger.debug('closeMediaSection() | cannot close first media section, disabling it instead [mid:%s]', mid); this.disableMediaSection(mid); return false; } mediaSection.close(); // Regenerate BUNDLE mids. this.regenerateBundleMids(); return true; } muxMediaSectionSimulcast(mid, encodings) { const mediaSection = this.findMediaSection(mid); mediaSection.muxSimulcastStreams(encodings); this.replaceMediaSection(mediaSection); } sendSctpAssociation({ offerMediaObject, }) { const mediaSection = new MediaSection_1.AnswerMediaSection({ iceParameters: this._iceParameters, iceCandidates: this._iceCandidates, dtlsParameters: this._dtlsParameters, sctpParameters: this._sctpParameters, plainRtpParameters: this._plainRtpParameters, offerMediaObject, }); this.addMediaSection(mediaSection); } receiveSctpAssociation() { const mediaSection = new MediaSection_1.OfferMediaSection({ iceParameters: this._iceParameters, iceCandidates: this._iceCandidates, dtlsParameters: this._dtlsParameters, sctpParameters: this._sctpParameters, plainRtpParameters: this._plainRtpParameters, mid: 'datachannel', kind: 'application', }); this.addMediaSection(mediaSection); } getSdp() { // Increase SDP version. this._sdpObject.origin.sessionVersion++; return sdpTransform.write(this._sdpObject); } addMediaSection(newMediaSection) { if (!this._firstMid) { this._firstMid = newMediaSection.mid; } // Add to the vector. this._mediaSections.push(newMediaSection); // Add to the map. this._midToIndex.set(newMediaSection.mid, this._mediaSections.length - 1); // Add to the SDP object. this._sdpObject.media.push(newMediaSection.getObject()); // Regenerate BUNDLE mids. this.regenerateBundleMids(); } replaceMediaSection(newMediaSection, reuseMid) { // Store it in the map. if (typeof reuseMid === 'string') { const idx = this._midToIndex.get(reuseMid); if (idx === undefined) { throw new Error(`no media section found for reuseMid '${reuseMid}'`); } const oldMediaSection = this._mediaSections[idx]; // Replace the index in the vector with the new media section. this._mediaSections[idx] = newMediaSection; // Update the map. this._midToIndex.delete(oldMediaSection.mid); this._midToIndex.set(newMediaSection.mid, idx); // Update the SDP object. this._sdpObject.media[idx] = newMediaSection.getObject(); // Regenerate BUNDLE mids. this.regenerateBundleMids(); } else { const idx = this._midToIndex.get(newMediaSection.mid); if (idx === undefined) { throw new Error(`no media section found with mid '${newMediaSection.mid}'`); } // Replace the index in the vector with the new media section. this._mediaSections[idx] = newMediaSection; // Update the SDP object. this._sdpObject.media[idx] = newMediaSection.getObject(); } } findMediaSection(mid) { const idx = this._midToIndex.get(mid); if (idx === undefined) { throw new Error(`no media section found with mid '${mid}'`); } return this._mediaSections[idx]; } regenerateBundleMids() { if (!this._dtlsParameters) { return; } this._sdpObject.groups[0].mids = this._mediaSections .filter((mediaSection) => !mediaSection.closed) .map((mediaSection) => mediaSection.mid) .join(' '); } } exports.RemoteSdp = RemoteSdp;