UNPKG

mediasoup-client

Version:

mediasoup client side TypeScript library

808 lines (807 loc) 34.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Chrome74 = void 0; const sdpTransform = require("sdp-transform"); const Logger_1 = require("../Logger"); const enhancedEvents_1 = require("../enhancedEvents"); const ortc = require("../ortc"); const errors_1 = require("../errors"); const scalabilityModes_1 = require("../scalabilityModes"); const RemoteSdp_1 = require("./sdp/RemoteSdp"); const sdpCommonUtils = require("./sdp/commonUtils"); const sdpUnifiedPlanUtils = require("./sdp/unifiedPlanUtils"); const ortcUtils = require("./ortc/utils"); const logger = new Logger_1.Logger('Chrome74'); const NAME = 'Chrome74'; const SCTP_NUM_STREAMS = { OS: 1024, MIS: 1024 }; class Chrome74 extends enhancedEvents_1.EnhancedEventEmitter { // Closed flag. _closed = false; // Handler direction. _direction; // Remote SDP handler. _remoteSdp; // Callback to request sending extended RTP capabilities on demand. _getSendExtendedRtpCapabilities; // Initial server side DTLS role. If not 'auto', it will force the opposite // value in client side. _forcedLocalDtlsRole; // RTCPeerConnection instance. _pc; // Map of RTCTransceivers indexed by MID. _mapMidTransceiver = new Map(); // Default local stream for sending if no `streamId` is given in send(). _sendStream = new MediaStream(); // Whether a DataChannel m=application section has been created. _hasDataChannelMediaSection = false; // Sending DataChannel id value counter. Incremented for each new DataChannel. _nextSendSctpStreamId = 0; // Got transport local and remote parameters. _transportReady = false; /** * Creates a factory function. */ static createFactory() { return { name: NAME, factory: (options) => new Chrome74(options), getNativeRtpCapabilities: async ({ direction, }) => { logger.debug('getNativeRtpCapabilities() [direction:%o]', direction); let pc = new RTCPeerConnection({ iceServers: [], iceTransportPolicy: 'all', bundlePolicy: 'max-bundle', rtcpMuxPolicy: 'require', }); try { pc.addTransceiver('audio', { direction }); pc.addTransceiver('video', { direction }); const offer = await pc.createOffer(); try { pc.close(); } catch (error) { } pc = undefined; const sdpObject = sdpTransform.parse(offer.sdp); const nativeRtpCapabilities = Chrome74.getLocalRtpCapabilities(sdpObject); return nativeRtpCapabilities; } catch (error) { try { pc?.close(); } catch (error2) { } // eslint-disable-next-line no-useless-assignment pc = undefined; throw error; } }, getNativeSctpCapabilities: async () => { logger.debug('getNativeSctpCapabilities()'); return { numStreams: SCTP_NUM_STREAMS, }; }, }; } static getLocalRtpCapabilities(localSdpObject, extraHeaderExtensions = []) { const nativeRtpCapabilities = sdpCommonUtils.extractRtpCapabilities({ sdpObject: localSdpObject, }); // Need to validate and normalize native RTP capabilities. ortc.validateAndNormalizeRtpCapabilities(nativeRtpCapabilities); // libwebrtc supports NACK for OPUS but doesn't announce it. ortcUtils.addNackSupportForOpus(nativeRtpCapabilities); for (const headerExtension of extraHeaderExtensions) { ortcUtils.addHeaderExtensionSupport(nativeRtpCapabilities, headerExtension); } return nativeRtpCapabilities; } constructor({ direction, iceParameters, iceCandidates, dtlsParameters, sctpParameters, iceServers, iceTransportPolicy, additionalSettings, getSendExtendedRtpCapabilities, }) { super(); logger.debug('constructor()'); this._direction = direction; this._remoteSdp = new RemoteSdp_1.RemoteSdp({ iceParameters, iceCandidates, dtlsParameters, sctpParameters, }); this._getSendExtendedRtpCapabilities = getSendExtendedRtpCapabilities; if (dtlsParameters.role && dtlsParameters.role !== 'auto') { this._forcedLocalDtlsRole = dtlsParameters.role === 'server' ? 'client' : 'server'; } this._pc = new RTCPeerConnection({ iceServers: iceServers ?? [], iceTransportPolicy: iceTransportPolicy ?? 'all', bundlePolicy: 'max-bundle', rtcpMuxPolicy: 'require', ...additionalSettings, }); this._pc.addEventListener('icegatheringstatechange', this.onIceGatheringStateChange); this._pc.addEventListener('icecandidateerror', this.onIceCandidateError); if (this._pc.connectionState) { this._pc.addEventListener('connectionstatechange', this.onConnectionStateChange); } else { logger.warn('run() | pc.connectionState not supported, using pc.iceConnectionState'); this._pc.addEventListener('iceconnectionstatechange', this.onIceConnectionStateChange); } } get name() { return NAME; } close() { logger.debug('close()'); if (this._closed) { return; } this._closed = true; // Close RTCPeerConnection. try { this._pc.close(); } catch (error) { } this._pc.removeEventListener('icegatheringstatechange', this.onIceGatheringStateChange); this._pc.removeEventListener('icecandidateerror', this.onIceCandidateError); this._pc.removeEventListener('connectionstatechange', this.onConnectionStateChange); this._pc.removeEventListener('iceconnectionstatechange', this.onIceConnectionStateChange); this.emit('@close'); // Invoke close() in EnhancedEventEmitter classes. super.close(); } async updateIceServers(iceServers) { this.assertNotClosed(); logger.debug('updateIceServers()'); const configuration = this._pc.getConfiguration(); configuration.iceServers = iceServers; this._pc.setConfiguration(configuration); } async restartIce(iceParameters) { this.assertNotClosed(); logger.debug('restartIce()'); // Provide the remote SDP handler with new remote ICE parameters. this._remoteSdp.updateIceParameters(iceParameters); if (!this._transportReady) { return; } if (this._direction === 'send') { const offer = await this._pc.createOffer({ iceRestart: true }); logger.debug('restartIce() | calling pc.setLocalDescription() [offer:%o]', offer); await this._pc.setLocalDescription(offer); const answer = { type: 'answer', sdp: this._remoteSdp.getSdp(), }; logger.debug('restartIce() | calling pc.setRemoteDescription() [answer:%o]', answer); await this._pc.setRemoteDescription(answer); } else { const offer = { type: 'offer', sdp: this._remoteSdp.getSdp(), }; logger.debug('restartIce() | calling pc.setRemoteDescription() [offer:%o]', offer); await this._pc.setRemoteDescription(offer); const answer = await this._pc.createAnswer(); logger.debug('restartIce() | calling pc.setLocalDescription() [answer:%o]', answer); await this._pc.setLocalDescription(answer); } } async getTransportStats() { this.assertNotClosed(); return this._pc.getStats(); } async send({ track, streamId, encodings, codecOptions, headerExtensionOptions, codec, }) { this.assertNotClosed(); this.assertSendDirection(); logger.debug('send() [kind:%s, track.id:%s, streamId:%s]', track.kind, track.id, streamId); if (encodings && encodings.length > 1) { encodings.forEach((encoding, idx) => { encoding.rid = `r${idx}`; }); } const mediaSectionIdx = this._remoteSdp.getNextMediaSectionIdx(); const transceiver = this._pc.addTransceiver(track, { direction: 'sendonly', streams: [this._sendStream], sendEncodings: encodings, }); let offer = await this._pc.createOffer(); let localSdpObject = sdpTransform.parse(offer.sdp); if (localSdpObject.extmapAllowMixed) { this._remoteSdp.setSessionExtmapAllowMixed(); } const extraHeaderExtensions = []; extraHeaderExtensions.push({ uri: 'http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time', kind: track.kind, direction: 'sendonly', }); const nativeRtpCapabilities = Chrome74.getLocalRtpCapabilities(localSdpObject, extraHeaderExtensions); const sendExtendedRtpCapabilities = this._getSendExtendedRtpCapabilities(nativeRtpCapabilities); // Generic sending RTP parameters. const sendingRtpParameters = ortc.getSendingRtpParameters(track.kind, sendExtendedRtpCapabilities); // This may throw. sendingRtpParameters.codecs = ortc.reduceCodecs(sendingRtpParameters.codecs, codec); // Generic sending RTP parameters suitable for the SDP remote answer. const sendingRemoteRtpParameters = ortc.getSendingRemoteRtpParameters(track.kind, sendExtendedRtpCapabilities); // This may throw. sendingRemoteRtpParameters.codecs = ortc.reduceCodecs(sendingRemoteRtpParameters.codecs, codec); if (!this._transportReady) { await this.setupTransport({ localDtlsRole: this._forcedLocalDtlsRole ?? 'client', localSdpObject, }); } // Special case for VP9 with SVC. let hackVp9Svc = false; const layers = (0, scalabilityModes_1.parse)((encodings ?? [{}])[0].scalabilityMode); let offerMediaObject; if (encodings?.length === 1 && layers.spatialLayers > 1 && sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/vp9') { logger.debug('send() | enabling legacy simulcast for VP9 SVC'); hackVp9Svc = true; localSdpObject = sdpTransform.parse(offer.sdp); offerMediaObject = localSdpObject.media[mediaSectionIdx.idx]; sdpUnifiedPlanUtils.addLegacySimulcast({ offerMediaObject, numStreams: layers.spatialLayers, }); offer = { type: 'offer', sdp: sdpTransform.write(localSdpObject), }; } logger.debug('send() | calling pc.setLocalDescription() [offer:%o]', offer); // Optimize. Only generate new offer if needed. if (headerExtensionOptions?.absCaptureTime) { offerMediaObject = localSdpObject.media[mediaSectionIdx.idx]; sdpCommonUtils.addHeaderExtension({ offerMediaObject, headerExtensionUri: 'http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time', headerExtensionId: sendingRemoteRtpParameters.headerExtensions.find(headerExtension => headerExtension.uri === 'http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time').id, }); offer = { type: 'offer', sdp: sdpTransform.write(localSdpObject), }; } await this._pc.setLocalDescription(offer); // We can now get the transceiver.mid. const localId = transceiver.mid; // Set MID. sendingRtpParameters.mid = localId; localSdpObject = sdpTransform.parse(this._pc.localDescription.sdp); offerMediaObject = localSdpObject.media[mediaSectionIdx.idx]; // Set RTCP CNAME. sendingRtpParameters.rtcp.cname = sdpCommonUtils.getCname({ offerMediaObject, }); // Set msid. sendingRtpParameters.msid = `${streamId ?? this._sendStream.id} ${track.id}`; // Set RTP encodings by parsing the SDP offer if no encodings are given. if (!encodings) { sendingRtpParameters.encodings = sdpUnifiedPlanUtils.getRtpEncodings({ offerMediaObject, codecs: sendingRtpParameters.codecs, }); } // Set RTP encodings by parsing the SDP offer and complete them with given // one if just a single encoding has been given. else if (encodings.length === 1) { let newEncodings = sdpUnifiedPlanUtils.getRtpEncodings({ offerMediaObject, codecs: sendingRtpParameters.codecs, }); Object.assign(newEncodings[0], encodings[0]); // Hack for VP9 SVC. if (hackVp9Svc) { newEncodings = [newEncodings[0]]; } sendingRtpParameters.encodings = newEncodings; } // Otherwise if more than 1 encoding are given use them verbatim. else { sendingRtpParameters.encodings = encodings; } // If VP8 or H264 and there is effective simulcast, add scalabilityMode to // each encoding. if (sendingRtpParameters.encodings.length > 1 && (sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/vp8' || sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/h264')) { for (const encoding of sendingRtpParameters.encodings) { if (encoding.scalabilityMode) { encoding.scalabilityMode = `L1T${layers.temporalLayers}`; } else { encoding.scalabilityMode = 'L1T3'; } } } this._remoteSdp.send({ offerMediaObject, reuseMid: mediaSectionIdx.reuseMid, offerRtpParameters: sendingRtpParameters, answerRtpParameters: sendingRemoteRtpParameters, codecOptions, }); const answer = { type: 'answer', sdp: this._remoteSdp.getSdp(), }; logger.debug('send() | calling pc.setRemoteDescription() [answer:%o]', answer); await this._pc.setRemoteDescription(answer); // Store in the map. this._mapMidTransceiver.set(localId, transceiver); return { localId, rtpParameters: sendingRtpParameters, rtpSender: transceiver.sender, }; } async stopSending(localId) { this.assertSendDirection(); logger.debug('stopSending() [localId:%s]', localId); if (this._closed) { return; } const transceiver = this._mapMidTransceiver.get(localId); if (!transceiver) { throw new Error('associated RTCRtpTransceiver not found'); } void transceiver.sender.replaceTrack(null); this._pc.removeTrack(transceiver.sender); const mediaSectionClosed = this._remoteSdp.closeMediaSection(transceiver.mid); if (mediaSectionClosed) { try { transceiver.stop(); } catch (error) { } } const offer = await this._pc.createOffer(); logger.debug('stopSending() | calling pc.setLocalDescription() [offer:%o]', offer); await this._pc.setLocalDescription(offer); const answer = { type: 'answer', sdp: this._remoteSdp.getSdp(), }; logger.debug('stopSending() | calling pc.setRemoteDescription() [answer:%o]', answer); await this._pc.setRemoteDescription(answer); this._mapMidTransceiver.delete(localId); } async pauseSending(localId) { this.assertNotClosed(); this.assertSendDirection(); logger.debug('pauseSending() [localId:%s]', localId); const transceiver = this._mapMidTransceiver.get(localId); if (!transceiver) { throw new Error('associated RTCRtpTransceiver not found'); } transceiver.direction = 'inactive'; this._remoteSdp.pauseMediaSection(localId); const offer = await this._pc.createOffer(); logger.debug('pauseSending() | calling pc.setLocalDescription() [offer:%o]', offer); await this._pc.setLocalDescription(offer); const answer = { type: 'answer', sdp: this._remoteSdp.getSdp(), }; logger.debug('pauseSending() | calling pc.setRemoteDescription() [answer:%o]', answer); await this._pc.setRemoteDescription(answer); } async resumeSending(localId) { this.assertNotClosed(); this.assertSendDirection(); logger.debug('resumeSending() [localId:%s]', localId); const transceiver = this._mapMidTransceiver.get(localId); this._remoteSdp.resumeSendingMediaSection(localId); if (!transceiver) { throw new Error('associated RTCRtpTransceiver not found'); } transceiver.direction = 'sendonly'; const offer = await this._pc.createOffer(); logger.debug('resumeSending() | calling pc.setLocalDescription() [offer:%o]', offer); await this._pc.setLocalDescription(offer); const answer = { type: 'answer', sdp: this._remoteSdp.getSdp(), }; logger.debug('resumeSending() | calling pc.setRemoteDescription() [answer:%o]', answer); await this._pc.setRemoteDescription(answer); } async replaceTrack(localId, track) { this.assertNotClosed(); this.assertSendDirection(); if (track) { logger.debug('replaceTrack() [localId:%s, track.id:%s]', localId, track.id); } else { logger.debug('replaceTrack() [localId:%s, no track]', localId); } const transceiver = this._mapMidTransceiver.get(localId); if (!transceiver) { throw new Error('associated RTCRtpTransceiver not found'); } await transceiver.sender.replaceTrack(track); } async setMaxSpatialLayer(localId, spatialLayer) { this.assertNotClosed(); this.assertSendDirection(); logger.debug('setMaxSpatialLayer() [localId:%s, spatialLayer:%s]', localId, spatialLayer); const transceiver = this._mapMidTransceiver.get(localId); if (!transceiver) { throw new Error('associated RTCRtpTransceiver not found'); } const parameters = transceiver.sender.getParameters(); parameters.encodings.forEach((encoding, idx) => { if (idx <= spatialLayer) { encoding.active = true; } else { encoding.active = false; } }); await transceiver.sender.setParameters(parameters); this._remoteSdp.muxMediaSectionSimulcast(localId, parameters.encodings); const offer = await this._pc.createOffer(); logger.debug('setMaxSpatialLayer() | calling pc.setLocalDescription() [offer:%o]', offer); await this._pc.setLocalDescription(offer); const answer = { type: 'answer', sdp: this._remoteSdp.getSdp(), }; logger.debug('setMaxSpatialLayer() | calling pc.setRemoteDescription() [answer:%o]', answer); await this._pc.setRemoteDescription(answer); } async setRtpEncodingParameters(localId, params) { this.assertNotClosed(); this.assertSendDirection(); logger.debug('setRtpEncodingParameters() [localId:%s, params:%o]', localId, params); const transceiver = this._mapMidTransceiver.get(localId); if (!transceiver) { throw new Error('associated RTCRtpTransceiver not found'); } const parameters = transceiver.sender.getParameters(); parameters.encodings.forEach((encoding, idx) => { parameters.encodings[idx] = { ...encoding, ...params }; }); await transceiver.sender.setParameters(parameters); this._remoteSdp.muxMediaSectionSimulcast(localId, parameters.encodings); const offer = await this._pc.createOffer(); logger.debug('setRtpEncodingParameters() | calling pc.setLocalDescription() [offer:%o]', offer); await this._pc.setLocalDescription(offer); const answer = { type: 'answer', sdp: this._remoteSdp.getSdp(), }; logger.debug('setRtpEncodingParameters() | calling pc.setRemoteDescription() [answer:%o]', answer); await this._pc.setRemoteDescription(answer); } async getSenderStats(localId) { this.assertNotClosed(); this.assertSendDirection(); const transceiver = this._mapMidTransceiver.get(localId); if (!transceiver) { throw new Error('associated RTCRtpTransceiver not found'); } return transceiver.sender.getStats(); } async sendDataChannel({ sctpStreamParameters, }) { this.assertNotClosed(); this.assertSendDirection(); const options = { negotiated: true, id: this._nextSendSctpStreamId, ordered: sctpStreamParameters.ordered, maxPacketLifeTime: sctpStreamParameters.maxPacketLifeTime, maxRetransmits: sctpStreamParameters.maxRetransmits, protocol: sctpStreamParameters.protocol, }; logger.debug('sendDataChannel() [options:%o]', options); const dataChannel = this._pc.createDataChannel(sctpStreamParameters.label, options); // Increase next id. this._nextSendSctpStreamId = ++this._nextSendSctpStreamId % SCTP_NUM_STREAMS.MIS; // If this is the first DataChannel we need to create the SDP answer with // m=application section. if (!this._hasDataChannelMediaSection) { const offer = await this._pc.createOffer(); const localSdpObject = sdpTransform.parse(offer.sdp); const offerMediaObject = localSdpObject.media.find(m => m.type === 'application'); if (!this._transportReady) { await this.setupTransport({ localDtlsRole: this._forcedLocalDtlsRole ?? 'client', localSdpObject, }); } logger.debug('sendDataChannel() | calling pc.setLocalDescription() [offer:%o]', offer); await this._pc.setLocalDescription(offer); this._remoteSdp.sendSctpAssociation({ offerMediaObject }); const answer = { type: 'answer', sdp: this._remoteSdp.getSdp(), }; logger.debug('sendDataChannel() | calling pc.setRemoteDescription() [answer:%o]', answer); await this._pc.setRemoteDescription(answer); this._hasDataChannelMediaSection = true; } const newSctpStreamParameters = { streamId: options.id, ordered: options.ordered, maxPacketLifeTime: options.maxPacketLifeTime, maxRetransmits: options.maxRetransmits, }; return { dataChannel, sctpStreamParameters: newSctpStreamParameters }; } async receive(optionsList) { this.assertNotClosed(); this.assertRecvDirection(); const results = []; const mapLocalId = new Map(); for (const options of optionsList) { const { trackId, kind, rtpParameters, streamId } = options; logger.debug('receive() [trackId:%s, kind:%s]', trackId, kind); const localId = rtpParameters.mid ?? String(this._mapMidTransceiver.size); mapLocalId.set(trackId, localId); // We ignore MSID `trackId` when consuming and always use our computed // `trackId` which matches the `consumer.id`. const { msidStreamId } = ortcUtils.getMsidStreamIdAndTrackId(rtpParameters.msid); this._remoteSdp.receive({ mid: localId, kind, offerRtpParameters: rtpParameters, streamId: streamId ?? msidStreamId ?? rtpParameters.rtcp?.cname ?? '-', trackId, }); } const offer = { type: 'offer', sdp: this._remoteSdp.getSdp(), }; logger.debug('receive() | calling pc.setRemoteDescription() [offer:%o]', offer); await this._pc.setRemoteDescription(offer); let answer = await this._pc.createAnswer(); const localSdpObject = sdpTransform.parse(answer.sdp); for (const options of optionsList) { const { trackId, rtpParameters } = options; const localId = mapLocalId.get(trackId); const answerMediaObject = localSdpObject.media.find(m => String(m.mid) === localId); // May need to modify codec parameters in the answer based on codec // parameters in the offer. sdpCommonUtils.applyCodecParameters({ offerRtpParameters: rtpParameters, answerMediaObject, }); } answer = { type: 'answer', sdp: sdpTransform.write(localSdpObject), }; if (!this._transportReady) { await this.setupTransport({ localDtlsRole: this._forcedLocalDtlsRole ?? 'client', localSdpObject, }); } logger.debug('receive() | calling pc.setLocalDescription() [answer:%o]', answer); await this._pc.setLocalDescription(answer); for (const options of optionsList) { const { trackId } = options; const localId = mapLocalId.get(trackId); const transceiver = this._pc .getTransceivers() .find((t) => t.mid === localId); if (!transceiver) { throw new Error('new RTCRtpTransceiver not found'); } else { // Store in the map. this._mapMidTransceiver.set(localId, transceiver); results.push({ localId, track: transceiver.receiver.track, rtpReceiver: transceiver.receiver, }); } } return results; } async stopReceiving(localIds) { this.assertRecvDirection(); if (this._closed) { return; } for (const localId of localIds) { logger.debug('stopReceiving() [localId:%s]', localId); const transceiver = this._mapMidTransceiver.get(localId); if (!transceiver) { throw new Error('associated RTCRtpTransceiver not found'); } this._remoteSdp.closeMediaSection(transceiver.mid); } const offer = { type: 'offer', sdp: this._remoteSdp.getSdp(), }; logger.debug('stopReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); await this._pc.setRemoteDescription(offer); const answer = await this._pc.createAnswer(); logger.debug('stopReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); await this._pc.setLocalDescription(answer); for (const localId of localIds) { this._mapMidTransceiver.delete(localId); } } async pauseReceiving(localIds) { this.assertNotClosed(); this.assertRecvDirection(); for (const localId of localIds) { logger.debug('pauseReceiving() [localId:%s]', localId); const transceiver = this._mapMidTransceiver.get(localId); if (!transceiver) { throw new Error('associated RTCRtpTransceiver not found'); } transceiver.direction = 'inactive'; this._remoteSdp.pauseMediaSection(localId); } const offer = { type: 'offer', sdp: this._remoteSdp.getSdp(), }; logger.debug('pauseReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); await this._pc.setRemoteDescription(offer); const answer = await this._pc.createAnswer(); logger.debug('pauseReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); await this._pc.setLocalDescription(answer); } async resumeReceiving(localIds) { this.assertNotClosed(); this.assertRecvDirection(); for (const localId of localIds) { logger.debug('resumeReceiving() [localId:%s]', localId); const transceiver = this._mapMidTransceiver.get(localId); if (!transceiver) { throw new Error('associated RTCRtpTransceiver not found'); } transceiver.direction = 'recvonly'; this._remoteSdp.resumeReceivingMediaSection(localId); } const offer = { type: 'offer', sdp: this._remoteSdp.getSdp(), }; logger.debug('resumeReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer); await this._pc.setRemoteDescription(offer); const answer = await this._pc.createAnswer(); logger.debug('resumeReceiving() | calling pc.setLocalDescription() [answer:%o]', answer); await this._pc.setLocalDescription(answer); } async getReceiverStats(localId) { this.assertNotClosed(); this.assertRecvDirection(); const transceiver = this._mapMidTransceiver.get(localId); if (!transceiver) { throw new Error('associated RTCRtpTransceiver not found'); } return transceiver.receiver.getStats(); } async receiveDataChannel({ maxMessageSize, sctpStreamParameters, label, protocol, }) { this.assertNotClosed(); this.assertRecvDirection(); const { streamId, ordered, maxPacketLifeTime, maxRetransmits, } = sctpStreamParameters; const options = { negotiated: true, id: streamId, ordered, maxPacketLifeTime, maxRetransmits, protocol, }; logger.debug('receiveDataChannel() [options:%o]', options); const dataChannel = this._pc.createDataChannel(label, options); // If this is the first DataChannel we need to create the SDP offer with // m=application section. if (!this._hasDataChannelMediaSection) { this._remoteSdp.receiveSctpAssociation(); const offer = { type: 'offer', sdp: this._remoteSdp.getSdp(), }; logger.debug('receiveDataChannel() | calling pc.setRemoteDescription() [offer:%o]', offer); await this._pc.setRemoteDescription(offer); let answer = await this._pc.createAnswer(); const localSdpObject = sdpTransform.parse(answer.sdp); const answerMediaObject = localSdpObject.media.find(m => m.type === 'application'); answerMediaObject.maxMessageSize = maxMessageSize; if (!this._transportReady) { await this.setupTransport({ localDtlsRole: this._forcedLocalDtlsRole ?? 'client', localSdpObject, }); } answer = { type: 'answer', sdp: sdpTransform.write(localSdpObject), }; logger.debug('receiveDataChannel() | calling pc.setLocalDescription() [answer:%o]', answer); await this._pc.setLocalDescription(answer); this._hasDataChannelMediaSection = true; } return { dataChannel }; } getDataChannelMaxMessageSize() { return this._pc.sctp?.maxMessageSize; } async setupTransport({ localDtlsRole, localSdpObject, }) { if (!localSdpObject) { localSdpObject = sdpTransform.parse(this._pc.localDescription.sdp); } // Get our local DTLS parameters. const dtlsParameters = sdpCommonUtils.extractDtlsParameters({ sdpObject: localSdpObject, }); // Set our DTLS role. dtlsParameters.role = localDtlsRole; // Update the remote DTLS role in the SDP. this._remoteSdp.updateDtlsRole(localDtlsRole === 'client' ? 'server' : 'client'); // Need to tell the remote transport about our parameters. await new Promise((resolve, reject) => { this.safeEmit('@connect', { dtlsParameters }, resolve, reject); }); this._transportReady = true; } onIceGatheringStateChange = () => { this.emit('@icegatheringstatechange', this._pc.iceGatheringState); }; onIceCandidateError = (event) => { this.emit('@icecandidateerror', event); }; onConnectionStateChange = () => { this.emit('@connectionstatechange', this._pc.connectionState); }; onIceConnectionStateChange = () => { switch (this._pc.iceConnectionState) { case 'checking': { this.emit('@connectionstatechange', 'connecting'); break; } case 'connected': case 'completed': { this.emit('@connectionstatechange', 'connected'); break; } case 'failed': { this.emit('@connectionstatechange', 'failed'); break; } case 'disconnected': { this.emit('@connectionstatechange', 'disconnected'); break; } case 'closed': { this.emit('@connectionstatechange', 'closed'); break; } } }; assertNotClosed() { if (this._closed) { throw new errors_1.InvalidStateError('method called in a closed handler'); } } assertSendDirection() { if (this._direction !== 'send') { throw new Error('method can just be called for handlers with "send" direction'); } } assertRecvDirection() { if (this._direction !== 'recv') { throw new Error('method can just be called for handlers with "recv" direction'); } } } exports.Chrome74 = Chrome74;