mediasoup-client: Chrome74 vs Chrome111

Created Diff never expires
67 removals
Lines
Total
Removed
Words
Total
Removed
To continue using this feature, upgrade to
Diffchecker logo
Diffchecker Pro
1012 lines
79 additions
Lines
Total
Added
Words
Total
Added
To continue using this feature, upgrade to
Diffchecker logo
Diffchecker Pro
1024 lines
import * as sdpTransform from 'sdp-transform';
import * as sdpTransform from 'sdp-transform';
import { Logger } from '../Logger';
import { Logger } from '../Logger';
import * as utils from '../utils';
import * as utils from '../utils';
import * as ortc from '../ortc';
import * as ortc from '../ortc';
import * as sdpCommonUtils from './sdp/commonUtils';
import * as sdpCommonUtils from './sdp/commonUtils';
import * as sdpUnifiedPlanUtils from './sdp/unifiedPlanUtils';
import * as sdpUnifiedPlanUtils from './sdp/unifiedPlanUtils';
import {
import {
HandlerFactory,
HandlerFactory,
HandlerInterface,
HandlerInterface,
HandlerRunOptions,
HandlerRunOptions,
HandlerSendOptions,
HandlerSendOptions,
HandlerSendResult,
HandlerSendResult,
HandlerReceiveOptions,
HandlerReceiveOptions,
HandlerReceiveResult,
HandlerReceiveResult,
HandlerSendDataChannelOptions,
HandlerSendDataChannelOptions,
HandlerSendDataChannelResult,
HandlerSendDataChannelResult,
HandlerReceiveDataChannelOptions,
HandlerReceiveDataChannelOptions,
HandlerReceiveDataChannelResult
HandlerReceiveDataChannelResult
} from './HandlerInterface';
} from './HandlerInterface';
import { RemoteSdp } from './sdp/RemoteSdp';
import { RemoteSdp } from './sdp/RemoteSdp';
import { parse as parseScalabilityMode } from '../scalabilityModes';
import { IceParameters, DtlsRole } from '../Transport';
import { IceParameters, DtlsRole } from '../Transport';
import {
import {
RtpCapabilities,
RtpCapabilities,
RtpParameters,
RtpParameters,
RtpEncodingParameters
RtpEncodingParameters
} from '../RtpParameters';
} from '../RtpParameters';
import { SctpCapabilities, SctpStreamParameters } from '../SctpParameters';
import { SctpCapabilities, SctpStreamParameters } from '../SctpParameters';


const logger = new Logger('Chrome74');
const logger = new Logger('Chrome111');


const SCTP_NUM_STREAMS = { OS: 1024, MIS: 1024 };
const SCTP_NUM_STREAMS = { OS: 1024, MIS: 1024 };


