mediasoup-client
Version:
mediasoup client side TypeScript library
610 lines (609 loc) • 25.5 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Chrome67 = void 0;
const sdpTransform = require("sdp-transform");
const Logger_1 = require("../Logger");
const utils = require("../utils");
const ortc = require("../ortc");
const sdpCommonUtils = require("./sdp/commonUtils");
const sdpPlanBUtils = require("./sdp/planBUtils");
const HandlerInterface_1 = require("./HandlerInterface");
const RemoteSdp_1 = require("./sdp/RemoteSdp");
const logger = new Logger_1.Logger('Chrome67');
const NAME = 'Chrome67';
const SCTP_NUM_STREAMS = { OS: 1024, MIS: 1024 };
class Chrome67 extends HandlerInterface_1.HandlerInterface {
// Handler direction.
_direction;
// Remote SDP handler.
_remoteSdp;
// Generic sending RTP parameters for audio and video.
_sendingRtpParametersByKind;
// Generic sending RTP parameters for audio and video suitable for the SDP
// remote answer.
_sendingRemoteRtpParametersByKind;
// Initial server side DTLS role. If not 'auto', it will force the opposite
// value in client side.
_forcedLocalDtlsRole;
// RTCPeerConnection instance.
_pc;
// Local stream for sending.
_sendStream = new MediaStream();
// Map of RTCRtpSender indexed by localId.
_mapSendLocalIdRtpSender = new Map();
// Next sending localId.
_nextSendLocalId = 0;
// Map of MID, RTP parameters and RTCRtpReceiver indexed by local id.
// Value is an Object with mid, rtpParameters and rtpReceiver.
_mapRecvLocalIdInfo = new Map();
// Whether a DataChannel m=application section has been created.
_hasDataChannelMediaSection = false;
// Sending DataChannel id value counter. Incremented for each new DataChannel.
_nextSendSctpStreamId = 0;
// Got transport local and remote parameters.
_transportReady = false;
/**
* Creates a factory function.
*/
static createFactory() {
return () => new Chrome67();
}
constructor() {
super();
}
get name() {
return NAME;
}
close() {
logger.debug('close()');
// Close RTCPeerConnection.
if (this._pc) {
try {
this._pc.close();
}
catch (error) { }
}
this.emit('@close');
}
async getNativeRtpCapabilities() {
logger.debug('getNativeRtpCapabilities()');
const pc = new RTCPeerConnection({
iceServers: [],
iceTransportPolicy: 'all',
bundlePolicy: 'max-bundle',
rtcpMuxPolicy: 'require',
sdpSemantics: 'plan-b',
});
try {
const offer = await pc.createOffer({
offerToReceiveAudio: true,
offerToReceiveVideo: true,
});
try {
pc.close();
}
catch (error) { }
const sdpObject = sdpTransform.parse(offer.sdp);
const nativeRtpCapabilities = sdpCommonUtils.extractRtpCapabilities({
sdpObject,
});
return nativeRtpCapabilities;
}
catch (error) {
try {
pc.close();
}
catch (error2) { }
throw error;
}
}
async getNativeSctpCapabilities() {
logger.debug('getNativeSctpCapabilities()');
return {
numStreams: SCTP_NUM_STREAMS,
};
}
run({ direction, iceParameters, iceCandidates, dtlsParameters, sctpParameters, iceServers, iceTransportPolicy, additionalSettings, proprietaryConstraints, extendedRtpCapabilities, }) {
logger.debug('run()');
this._direction = direction;
this._remoteSdp = new RemoteSdp_1.RemoteSdp({
iceParameters,
iceCandidates,
dtlsParameters,
sctpParameters,
planB: true,
});
this._sendingRtpParametersByKind = {
audio: ortc.getSendingRtpParameters('audio', extendedRtpCapabilities),
video: ortc.getSendingRtpParameters('video', extendedRtpCapabilities),
};
this._sendingRemoteRtpParametersByKind = {
audio: ortc.getSendingRemoteRtpParameters('audio', extendedRtpCapabilities),
video: ortc.getSendingRemoteRtpParameters('video', extendedRtpCapabilities),
};
if (dtlsParameters.role && dtlsParameters.role !== 'auto') {
this._forcedLocalDtlsRole =
dtlsParameters.role === 'server' ? 'client' : 'server';
}
this._pc = new RTCPeerConnection({
iceServers: iceServers ?? [],
iceTransportPolicy: iceTransportPolicy ?? 'all',
bundlePolicy: 'max-bundle',
rtcpMuxPolicy: 'require',
sdpSemantics: 'plan-b',
...additionalSettings,
}, proprietaryConstraints);
this._pc.addEventListener('icegatheringstatechange', () => {
this.emit('@icegatheringstatechange', this._pc.iceGatheringState);
});
this._pc.addEventListener('icecandidateerror', (event) => {
this.emit('@icecandidateerror', event);
});
if (this._pc.connectionState) {
this._pc.addEventListener('connectionstatechange', () => {
this.emit('@connectionstatechange', this._pc.connectionState);
});
}
else {
this._pc.addEventListener('iceconnectionstatechange', () => {
logger.warn('run() | pc.connectionState not supported, using pc.iceConnectionState');
switch (this._pc.iceConnectionState) {
case 'checking': {
this.emit('@connectionstatechange', 'connecting');
break;
}
case 'connected':
case 'completed': {
this.emit('@connectionstatechange', 'connected');
break;
}
case 'failed': {
this.emit('@connectionstatechange', 'failed');
break;
}
case 'disconnected': {
this.emit('@connectionstatechange', 'disconnected');
break;
}
case 'closed': {
this.emit('@connectionstatechange', 'closed');
break;
}
}
});
}
}
async updateIceServers(iceServers) {
logger.debug('updateIceServers()');
const configuration = this._pc.getConfiguration();
configuration.iceServers = iceServers;
this._pc.setConfiguration(configuration);
}
async restartIce(iceParameters) {
logger.debug('restartIce()');
// Provide the remote SDP handler with new remote ICE parameters.
this._remoteSdp.updateIceParameters(iceParameters);
if (!this._transportReady) {
return;
}
if (this._direction === 'send') {
const offer = await this._pc.createOffer({ iceRestart: true });
logger.debug('restartIce() | calling pc.setLocalDescription() [offer:%o]', offer);
await this._pc.setLocalDescription(offer);
const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() };
logger.debug('restartIce() | calling pc.setRemoteDescription() [answer:%o]', answer);
await this._pc.setRemoteDescription(answer);
}
else {
const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() };
logger.debug('restartIce() | calling pc.setRemoteDescription() [offer:%o]', offer);
await this._pc.setRemoteDescription(offer);
const answer = await this._pc.createAnswer();
logger.debug('restartIce() | calling pc.setLocalDescription() [answer:%o]', answer);
await this._pc.setLocalDescription(answer);
}
}
async getTransportStats() {
return this._pc.getStats();
}
async send({ track, encodings, codecOptions, codec, }) {
this.assertSendDirection();
logger.debug('send() [kind:%s, track.id:%s]', track.kind, track.id);
if (codec) {
logger.warn('send() | codec selection is not available in %s handler', this.name);
}
this._sendStream.addTrack(track);
this._pc.addTrack(track, this._sendStream);
let offer = await this._pc.createOffer();
let localSdpObject = sdpTransform.parse(offer.sdp);
// @ts-expect-error --- sdpTransform.SessionDescription type doesn't
// define extmapAllowMixed field.
if (localSdpObject.extmapAllowMixed) {
this._remoteSdp.setSessionExtmapAllowMixed();
}
let offerMediaObject;
const sendingRtpParameters = utils.clone(this._sendingRtpParametersByKind[track.kind]);
sendingRtpParameters.codecs = ortc.reduceCodecs(sendingRtpParameters.codecs);
const sendingRemoteRtpParameters = utils.clone(this._sendingRemoteRtpParametersByKind[track.kind]);
sendingRemoteRtpParameters.codecs = ortc.reduceCodecs(sendingRemoteRtpParameters.codecs);
if (!this._transportReady) {
await this.setupTransport({
localDtlsRole: this._forcedLocalDtlsRole ?? 'client',
localSdpObject,
});
}
if (track.kind === 'video' && encodings && encodings.length > 1) {
logger.debug('send() | enabling simulcast');
localSdpObject = sdpTransform.parse(offer.sdp);
offerMediaObject = localSdpObject.media.find((m) => m.type === 'video');
sdpPlanBUtils.addLegacySimulcast({
offerMediaObject,
track,
numStreams: encodings.length,
});
offer = { type: 'offer', sdp: sdpTransform.write(localSdpObject) };
}
logger.debug('send() | calling pc.setLocalDescription() [offer:%o]', offer);
await this._pc.setLocalDescription(offer);
localSdpObject = sdpTransform.parse(this._pc.localDescription.sdp);
offerMediaObject = localSdpObject.media.find((m) => m.type === track.kind);
// Set RTCP CNAME.
sendingRtpParameters.rtcp.cname = sdpCommonUtils.getCname({
offerMediaObject,
});
// Set RTP encodings.
sendingRtpParameters.encodings = sdpPlanBUtils.getRtpEncodings({
offerMediaObject,
track,
});
// Complete encodings with given values.
if (encodings) {
for (let idx = 0; idx < sendingRtpParameters.encodings.length; ++idx) {
if (encodings[idx]) {
Object.assign(sendingRtpParameters.encodings[idx], encodings[idx]);
}
}
}
// If VP8 and there is effective simulcast, add scalabilityMode to each
// encoding.
if (sendingRtpParameters.encodings.length > 1 &&
sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/vp8') {
for (const encoding of sendingRtpParameters.encodings) {
encoding.scalabilityMode = 'L1T3';
}
}
this._remoteSdp.send({
offerMediaObject,
offerRtpParameters: sendingRtpParameters,
answerRtpParameters: sendingRemoteRtpParameters,
codecOptions,
});
const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() };
logger.debug('send() | calling pc.setRemoteDescription() [answer:%o]', answer);
await this._pc.setRemoteDescription(answer);
const localId = String(this._nextSendLocalId);
this._nextSendLocalId++;
const rtpSender = this._pc
.getSenders()
.find((s) => s.track === track);
// Insert into the map.
this._mapSendLocalIdRtpSender.set(localId, rtpSender);
return {
localId: localId,
rtpParameters: sendingRtpParameters,
rtpSender,
};
}
async stopSending(localId) {
this.assertSendDirection();
logger.debug('stopSending() [localId:%s]', localId);
const rtpSender = this._mapSendLocalIdRtpSender.get(localId);
if (!rtpSender) {
throw new Error('associated RTCRtpSender not found');
}
this._pc.removeTrack(rtpSender);
if (rtpSender.track) {
this._sendStream.removeTrack(rtpSender.track);
}
this._mapSendLocalIdRtpSender.delete(localId);
const offer = await this._pc.createOffer();
logger.debug('stopSending() | calling pc.setLocalDescription() [offer:%o]', offer);
try {
await this._pc.setLocalDescription(offer);
}
catch (error) {
// NOTE: If there are no sending tracks, setLocalDescription() will fail with
// "Failed to create channels". If so, ignore it.
if (this._sendStream.getTracks().length === 0) {
logger.warn('stopSending() | ignoring expected error due no sending tracks: %s', error.toString());
return;
}
throw error;
}
if (this._pc.signalingState === 'stable') {
return;
}
const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() };
logger.debug('stopSending() | calling pc.setRemoteDescription() [answer:%o]', answer);
await this._pc.setRemoteDescription(answer);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async pauseSending(localId) {
// Unimplemented.
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async resumeSending(localId) {
// Unimplemented.
}
async replaceTrack(localId, track) {
this.assertSendDirection();
if (track) {
logger.debug('replaceTrack() [localId:%s, track.id:%s]', localId, track.id);
}
else {
logger.debug('replaceTrack() [localId:%s, no track]', localId);
}
const rtpSender = this._mapSendLocalIdRtpSender.get(localId);
if (!rtpSender) {
throw new Error('associated RTCRtpSender not found');
}
const oldTrack = rtpSender.track;
await rtpSender.replaceTrack(track);
// Remove the old track from the local stream.
if (oldTrack) {
this._sendStream.removeTrack(oldTrack);
}
// Add the new track to the local stream.
if (track) {
this._sendStream.addTrack(track);
}
}
async setMaxSpatialLayer(localId, spatialLayer) {
this.assertSendDirection();
logger.debug('setMaxSpatialLayer() [localId:%s, spatialLayer:%s]', localId, spatialLayer);
const rtpSender = this._mapSendLocalIdRtpSender.get(localId);
if (!rtpSender) {
throw new Error('associated RTCRtpSender not found');
}
const parameters = rtpSender.getParameters();
parameters.encodings.forEach((encoding, idx) => {
if (idx <= spatialLayer) {
encoding.active = true;
}
else {
encoding.active = false;
}
});
await rtpSender.setParameters(parameters);
}
async setRtpEncodingParameters(localId, params) {
this.assertSendDirection();
logger.debug('setRtpEncodingParameters() [localId:%s, params:%o]', localId, params);
const rtpSender = this._mapSendLocalIdRtpSender.get(localId);
if (!rtpSender) {
throw new Error('associated RTCRtpSender not found');
}
const parameters = rtpSender.getParameters();
parameters.encodings.forEach((encoding, idx) => {
parameters.encodings[idx] = { ...encoding, ...params };
});
await rtpSender.setParameters(parameters);
}
async getSenderStats(localId) {
this.assertSendDirection();
const rtpSender = this._mapSendLocalIdRtpSender.get(localId);
if (!rtpSender) {
throw new Error('associated RTCRtpSender not found');
}
return rtpSender.getStats();
}
async sendDataChannel({ ordered, maxPacketLifeTime, maxRetransmits, label, protocol, }) {
this.assertSendDirection();
const options = {
negotiated: true,
id: this._nextSendSctpStreamId,
ordered,
maxPacketLifeTime,
maxRetransmitTime: maxPacketLifeTime, // NOTE: Old spec.
maxRetransmits,
protocol,
};
logger.debug('sendDataChannel() [options:%o]', options);
const dataChannel = this._pc.createDataChannel(label, options);
// Increase next id.
this._nextSendSctpStreamId =
++this._nextSendSctpStreamId % SCTP_NUM_STREAMS.MIS;
// If this is the first DataChannel we need to create the SDP answer with
// m=application section.
if (!this._hasDataChannelMediaSection) {
const offer = await this._pc.createOffer();
const localSdpObject = sdpTransform.parse(offer.sdp);
const offerMediaObject = localSdpObject.media.find((m) => m.type === 'application');
if (!this._transportReady) {
await this.setupTransport({
localDtlsRole: this._forcedLocalDtlsRole ?? 'client',
localSdpObject,
});
}
logger.debug('sendDataChannel() | calling pc.setLocalDescription() [offer:%o]', offer);
await this._pc.setLocalDescription(offer);
this._remoteSdp.sendSctpAssociation({ offerMediaObject });
const answer = { type: 'answer', sdp: this._remoteSdp.getSdp() };
logger.debug('sendDataChannel() | calling pc.setRemoteDescription() [answer:%o]', answer);
await this._pc.setRemoteDescription(answer);
this._hasDataChannelMediaSection = true;
}
const sctpStreamParameters = {
streamId: options.id,
ordered: options.ordered,
maxPacketLifeTime: options.maxPacketLifeTime,
maxRetransmits: options.maxRetransmits,
};
return { dataChannel, sctpStreamParameters };
}
async receive(optionsList) {
this.assertRecvDirection();
const results = [];
for (const options of optionsList) {
const { trackId, kind, rtpParameters, streamId } = options;
logger.debug('receive() [trackId:%s, kind:%s]', trackId, kind);
const mid = kind;
this._remoteSdp.receive({
mid,
kind,
offerRtpParameters: rtpParameters,
streamId: streamId ?? rtpParameters.rtcp.cname,
trackId,
});
}
const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() };
logger.debug('receive() | calling pc.setRemoteDescription() [offer:%o]', offer);
await this._pc.setRemoteDescription(offer);
let answer = await this._pc.createAnswer();
const localSdpObject = sdpTransform.parse(answer.sdp);
for (const options of optionsList) {
const { kind, rtpParameters } = options;
const mid = kind;
const answerMediaObject = localSdpObject.media.find((m) => String(m.mid) === mid);
// May need to modify codec parameters in the answer based on codec
// parameters in the offer.
sdpCommonUtils.applyCodecParameters({
offerRtpParameters: rtpParameters,
answerMediaObject,
});
}
answer = { type: 'answer', sdp: sdpTransform.write(localSdpObject) };
if (!this._transportReady) {
await this.setupTransport({
localDtlsRole: this._forcedLocalDtlsRole ?? 'client',
localSdpObject,
});
}
logger.debug('receive() | calling pc.setLocalDescription() [answer:%o]', answer);
await this._pc.setLocalDescription(answer);
for (const options of optionsList) {
const { kind, trackId, rtpParameters } = options;
const localId = trackId;
const mid = kind;
const rtpReceiver = this._pc
.getReceivers()
.find((r) => r.track && r.track.id === localId);
if (!rtpReceiver) {
throw new Error('new RTCRtpReceiver not');
}
// Insert into the map.
this._mapRecvLocalIdInfo.set(localId, {
mid,
rtpParameters,
rtpReceiver,
});
results.push({
localId,
track: rtpReceiver.track,
rtpReceiver,
});
}
return results;
}
async stopReceiving(localIds) {
this.assertRecvDirection();
for (const localId of localIds) {
logger.debug('stopReceiving() [localId:%s]', localId);
const { mid, rtpParameters } = this._mapRecvLocalIdInfo.get(localId) ?? {};
// Remove from the map.
this._mapRecvLocalIdInfo.delete(localId);
this._remoteSdp.planBStopReceiving({
mid: mid,
offerRtpParameters: rtpParameters,
});
}
const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() };
logger.debug('stopReceiving() | calling pc.setRemoteDescription() [offer:%o]', offer);
await this._pc.setRemoteDescription(offer);
const answer = await this._pc.createAnswer();
logger.debug('stopReceiving() | calling pc.setLocalDescription() [answer:%o]', answer);
await this._pc.setLocalDescription(answer);
}
async pauseReceiving(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
localIds) {
// Unimplemented.
}
async resumeReceiving(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
localIds) {
// Unimplemented.
}
async getReceiverStats(localId) {
this.assertRecvDirection();
const { rtpReceiver } = this._mapRecvLocalIdInfo.get(localId) ?? {};
if (!rtpReceiver) {
throw new Error('associated RTCRtpReceiver not found');
}
return rtpReceiver.getStats();
}
async receiveDataChannel({ sctpStreamParameters, label, protocol, }) {
this.assertRecvDirection();
const { streamId, ordered, maxPacketLifeTime, maxRetransmits } = sctpStreamParameters;
const options = {
negotiated: true,
id: streamId,
ordered,
maxPacketLifeTime,
maxRetransmitTime: maxPacketLifeTime, // NOTE: Old spec.
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({ oldDataChannelSpec: true });
const offer = { type: 'offer', sdp: this._remoteSdp.getSdp() };
logger.debug('receiveDataChannel() | calling pc.setRemoteDescription() [offer:%o]', offer);
await this._pc.setRemoteDescription(offer);
const answer = await this._pc.createAnswer();
if (!this._transportReady) {
const localSdpObject = sdpTransform.parse(answer.sdp);
await this.setupTransport({
localDtlsRole: this._forcedLocalDtlsRole ?? 'client',
localSdpObject,
});
}
logger.debug('receiveDataChannel() | calling pc.setRemoteDescription() [answer:%o]', answer);
await this._pc.setLocalDescription(answer);
this._hasDataChannelMediaSection = true;
}
return { dataChannel };
}
async setupTransport({ localDtlsRole, localSdpObject, }) {
if (!localSdpObject) {
localSdpObject = sdpTransform.parse(this._pc.localDescription.sdp);
}
// Get our local DTLS parameters.
const dtlsParameters = sdpCommonUtils.extractDtlsParameters({
sdpObject: localSdpObject,
});
// Set our DTLS role.
dtlsParameters.role = localDtlsRole;
// Update the remote DTLS role in the SDP.
this._remoteSdp.updateDtlsRole(localDtlsRole === 'client' ? 'server' : 'client');
// Need to tell the remote transport about our parameters.
await new Promise((resolve, reject) => {
this.safeEmit('@connect', { dtlsParameters }, resolve, reject);
});
this._transportReady = true;
}
assertSendDirection() {
if (this._direction !== 'send') {
throw new Error('method can just be called for handlers with "send" direction');
}
}
assertRecvDirection() {
if (this._direction !== 'recv') {
throw new Error('method can just be called for handlers with "recv" direction');
}
}
}
exports.Chrome67 = Chrome67;