UNPKG

mediasoup-client

Version:

mediasoup client side TypeScript library

711 lines (710 loc) 31.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Chrome111 = void 0; const sdpTransform = require("sdp-transform"); const Logger_1 = require("../Logger"); const utils = require("../utils"); const ortc = require("../ortc"); const sdpCommonUtils = require("./sdp/commonUtils"); const sdpUnifiedPlanUtils = require("./sdp/unifiedPlanUtils"); const ortcUtils = require("./ortc/utils"); const errors_1 = require("../errors"); const HandlerInterface_1 = require("./HandlerInterface"); const RemoteSdp_1 = require("./sdp/RemoteSdp"); const scalabilityModes_1 = require("../scalabilityModes"); const logger = new Logger_1.Logger('Chrome111'); const NAME = 'Chrome111'; const SCTP_NUM_STREAMS = { OS: 1024, MIS: 1024 }; class Chrome111 extends HandlerInterface_1.HandlerInterface { // Closed flag. _closed = false; // Handler direction. _direction; // Remote SDP handler. _remoteSdp; // Generic sending RTP parameters for audio and video. _sendingRtpParametersByKind; // Generic sending RTP parameters for audio and video suitable for the SDP // remote answer. _sendingRemoteRtpParametersByKind; // 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(); // Local stream for sending. _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 () => new Chrome111(); } constructor() { super(); } get name() { return NAME; } close() { logger.debug('close()'); if (this._closed) { return; } this._closed = true; // Close RTCPeerConnection. if (this._pc) { try { this._pc.close(); } catch (error) { } } this.emit('@close'); } async getNativeRtpCapabilities() { logger.debug('getNativeRtpCapabilities()'); const pc = new RTCPeerConnection({ iceServers: [], iceTransportPolicy: 'all', bundlePolicy: 'max-bundle', rtcpMuxPolicy: 'require', sdpSemantics: 'unified-plan', }); try { pc.addTransceiver('audio'); pc.addTransceiver('video'); const offer = await pc.createOffer(); try { pc.close(); } catch (error) { } const sdpObject = sdpTransform.parse(offer.sdp); const nativeRtpCapabilities = sdpCommonUtils.extractRtpCapabilities({ sdpObject, }); // libwebrtc supports NACK for OPUS but doesn't announce it. ortcUtils.addNackSupportForOpus(nativeRtpCapabilities); return nativeRtpCapabilities; } catch (error) { try { pc.close(); } catch (error2) { } throw error; } } async getNativeSctpCapabilities() { logger.debug('getNativeSctpCapabilities()'); return { numStreams: SCTP_NUM_STREAMS, }; } run({ direction, iceParameters, iceCandidates, dtlsParameters, sctpParameters, iceServers, iceTransportPolicy, additionalSettings, proprietaryConstraints, extendedRtpCapabilities, }) { this.assertNotClosed(); logger.debug('run()'); this._direction = direction; this._remoteSdp = new RemoteSdp_1.RemoteSdp({ iceParameters, iceCandidates, dtlsParameters, sctpParameters, }); this._sendingRtpParametersByKind = { audio: ortc.getSendingRtpParameters('audio', extendedRtpCapabilities), video: ortc.getSendingRtpParameters('video', extendedRtpCapabilities), }; this._sendingRemoteRtpParametersByKind = { audio: ortc.getSendingRemoteRtpParameters('audio', extendedRtpCapabilities), video: ortc.getSendingRemoteRtpParameters('video', extendedRtpCapabilities), }; 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', sdpSemantics: 'unified-plan', ...additionalSettings, }, proprietaryConstraints); this._pc.addEventListener('icegatheringstatechange', () => { this.emit('@icegatheringstatechange', this._pc.iceGatheringState); }); this._pc.addEventListener('icecandidateerror', (event) => { this.emit('@icecandidateerror', event); }); if (this._pc.connectionState) { this._pc.addEventListener('connectionstatechange', () => { this.emit('@connectionstatechange', this._pc.connectionState); }); } else { logger.warn('run() | pc.connectionState not supported, using pc.iceConnectionState'); this._pc.addEventListener('iceconnectionstatechange', () => { 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; } } }); } } 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, encodings, codecOptions, codec, onRtpSender, }) { this.assertNotClosed(); this.assertSendDirection(); logger.debug('send() [kind:%s, track.id:%s]', track.kind, track.id); if (encodings && encodings.length > 1) { // Set rid and verify scalabilityMode in each encoding. // NOTE: Even if WebRTC allows different scalabilityMode (different number // of temporal layers) per simulcast stream, we need that those are the // same in all them, so let's pick up the highest value. // NOTE: If scalabilityMode is not given, Chrome will use L1T3. let maxTemporalLayers = 1; for (const encoding of encodings) { const temporalLayers = encoding.scalabilityMode ? (0, scalabilityModes_1.parse)(encoding.scalabilityMode).temporalLayers : 3; if (temporalLayers > maxTemporalLayers) { maxTemporalLayers = temporalLayers; } } encodings.forEach((encoding, idx) => { encoding.rid = `r${idx}`; encoding.scalabilityMode = `L1T${maxTemporalLayers}`; }); } const sendingRtpParameters = utils.clone(this._sendingRtpParametersByKind[track.kind]); // This may throw. sendingRtpParameters.codecs = ortc.reduceCodecs(sendingRtpParameters.codecs, codec); const sendingRemoteRtpParameters = utils.clone(this._sendingRemoteRtpParametersByKind[track.kind]); // This may throw. sendingRemoteRtpParameters.codecs = ortc.reduceCodecs(sendingRemoteRtpParameters.codecs, codec); const mediaSectionIdx = this._remoteSdp.getNextMediaSectionIdx(); const transceiver = this._pc.addTransceiver(track, { direction: 'sendonly', streams: [this._sendStream], sendEncodings: encodings, }); if (onRtpSender) { onRtpSender(transceiver.sender); } const offer = await this._pc.createOffer(); let localSdpObject = sdpTransform.parse(offer.sdp); // @ts-expect-error --- sdpTransform.SessionDescription type doesn't // define extmapAllowMixed field. if (localSdpObject.extmapAllowMixed) { this._remoteSdp.setSessionExtmapAllowMixed(); } if (!this._transportReady) { await this.setupTransport({ localDtlsRole: this._forcedLocalDtlsRole ?? 'client', localSdpObject, }); } logger.debug('send() | calling pc.setLocalDescription() [offer:%o]', offer); 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); const offerMediaObject = localSdpObject.media[mediaSectionIdx.idx]; // Set RTCP CNAME. sendingRtpParameters.rtcp.cname = sdpCommonUtils.getCname({ offerMediaObject, }); // Set RTP encodings by parsing the SDP offer if no encodings are given. if (!encodings) { sendingRtpParameters.encodings = sdpUnifiedPlanUtils.getRtpEncodings({ offerMediaObject, }); } // 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) { const newEncodings = sdpUnifiedPlanUtils.getRtpEncodings({ offerMediaObject, }); Object.assign(newEncodings[0], encodings[0]); sendingRtpParameters.encodings = newEncodings; } // Otherwise if more than 1 encoding are given use them verbatim. else { sendingRtpParameters.encodings = encodings; } 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({ ordered, maxPacketLifeTime, maxRetransmits, label, protocol, }) { this.assertNotClosed(); this.assertSendDirection(); const options = { negotiated: true, id: this._nextSendSctpStreamId, ordered, maxPacketLifeTime, maxRetransmits, protocol, }; logger.debug('sendDataChannel() [options:%o]', options); const dataChannel = this._pc.createDataChannel(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 sctpStreamParameters = { streamId: options.id, ordered: options.ordered, maxPacketLifeTime: options.maxPacketLifeTime, maxRetransmits: options.maxRetransmits, }; return { dataChannel, sctpStreamParameters }; } 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); this._remoteSdp.receive({ mid: localId, kind, offerRtpParameters: rtpParameters, streamId: streamId ?? 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); for (const options of optionsList) { const { trackId, onRtpReceiver } = options; if (onRtpReceiver) { const localId = mapLocalId.get(trackId); const transceiver = this._pc .getTransceivers() .find((t) => t.mid === localId); if (!transceiver) { throw new Error('transceiver not found'); } onRtpReceiver(transceiver.receiver); } } 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({ 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); const answer = await this._pc.createAnswer(); if (!this._transportReady) { const localSdpObject = sdpTransform.parse(answer.sdp); await this.setupTransport({ localDtlsRole: this._forcedLocalDtlsRole ?? 'client', localSdpObject, }); } logger.debug('receiveDataChannel() | calling pc.setRemoteDescription() [answer:%o]', answer); await this._pc.setLocalDescription(answer); this._hasDataChannelMediaSection = true; } return { dataChannel }; } 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; } 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.Chrome111 = Chrome111;