export class Chrome74 extends HandlerInterface
export class Chrome111 extends HandlerInterface
{
{
// Handler direction.
// Handler direction.
private _direction?: 'send' | 'recv';
private _direction?: 'send' | 'recv';
// Remote SDP handler.
// Remote SDP handler.
private _remoteSdp?: RemoteSdp;
private _remoteSdp?: RemoteSdp;
// Generic sending RTP parameters for audio and video.
// Generic sending RTP parameters for audio and video.
private _sendingRtpParametersByKind?: { [key: string]: RtpParameters };
private _sendingRtpParametersByKind?: { [key: string]: RtpParameters };
// Generic sending RTP parameters for audio and video suitable for the SDP
// Generic sending RTP parameters for audio and video suitable for the SDP
// remote answer.
// remote answer.
private _sendingRemoteRtpParametersByKind?: { [key: string]: RtpParameters };
private _sendingRemoteRtpParametersByKind?: { [key: string]: RtpParameters };
// Initial server side DTLS role. If not 'auto', it will force the opposite
// Initial server side DTLS role. If not 'auto', it will force the opposite
// value in client side.
// value in client side.
private _forcedLocalDtlsRole?: DtlsRole;
private _forcedLocalDtlsRole?: DtlsRole;
// RTCPeerConnection instance.
// RTCPeerConnection instance.
private _pc: any;
private _pc: any;
// Map of RTCTransceivers indexed by MID.
// Map of RTCTransceivers indexed by MID.
private readonly _mapMidTransceiver: Map<string, RTCRtpTransceiver> =
private readonly _mapMidTransceiver: Map<string, RTCRtpTransceiver> =
new Map();
new Map();
// Local stream for sending.
// Local stream for sending.
private readonly _sendStream = new MediaStream();
private readonly _sendStream = new MediaStream();
// Whether a DataChannel m=application section has been created.
// Whether a DataChannel m=application section has been created.
private _hasDataChannelMediaSection = false;
private _hasDataChannelMediaSection = false;
// Sending DataChannel id value counter. Incremented for each new DataChannel.
// Sending DataChannel id value counter. Incremented for each new DataChannel.
private _nextSendSctpStreamId = 0;
private _nextSendSctpStreamId = 0;
// Got transport local and remote parameters.
// Got transport local and remote parameters.
private _transportReady = false;
private _transportReady = false;


/**
/**
* Creates a factory function.
* Creates a factory function.
*/
*/
static createFactory(): HandlerFactory
static createFactory(): HandlerFactory
{
{
return (): Chrome74 => new Chrome74();
return (): Chrome111 => new Chrome111();
}
}


constructor()
constructor()
{
{
super();
super();
}
}


get name(): string
get name(): string
{
{
return 'Chrome74';
return 'Chrome111';
}
}


close(): void
close(): void
{
{
logger.debug('close()');
logger.debug('close()');


// Close RTCPeerConnection.
// Close RTCPeerConnection.
if (this._pc)
if (this._pc)
{
{
try { this._pc.close(); }
try { this._pc.close(); }
catch (error) {}
catch (error) {}
}
}


this.emit('@close');
this.emit('@close');
}
}


async getNativeRtpCapabilities(): Promise<RtpCapabilities>
async getNativeRtpCapabilities(): Promise<RtpCapabilities>
{
{
logger.debug('getNativeRtpCapabilities()');
logger.debug('getNativeRtpCapabilities()');


const pc = new (RTCPeerConnection as any)(
const pc = new (RTCPeerConnection as any)(
{
{
iceServers : [],
iceServers : [],
iceTransportPolicy : 'all',
iceTransportPolicy : 'all',
bundlePolicy : 'max-bundle',
bundlePolicy : 'max-bundle',
rtcpMuxPolicy : 'require',
rtcpMuxPolicy : 'require',
sdpSemantics : 'unified-plan'
sdpSemantics : 'unified-plan'
});
});


try
try
{
{
pc.addTransceiver('audio');
pc.addTransceiver('audio');
pc.addTransceiver('video');
pc.addTransceiver('video');


const offer = await pc.createOffer();
const offer = await pc.createOffer();


try { pc.close(); }
try { pc.close(); }
catch (error) {}
catch (error) {}


const sdpObject = sdpTransform.parse(offer.sdp);
const sdpObject = sdpTransform.parse(offer.sdp);
const nativeRtpCapabilities =
const nativeRtpCapabilities =
sdpCommonUtils.extractRtpCapabilities({ sdpObject });
sdpCommonUtils.extractRtpCapabilities({ sdpObject });


return nativeRtpCapabilities;
return nativeRtpCapabilities;
}
}
catch (error)
catch (error)
{
{
try { pc.close(); }
try { pc.close(); }
catch (error2) {}
catch (error2) {}


throw error;
throw error;
}
}
}
}


async getNativeSctpCapabilities(): Promise<SctpCapabilities>
async getNativeSctpCapabilities(): Promise<SctpCapabilities>
{
{
logger.debug('getNativeSctpCapabilities()');
logger.debug('getNativeSctpCapabilities()');


return {
return {
numStreams : SCTP_NUM_STREAMS
numStreams : SCTP_NUM_STREAMS
};
};
}
}


run(
run(
{
{
direction,
direction,
iceParameters,
iceParameters,
iceCandidates,
iceCandidates,
dtlsParameters,
dtlsParameters,
sctpParameters,
sctpParameters,
iceServers,
iceServers,
iceTransportPolicy,
iceTransportPolicy,
additionalSettings,
additionalSettings,
proprietaryConstraints,
proprietaryConstraints,
extendedRtpCapabilities
extendedRtpCapabilities
}: HandlerRunOptions
}: HandlerRunOptions
): void
): void
{
{
logger.debug('run()');
logger.debug('run()');


this._direction = direction;
this._direction = direction;


this._remoteSdp = new RemoteSdp(
this._remoteSdp = new RemoteSdp(
{
{
iceParameters,
iceParameters,
iceCandidates,
iceCandidates,
dtlsParameters,
dtlsParameters,
sctpParameters
sctpParameters
});
});


this._sendingRtpParametersByKind =
this._sendingRtpParametersByKind =
{
{
audio : ortc.getSendingRtpParameters('audio', extendedRtpCapabilities),
audio : ortc.getSendingRtpParameters('audio', extendedRtpCapabilities),
video : ortc.getSendingRtpParameters('video', extendedRtpCapabilities)
video : ortc.getSendingRtpParameters('video', extendedRtpCapabilities)
};
};


this._sendingRemoteRtpParametersByKind =
this._sendingRemoteRtpParametersByKind =
{
{
audio : ortc.getSendingRemoteRtpParameters('audio', extendedRtpCapabilities),
audio : ortc.getSendingRemoteRtpParameters('audio', extendedRtpCapabilities),
video : ortc.getSendingRemoteRtpParameters('video', extendedRtpCapabilities)
video : ortc.getSendingRemoteRtpParameters('video', extendedRtpCapabilities)
};
};


if (dtlsParameters.role && dtlsParameters.role !== 'auto')
if (dtlsParameters.role && dtlsParameters.role !== 'auto')
{
{
this._forcedLocalDtlsRole = dtlsParameters.role === 'server'
this._forcedLocalDtlsRole = dtlsParameters.role === 'server'
? 'client'
? 'client'
: 'server';
: 'server';
}
}


this._pc = new (RTCPeerConnection as any)(
this._pc = new (RTCPeerConnection as any)(
{
{
iceServers : iceServers || [],
iceServers : iceServers || [],
iceTransportPolicy : iceTransportPolicy || 'all',
iceTransportPolicy : iceTransportPolicy || 'all',
bundlePolicy : 'max-bundle',
bundlePolicy : 'max-bundle',
rtcpMuxPolicy : 'require',
rtcpMuxPolicy : 'require',
sdpSemantics : 'unified-plan',
sdpSemantics : 'unified-plan',
...additionalSettings
...additionalSettings
},
},
proprietaryConstraints);
proprietaryConstraints);


if (this._pc.connectionState)
if (this._pc.connectionState)
{
{
this._pc.addEventListener('connectionstatechange', () =>
this._pc.addEventListener('connectionstatechange', () =>
{
{
this.emit('@connectionstatechange', this._pc.connectionState);
this.emit('@connectionstatechange', this._pc.connectionState);
});
});
}
}
else
else
{
{
logger.warn(
logger.warn(
'run() | pc.connectionState not supported, using pc.iceConnectionState');
'run() | pc.connectionState not supported, using pc.iceConnectionState');


this._pc.addEventListener('iceconnectionstatechange', () =>
this._pc.addEventListener('iceconnectionstatechange', () =>
{
{
switch (this._pc.iceConnectionState)
switch (this._pc.iceConnectionState)
{
{
case 'checking':
case 'checking':
this.emit('@connectionstatechange', 'connecting');
this.emit('@connectionstatechange', 'connecting');
break;
break;
case 'connected':
case 'connected':
case 'completed':
case 'completed':
this.emit('@connectionstatechange', 'connected');
this.emit('@connectionstatechange', 'connected');
break;
break;
case 'failed':
case 'failed':
this.emit('@connectionstatechange', 'failed');
this.emit('@connectionstatechange', 'failed');
break;
break;
case 'disconnected':
case 'disconnected':
this.emit('@connectionstatechange', 'disconnected');
this.emit('@connectionstatechange', 'disconnected');
break;
break;
case 'closed':
case 'closed':
this.emit('@connectionstatechange', 'closed');
this.emit('@connectionstatechange', 'closed');
break;
break;
}
}
});
});
}
}
}
}


async updateIceServers(iceServers: RTCIceServer[]): Promise<void>
async updateIceServers(iceServers: RTCIceServer[]): Promise<void>
{
{
logger.debug('updateIceServers()');
logger.debug('updateIceServers()');


const configuration = this._pc.getConfiguration();
const configuration = this._pc.getConfiguration();


configuration.iceServers = iceServers;
configuration.iceServers = iceServers;


this._pc.setConfiguration(configuration);
this._pc.setConfiguration(configuration);
}
}


async restartIce(iceParameters: IceParameters): Promise<void>
async restartIce(iceParameters: IceParameters): Promise<void>
{
{
logger.debug('restartIce()');
logger.debug('restartIce()');


// Provide the remote SDP handler with new remote ICE parameters.
// Provide the remote SDP handler with new remote ICE parameters.
this._remoteSdp!.updateIceParameters(iceParameters);
this._remoteSdp!.updateIceParameters(iceParameters);


if (!this._transportReady)
if (!this._transportReady)
return;
return;


if (this._direction === 'send')
if (this._direction === 'send')
{
{
const offer = await this._pc.createOffer({ iceRestart: true });
const offer = await this._pc.createOffer({ iceRestart: true });


logger.debug(
logger.debug(
'restartIce() | calling pc.setLocalDescription() [offer:%o]',
'restartIce() | calling pc.setLocalDescription() [offer:%o]',
offer);
offer);


await this._pc.setLocalDescription(offer);
await this._pc.setLocalDescription(offer);


const answer = { type: 'answer', sdp: this._remoteSdp!.getSdp() };
const answer = { type: 'answer', sdp: this._remoteSdp!.getSdp() };


logger.debug(
logger.debug(
'restartIce() | calling pc.setRemoteDescription() [answer:%o]',
'restartIce() | calling pc.setRemoteDescription() [answer:%o]',
answer);
answer);


await this._pc.setRemoteDescription(answer);
await this._pc.setRemoteDescription(answer);
}
}
else
else
{
{
const offer = { type: 'offer', sdp: this._remoteSdp!.getSdp() };
const offer = { type: 'offer', sdp: this._remoteSdp!.getSdp() };


logger.debug(
logger.debug(
'restartIce() | calling pc.setRemoteDescription() [offer:%o]',
'restartIce() | calling pc.setRemoteDescription() [offer:%o]',
offer);
offer);


await this._pc.setRemoteDescription(offer);
await this._pc.setRemoteDescription(offer);


const answer = await this._pc.createAnswer();
const answer = await this._pc.createAnswer();


logger.debug(
logger.debug(
'restartIce() | calling pc.setLocalDescription() [answer:%o]',
'restartIce() | calling pc.setLocalDescription() [answer:%o]',
answer);
answer);


await this._pc.setLocalDescription(answer);
await this._pc.setLocalDescription(answer);
}
}
}
}


async getTransportStats(): Promise<RTCStatsReport>
async getTransportStats(): Promise<RTCStatsReport>
{
{
return this._pc.getStats();
return this._pc.getStats();
}
}


async send(
async send(
{ track, encodings, codecOptions, codec }: HandlerSendOptions
{ track, encodings, codecOptions, codec }: HandlerSendOptions
): Promise<HandlerSendResult>
): Promise<HandlerSendResult>
{
{
this.assertSendDirection();
this.assertSendDirection();


logger.debug('send() [kind:%s, track.id:%s]', track.kind, track.id);
logger.debug('send() [kind:%s, track.id:%s]', track.kind, track.id);


if (encodings && encodings.length > 1)
if (encodings && encodings.length > 1)
{
{
encodings.forEach((encoding: RtpEncodingParameters, idx: number) =>
encodings.forEach((encoding: RtpEncodingParameters, idx: number) =>
{
{
encoding.rid = `r${idx}`;
encoding.rid = `r${idx}`;
});
});
}
}


const sendingRtpParameters =
const sendingRtpParameters =
utils.clone(this._sendingRtpParametersByKind![track.kind], {});
utils.clone(this._sendingRtpParametersByKind![track.kind], {});


// This may throw.
// This may throw.
sendingRtpParameters.codecs =
sendingRtpParameters.codecs =
ortc.reduceCodecs(sendingRtpParameters.codecs, codec);
ortc.reduceCodecs(sendingRtpParameters.codecs, codec);


const sendingRemoteRtpParameters =
const sendingRemoteRtpParameters =
utils.clone(this._sendingRemoteRtpParametersByKind![track.kind], {});
utils.clone(this._sendingRemoteRtpParametersByKind![track.kind], {});


// This may throw.
// This may throw.
sendingRemoteRtpParameters.codecs =
sendingRemoteRtpParameters.codecs =
ortc.reduceCodecs(sendingRemoteRtpParameters.codecs, codec);
ortc.reduceCodecs(sendingRemoteRtpParameters.codecs, codec);


const mediaSectionIdx = this._remoteSdp!.getNextMediaSectionIdx();
const mediaSectionIdx = this._remoteSdp!.getNextMediaSectionIdx();
const transceiver = this._pc.addTransceiver(
const transceiver = this._pc.addTransceiver(
track,
track,
{
{
direction : 'sendonly',
direction : 'sendonly',
streams : [ this._sendStream ],
streams : [ this._sendStream ],
sendEncodings : encodings
sendEncodings : encodings
});
});
let offer = await this._pc.createOffer();
const offer = await this._pc.createOffer();
let localSdpObject = sdpTransform.parse(offer.sdp);
let localSdpObject = sdpTransform.parse(offer.sdp);
let offerMediaObject;


if (!this._transportReady)
if (!this._transportReady)
{
{
await this.setupTransport(
await this.setupTransport(
{
{
localDtlsRole : this._forcedLocalDtlsRole ?? 'client',
localDtlsRole : this._forcedLocalDtlsRole ?? 'client',
localSdpObject
localSdpObject
});
});
}
}


// Special case for VP9 with SVC.
let hackVp9Svc = false;

const layers =
parseScalabilityMode((encodings || [ {} ])[0].scalabilityMode);

if (
encodings &&
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(
logger.debug(
'send() | calling pc.setLocalDescription() [offer:%o]',
'send() | calling pc.setLocalDescription() [offer:%o]',
offer);
offer);


await this._pc.setLocalDescription(offer);
await this._pc.setLocalDescription(offer);


// We can now get the transceiver.mid.
// We can now get the transceiver.mid.
const localId = transceiver.mid;
const localId = transceiver.mid;


// Set MID.
// Set MID.
sendingRtpParameters.mid = localId;
sendingRtpParameters.mid = localId;


localSdpObject = sdpTransform.parse(this._pc.localDescription.sdp);
localSdpObject = sdpTransform.parse(this._pc.localDescription.sdp);
offerMediaObject = localSdpObject.media[mediaSectionIdx.idx];

const offerMediaObject = localSdpObject.media[mediaSectionIdx.idx];


// Set RTCP CNAME.
// Set RTCP CNAME.
sendingRtpParameters.rtcp.cname =
sendingRtpParameters.rtcp.cname =
sdpCommonUtils.getCname({ offerMediaObject });
sdpCommonUtils.getCname({ offerMediaObject });


// Set RTP encodings by parsing the SDP offer if no encodings are given.
// Set RTP encodings by parsing the SDP offer if no encodings are given.
if (!encodings)
if (!encodings)
{
{
sendingRtpParameters.encodings =
sendingRtpParameters.encodings =
sdpUnifiedPlanUtils.getRtpEncodings({ offerMediaObject });
sdpUnifiedPlanUtils.getRtpEncodings({ offerMediaObject });
}
}
// Set RTP encodings by parsing the SDP offer and complete them with given
// Set RTP encodings by parsing the SDP offer and complete them with given
// one if just a single encoding has been given.
// one if just a single encoding has been given.
else if (encodings.length === 1)
else if (encodings.length === 1)
{
{
let newEncodings =
const newEncodings =
sdpUnifiedPlanUtils.getRtpEncodings({ offerMediaObject });
sdpUnifiedPlanUtils.getRtpEncodings({ offerMediaObject });


Object.assign(newEncodings[0], encodings[0]);
Object.assign(newEncodings[0], encodings[0]);


// Hack for VP9 SVC.
if (hackVp9Svc)
newEncodings = [ newEncodings[0] ];

sendingRtpParameters.encodings = newEncodings;
sendingRtpParameters.encodings = newEncodings;
}
}
// Otherwise if more than 1 encoding are given use them verbatim.
// Otherwise if more than 1 encoding are given use them verbatim.
else
else
{
{
sendingRtpParameters.encodings = encodings;
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
{
// By default Chrome enables 2 temporal layers (not in all OS but
// anyway).
encoding.scalabilityMode = 'L1T2';
}
}
}
}


this._remoteSdp!.send(
this._remoteSdp!.send(
{
{
offerMediaObject,
offerMediaObject,
reuseMid : mediaSectionIdx.reuseMid,
reuseMid : mediaSectionIdx.reuseMid,
offerRtpParameters : sendingRtpParameters,
offerRtpParameters : sendingRtpParameters,
answerRtpParameters : sendingRemoteRtpParameters,
answerRtpParameters : sendingRemoteRtpParameters,
codecOptions,
codecOptions,
extmapAllowMixed : true
extmapAllowMixed : true
});
});


const answer = { type: 'answer', sdp: this._remoteSdp!.getSdp() };
const answer = { type: 'answer', sdp: this._remoteSdp!.getSdp() };


logger.debug(
logger.debug(
'send() | calling pc.setRemoteDescription() [answer:%o]',
'send() | calling pc.setRemoteDescription() [answer:%o]',
answer);
answer);


await this._pc.setRemoteDescription(answer);
await this._pc.setRemoteDescription(answer);


// Store in the map.
// Store in the map.
this._mapMidTransceiver.set(localId, transceiver);
this._mapMidTransceiver.set(localId, transceiver);


return {
return {
localId,
localId,
rtpParameters : sendingRtpParameters,
rtpParameters : sendingRtpParameters,
rtpSender : transceiver.sender
rtpSender : transceiver.sender
};
};
}
}


async stopSending(localId: string): Promise<void>
async stopSending(localId: string): Promise<void>
{
{
this.assertSendDirection();
this.assertSendDirection();


logger.debug('stopSending() [localId:%s]', localId);
logger.debug('stopSending() [localId:%s]', localId);


const transceiver = this._mapMidTransceiver.get(localId);
const transceiver = this._mapMidTransceiver.get(localId);


if (!transceiver)
if (!transceiver)
throw new Error('associated RTCRtpTransceiver not found');
throw new Error('associated RTCRtpTransceiver not found');


transceiver.sender.replaceTrack(null);
transceiver.sender.replaceTrack(null);


this._pc.removeTrack(transceiver.sender);
this._pc.removeTrack(transceiver.sender);


const mediaSectionClosed =
const mediaSectionClosed =
this._remoteSdp!.closeMediaSection(transceiver.mid!);
this._remoteSdp!.closeMediaSection(transceiver.mid!);


if (mediaSectionClosed)
if (mediaSectionClosed)
{
{
try
try
{
{
transceiver.stop();
transceiver.stop();
}
}
catch (error)
catch (error)
{}
{}
}
}


const offer = await this._pc.createOffer();
const offer = await this._pc.createOffer();


logger.debug(
logger.debug(
'stopSending() | calling pc.setLocalDescription() [offer:%o]',
'stopSending() | calling pc.setLocalDescription() [offer:%o]',
offer);
offer);


await this._pc.setLocalDescription(offer);
await this._pc.setLocalDescription(offer);


const answer = { type: 'answer', sdp: this._remoteSdp!.getSdp() };
const answer = { type: 'answer', sdp: this._remoteSdp!.getSdp() };


logger.debug(
logger.debug(
'stopSending() | calling pc.setRemoteDescription() [answer:%o]',
'stopSending() | calling pc.setRemoteDescription() [answer:%o]',
answer);
answer);


await this._pc.setRemoteDescription(answer);
await this._pc.setRemoteDescription(answer);


this._mapMidTransceiver.delete(localId);
this._mapMidTransceiver.delete(localId);
}
}


async pauseSending(localId: string): Promise<void>
async pauseSending(localId: string): Promise<void>
{
{
this.assertSendDirection();
this.assertSendDirection();


logger.debug('pauseSending() [localId:%s]', localId);
logger.debug('pauseSending() [localId:%s]', localId);


const transceiver = this._mapMidTransceiver.get(localId);
const transceiver = this._mapMidTransceiver.get(localId);


if (!transceiver)
if (!transceiver)
throw new Error('associated RTCRtpTransceiver not found');
throw new Error('associated RTCRtpTransceiver not found');


transceiver.direction = 'inactive';
transceiver.direction = 'inactive';
this._remoteSdp!.pauseMediaSection(localId);
this._remoteSdp!.pauseMediaSection(localId);


const offer = await this._pc.createOffer();
const offer = await this._pc.createOffer();


logger.debug(
logger.debug(
'pauseSending() | calling pc.setLocalDescription() [offer:%o]',
'pauseSending() | calling pc.setLocalDescription() [offer:%o]',
offer);
offer);


await this._pc.setLocalDescription(offer);
await this._pc.setLocalDescription(offer);


const answer = { type: 'answer', sdp: this._remoteSdp!.getSdp() };
const answer = { type: 'answer', sdp: this._remoteSdp!.getSdp() };


logger.debug(
logger.debug(
'pauseSending() | calling pc.setRemoteDescription() [answer:%o]',
'pauseSending() | calling pc.setRemoteDescription() [answer:%o]',
answer);
answer);


await this._pc.setRemoteDescription(answer);
await this._pc.setRemoteDescription(answer);
}
}


async resumeSending(localId: string): Promise<void>
async resumeSending(localId: string): Promise<void>
{
{
this.assertSendDirection();
this.assertSendDirection();


logger.debug('resumeSending() [localId:%s]', localId);
logger.debug('resumeSending() [localId:%s]', localId);


const transceiver = this._mapMidTransceiver.get(localId);
const transceiver = this._mapMidTransceiver.get(localId);


this._remoteSdp!.resumeSendingMediaSection(localId);
this._remoteSdp!.resumeSendingMediaSection(localId);


if (!transceiver)
if (!transceiver)
throw new Error('associated RTCRtpTransceiver not found');
throw new Error('associated RTCRtpTransceiver not found');


transceiver.direction = 'sendonly';
transceiver.direction = 'sendonly';


const offer = await this._pc.createOffer();
const offer = await this._pc.createOffer();


logger.debug(
logger.debug(
'resumeSending() | calling pc.setLocalDescription() [offer:%o]',
'resumeSending() | calling pc.setLocalDescription() [offer:%o]',
offer);
offer);


await this._pc.setLocalDescription(offer);
await this._pc.setLocalDescription(offer);


const answer = { type: 'answer', sdp: this._remoteSdp!.getSdp() };
const answer = { type: 'answer', sdp: this._remoteSdp!.getSdp() };


logger.debug(
logger.debug(
'resumeSending() | calling pc.setRemoteDescription() [answer:%o]',
'resumeSending() | calling pc.setRemoteDescription() [answer:%o]',
answer);
answer);


await this._pc.setRemoteDescription(answer);
await this._pc.setRemoteDescription(answer);
}
}


async replaceTrack(
async replaceTrack(
localId: string, track: MediaStreamTrack | null
localId: string, track: MediaStreamTrack | null
): Promise<void>
): Promise<void>
{
{
this.assertSendDirection();
this.assertSendDirection();


if (track)
if (track)
{
{
logger.debug(
logger.debug(
'replaceTrack() [localId:%s, track.id:%s]', localId, track.id);
'replaceTrack() [localId:%s, track.id:%s]', localId, track.id);
}
}
else
else
{
{
logger.debug('replaceTrack() [localId:%s, no track]', localId);
logger.debug('replaceTrack() [localId:%s, no track]', localId);
}
}


const transceiver = this._mapMidTransceiver.get(localId);
const transceiver = this._mapMidTransceiver.get(localId);


if (!transceiver)
if (!transceiver)
throw new Error('associated RTCRtpTransceiver not found');
throw new Error('associated RTCRtpTransceiver not found');


await transceiver.sender.replaceTrack(track);
await transceiver.sender.replaceTrack(track);
}
}


async setMaxSpatialLayer(localId: string, spatialLayer: number): Promise<void>
async setMaxSpatialLayer(localId: string, spatialLayer: number): Promise<void>
{
{
this.assertSendDirection();
this.assertSendDirection();


logger.debug(
logger.debug(
'setMaxSpatialLayer() [localId:%s, spatialLayer:%s]',
'setMaxSpatialLayer() [localId:%s, spatialLayer:%s]',
localId, spatialLayer);
localId, spatialLayer);


const transceiver = this._mapMidTransceiver.get(localId);
const transceiver = this._mapMidTransceiver.get(localId);


if (!transceiver)
if (!transceiver)
throw new Error('associated RTCRtpTransceiver not found');
throw new Error('associated RTCRtpTransceiver not found');


const parameters = transceiver.sender.getParameters();
const parameters = transceiver.sender.getParameters();


parameters.encodings.forEach((encoding: RTCRtpEncodingParameters, idx: number) =>
parameters.encodings.forEach((encoding: RTCRtpEncodingParameters, idx: number) =>
{
{
if (idx <= spatialLayer)
if (idx <= spatialLayer)
encoding.active = true;
encoding.active = true;
else
else
encoding.active = false;
encoding.active = false;
});
});


await transceiver.sender.setParameters(parameters);
await transceiver.sender.setParameters(parameters);


this._remoteSdp!.muxMediaSectionSimulcast(localId, parameters.encodings);
this._remoteSdp!.muxMediaSectionSimulcast(localId, parameters.encodings);


const offer = await this._pc.createOffer();
const offer = await this._pc.createOffer();


logger.debug(
logger.debug(
'setMaxSpatialLayer() | calling pc.setLocalDescription() [offer:%o]',
'setMaxSpatialLayer() | calling pc.setLocalDescription() [offer:%o]',
offer);
offer);


await this._pc.setLocalDescription(offer);
await this._pc.setLocalDescription(offer);


const answer = { type: 'answer', sdp: this._remoteSdp!.getSdp() };
const answer = { type: 'answer', sdp: this._remoteSdp!.getSdp() };


logger.debug(
logger.debug(
'setMaxSpatialLayer() | calling pc.setRemoteDescription() [answer:%o]',
'setMaxSpatialLayer() | calling pc.setRemoteDescription() [answer:%o]',
answer);
answer);


await this._pc.setRemoteDescription(answer);
await this._pc.setRemoteDescription(answer);
}
}


async setRtpEncodingParameters(localId: string, params: any): Promise<void>
async setRtpEncodingParameters(localId: string, params: any): Promise<void>
{
{
this.assertSendDirection();
this.assertSendDirection();


logger.debug(
logger.debug(
'setRtpEncodingParameters() [localId:%s, params:%o]',
'setRtpEncodingParameters() [localId:%s, params:%o]',
localId, params);
localId, params);


const transceiver = this._mapMidTransceiver.get(localId);
const transceiver = this._mapMidTransceiver.get(localId);


if (!transceiver)
if (!transceiver)
throw new Error('associated RTCRtpTransceiver not found');
throw new Error('associated RTCRtpTransceiver not found');


const parameters = transceiver.sender.getParameters();
const parameters = transceiver.sender.getParameters();


parameters.encodings.forEach((encoding: RTCRtpEncodingParameters, idx: number) =>
parameters.encodings.forEach((encoding: RTCRtpEncodingParameters, idx: number) =>
{
{
parameters.encodings[idx] = { ...encoding, ...params };
parameters.encodings[idx] = { ...encoding, ...params };
});
});


await transceiver.sender.setParameters(parameters);
await transceiver.sender.setParameters(parameters);


this._remoteSdp!.muxMediaSectionSimulcast(localId, parameters.encodings);
this._remoteSdp!.muxMediaSectionSimulcast(localId, parameters.encodings);


const offer = await this._pc.createOffer();
const offer = await this._pc.createOffer();


logger.debug(
logger.debug(
'setRtpEncodingParameters() | calling pc.setLocalDescription() [offer:%o]',
'setRtpEncodingParameters() | calling pc.setLocalDescription() [offer:%o]',
offer);
offer);


await this._pc.setLocalDescription(offer);
await this._pc.setLocalDescription(offer);


const answer = { type: 'answer', sdp: this._remoteSdp!.getSdp() };
const answer = { type: 'answer', sdp: this._remoteSdp!.getSdp() };


logger.debug(
logger.debug(
'setRtpEncodingParameters() | calling pc.setRemoteDescription() [answer:%o]',
'setRtpEncodingParameters() | calling pc.setRemoteDescription() [answer:%o]',
answer);
answer);


await this._pc.setRemoteDescription(answer);
await this._pc.setRemoteDescription(answer);
}
}


async getSenderStats(localId: string): Promise<RTCStatsReport>
async getSenderStats(localId: string): Promise<RTCStatsReport>
{
{
this.assertSendDirection();
this.assertSendDirection();


const transceiver = this._mapMidTransceiver.get(localId);
const transceiver = this._mapMidTransceiver.get(localId);


if (!transceiver)
if (!transceiver)
throw new Error('associated RTCRtpTransceiver not found');
throw new Error('associated RTCRtpTransceiver not found');


return transceiver.sender.getStats();
return transceiver.sender.getStats();
}
}


async sendDataChannel(
async sendDataChannel(
{
{
ordered,
ordered,
maxPacketLifeTime,
maxPacketLifeTime,
maxRetransmits,
maxRetransmits,
label,
label,
protocol
protocol
}: HandlerSendDataChannelOptions
}: HandlerSendDataChannelOptions
): Promise<HandlerSendDataChannelResult>
): Promise<HandlerSendDataChannelResult>
{
{
this.assertSendDirection();
this.assertSendDirection();


const options =
const options =
{
{
negotiated : true,
negotiated : true,
id : this._nextSendSctpStreamId,
id : this._nextSendSctpStreamId,
ordered,
ordered,
maxPacketLifeTime,
maxPacketLifeTime,
maxRetransmits,
maxRetransmits,
protocol
protocol
};
};


logger.debug('sendDataChannel() [options:%o]', options);
logger.debug('sendDataChannel() [options:%o]', options);


const dataChannel = this._pc.createDataChannel(label, options);
const dataChannel = this._pc.createDataChannel(label, options);


// Increase next id.
// Increase next id.
this._nextSendSctpStreamId =
this._nextSendSctpStreamId =
++this._nextSendSctpStreamId % SCTP_NUM_STREAMS.MIS;
++this._nextSendSctpStreamId % SCTP_NUM_STREAMS.MIS;


// If this is the first DataChannel we need to create the SDP answer with
// If this is the first DataChannel we need to create the SDP answer with
// m=application section.
// m=application section.
if (!this._hasDataChannelMediaSection)
if (!this._hasDataChannelMediaSection)
{
{
const offer = await this._pc.createOffer();
const offer = await this._pc.createOffer();
const localSdpObject = sdpTransform.parse(offer.sdp);
const localSdpObject = sdpTransform.parse(offer.sdp);
const offerMediaObject = localSdpObject.media
const offerMediaObject = localSdpObject.media
.find((m: any) => m.type === 'application');
.find((m: any) => m.type === 'application');


if (!this._transportReady)
if (!this._transportReady)
{
{
await this.setupTransport(
await this.setupTransport(
{
{
localDtlsRole : this._forcedLocalDtlsRole ?? 'client',
localDtlsRole : this._forcedLocalDtlsRole ?? 'client',
localSdpObject
localSdpObject
});
});
}
}


logger.debug(
logger.debug(
'sendDataChannel() | calling pc.setLocalDescription() [offer:%o]',
'sendDataChannel() | calling pc.setLocalDescription() [offer:%o]',
offer);
offer);


await this._pc.setLocalDescription(offer);
await this._pc.setLocalDescription(offer);


this._remoteSdp!.sendSctpAssociation({ offerMediaObject });
this._remoteSdp!.sendSctpAssociation({ offerMediaObject });


const answer = { type: 'answer', sdp: this._remoteSdp!.getSdp() };
const answer = { type: 'answer', sdp: this._remoteSdp!.getSdp() };


logger.debug(
logger.debug(
'sendDataChannel() | calling pc.setRemoteDescription() [answer:%o]',
'sendDataChannel() | calling pc.setRemoteDescription() [answer:%o]',
answer);
answer);


await this._pc.setRemoteDescription(answer);
await this._pc.setRemoteDescription(answer);


this._hasDataChannelMediaSection = true;
this._hasDataChannelMediaSection = true;
}
}


const sctpStreamParameters: SctpStreamParameters =
const sctpStreamParameters: SctpStreamParameters =
{
{
streamId : options.id,
streamId : options.id,
ordered : options.ordered,
ordered : options.ordered,
maxPacketLifeTime : options.maxPacketLifeTime,
maxPacketLifeTime : options.maxPacketLifeTime,
maxRetransmits : options.maxRetransmits
maxRetransmits : options.maxRetransmits
};
};


return { dataChannel, sctpStreamParameters };
return { dataChannel, sctpStreamParameters };
}
}


async receive(
async receive(
optionsList: HandlerReceiveOptions[]
optionsList: HandlerReceiveOptions[]
) : Promise<HandlerReceiveResult[]>
) : Promise<HandlerReceiveResult[]>
{
{
this.assertRecvDirection();
this.assertRecvDirection();


const results: HandlerReceiveResult[] = [];
const results: HandlerReceiveResult[] = [];
const mapLocalId: Map<string, string> = new Map();
const mapLocalId: Map<string, string> = new Map();


for (const options of optionsList)
for (const options of optionsList)
{
{
const { trackId, kind, rtpParameters, streamId } = options;
const { trackId, kind, rtpParameters, streamId } = options;


logger.debug('receive() [trackId:%s, kind:%s]', trackId, kind);
logger.debug('receive() [trackId:%s, kind:%s]', trackId, kind);


const localId = rtpParameters.mid || String(this._mapMidTransceiver.size);
const localId = rtpParameters.mid || String(this._mapMidTransceiver.size);


mapLocalId.set(trackId, localId);
mapLocalId.set(trackId, localId);


this._remoteSdp!.receive(
this._remoteSdp!.receive(
{
{
mid : localId,
mid : localId,
kind,
kind,
offerRtpParameters : rtpParameters,
offerRtpParameters : rtpParameters,
streamId : streamId || rtpParameters.rtcp!.cname!,
streamId : streamId || rtpParameters.rtcp!.cname!,
trackId
trackId
});
});
}
}


const offer = { type: 'offer', sdp: this._remoteSdp!.getSdp() };
const offer = { type: 'offer', sdp: this._remoteSdp!.getSdp() };


logger.debug(
logger.debug(
'receive() | calling pc.setRemoteDescription() [offer:%o]',
'receive() | calling pc.setRemoteDescription() [offer:%o]',
offer);
offer);


await this._pc.setRemoteDescription(offer);
await this._pc.setRemoteDescription(offer);


let answer = await this._pc.createAnswer();
let answer = await this._pc.createAnswer();
const localSdpObject = sdpTransform.parse(answer.sdp);
const localSdpObject = sdpTransform.parse(answer.sdp);


for (const options of optionsList)
for (const options of optionsList)
{
{
const { trackId, rtpParameters } = options;
const { trackId, rtpParameters } = options;
const localId = mapLocalId.get(trackId);
const localId = mapLocalId.get(trackId);
const answerMediaObject = localSdpObject.media
const answerMediaObject = localSdpObject.media
.find((m: any) => String(m.mid) === localId);
.find((m: any) => String(m.mid) === localId);


// May need to modify codec parameters in the answer based on codec
// May need to modify codec parameters in the answer based on codec
// parameters in the offer.
// parameters in the offer.
sdpCommonUtils.applyCodecParameters(
sdpCommonUtils.applyCodecParameters(
{
{
offerRtpParameters : rtpParameters,
offerRtpParameters : rtpParameters,
answerMediaObject
answerMediaObject
});
});
}
}


answer = { type: 'answer', sdp: sdpTransform.write(localSdpObject) };
answer = { type: 'answer', sdp: sdpTransform.write(localSdpObject) };


if (!this._transportReady)
if (!this._transportReady)
{
{
await this.setupTransport(
await this.setupTransport(
{
{
localDtlsRole : this._forcedLocalDtlsRole ?? 'client',
localDtlsRole : this._forcedLocalDtlsRole ?? 'client',
localSdpObject
localSdpObject
});
});
}
}


logger.debug(
logger.debug(
'receive() | calling pc.setLocalDescription() [answer:%o]',
'receive() | calling pc.setLocalDescription() [answer:%o]',
answer);
answer);


await this._pc.setLocalDescription(answer);
await this._pc.setLocalDescription(answer);


for (const options of optionsList)
for (const options of optionsList)
{
{
const { trackId } = options;
const { trackId } = options;
const localId = mapLocalId.get(trackId)!;
const localId = mapLocalId.get(trackId)!;
const transceiver = this._pc.getTransceivers()
const transceiver = this._pc.getTransceivers()
.find((t: RTCRtpTransceiver) => t.mid === localId);
.find((t: RTCRtpTransceiver) => t.mid === localId);


if (!transceiver)
if (!transceiver)
{
{
throw new Error('new RTCRtpTransceiver not found');
throw new Error('new RTCRtpTransceiver not found');
}
}
else
else
{
{
// Store in the map.
// Store in the map.
this._mapMidTransceiver.set(localId, transceiver);
this._mapMidTransceiver.set(localId, transceiver);


results.push({
results.push({
localId,
localId,
track : transceiver.receiver.track,
track : transceiver.receiver.track,
rtpReceiver : transceiver.receiver
rtpReceiver : transceiver.receiver
});
});
}
}
}
}


return results;
return results;
}
}


async stopReceiving(localIds: string[]): Promise<void>
async stopReceiving(localIds: string[]): Promise<void>
{
{
this.assertRecvDirection();
this.assertRecvDirection();


for (const localId of localIds)
for (const localId of localIds)
{
{
logger.debug('stopReceiving() [localId:%s]', localId);
logger.debug('stopReceiving() [localId:%s]', localId);


const transceiver = this._mapMidTransceiver.get(localId);
const transceiver = this._mapMidTransceiver.get(localId);


if (!transceiver)
if (!transceiver)
throw new Error('associated RTCRtpTransceiver not found');
throw new Error('associated RTCRtpTransceiver not found');


this._remoteSdp!.closeMediaSection(transceiver.mid!);
this._remoteSdp!.closeMediaSection(transceiver.mid!);
}
}


const offer = { type: 'offer', sdp: this._remoteSdp!.getSdp() };
const offer = { type: 'offer', sdp: this._remoteSdp!.getSdp() };


logger.debug(
logger.debug(
'stopReceiving() | calling pc.setRemoteDescription() [offer:%o]',
'stopReceiving() | calling pc.setRemoteDescription() [offer:%o]',
offer);
offer);


await this._pc.setRemoteDescription(offer);
await this._pc.setRemoteDescription(offer);


const answer = await this._pc.createAnswer();
const answer = await this._pc.createAnswer();


logger.debug(
logger.debug(
'stopReceiving() | calling pc.setLocalDescription() [answer:%o]',
'stopReceiving() | calling pc.setLocalDescription() [answer:%o]',
answer);
answer);


await this._pc.setLocalDescription(answer);
await this._pc.setLocalDescription(answer);


for (const localId of localIds)
for (const localId of localIds)
{
{
this._mapMidTransceiver.delete(localId);
this._mapMidTransceiver.delete(localId);
}
}
}
}


async pauseReceiving(localIds: string[]): Promise<void>
async pauseReceiving(localIds: string[]): Promise<void>
{
{
this.assertRecvDirection();
this.assertRecvDirection();


for (const localId of localIds)
for (const localId of localIds)
{
{
logger.debug('pauseReceiving() [localId:%s]', localId);
logger.debug('pauseReceiving() [localId:%s]', localId);


const transceiver = this._mapMidTransceiver.get(localId);
const transceiver = this._mapMidTransceiver.get(localId);


if (!transceiver)
if (!transceiver)
throw new Error('associated RTCRtpTransceiver not found');
throw new Error('associated RTCRtpTransceiver not found');


transceiver.direction = 'inactive';
transceiver.direction = 'inactive';
this._remoteSdp!.pauseMediaSection(localId);
this._remoteSdp!.pauseMediaSection(localId);
}
}


const offer = { type: 'offer', sdp: this._remoteSdp!.getSdp() };
const offer = { type: 'offer', sdp: this._remoteSdp!.getSdp() };


logger.debug(
logger.debug(
'pauseReceiving() | calling pc.setRemoteDescription() [offer:%o]',
'pauseReceiving() | calling pc.setRemoteDescription() [offer:%o]',
offer);
offer);


await this._pc.setRemoteDescription(offer);
await this._pc.setRemoteDescription(offer);


const answer = await this._pc.createAnswer();
const answer = await this._pc.createAnswer();


logger.debug(
logger.debug(
'pauseReceiving() | calling pc.setLocalDescription() [answer:%o]',
'pauseReceiving() | calling pc.setLocalDescription() [answer:%o]',
answer);
answer);


await this._pc.setLocalDescription(answer);
await this._pc.setLocalDescription(answer);
}
}


async resumeReceiving(localIds: string[]): Promise<void>
async resumeReceiving(localIds: string[]): Promise<void>
{
{
this.assertRecvDirection();
this.assertRecvDirection();


for (const localId of localIds)
for (const localId of localIds)
{
{
logger.debug('resumeReceiving() [localId:%s]', localId);
logger.debug('resumeReceiving() [localId:%s]', localId);


const transceiver = this._mapMidTransceiver.get(localId);
const transceiver = this._mapMidTransceiver.get(localId);


if (!transceiver)
if (!transceiver)
throw new Error('associated RTCRtpTransceiver not found');
throw new Error('associated RTCRtpTransceiver not found');


transceiver.direction = 'recvonly';
transceiver.direction = 'recvonly';
this._remoteSdp!.resumeReceivingMediaSection(localId);
this._remoteSdp!.resumeReceivingMediaSection(localId);
}
}


const offer = { type: 'offer', sdp: this._remoteSdp!.getSdp() };
const offer = { type: 'offer', sdp: this._remoteSdp!.getSdp() };


logger.debug(
logger.debug(
'resumeReceiving() | calling pc.setRemoteDescription() [offer:%o]',
'resumeReceiving() | calling pc.setRemoteDescription() [offer:%o]',
offer);
offer);


await this._pc.setRemoteDescription(offer);
await this._pc.setRemoteDescription(offer);


const answer = await this._pc.createAnswer();
const answer = await this._pc.createAnswer();


logger.debug(
logger.debug(
'resumeReceiving() | calling pc.setLocalDescription() [answer:%o]',
'resumeReceiving() | calling pc.setLocalDescription() [answer:%o]',
answer);
answer);


await this._pc.setLocalDescription(answer);
await this._pc.setLocalDescription(answer);
}
}


async getReceiverStats(localId: string): Promise<RTCStatsReport>
async getReceiverStats(localId: string): Promise<RTCStatsReport>
{
{
this.assertRecvDirection();
this.assertRecvDirection();


const transceiver = this._mapMidTransceiver.get(localId);
const transceiver = this._mapMidTransceiver.get(localId);


if (!transceiver)
if (!transceiver)
throw new Error('associated RTCRtpTransceiver not found');
throw new Error('associated RTCRtpTransceiver not found');


return transceiver.receiver.getStats();
return transceiver.receiver.getStats();
}
}


async receiveDataChannel(
async receiveDataChannel(
{ sctpStreamParameters, label, protocol }: HandlerReceiveDataChannelOptions
{ sctpStreamParameters, label, protocol }: HandlerReceiveDataChannelOptions
): Promise<HandlerReceiveDataChannelResult>
): Promise<HandlerReceiveDataChannelResult>
{
{
this.assertRecvDirection();
this.assertRecvDirection();


const {
const {
streamId,
streamId,
ordered,
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<void>
{
if (!localSdpObject)
localSdpObject = sdpTransform.parse(this._pc.localD