diff --git a/src/Device.ts b/src/Device.ts index e01627cb..2ae09a34 100644 --- a/src/Device.ts +++ b/src/Device.ts @@ -6,6 +6,7 @@ import * as utils from './utils'; import * as ortc from './ortc'; import { Transport, TransportOptions, CanProduceByKind } from './Transport'; import { HandlerFactory, HandlerInterface } from './handlers/HandlerInterface'; +import { Chrome111 } from './handlers/Chrome111'; import { Chrome74 } from './handlers/Chrome74'; import { Chrome70 } from './handlers/Chrome70'; import { Chrome67 } from './handlers/Chrome67'; @@ -22,6 +23,7 @@ import { SctpCapabilities } from './SctpParameters'; const logger = new Logger('Device'); export type BuiltinHandlerName = + | 'Chrome111' | 'Chrome74' | 'Chrome70' | 'Chrome67' @@ -91,7 +93,11 @@ export function detectDevice(): BuiltinHandlerName | undefined const engine = browser.getEngine(); // Chrome, Chromium, and Edge. - if (browser.satisfies({ chrome: '>=74', chromium: '>=74', 'microsoft edge': '>=88' })) + if (browser.satisfies({ chrome: '>=111', chromium: '>=111', 'microsoft edge': '>=111' })) + { + return 'Chrome111'; + } + else if (browser.satisfies({ chrome: '>=74', chromium: '>=74', 'microsoft edge': '>=88' })) { return 'Chrome74'; } @@ -148,7 +154,11 @@ export function detectDevice(): BuiltinHandlerName | undefined { const version = Number(match[1]); - if (version >= 74) + if (version >= 111) + { + return 'Chrome111'; + } + else if (version >= 74) { return 'Chrome74'; } @@ -167,7 +177,7 @@ export function detectDevice(): BuiltinHandlerName | undefined } else { - return 'Chrome74'; + return 'Chrome111'; } } // Unsupported browser. @@ -271,6 +281,9 @@ export class Device switch (handlerName) { + case 'Chrome111': + this._handlerFactory = Chrome111.createFactory(); + break; case 'Chrome74': this._handlerFactory = Chrome74.createFactory(); break; diff --git a/src/handlers/Chrome111.ts b/src/handlers/Chrome111.ts new file mode 100644 index 00000000..e29ad435 --- /dev/null +++ b/src/handlers/Chrome111.ts @@ -0,0 +1,1092 @@ +import * as sdpTransform from 'sdp-transform'; +import { Logger } from '../Logger'; +import * as utils from '../utils'; +import * as ortc from '../ortc'; +import * as sdpCommonUtils from './sdp/commonUtils'; +import * as sdpUnifiedPlanUtils from './sdp/unifiedPlanUtils'; +import { + HandlerFactory, + HandlerInterface, + HandlerRunOptions, + HandlerSendOptions, + HandlerSendResult, + HandlerReceiveOptions, + HandlerReceiveResult, + HandlerSendDataChannelOptions, + HandlerSendDataChannelResult, + HandlerReceiveDataChannelOptions, + HandlerReceiveDataChannelResult +} from './HandlerInterface'; +import { RemoteSdp } from './sdp/RemoteSdp'; +import { parse as parseScalabilityMode } from '../scalabilityModes'; +import { IceParameters, DtlsRole } from '../Transport'; +import { RtpCapabilities, RtpParameters } from '../RtpParameters'; +import { SctpCapabilities, SctpStreamParameters } from '../SctpParameters'; + +const logger = new Logger('Chrome111'); + +const SCTP_NUM_STREAMS = { OS: 1024, MIS: 1024 }; + +export class Chrome111 extends HandlerInterface +{ + // Handler direction. + private _direction?: 'send' | 'recv'; + // Remote SDP handler. + private _remoteSdp?: RemoteSdp; + // Generic sending RTP parameters for audio and video. + private _sendingRtpParametersByKind?: { [key: string]: RtpParameters }; + // Generic sending RTP parameters for audio and video suitable for the SDP + // remote answer. + private _sendingRemoteRtpParametersByKind?: { [key: string]: RtpParameters }; + // Initial server side DTLS role. If not 'auto', it will force the opposite + // value in client side. + private _forcedLocalDtlsRole?: DtlsRole; + // RTCPeerConnection instance. + private _pc: any; + // Map of RTCTransceivers indexed by MID. + private readonly _mapMidTransceiver: Map = + new Map(); + // Local stream for sending. + private readonly _sendStream = new MediaStream(); + // Whether a DataChannel m=application section has been created. + private _hasDataChannelMediaSection = false; + // Sending DataChannel id value counter. Incremented for each new DataChannel. + private _nextSendSctpStreamId = 0; + // Got transport local and remote parameters. + private _transportReady = false; + + /** + * Creates a factory function. + */ + static createFactory(): HandlerFactory + { + return (): Chrome111 => new Chrome111(); + } + + constructor() + { + super(); + } + + get name(): string + { + return 'Chrome111'; + } + + close(): void + { + logger.debug('close()'); + + // Close RTCPeerConnection. + if (this._pc) + { + try { this._pc.close(); } + catch (error) {} + } + + this.emit('@close'); + } + + async getNativeRtpCapabilities(): Promise + { + logger.debug('getNativeRtpCapabilities()'); + + const pc = new (RTCPeerConnection as any)( + { + 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 }); + + return nativeRtpCapabilities; + } + catch (error) + { + try { pc.close(); } + catch (error2) {} + + throw error; + } + } + + async getNativeSctpCapabilities(): Promise + { + logger.debug('getNativeSctpCapabilities()'); + + return { + numStreams : SCTP_NUM_STREAMS + }; + } + + run( + { + direction, + iceParameters, + iceCandidates, + dtlsParameters, + sctpParameters, + iceServers, + iceTransportPolicy, + additionalSettings, + proprietaryConstraints, + extendedRtpCapabilities + }: HandlerRunOptions + ): void + { + logger.debug('run()'); + + this._direction = direction; + + this._remoteSdp = new 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 as any)( + { + iceServers : iceServers || [], + iceTransportPolicy : iceTransportPolicy || 'all', + bundlePolicy : 'max-bundle', + rtcpMuxPolicy : 'require', + sdpSemantics : 'unified-plan', + ...additionalSettings + }, + proprietaryConstraints); + + 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: RTCIceServer[]): Promise + { + logger.debug('updateIceServers()'); + + const configuration = this._pc.getConfiguration(); + + configuration.iceServers = iceServers; + + this._pc.setConfiguration(configuration); + } + + async restartIce(iceParameters: IceParameters): Promise + { + 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(): Promise + { + return this._pc.getStats(); + } + + async send( + { track, encodings, codecOptions, codec }: HandlerSendOptions + ): Promise + { + this.assertSendDirection(); + + logger.debug('send() [kind:%s, track.id:%s]', track.kind, track.id); + + if (encodings && encodings.length > 1) + { + encodings.forEach((encoding, idx: number) => + { + encoding.rid = `r${idx}`; + }); + + // 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 nextRid = 1; + let maxTemporalLayers = 1; + + for (const encoding of encodings) + { + const temporalLayers = encoding.scalabilityMode + ? parseScalabilityMode(encoding.scalabilityMode).temporalLayers + : 3; + + if (temporalLayers > maxTemporalLayers) + { + maxTemporalLayers = temporalLayers; + } + } + + for (const encoding of encodings) + { + encoding.rid = `r${nextRid++}`; + encoding.scalabilityMode = `L1T${maxTemporalLayers}`; + } + } + + const sendingRtpParameters: RtpParameters = + utils.clone(this._sendingRtpParametersByKind![track.kind], {}); + + // This may throw. + sendingRtpParameters.codecs = + ortc.reduceCodecs(sendingRtpParameters.codecs, codec); + + const sendingRemoteRtpParameters: RtpParameters = + 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 + }); + const offer = await this._pc.createOffer(); + let localSdpObject = sdpTransform.parse(offer.sdp); + + 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, + extmapAllowMixed : true + }); + + 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: string): Promise + { + this.assertSendDirection(); + + logger.debug('stopSending() [localId:%s]', localId); + + const transceiver = this._mapMidTransceiver.get(localId); + + if (!transceiver) + throw new Error('associated RTCRtpTransceiver not found'); + + 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: string): Promise + { + 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: string): Promise + { + 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: string, track: MediaStreamTrack | null + ): Promise + { + 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: string, spatialLayer: number): Promise + { + 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: RTCRtpEncodingParameters, idx: number) => + { + 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: string, params: any): Promise + { + 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: RTCRtpEncodingParameters, idx: number) => + { + 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: string): Promise + { + 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 + }: HandlerSendDataChannelOptions + ): Promise + { + 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: any) => 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: SctpStreamParameters = + { + streamId : options.id, + ordered : options.ordered, + maxPacketLifeTime : options.maxPacketLifeTime, + maxRetransmits : options.maxRetransmits + }; + + return { dataChannel, sctpStreamParameters }; + } + + async receive( + optionsList: HandlerReceiveOptions[] + ) : Promise + { + this.assertRecvDirection(); + + const results: HandlerReceiveResult[] = []; + const mapLocalId: Map = 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); + + 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: any) => 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: RTCRtpTransceiver) => 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: string[]): Promise + { + this.assertRecvDirection(); + + 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: string[]): Promise + { + 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: string[]): Promise + { + 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: string): Promise + { + 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 }: HandlerReceiveDataChannelOptions + ): Promise + { + this.assertRecvDirection(); + + const { + streamId, + ordered, + maxPacketLifeTime, + maxRetransmits + }: SctpStreamParameters = 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 }; + } + + private async setupTransport( + { + localDtlsRole, + localSdpObject + }: + { + localDtlsRole: DtlsRole; + localSdpObject?: any; + } + ): Promise + { + 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; + } + + private assertSendDirection(): void + { + if (this._direction !== 'send') + { + throw new Error( + 'method can just be called for handlers with "send" direction'); + } + } + + private assertRecvDirection(): void + { + if (this._direction !== 'recv') + { + throw new Error( + 'method can just be called for handlers with "recv" direction'); + } + } +} diff --git a/src/handlers/Chrome55.ts b/src/handlers/Chrome55.ts index ae4b0e0b..e22893b0 100644 --- a/src/handlers/Chrome55.ts +++ b/src/handlers/Chrome55.ts @@ -402,7 +402,7 @@ export class Chrome55 extends HandlerInterface { for (const encoding of sendingRtpParameters.encodings) { - encoding.scalabilityMode = 'S1T3'; + encoding.scalabilityMode = 'L1T3'; } } diff --git a/src/handlers/Chrome67.ts b/src/handlers/Chrome67.ts index 01aff03f..c691ffec 100644 --- a/src/handlers/Chrome67.ts +++ b/src/handlers/Chrome67.ts @@ -403,7 +403,7 @@ export class Chrome67 extends HandlerInterface { for (const encoding of sendingRtpParameters.encodings) { - encoding.scalabilityMode = 'S1T3'; + encoding.scalabilityMode = 'L1T3'; } } diff --git a/src/handlers/Chrome70.ts b/src/handlers/Chrome70.ts index 3fb9d1db..d5265b3c 100644 --- a/src/handlers/Chrome70.ts +++ b/src/handlers/Chrome70.ts @@ -447,7 +447,7 @@ export class Chrome70 extends HandlerInterface { for (const encoding of sendingRtpParameters.encodings) { - encoding.scalabilityMode = 'S1T3'; + encoding.scalabilityMode = 'L1T3'; } } diff --git a/src/handlers/Chrome74.ts b/src/handlers/Chrome74.ts index 21c9464e..86eacb6f 100644 --- a/src/handlers/Chrome74.ts +++ b/src/handlers/Chrome74.ts @@ -439,9 +439,7 @@ export class Chrome74 extends HandlerInterface } else { - // By default Chrome enables 2 temporal layers (not in all OS but - // anyway). - encoding.scalabilityMode = 'L1T2'; + encoding.scalabilityMode = 'L1T3'; } } } diff --git a/src/handlers/Firefox60.ts b/src/handlers/Firefox60.ts index 483c115c..96c76e05 100644 --- a/src/handlers/Firefox60.ts +++ b/src/handlers/Firefox60.ts @@ -439,8 +439,7 @@ export class Firefox60 extends HandlerInterface } else { - // By default Firefox enables 2 temporal layers. - encoding.scalabilityMode = 'L1T2'; + encoding.scalabilityMode = 'L1T3'; } } } diff --git a/src/handlers/ReactNative.ts b/src/handlers/ReactNative.ts index 9c22f58c..e0198b23 100644 --- a/src/handlers/ReactNative.ts +++ b/src/handlers/ReactNative.ts @@ -410,7 +410,7 @@ export class ReactNative extends HandlerInterface { for (const encoding of sendingRtpParameters.encodings) { - encoding.scalabilityMode = 'S1T3'; + encoding.scalabilityMode = 'L1T3'; } } diff --git a/src/handlers/ReactNativeUnifiedPlan.ts b/src/handlers/ReactNativeUnifiedPlan.ts index 4a8def38..ee478cad 100644 --- a/src/handlers/ReactNativeUnifiedPlan.ts +++ b/src/handlers/ReactNativeUnifiedPlan.ts @@ -438,7 +438,14 @@ export class ReactNativeUnifiedPlan extends HandlerInterface { for (const encoding of sendingRtpParameters.encodings) { - encoding.scalabilityMode = 'S1T3'; + if (encoding.scalabilityMode) + { + encoding.scalabilityMode = `L1T${layers.temporalLayers}`; + } + else + { + encoding.scalabilityMode = 'L1T3'; + } } } diff --git a/src/handlers/Safari11.ts b/src/handlers/Safari11.ts index bcad7f87..49298ce6 100644 --- a/src/handlers/Safari11.ts +++ b/src/handlers/Safari11.ts @@ -402,7 +402,7 @@ export class Safari11 extends HandlerInterface { for (const encoding of sendingRtpParameters.encodings) { - encoding.scalabilityMode = 'S1T3'; + encoding.scalabilityMode = 'L1T3'; } } diff --git a/src/handlers/Safari12.ts b/src/handlers/Safari12.ts index 1e78859a..4a10861d 100644 --- a/src/handlers/Safari12.ts +++ b/src/handlers/Safari12.ts @@ -394,12 +394,11 @@ export class Safari12 extends HandlerInterface { if (encoding.scalabilityMode) { - encoding.scalabilityMode = `S1T${layers.temporalLayers}`; + encoding.scalabilityMode = `L1T${layers.temporalLayers}`; } else { - // By default Safari enables 3 temporal layers. - encoding.scalabilityMode = 'S1T3'; + encoding.scalabilityMode = 'L1T3'; } } }