mediasoup-client: Chrome74 vs Chrome111

Created Diff never expires
67 removals
Words removed168
Total words2156
Words removed (%)7.79
1012 lines
79 additions
Words added140
Total words2128
Words added (%)6.58
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