2489 lines
60 KiB
JavaScript
2489 lines
60 KiB
JavaScript
/* global __filename, RTCIceGatherer, RTCIceTransport, RTCDtlsTransport,
|
|
RTCRtpSender, RTCRtpReceiver */
|
|
|
|
import yaeti from 'yaeti';
|
|
import Logger from '../Logger';
|
|
import RTCSessionDescription from './RTCSessionDescription';
|
|
import * as utils from '../utils';
|
|
import * as ortcUtils from './ortcUtils';
|
|
import { InvalidStateError } from './errors';
|
|
|
|
const logger = new Logger('edge/RTCPeerConnection');
|
|
|
|
const RTCSignalingState = {
|
|
stable: 'stable',
|
|
haveLocalOffer: 'have-local-offer',
|
|
haveRemoteOffer: 'have-remote-offer',
|
|
closed: 'closed'
|
|
};
|
|
|
|
const RTCIceGatheringState = {
|
|
new: 'new',
|
|
gathering: 'gathering',
|
|
complete: 'complete'
|
|
};
|
|
|
|
const CNAME = `jitsi-ortc-cname-${utils.randomNumber()}`;
|
|
|
|
/**
|
|
* RTCPeerConnection shim for ORTC based endpoints (such as Edge).
|
|
*
|
|
* The interface is based on the W3C specification of 2015, which matches
|
|
* the implementation of Chrome nowadays:
|
|
*
|
|
* https://www.w3.org/TR/2015/WD-webrtc-20150210/
|
|
*
|
|
* It also implements Plan-B for multi-stream, and assumes single BUNDLEd
|
|
* transport and rtcp-mux.
|
|
*/
|
|
export default class ortcRTCPeerConnection extends yaeti.EventTarget {
|
|
/**
|
|
*/
|
|
constructor(pcConfig) {
|
|
super();
|
|
|
|
logger.debug('constructor() pcConfig:', pcConfig);
|
|
|
|
// Buffered local ICE candidates (in WebRTC format).
|
|
// @type {sequence<RTCIceCandidate>}
|
|
this._bufferedIceCandidates = [];
|
|
|
|
// Closed flag.
|
|
// @type {Boolean}
|
|
this._closed = false;
|
|
|
|
// RTCDtlsTransport.
|
|
// @type {RTCDtlsTransport}
|
|
this._dtlsTransport = null;
|
|
|
|
// RTCIceGatherer.
|
|
// @type {RTCIceGatherer}
|
|
this._iceGatherer = null;
|
|
|
|
// RTCPeerConnection iceGatheringState.
|
|
// NOTE: This should not be needed, but Edge does not implement
|
|
// iceGatherer.state.
|
|
// @type {RTCIceGatheringState}
|
|
this._iceGatheringState = RTCIceGatheringState.new;
|
|
|
|
// RTCIceTransport.
|
|
// @type {RTCIceTransport}
|
|
this._iceTransport = null;
|
|
|
|
// Local RTP capabilities (filtered with remote ones).
|
|
// @type {RTCRtpCapabilities}
|
|
this._localCapabilities = null;
|
|
|
|
// Local RTCSessionDescription.
|
|
// @type {RTCSessionDescription}
|
|
this._localDescription = null;
|
|
|
|
// Map with info regarding local media.
|
|
// - index: MediaStreamTrack.id
|
|
// - value: Object
|
|
// - rtpSender: Associated RTCRtpSender instance
|
|
// - stream: Associated MediaStream instance
|
|
// - ssrc: Provisional or definitive SSRC
|
|
// - rtxSsrc: Provisional or definitive SSRC for RTX
|
|
// - sending: Boolean indicating whether rtpSender.send() was called.
|
|
this._localTrackInfos = new Map();
|
|
|
|
// Ordered Map with MID as key and kind as value.
|
|
// @type {map<String, String>}
|
|
this._mids = new Map();
|
|
|
|
// Remote RTCSessionDescription.
|
|
// @type {RTCSessionDescription}
|
|
this._remoteDescription = null;
|
|
|
|
// Map of remote streams.
|
|
// - index: MediaStream.jitsiRemoteId (as signaled in remote SDP)
|
|
// - value: MediaStream (locally generated so id does not match)
|
|
// @type {map<Number, MediaStream>}
|
|
this._remoteStreams = new Map();
|
|
|
|
// Map with info about receiving media.
|
|
// - index: Media SSRC
|
|
// - value: Object
|
|
// - kind: 'audio' / 'video'
|
|
// - ssrc: Media SSRC
|
|
// - rtxSsrc: RTX SSRC (may be unset)
|
|
// - streamId: MediaStream.jitsiRemoteId
|
|
// - trackId: MediaStreamTrack.jitsiRemoteId
|
|
// - cname: CNAME
|
|
// - stream: MediaStream
|
|
// - track: MediaStreamTrack
|
|
// - rtpReceiver: Associated RTCRtpReceiver instance
|
|
// @type {map<Number, Object>}
|
|
this._remoteTrackInfos = new Map();
|
|
|
|
// Local SDP global fields.
|
|
this._sdpGlobalFields = {
|
|
id: utils.randomNumber(),
|
|
version: 0
|
|
};
|
|
|
|
// RTCPeerConnection signalingState.
|
|
// @type {RTCSignalingState}
|
|
this._signalingState = RTCSignalingState.stable;
|
|
|
|
// Create the RTCIceGatherer.
|
|
this._setIceGatherer(pcConfig);
|
|
|
|
// Create the RTCIceTransport.
|
|
this._setIceTransport(this._iceGatherer);
|
|
|
|
// Create the RTCDtlsTransport.
|
|
this._setDtlsTransport(this._iceTransport);
|
|
}
|
|
|
|
/**
|
|
* Current ICE+DTLS connection state.
|
|
* @return {RTCPeerConnectionState}
|
|
*/
|
|
get connectionState() {
|
|
return this._dtlsTransport.state;
|
|
}
|
|
|
|
/**
|
|
* Current ICE connection state.
|
|
* @return {RTCIceConnectionState}
|
|
*/
|
|
get iceConnectionState() {
|
|
return this._iceTransport.state;
|
|
}
|
|
|
|
/**
|
|
* Current ICE gathering state.
|
|
* @return {RTCIceGatheringState}
|
|
*/
|
|
get iceGatheringState() {
|
|
return this._iceGatheringState;
|
|
}
|
|
|
|
/**
|
|
* Gets the local description.
|
|
* @return {RTCSessionDescription}
|
|
*/
|
|
get localDescription() {
|
|
return this._localDescription;
|
|
}
|
|
|
|
/**
|
|
* Gets the remote description.
|
|
* @return {RTCSessionDescription}
|
|
*/
|
|
get remoteDescription() {
|
|
return this._remoteDescription;
|
|
}
|
|
|
|
/**
|
|
* Current signaling state.
|
|
* @return {RTCSignalingState}
|
|
*/
|
|
get signalingState() {
|
|
return this._signalingState;
|
|
}
|
|
|
|
/**
|
|
* Adds a remote ICE candidate. Implements both the old callbacks based
|
|
* signature and the new Promise based style.
|
|
*
|
|
* Arguments in Promise mode:
|
|
* @param {RTCIceCandidate} candidate
|
|
*
|
|
* Arguments in callbacks mode:
|
|
* @param {RTCIceCandidate} candidate
|
|
* @param {function()} callback
|
|
* @param {function(error)} errback
|
|
*/
|
|
addIceCandidate(candidate, ...args) {
|
|
let usePromise;
|
|
let callback;
|
|
let errback;
|
|
|
|
if (!candidate) {
|
|
throw new TypeError('candidate missing');
|
|
}
|
|
|
|
if (args.length === 0) {
|
|
usePromise = true;
|
|
} else {
|
|
usePromise = false;
|
|
callback = args[0];
|
|
errback = args[1];
|
|
|
|
if (typeof callback !== 'function') {
|
|
throw new TypeError('callback missing');
|
|
}
|
|
|
|
if (typeof errback !== 'function') {
|
|
throw new TypeError('errback missing');
|
|
}
|
|
}
|
|
|
|
logger.debug('addIceCandidate() candidate:', candidate);
|
|
|
|
if (usePromise) {
|
|
return this._addIceCandidate(candidate);
|
|
}
|
|
|
|
this._addIceCandidate(candidate)
|
|
.then(() => callback())
|
|
.catch(error => errback(error));
|
|
}
|
|
|
|
/**
|
|
* Adds a local MediaStream.
|
|
* @param {MediaStream} stream.
|
|
* NOTE: Deprecated API.
|
|
*/
|
|
addStream(stream) {
|
|
logger.debug('addStream()');
|
|
|
|
this._addStream(stream);
|
|
}
|
|
|
|
/**
|
|
* Closes the RTCPeerConnection and all the underlying ORTC objects.
|
|
*/
|
|
close() {
|
|
if (this._closed) {
|
|
return;
|
|
}
|
|
|
|
this._closed = true;
|
|
|
|
logger.debug('close()');
|
|
|
|
this._updateAndEmitSignalingStateChange(RTCSignalingState.closed);
|
|
|
|
// Close RTCIceGatherer.
|
|
// NOTE: Not yet implemented by Edge.
|
|
try {
|
|
this._iceGatherer.close();
|
|
} catch (error) {
|
|
logger.warn(`iceGatherer.close() failed:${error}`);
|
|
}
|
|
|
|
// Close RTCIceTransport.
|
|
try {
|
|
this._iceTransport.stop();
|
|
} catch (error) {
|
|
logger.warn(`iceTransport.stop() failed:${error}`);
|
|
}
|
|
|
|
// Close RTCDtlsTransport.
|
|
try {
|
|
this._dtlsTransport.stop();
|
|
} catch (error) {
|
|
logger.warn(`dtlsTransport.stop() failed:${error}`);
|
|
}
|
|
|
|
// Close and clear RTCRtpSenders.
|
|
for (const info of this._localTrackInfos.values()) {
|
|
const rtpSender = info.rtpSender;
|
|
|
|
try {
|
|
rtpSender.stop();
|
|
} catch (error) {
|
|
logger.warn(`rtpSender.stop() failed:${error}`);
|
|
}
|
|
}
|
|
|
|
this._localTrackInfos.clear();
|
|
|
|
// Close and clear RTCRtpReceivers.
|
|
for (const info of this._remoteTrackInfos.values()) {
|
|
const rtpReceiver = info.rtpReceiver;
|
|
|
|
try {
|
|
rtpReceiver.stop();
|
|
} catch (error) {
|
|
logger.warn(`rtpReceiver.stop() failed:${error}`);
|
|
}
|
|
}
|
|
|
|
this._remoteTrackInfos.clear();
|
|
|
|
// Clear remote streams.
|
|
this._remoteStreams.clear();
|
|
}
|
|
|
|
/**
|
|
* Creates a local answer. Implements both the old callbacks based signature
|
|
* and the new Promise based style.
|
|
*
|
|
* Arguments in Promise mode:
|
|
* @param {RTCOfferOptions} [options]
|
|
*
|
|
* Arguments in callbacks mode:
|
|
* @param {function(desc)} callback
|
|
* @param {function(error)} errback
|
|
* @param {MediaConstraints} [constraints]
|
|
*/
|
|
createAnswer(...args) {
|
|
let usePromise;
|
|
let options;
|
|
let callback;
|
|
let errback;
|
|
|
|
if (args.length <= 1) {
|
|
usePromise = true;
|
|
options = args[0];
|
|
} else {
|
|
usePromise = false;
|
|
callback = args[0];
|
|
errback = args[1];
|
|
options = args[2];
|
|
|
|
if (typeof callback !== 'function') {
|
|
throw new TypeError('callback missing');
|
|
}
|
|
|
|
if (typeof errback !== 'function') {
|
|
throw new TypeError('errback missing');
|
|
}
|
|
}
|
|
|
|
logger.debug('createAnswer() options:', options);
|
|
|
|
if (usePromise) {
|
|
return this._createAnswer(options);
|
|
}
|
|
|
|
this._createAnswer(options)
|
|
.then(desc => callback(desc))
|
|
.catch(error => errback(error));
|
|
}
|
|
|
|
/**
|
|
* Creates a RTCDataChannel.
|
|
*/
|
|
createDataChannel() {
|
|
logger.debug('createDataChannel()');
|
|
|
|
// NOTE: DataChannels not implemented in Edge.
|
|
throw new Error('createDataChannel() not supported in Edge');
|
|
}
|
|
|
|
/**
|
|
* Creates a local offer. Implements both the old callbacks based signature
|
|
* and the new Promise based style.
|
|
*
|
|
* Arguments in Promise mode:
|
|
* @param {RTCOfferOptions} [options]
|
|
*
|
|
* Arguments in callbacks mode:
|
|
* @param {function(desc)} callback
|
|
* @param {function(error)} errback
|
|
* @param {MediaConstraints} [constraints]
|
|
*/
|
|
createOffer(...args) {
|
|
let usePromise;
|
|
let options;
|
|
let callback;
|
|
let errback;
|
|
|
|
if (args.length <= 1) {
|
|
usePromise = true;
|
|
options = args[0];
|
|
} else {
|
|
usePromise = false;
|
|
callback = args[0];
|
|
errback = args[1];
|
|
options = args[2];
|
|
|
|
if (typeof callback !== 'function') {
|
|
throw new TypeError('callback missing');
|
|
}
|
|
|
|
if (typeof errback !== 'function') {
|
|
throw new TypeError('errback missing');
|
|
}
|
|
}
|
|
|
|
logger.debug('createOffer() options:', options);
|
|
|
|
if (usePromise) {
|
|
return this._createOffer(options);
|
|
}
|
|
|
|
this._createOffer(options)
|
|
.then(desc => callback(desc))
|
|
.catch(error => errback(error));
|
|
}
|
|
|
|
/**
|
|
* Gets a sequence of local MediaStreams.
|
|
* @return {sequence<MediaStream>}
|
|
*/
|
|
getLocalStreams() {
|
|
return Array.from(this._localTrackInfos.values())
|
|
.map(info => info.stream)
|
|
.filter((elem, pos, arr) => arr.indexOf(elem) === pos);
|
|
}
|
|
|
|
/**
|
|
* Gets a sequence of remote MediaStreams.
|
|
* @return {sequence<MediaStream>}
|
|
*/
|
|
getRemoteStreams() {
|
|
return Array.from(this._remoteStreams.values());
|
|
}
|
|
|
|
/**
|
|
* Get RTP statistics. Implements both the old callbacks based signature
|
|
* and the new Promise based style.
|
|
*
|
|
* Arguments in Promise mode:
|
|
* @param {MediaStreamTrack} [selector]
|
|
*
|
|
* Arguments in callbacks mode:
|
|
* @param {MediaStreamTrack} [selector]
|
|
* @param {function(desc)} callback
|
|
* @param {function(error)} errback
|
|
*/
|
|
getStats(...args) {
|
|
let usePromise;
|
|
let selector;
|
|
let callback;
|
|
let errback;
|
|
|
|
if (args.length <= 1) {
|
|
usePromise = true;
|
|
selector = args[0];
|
|
} else {
|
|
usePromise = false;
|
|
|
|
if (args.length === 2) {
|
|
callback = args[0];
|
|
errback = args[1];
|
|
} else {
|
|
selector = args[0];
|
|
callback = args[1];
|
|
errback = args[2];
|
|
}
|
|
|
|
if (typeof callback !== 'function') {
|
|
throw new TypeError('callback missing');
|
|
}
|
|
|
|
if (typeof errback !== 'function') {
|
|
throw new TypeError('errback missing');
|
|
}
|
|
}
|
|
|
|
logger.debug('getStats()');
|
|
|
|
if (usePromise) {
|
|
return this._getStats(selector);
|
|
}
|
|
|
|
this._getStats(selector)
|
|
.then(stats => callback(stats))
|
|
.catch(error => errback(error));
|
|
}
|
|
|
|
/**
|
|
* Removes a local MediaStream.
|
|
* @param {MediaStream} stream.
|
|
* NOTE: Deprecated API.
|
|
*/
|
|
removeStream(stream) {
|
|
logger.debug('removeStream()');
|
|
|
|
this._removeStream(stream);
|
|
}
|
|
|
|
/**
|
|
* Applies a local description. Implements both the old callbacks based
|
|
* signature and the new Promise based style.
|
|
*
|
|
* Arguments in Promise mode:
|
|
* @param {RTCSessionDescriptionInit} desc
|
|
*
|
|
* Arguments in callbacks mode:
|
|
* @param {RTCSessionDescription} desc
|
|
* @param {function()} callback
|
|
* @param {function(error)} errback
|
|
*/
|
|
setLocalDescription(desc, ...args) {
|
|
let usePromise;
|
|
let callback;
|
|
let errback;
|
|
|
|
if (!desc) {
|
|
throw new TypeError('description missing');
|
|
}
|
|
|
|
if (args.length === 0) {
|
|
usePromise = true;
|
|
} else {
|
|
usePromise = false;
|
|
callback = args[0];
|
|
errback = args[1];
|
|
|
|
if (typeof callback !== 'function') {
|
|
throw new TypeError('callback missing');
|
|
}
|
|
|
|
if (typeof errback !== 'function') {
|
|
throw new TypeError('errback missing');
|
|
}
|
|
}
|
|
|
|
logger.debug('setLocalDescription() desc:', desc);
|
|
|
|
if (usePromise) {
|
|
return this._setLocalDescription(desc);
|
|
}
|
|
|
|
this._setLocalDescription(desc)
|
|
.then(() => callback())
|
|
.catch(error => errback(error));
|
|
}
|
|
|
|
/**
|
|
* Applies a remote description. Implements both the old callbacks based
|
|
* signature and the new Promise based style.
|
|
*
|
|
* Arguments in Promise mode:
|
|
* @param {RTCSessionDescriptionInit} desc
|
|
*
|
|
* Arguments in callbacks mode:
|
|
* @param {RTCSessionDescription} desc
|
|
* @param {function()} callback
|
|
* @param {function(error)} errback
|
|
*/
|
|
setRemoteDescription(desc, ...args) {
|
|
let usePromise;
|
|
let callback;
|
|
let errback;
|
|
|
|
if (!desc) {
|
|
throw new TypeError('description missing');
|
|
}
|
|
|
|
if (args.length === 0) {
|
|
usePromise = true;
|
|
} else {
|
|
usePromise = false;
|
|
callback = args[0];
|
|
errback = args[1];
|
|
|
|
if (typeof callback !== 'function') {
|
|
throw new TypeError('callback missing');
|
|
}
|
|
|
|
if (typeof errback !== 'function') {
|
|
throw new TypeError('errback missing');
|
|
}
|
|
}
|
|
|
|
logger.debug('setRemoteDescription() desc:', desc);
|
|
|
|
if (usePromise) {
|
|
return this._setRemoteDescription(desc);
|
|
}
|
|
|
|
this._setRemoteDescription(desc)
|
|
.then(() => callback())
|
|
.catch(error => errback(error));
|
|
}
|
|
|
|
/**
|
|
* Promise based implementation for addIceCandidate().
|
|
* @return {Promise}
|
|
* @private
|
|
*/
|
|
_addIceCandidate(candidate) { // eslint-disable-line no-unused-vars
|
|
if (this._closed) {
|
|
return Promise.reject(
|
|
new InvalidStateError('RTCPeerConnection closed'));
|
|
}
|
|
|
|
// NOTE: Edge does not support Trickle-ICE so just candidates in the
|
|
// remote SDP are applied. Candidates given later would be just
|
|
// ignored, so notify the called about that.
|
|
return Promise.reject(new Error('addIceCandidate() not supported'));
|
|
}
|
|
|
|
/**
|
|
* Implementation for addStream().
|
|
* @private
|
|
*/
|
|
_addStream(stream) {
|
|
if (this._closed) {
|
|
throw new InvalidStateError('RTCPeerConnection closed');
|
|
}
|
|
|
|
// Create a RTCRtpSender for each track.
|
|
for (const track of stream.getTracks()) {
|
|
// Ignore if ended.
|
|
if (track.readyState === 'ended') {
|
|
logger.warn('ignoring ended MediaStreamTrack');
|
|
|
|
continue; // eslint-disable-line no-continue
|
|
}
|
|
|
|
// Ignore if track is already present.
|
|
if (this._localTrackInfos.has(track.id)) {
|
|
logger.warn('ignoring already handled MediaStreamTrack');
|
|
|
|
continue; // eslint-disable-line no-continue
|
|
}
|
|
|
|
const rtpSender = new RTCRtpSender(track, this._dtlsTransport);
|
|
|
|
// Store it in the map.
|
|
this._localTrackInfos.set(track.id, {
|
|
rtpSender,
|
|
stream
|
|
});
|
|
}
|
|
|
|
// It may need to renegotiate.
|
|
this._emitNegotiationNeeded();
|
|
}
|
|
|
|
/**
|
|
* Promise based implementation for createAnswer().
|
|
* @returns {Promise}
|
|
* @private
|
|
*/
|
|
_createAnswer(options) { // eslint-disable-line no-unused-vars
|
|
if (this._closed) {
|
|
return Promise.reject(
|
|
new InvalidStateError('RTCPeerConnection closed'));
|
|
}
|
|
|
|
if (this.signalingState !== RTCSignalingState.haveRemoteOffer) {
|
|
return Promise.reject(new InvalidStateError(
|
|
`invalid signalingState "${this.signalingState}"`));
|
|
}
|
|
|
|
// Create an answer.
|
|
const localDescription = this._createLocalDescription('answer');
|
|
|
|
// Resolve with it.
|
|
return Promise.resolve(localDescription);
|
|
}
|
|
|
|
/**
|
|
* Creates the local RTCSessionDescription.
|
|
* @param {String} type - 'offer' / 'answer'.
|
|
* @return {RTCSessionDescription}
|
|
*/
|
|
_createLocalDescription(type) {
|
|
const sdpObject = {};
|
|
const localIceParameters = this._iceGatherer.getLocalParameters();
|
|
const localIceCandidates = this._iceGatherer.getLocalCandidates();
|
|
const localDtlsParameters = this._dtlsTransport.getLocalParameters();
|
|
const remoteDtlsParameters = this._dtlsTransport.getRemoteParameters();
|
|
const localCapabilities = this._localCapabilities;
|
|
const localTrackInfos = this._localTrackInfos;
|
|
|
|
// Increase SDP version if an offer.
|
|
if (type === 'offer') {
|
|
this._sdpGlobalFields.version++;
|
|
}
|
|
|
|
// SDP global fields.
|
|
sdpObject.version = 0;
|
|
sdpObject.origin = {
|
|
address: '127.0.0.1',
|
|
ipVer: 4,
|
|
netType: 'IN',
|
|
sessionId: this._sdpGlobalFields.id,
|
|
sessionVersion: this._sdpGlobalFields.version,
|
|
username: 'jitsi-ortc-webrtc-shim'
|
|
};
|
|
sdpObject.name = '-';
|
|
sdpObject.timing = {
|
|
start: 0,
|
|
stop: 0
|
|
};
|
|
sdpObject.msidSemantic = {
|
|
semantic: 'WMS',
|
|
token: '*'
|
|
};
|
|
sdpObject.groups = [
|
|
{
|
|
mids: Array.from(this._mids.keys()).join(' '),
|
|
type: 'BUNDLE'
|
|
}
|
|
];
|
|
sdpObject.media = [];
|
|
|
|
// DTLS fingerprint.
|
|
sdpObject.fingerprint = {
|
|
hash: localDtlsParameters.fingerprints[0].value,
|
|
type: localDtlsParameters.fingerprints[0].algorithm
|
|
};
|
|
|
|
// Let's check whether there is video RTX.
|
|
let hasVideoRtx = false;
|
|
|
|
for (const codec of localCapabilities.codecs) {
|
|
if (codec.kind === 'video' && codec.name === 'rtx') {
|
|
hasVideoRtx = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Add m= sections.
|
|
for (const [ mid, kind ] of this._mids) {
|
|
addMediaSection.call(this, mid, kind);
|
|
}
|
|
|
|
// Create a RTCSessionDescription.
|
|
const localDescription = new RTCSessionDescription({
|
|
type,
|
|
_sdpObject: sdpObject
|
|
});
|
|
|
|
logger.debug('_createLocalDescription():', localDescription);
|
|
|
|
return localDescription;
|
|
|
|
/**
|
|
* Add a m= section.
|
|
*/
|
|
function addMediaSection(mid, kind) {
|
|
const mediaObject = {};
|
|
|
|
// m= line.
|
|
mediaObject.type = kind;
|
|
|
|
switch (kind) {
|
|
case 'audio':
|
|
case 'video':
|
|
mediaObject.protocol = 'RTP/SAVPF';
|
|
mediaObject.port = 9;
|
|
mediaObject.direction = 'sendrecv';
|
|
break;
|
|
case 'application':
|
|
mediaObject.protocol = 'DTLS/SCTP';
|
|
mediaObject.port = 0; // Reject m section.
|
|
mediaObject.payloads = '0'; // Just put something.
|
|
mediaObject.direction = 'inactive';
|
|
break;
|
|
}
|
|
|
|
// c= line.
|
|
mediaObject.connection = {
|
|
ip: '127.0.0.1',
|
|
version: 4
|
|
};
|
|
|
|
// a=mid attribute.
|
|
mediaObject.mid = mid;
|
|
|
|
// ICE.
|
|
mediaObject.iceUfrag = localIceParameters.usernameFragment;
|
|
mediaObject.icePwd = localIceParameters.password;
|
|
mediaObject.candidates = [];
|
|
|
|
for (const candidate of localIceCandidates) {
|
|
const candidateObject = {};
|
|
|
|
// rtcp-mux is assumed, so component is always 1 (RTP).
|
|
candidateObject.component = 1;
|
|
candidateObject.foundation = candidate.foundation;
|
|
candidateObject.ip = candidate.ip;
|
|
candidateObject.port = candidate.port;
|
|
candidateObject.priority = candidate.priority;
|
|
candidateObject.transport
|
|
= candidate.protocol.toLowerCase();
|
|
candidateObject.type = candidate.type;
|
|
if (candidateObject.transport === 'tcp') {
|
|
candidateObject.tcptype = candidate.tcpType;
|
|
}
|
|
|
|
mediaObject.candidates.push(candidateObject);
|
|
}
|
|
|
|
mediaObject.endOfCandidates = 'end-of-candidates';
|
|
|
|
// DTLS.
|
|
// If 'offer' always use 'actpass'.
|
|
if (type === 'offer') {
|
|
mediaObject.setup = 'actpass';
|
|
} else {
|
|
mediaObject.setup = remoteDtlsParameters.role === 'server'
|
|
? 'active' : 'passive';
|
|
}
|
|
|
|
if (kind === 'audio' || kind === 'video') {
|
|
mediaObject.rtp = [];
|
|
mediaObject.rtcpFb = [];
|
|
mediaObject.fmtp = [];
|
|
|
|
// Array of payload types.
|
|
const payloads = [];
|
|
|
|
// Add codecs.
|
|
for (const codec of localCapabilities.codecs) {
|
|
if (codec.kind && codec.kind !== kind) {
|
|
continue; // eslint-disable-line no-continue
|
|
}
|
|
|
|
payloads.push(codec.preferredPayloadType);
|
|
|
|
const rtpObject = {
|
|
codec: codec.name,
|
|
payload: codec.preferredPayloadType,
|
|
rate: codec.clockRate
|
|
};
|
|
|
|
if (codec.numChannels > 1) {
|
|
rtpObject.encoding = codec.numChannels;
|
|
}
|
|
|
|
mediaObject.rtp.push(rtpObject);
|
|
|
|
// If codec has parameters add them into a=fmtp attributes.
|
|
if (codec.parameters) {
|
|
const paramFmtp = {
|
|
config: '',
|
|
payload: codec.preferredPayloadType
|
|
};
|
|
|
|
for (const name of Object.keys(codec.parameters)) {
|
|
/* eslint-disable max-depth */
|
|
if (paramFmtp.config) {
|
|
paramFmtp.config += ';';
|
|
}
|
|
/* eslint-enable max-depth */
|
|
|
|
paramFmtp.config
|
|
+= `${name}=${codec.parameters[name]}`;
|
|
}
|
|
|
|
if (paramFmtp.config) {
|
|
mediaObject.fmtp.push(paramFmtp);
|
|
}
|
|
}
|
|
|
|
// Set RTCP feedback.
|
|
for (const fb of codec.rtcpFeedback || []) {
|
|
mediaObject.rtcpFb.push({
|
|
payload: codec.preferredPayloadType,
|
|
subtype: fb.parameter || undefined,
|
|
type: fb.type
|
|
});
|
|
}
|
|
}
|
|
|
|
// If there are no codecs, set this m section as unavailable.
|
|
if (payloads.length === 0) {
|
|
mediaObject.payloads = '9'; // Just put something.
|
|
mediaObject.port = 0;
|
|
mediaObject.direction = 'inactive';
|
|
} else {
|
|
mediaObject.payloads = payloads.join(' ');
|
|
}
|
|
|
|
// SSRCs.
|
|
mediaObject.ssrcs = [];
|
|
mediaObject.ssrcGroups = [];
|
|
|
|
// Add RTP sending stuff.
|
|
for (const info of localTrackInfos.values()) {
|
|
const rtpSender = info.rtpSender;
|
|
const streamId = info.stream.id;
|
|
const track = rtpSender.track;
|
|
|
|
// Ignore if ended.
|
|
if (track.readyState === 'ended') {
|
|
continue; // eslint-disable-line no-continue
|
|
}
|
|
|
|
if (track.kind !== kind) {
|
|
continue; // eslint-disable-line no-continue
|
|
}
|
|
|
|
// Set a random provisional SSRC if not set.
|
|
if (!info.ssrc) {
|
|
info.ssrc = utils.randomNumber();
|
|
}
|
|
|
|
// Whether RTX should be enabled.
|
|
const enableRtx = hasVideoRtx && track.kind === 'video';
|
|
|
|
// Set a random provisional RTX SSRC if not set.
|
|
if (enableRtx && !info.rtxSsrc) {
|
|
info.rtxSsrc = info.ssrc + 1;
|
|
}
|
|
|
|
mediaObject.ssrcs.push({
|
|
attribute: 'cname',
|
|
id: info.ssrc,
|
|
value: CNAME
|
|
});
|
|
|
|
mediaObject.ssrcs.push({
|
|
attribute: 'msid',
|
|
id: info.ssrc,
|
|
value: `${streamId} ${track.id}`
|
|
});
|
|
|
|
mediaObject.ssrcs.push({
|
|
attribute: 'mslabel',
|
|
id: info.ssrc,
|
|
value: streamId
|
|
});
|
|
|
|
mediaObject.ssrcs.push({
|
|
attribute: 'label',
|
|
id: info.ssrc,
|
|
value: track.id
|
|
});
|
|
|
|
if (enableRtx) {
|
|
mediaObject.ssrcs.push({
|
|
attribute: 'cname',
|
|
id: info.rtxSsrc,
|
|
value: CNAME
|
|
});
|
|
|
|
mediaObject.ssrcs.push({
|
|
attribute: 'msid',
|
|
id: info.rtxSsrc,
|
|
value: `${streamId} ${track.id}`
|
|
});
|
|
|
|
mediaObject.ssrcs.push({
|
|
attribute: 'mslabel',
|
|
id: info.rtxSsrc,
|
|
value: streamId
|
|
});
|
|
|
|
mediaObject.ssrcs.push({
|
|
attribute: 'label',
|
|
id: info.rtxSsrc,
|
|
value: track.id
|
|
});
|
|
|
|
mediaObject.ssrcGroups.push({
|
|
semantics: 'FID',
|
|
ssrcs: `${info.ssrc} ${info.rtxSsrc}`
|
|
});
|
|
}
|
|
}
|
|
|
|
// RTP header extensions.
|
|
mediaObject.ext = [];
|
|
|
|
for (const extension of localCapabilities.headerExtensions) {
|
|
if (extension.kind && extension.kind !== kind) {
|
|
continue; // eslint-disable-line no-continue
|
|
}
|
|
|
|
mediaObject.ext.push({
|
|
value: extension.preferredId,
|
|
uri: extension.uri
|
|
});
|
|
}
|
|
|
|
// a=rtcp-mux attribute.
|
|
mediaObject.rtcpMux = 'rtcp-mux';
|
|
|
|
// a=rtcp-rsize.
|
|
mediaObject.rtcpRsize = 'rtcp-rsize';
|
|
}
|
|
|
|
// Add the media section.
|
|
sdpObject.media.push(mediaObject);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Promise based implementation for createOffer().
|
|
* @returns {Promise}
|
|
* @private
|
|
*/
|
|
_createOffer(options) { // eslint-disable-line no-unused-vars
|
|
if (this._closed) {
|
|
return Promise.reject(
|
|
new InvalidStateError('RTCPeerConnection closed'));
|
|
}
|
|
|
|
if (this.signalingState !== RTCSignalingState.stable) {
|
|
return Promise.reject(new InvalidStateError(
|
|
`invalid signalingState "${this.signalingState}"`));
|
|
}
|
|
|
|
// NOTE: P2P mode not yet supported, so createOffer() should never be
|
|
// called.
|
|
// return Promise.reject(new Error('createoOffer() not yet supported'));
|
|
|
|
// HACK: Create an offer assuming this is called before any
|
|
// setRemoteDescription() and assuming that setLocalDescription()
|
|
// wont be called with this offer.
|
|
|
|
const sdpObject = {};
|
|
const localIceParameters = this._iceGatherer.getLocalParameters();
|
|
const localIceCandidates = this._iceGatherer.getLocalCandidates();
|
|
const localDtlsParameters = this._dtlsTransport.getLocalParameters();
|
|
const localCapabilities = RTCRtpReceiver.getCapabilities();
|
|
const localTrackInfos = this._localTrackInfos;
|
|
const mids = new Map([ ['audio', 'audio'], ['video', 'video'] ]);
|
|
|
|
// SDP global fields.
|
|
sdpObject.version = 0;
|
|
sdpObject.origin = {
|
|
address: '127.0.0.1',
|
|
ipVer: 4,
|
|
netType: 'IN',
|
|
sessionId: this._sdpGlobalFields.id,
|
|
sessionVersion: this._sdpGlobalFields.version,
|
|
username: 'jitsi-ortc-webrtc-shim'
|
|
};
|
|
sdpObject.name = '-';
|
|
sdpObject.timing = {
|
|
start: 0,
|
|
stop: 0
|
|
};
|
|
sdpObject.msidSemantic = {
|
|
semantic: 'WMS',
|
|
token: '*'
|
|
};
|
|
sdpObject.groups = [
|
|
{
|
|
mids: Array.from(mids.keys()).join(' '),
|
|
type: 'BUNDLE'
|
|
}
|
|
];
|
|
sdpObject.media = [];
|
|
|
|
// DTLS fingerprint.
|
|
sdpObject.fingerprint = {
|
|
hash: localDtlsParameters.fingerprints[0].value,
|
|
type: localDtlsParameters.fingerprints[0].algorithm
|
|
};
|
|
|
|
// Let's check whether there is video RTX.
|
|
let hasVideoRtx = false;
|
|
|
|
for (const codec of localCapabilities.codecs) {
|
|
if (codec.kind === 'video' && codec.name === 'rtx') {
|
|
hasVideoRtx = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Add m= sections.
|
|
for (const [ mid, kind ] of mids) {
|
|
addMediaSection.call(this, mid, kind);
|
|
}
|
|
|
|
// Create a RTCSessionDescription.
|
|
const localDescription = new RTCSessionDescription({
|
|
type: 'offer',
|
|
_sdpObject: sdpObject
|
|
});
|
|
|
|
logger.debug('_createLocalDescription():', localDescription);
|
|
|
|
// Resolve with it.
|
|
return Promise.resolve(localDescription);
|
|
|
|
/**
|
|
* Add a m= section.
|
|
*/
|
|
function addMediaSection(mid, kind) {
|
|
const mediaObject = {};
|
|
|
|
// m= line.
|
|
mediaObject.type = kind;
|
|
|
|
switch (kind) {
|
|
case 'audio':
|
|
case 'video':
|
|
mediaObject.protocol = 'RTP/SAVPF';
|
|
mediaObject.port = 9;
|
|
mediaObject.direction = 'sendrecv';
|
|
break;
|
|
case 'application':
|
|
mediaObject.protocol = 'DTLS/SCTP';
|
|
mediaObject.port = 0; // Reject m section.
|
|
mediaObject.payloads = '0'; // Just put something.
|
|
mediaObject.direction = 'inactive';
|
|
break;
|
|
}
|
|
|
|
// c= line.
|
|
mediaObject.connection = {
|
|
ip: '127.0.0.1',
|
|
version: 4
|
|
};
|
|
|
|
// a=mid attribute.
|
|
mediaObject.mid = mid;
|
|
|
|
// ICE.
|
|
mediaObject.iceUfrag = localIceParameters.usernameFragment;
|
|
mediaObject.icePwd = localIceParameters.password;
|
|
mediaObject.candidates = [];
|
|
|
|
for (const candidate of localIceCandidates) {
|
|
const candidateObject = {};
|
|
|
|
// rtcp-mux is assumed, so component is always 1 (RTP).
|
|
candidateObject.component = 1;
|
|
candidateObject.foundation = candidate.foundation;
|
|
candidateObject.ip = candidate.ip;
|
|
candidateObject.port = candidate.port;
|
|
candidateObject.priority = candidate.priority;
|
|
candidateObject.transport
|
|
= candidate.protocol.toLowerCase();
|
|
candidateObject.type = candidate.type;
|
|
if (candidateObject.transport === 'tcp') {
|
|
candidateObject.tcptype = candidate.tcpType;
|
|
}
|
|
|
|
mediaObject.candidates.push(candidateObject);
|
|
}
|
|
|
|
mediaObject.endOfCandidates = 'end-of-candidates';
|
|
|
|
// DTLS.
|
|
// 'offer' so always use 'actpass'.
|
|
mediaObject.setup = 'actpass';
|
|
|
|
if (kind === 'audio' || kind === 'video') {
|
|
mediaObject.rtp = [];
|
|
mediaObject.rtcpFb = [];
|
|
mediaObject.fmtp = [];
|
|
|
|
// Array of payload types.
|
|
const payloads = [];
|
|
|
|
// Add codecs.
|
|
for (const codec of localCapabilities.codecs) {
|
|
if (codec.kind && codec.kind !== kind) {
|
|
continue; // eslint-disable-line no-continue
|
|
}
|
|
|
|
payloads.push(codec.preferredPayloadType);
|
|
|
|
const rtpObject = {
|
|
codec: codec.name,
|
|
payload: codec.preferredPayloadType,
|
|
rate: codec.clockRate
|
|
};
|
|
|
|
if (codec.numChannels > 1) {
|
|
rtpObject.encoding = codec.numChannels;
|
|
}
|
|
|
|
mediaObject.rtp.push(rtpObject);
|
|
|
|
// If codec has parameters add them into a=fmtp attributes.
|
|
if (codec.parameters) {
|
|
const paramFmtp = {
|
|
config: '',
|
|
payload: codec.preferredPayloadType
|
|
};
|
|
|
|
for (const name of Object.keys(codec.parameters)) {
|
|
/* eslint-disable max-depth */
|
|
if (paramFmtp.config) {
|
|
paramFmtp.config += ';';
|
|
}
|
|
/* eslint-enable max-depth */
|
|
|
|
paramFmtp.config
|
|
+= `${name}=${codec.parameters[name]}`;
|
|
}
|
|
|
|
if (paramFmtp.config) {
|
|
mediaObject.fmtp.push(paramFmtp);
|
|
}
|
|
}
|
|
|
|
// Set RTCP feedback.
|
|
for (const fb of codec.rtcpFeedback || []) {
|
|
mediaObject.rtcpFb.push({
|
|
payload: codec.preferredPayloadType,
|
|
subtype: fb.parameter || undefined,
|
|
type: fb.type
|
|
});
|
|
}
|
|
}
|
|
|
|
// If there are no codecs, set this m section as unavailable.
|
|
if (payloads.length === 0) {
|
|
mediaObject.payloads = '9'; // Just put something.
|
|
mediaObject.port = 0;
|
|
mediaObject.direction = 'inactive';
|
|
} else {
|
|
mediaObject.payloads = payloads.join(' ');
|
|
}
|
|
|
|
// SSRCs.
|
|
mediaObject.ssrcs = [];
|
|
mediaObject.ssrcGroups = [];
|
|
|
|
// Add RTP sending stuff.
|
|
for (const info of localTrackInfos.values()) {
|
|
const rtpSender = info.rtpSender;
|
|
const streamId = info.stream.id;
|
|
const track = rtpSender.track;
|
|
|
|
// Ignore if ended.
|
|
if (track.readyState === 'ended') {
|
|
continue; // eslint-disable-line no-continue
|
|
}
|
|
|
|
if (track.kind !== kind) {
|
|
continue; // eslint-disable-line no-continue
|
|
}
|
|
|
|
// Set a random provisional SSRC if not set.
|
|
if (!info.ssrc) {
|
|
info.ssrc = utils.randomNumber();
|
|
}
|
|
|
|
// Whether RTX should be enabled.
|
|
const enableRtx = hasVideoRtx && track.kind === 'video';
|
|
|
|
// Set a random provisional RTX SSRC if not set.
|
|
if (enableRtx && !info.rtxSsrc) {
|
|
info.rtxSsrc = info.ssrc + 1;
|
|
}
|
|
|
|
mediaObject.ssrcs.push({
|
|
attribute: 'cname',
|
|
id: info.ssrc,
|
|
value: CNAME
|
|
});
|
|
|
|
mediaObject.ssrcs.push({
|
|
attribute: 'msid',
|
|
id: info.ssrc,
|
|
value: `${streamId} ${track.id}`
|
|
});
|
|
|
|
mediaObject.ssrcs.push({
|
|
attribute: 'mslabel',
|
|
id: info.ssrc,
|
|
value: streamId
|
|
});
|
|
|
|
mediaObject.ssrcs.push({
|
|
attribute: 'label',
|
|
id: info.ssrc,
|
|
value: track.id
|
|
});
|
|
|
|
if (enableRtx) {
|
|
mediaObject.ssrcs.push({
|
|
attribute: 'cname',
|
|
id: info.rtxSsrc,
|
|
value: CNAME
|
|
});
|
|
|
|
mediaObject.ssrcs.push({
|
|
attribute: 'msid',
|
|
id: info.rtxSsrc,
|
|
value: `${streamId} ${track.id}`
|
|
});
|
|
|
|
mediaObject.ssrcs.push({
|
|
attribute: 'mslabel',
|
|
id: info.rtxSsrc,
|
|
value: streamId
|
|
});
|
|
|
|
mediaObject.ssrcs.push({
|
|
attribute: 'label',
|
|
id: info.rtxSsrc,
|
|
value: track.id
|
|
});
|
|
|
|
mediaObject.ssrcGroups.push({
|
|
semantics: 'FID',
|
|
ssrcs: `${info.ssrc} ${info.rtxSsrc}`
|
|
});
|
|
}
|
|
}
|
|
|
|
// RTP header extensions.
|
|
mediaObject.ext = [];
|
|
|
|
for (const extension of localCapabilities.headerExtensions) {
|
|
if (extension.kind && extension.kind !== kind) {
|
|
continue; // eslint-disable-line no-continue
|
|
}
|
|
|
|
mediaObject.ext.push({
|
|
value: extension.preferredId,
|
|
uri: extension.uri
|
|
});
|
|
}
|
|
|
|
// a=rtcp-mux attribute.
|
|
mediaObject.rtcpMux = 'rtcp-mux';
|
|
|
|
// a=rtcp-rsize.
|
|
mediaObject.rtcpRsize = 'rtcp-rsize';
|
|
}
|
|
|
|
// Add the media section.
|
|
sdpObject.media.push(mediaObject);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Emit 'addstream' event.
|
|
* @private
|
|
*/
|
|
_emitAddStream(stream) {
|
|
if (this._closed) {
|
|
return;
|
|
}
|
|
|
|
logger.debug('emitting "addstream"');
|
|
|
|
const event = new yaeti.Event('addstream');
|
|
|
|
event.stream = stream;
|
|
this.dispatchEvent(event);
|
|
}
|
|
|
|
/**
|
|
* May emit buffered ICE candidates.
|
|
* @private
|
|
*/
|
|
_emitBufferedIceCandidates() {
|
|
if (this._closed) {
|
|
return;
|
|
}
|
|
|
|
for (const sdpCandidate of this._bufferedIceCandidates) {
|
|
if (!sdpCandidate) {
|
|
continue; // eslint-disable-line no-continue
|
|
}
|
|
|
|
// Now we have set the MID values of the SDP O/A, so let's fill the
|
|
// sdpMIndex of the candidate.
|
|
sdpCandidate.sdpMIndex = this._mids.keys().next().value;
|
|
|
|
logger.debug(
|
|
'emitting buffered "icecandidate", candidate:', sdpCandidate);
|
|
|
|
const event = new yaeti.Event('icecandidate');
|
|
|
|
event.candidate = sdpCandidate;
|
|
this.dispatchEvent(event);
|
|
}
|
|
|
|
this._bufferedIceCandidates = [];
|
|
}
|
|
|
|
/**
|
|
* May emit 'connectionstatechange' event.
|
|
* @private
|
|
*/
|
|
_emitConnectionStateChange() {
|
|
if (this._closed && this.connectionState !== 'closed') {
|
|
return;
|
|
}
|
|
|
|
logger.debug(
|
|
'emitting "connectionstatechange", connectionState:',
|
|
this.connectionState);
|
|
|
|
const event = new yaeti.Event('connectionstatechange');
|
|
|
|
this.dispatchEvent(event);
|
|
}
|
|
|
|
/**
|
|
* May emit 'icecandidate' event.
|
|
* @private
|
|
*/
|
|
_emitIceCandidate(candidate) {
|
|
if (this._closed) {
|
|
return;
|
|
}
|
|
|
|
let sdpCandidate = null;
|
|
|
|
if (candidate) {
|
|
// NOTE: We assume BUNDLE so let's just emit candidates for the
|
|
// first m= section.
|
|
const sdpMIndex = this._mids.keys().next().value;
|
|
const sdpMLineIndex = 0;
|
|
let sdpAttribute
|
|
= `candidate:${candidate.foundation} 1 ${candidate.protocol}`
|
|
+ ` ${candidate.priority} ${candidate.ip} ${candidate.port}`
|
|
+ ` typ ${candidate.type}`;
|
|
|
|
if (candidate.relatedAddress) {
|
|
sdpAttribute += ` raddr ${candidate.relatedAddress}`;
|
|
}
|
|
if (candidate.relatedPort) {
|
|
sdpAttribute += ` rport ${candidate.relatedPort}`;
|
|
}
|
|
if (candidate.protocol === 'tcp') {
|
|
sdpAttribute += ` tcptype ${candidate.tcpType}`;
|
|
}
|
|
|
|
sdpCandidate = {
|
|
candidate: sdpAttribute,
|
|
component: 1, // rtcp-mux assumed, so always 1 (RTP).
|
|
foundation: candidate.foundation,
|
|
ip: candidate.ip,
|
|
port: candidate.port,
|
|
priority: candidate.priority,
|
|
protocol: candidate.protocol,
|
|
type: candidate.type,
|
|
sdpMIndex,
|
|
sdpMLineIndex
|
|
};
|
|
|
|
if (candidate.protocol === 'tcp') {
|
|
sdpCandidate.tcptype = candidate.tcpType;
|
|
}
|
|
if (candidate.relatedAddress) {
|
|
sdpCandidate.relatedAddress = candidate.relatedAddress;
|
|
}
|
|
if (candidate.relatedPort) {
|
|
sdpCandidate.relatedPort = candidate.relatedPort;
|
|
}
|
|
}
|
|
|
|
// If we don't have yet a local description, buffer the candidate.
|
|
if (this._localDescription) {
|
|
logger.debug(
|
|
'emitting "icecandidate", candidate:', sdpCandidate);
|
|
|
|
const event = new yaeti.Event('icecandidate');
|
|
|
|
event.candidate = sdpCandidate;
|
|
this.dispatchEvent(event);
|
|
} else {
|
|
logger.debug(
|
|
'buffering gathered ICE candidate:', sdpCandidate);
|
|
|
|
this._bufferedIceCandidates.push(sdpCandidate);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* May emit 'iceconnectionstatechange' event.
|
|
* @private
|
|
*/
|
|
_emitIceConnectionStateChange() {
|
|
if (this._closed && this.iceConnectionState !== 'closed') {
|
|
return;
|
|
}
|
|
|
|
logger.debug(
|
|
'emitting "iceconnectionstatechange", iceConnectionState:',
|
|
this.iceConnectionState);
|
|
|
|
const event = new yaeti.Event('iceconnectionstatechange');
|
|
|
|
this.dispatchEvent(event);
|
|
}
|
|
|
|
/**
|
|
* May emit 'negotiationneeded' event.
|
|
* @private
|
|
*/
|
|
_emitNegotiationNeeded() {
|
|
// Ignore if signalingState is not 'stable'.
|
|
if (this.signalingState !== RTCSignalingState.stable) {
|
|
return;
|
|
}
|
|
|
|
logger.debug('emitting "negotiationneeded"');
|
|
|
|
const event = new yaeti.Event('negotiationneeded');
|
|
|
|
this.dispatchEvent(event);
|
|
}
|
|
|
|
/**
|
|
* Emit 'removestream' event.
|
|
* @private
|
|
*/
|
|
_emitRemoveStream(stream) {
|
|
if (this._closed) {
|
|
return;
|
|
}
|
|
|
|
logger.debug('emitting "removestream"');
|
|
|
|
const event = new yaeti.Event('removestream');
|
|
|
|
event.stream = stream;
|
|
this.dispatchEvent(event);
|
|
}
|
|
|
|
/**
|
|
* Get RTP parameters for a RTCRtpReceiver.
|
|
* @private
|
|
* @return {RTCRtpParameters}
|
|
*/
|
|
_getParametersForRtpReceiver(kind, data) {
|
|
const ssrc = data.ssrc;
|
|
const rtxSsrc = data.rtxSsrc;
|
|
const cname = data.cname;
|
|
const localCapabilities = this._localCapabilities;
|
|
const parameters = {
|
|
codecs: [],
|
|
degradationPreference: 'balanced',
|
|
encodings: [],
|
|
headerExtensions: [],
|
|
muxId: '',
|
|
rtcp: {
|
|
cname,
|
|
compound: true, // NOTE: Implemented in Edge.
|
|
mux: true,
|
|
reducedSize: true // NOTE: Not yet implemented in Edge.
|
|
}
|
|
};
|
|
|
|
const codecs = [];
|
|
let codecPayloadType;
|
|
|
|
for (const codecCapability of localCapabilities.codecs) {
|
|
if (codecCapability.kind !== kind
|
|
|| codecCapability.name === 'rtx') {
|
|
continue; // eslint-disable-line no-continue
|
|
}
|
|
|
|
codecPayloadType = codecCapability.preferredPayloadType;
|
|
codecs.push({
|
|
clockRate: codecCapability.clockRate,
|
|
maxptime: codecCapability.maxptime,
|
|
mimeType: codecCapability.mimeType,
|
|
name: codecCapability.name,
|
|
numChannels: codecCapability.numChannels,
|
|
parameters: codecCapability.parameters,
|
|
payloadType: codecCapability.preferredPayloadType,
|
|
ptime: codecCapability.ptime,
|
|
rtcpFeedback: codecCapability.rtcpFeedback
|
|
});
|
|
|
|
break;
|
|
}
|
|
|
|
if (rtxSsrc) {
|
|
for (const codecCapability of localCapabilities.codecs) {
|
|
if (codecCapability.kind !== kind
|
|
|| codecCapability.name !== 'rtx') {
|
|
continue; // eslint-disable-line no-continue
|
|
}
|
|
|
|
codecs.push({
|
|
clockRate: codecCapability.clockRate,
|
|
mimeType: codecCapability.mimeType,
|
|
name: 'rtx',
|
|
parameters: codecCapability.parameters,
|
|
payloadType: codecCapability.preferredPayloadType,
|
|
rtcpFeedback: codecCapability.rtcpFeedback
|
|
});
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
parameters.codecs = codecs;
|
|
|
|
const encoding = {
|
|
active: true,
|
|
codecPayloadType,
|
|
ssrc
|
|
};
|
|
|
|
if (rtxSsrc) {
|
|
encoding.rtx = {
|
|
ssrc: rtxSsrc
|
|
};
|
|
}
|
|
|
|
parameters.encodings.push(encoding);
|
|
|
|
for (const extension of localCapabilities.headerExtensions) {
|
|
if (extension.kind !== kind) {
|
|
continue; // eslint-disable-line no-continue
|
|
}
|
|
|
|
parameters.headerExtensions.push({
|
|
encrypt: extension.preferredEncrypt,
|
|
id: extension.preferredId,
|
|
uri: extension.uri
|
|
});
|
|
}
|
|
|
|
return parameters;
|
|
}
|
|
|
|
/**
|
|
* Get RTP parameters for a RTCRtpSender.
|
|
* @private
|
|
* @return {RTCRtpParameters}
|
|
*/
|
|
_getParametersForRtpSender(kind, data) {
|
|
const ssrc = data.ssrc;
|
|
const rtxSsrc = data.rtxSsrc;
|
|
const cname = CNAME;
|
|
const localCapabilities = this._localCapabilities;
|
|
const parameters = {
|
|
codecs: [],
|
|
degradationPreference: 'balanced',
|
|
encodings: [],
|
|
headerExtensions: [],
|
|
muxId: '',
|
|
rtcp: {
|
|
cname,
|
|
compound: true, // NOTE: Implemented in Edge.
|
|
mux: true,
|
|
reducedSize: true // NOTE: Not yet implemented in Edge.
|
|
}
|
|
};
|
|
|
|
const codecs = [];
|
|
let codecPayloadType;
|
|
|
|
for (const codecCapability of localCapabilities.codecs) {
|
|
if (codecCapability.kind !== kind
|
|
|| codecCapability.name === 'rtx') {
|
|
continue; // eslint-disable-line no-continue
|
|
}
|
|
|
|
codecPayloadType = codecCapability.preferredPayloadType;
|
|
codecs.push({
|
|
clockRate: codecCapability.clockRate,
|
|
maxptime: codecCapability.maxptime,
|
|
mimeType: codecCapability.mimeType,
|
|
name: codecCapability.name,
|
|
numChannels: codecCapability.numChannels,
|
|
parameters: codecCapability.parameters,
|
|
payloadType: codecCapability.preferredPayloadType,
|
|
ptime: codecCapability.ptime,
|
|
rtcpFeedback: codecCapability.rtcpFeedback
|
|
});
|
|
|
|
break;
|
|
}
|
|
|
|
if (rtxSsrc) {
|
|
for (const codecCapability of localCapabilities.codecs) {
|
|
if (codecCapability.kind !== kind
|
|
|| codecCapability.name !== 'rtx') {
|
|
continue; // eslint-disable-line no-continue
|
|
}
|
|
|
|
codecs.push({
|
|
clockRate: codecCapability.clockRate,
|
|
mimeType: codecCapability.mimeType,
|
|
name: 'rtx',
|
|
parameters: codecCapability.parameters,
|
|
payloadType: codecCapability.preferredPayloadType,
|
|
rtcpFeedback: codecCapability.rtcpFeedback
|
|
});
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
parameters.codecs = codecs;
|
|
|
|
const encoding = {
|
|
active: true,
|
|
codecPayloadType,
|
|
ssrc
|
|
};
|
|
|
|
if (rtxSsrc) {
|
|
encoding.rtx = {
|
|
ssrc: rtxSsrc
|
|
};
|
|
}
|
|
|
|
parameters.encodings.push(encoding);
|
|
|
|
for (const extension of localCapabilities.headerExtensions) {
|
|
if (extension.kind !== kind) {
|
|
continue; // eslint-disable-line no-continue
|
|
}
|
|
|
|
parameters.headerExtensions.push({
|
|
encrypt: extension.preferredEncrypt,
|
|
id: extension.preferredId,
|
|
uri: extension.uri
|
|
});
|
|
}
|
|
|
|
return parameters;
|
|
}
|
|
|
|
/**
|
|
* Promise based implementation for getStats().
|
|
* @return {Promise}
|
|
* @private
|
|
*/
|
|
_getStats(selector) { // eslint-disable-line no-unused-vars
|
|
if (this._closed) {
|
|
return Promise.reject(
|
|
new InvalidStateError('RTCPeerConnection closed'));
|
|
}
|
|
|
|
// TODO: TBD
|
|
return Promise.reject(new Error('getStats() not yet implemented'));
|
|
}
|
|
|
|
/**
|
|
* Handles the local initial answer.
|
|
* @return {Promise}
|
|
* @private
|
|
*/
|
|
_handleLocalInitialAnswer(desc) {
|
|
logger.debug('_handleLocalInitialAnswer(), desc:', desc);
|
|
|
|
const sdpObject = desc.sdpObject;
|
|
|
|
// Update local capabilities as decided by the app.
|
|
this._localCapabilities = ortcUtils.extractCapabilities(sdpObject);
|
|
|
|
logger.debug('local capabilities:', this._localCapabilities);
|
|
|
|
// TODO: Should inspect the answer given by the app and update our
|
|
// sending RTP parameters if, for example, the app modified SSRC
|
|
// values.
|
|
}
|
|
|
|
/**
|
|
* Handles a local re-answer.
|
|
* @return {Promise}
|
|
* @private
|
|
*/
|
|
_handleLocalReAnswer(desc) {
|
|
logger.debug('_handleLocalReAnswer(), desc:', desc);
|
|
|
|
const sdpObject = desc.sdpObject;
|
|
|
|
// Update local capabilities as decided by the app.
|
|
this._localCapabilities = ortcUtils.extractCapabilities(sdpObject);
|
|
|
|
logger.debug('local capabilities:', this._localCapabilities);
|
|
|
|
// TODO: Should inspect the answer given by the app and update our
|
|
// sending RTP parameters if, for example, the app modified SSRC
|
|
// values.
|
|
}
|
|
|
|
/**
|
|
* Handles the remote initial offer.
|
|
* @return {Promise}
|
|
* @private
|
|
*/
|
|
_handleRemoteInitialOffer(desc) {
|
|
logger.debug('_handleRemoteInitialOffer(), desc:', desc);
|
|
|
|
const sdpObject = desc.sdpObject;
|
|
|
|
// Set MID values.
|
|
this._mids = ortcUtils.extractMids(sdpObject);
|
|
|
|
// Get remote RTP capabilities.
|
|
const remoteCapabilities = ortcUtils.extractCapabilities(sdpObject);
|
|
|
|
logger.debug('remote capabilities:', remoteCapabilities);
|
|
|
|
// Get local RTP capabilities (filter them with remote capabilities).
|
|
this._localCapabilities
|
|
= ortcUtils.getLocalCapabilities(remoteCapabilities);
|
|
|
|
// Start ICE and DTLS.
|
|
this._startIceAndDtls(desc);
|
|
}
|
|
|
|
/**
|
|
* Handles a remote re-offer.
|
|
* @return {Promise}
|
|
* @private
|
|
*/
|
|
_handleRemoteReOffer(desc) {
|
|
logger.debug('_handleRemoteReOffer(), desc:', desc);
|
|
|
|
const sdpObject = desc.sdpObject;
|
|
|
|
// Update MID values (just in case).
|
|
this._mids = ortcUtils.extractMids(sdpObject);
|
|
|
|
// Get remote RTP capabilities (filter them with remote capabilities).
|
|
const remoteCapabilities = ortcUtils.extractCapabilities(sdpObject);
|
|
|
|
logger.debug('remote capabilities:', remoteCapabilities);
|
|
|
|
// Update local RTP capabilities (just in case).
|
|
this._localCapabilities
|
|
= ortcUtils.getLocalCapabilities(remoteCapabilities);
|
|
}
|
|
|
|
/**
|
|
* Start receiving remote media.
|
|
*/
|
|
_receiveMedia() {
|
|
logger.debug('_receiveMedia()');
|
|
|
|
const currentRemoteSsrcs = new Set(this._remoteTrackInfos.keys());
|
|
const newRemoteTrackInfos
|
|
= ortcUtils.extractTrackInfos(this._remoteDescription.sdpObject);
|
|
|
|
// Map of new remote MediaStream indexed by MediaStream.jitsiRemoteId.
|
|
const addedRemoteStreams = new Map();
|
|
|
|
// Map of remote MediaStream indexed by added MediaStreamTrack.
|
|
// NOTE: Just filled for already existing streams.
|
|
const addedRemoteTracks = new Map();
|
|
|
|
// Map of remote MediaStream indexed by removed MediaStreamTrack.
|
|
const removedRemoteTracks = new Map();
|
|
|
|
logger.debug(
|
|
'_receiveMedia() remote track infos:', newRemoteTrackInfos);
|
|
|
|
// Check new tracks.
|
|
for (const [ ssrc, info ] of newRemoteTrackInfos) {
|
|
// If already handled, ignore it.
|
|
if (currentRemoteSsrcs.has(ssrc)) {
|
|
continue; // eslint-disable-line no-continue
|
|
}
|
|
|
|
logger.debug(`_receiveMedia() new remote track, ssrc:${ssrc}`);
|
|
|
|
// Otherwise append to the map.
|
|
this._remoteTrackInfos.set(ssrc, info);
|
|
|
|
const kind = info.kind;
|
|
const rtxSsrc = info.rtxSsrc;
|
|
const streamRemoteId = info.streamId;
|
|
const trackRemoteId = info.trackId;
|
|
const cname = info.cname;
|
|
const isNewStream = !this._remoteStreams.has(streamRemoteId);
|
|
let stream;
|
|
|
|
if (isNewStream) {
|
|
logger.debug(
|
|
`_receiveMedia() new remote stream, id:${streamRemoteId}`);
|
|
|
|
// Create a new MediaStream.
|
|
stream = new MediaStream();
|
|
|
|
// Set custom property with the remote id.
|
|
stream.jitsiRemoteId = streamRemoteId;
|
|
|
|
addedRemoteStreams.set(streamRemoteId, stream);
|
|
this._remoteStreams.set(streamRemoteId, stream);
|
|
} else {
|
|
stream = this._remoteStreams.get(streamRemoteId);
|
|
}
|
|
|
|
const rtpReceiver = new RTCRtpReceiver(this._dtlsTransport, kind);
|
|
const parameters = this._getParametersForRtpReceiver(kind, {
|
|
ssrc,
|
|
rtxSsrc,
|
|
cname
|
|
});
|
|
|
|
// Store the track into the info object.
|
|
// NOTE: This should not be needed, but Edge has a bug:
|
|
// https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/12399497/
|
|
info.track = rtpReceiver.track;
|
|
|
|
// Set error handler.
|
|
rtpReceiver.onerror = ev => {
|
|
logger.error('rtpReceiver "error" event, event:');
|
|
logger.error(ev);
|
|
};
|
|
|
|
// Fill the info with the stream and rtpReceiver.
|
|
info.stream = stream;
|
|
info.rtpReceiver = rtpReceiver;
|
|
|
|
logger.debug(
|
|
'calling rtpReceiver.receive(), parameters:', parameters);
|
|
|
|
// Start receiving media.
|
|
try {
|
|
rtpReceiver.receive(parameters);
|
|
|
|
// Get the associated MediaStreamTrack.
|
|
const track = info.track;
|
|
|
|
// Set custom property with the remote id.
|
|
track.jitsiRemoteId = trackRemoteId;
|
|
|
|
// TODO: TMP
|
|
logger.warn(`new remote track [stream.jitsiRemoteId:${stream.jitsiRemoteId}, track.jitsiRemoteId:${track.jitsiRemoteId}, track.id:${track.id}]`);
|
|
|
|
// Add the track to the stream.
|
|
stream.addTrack(track);
|
|
|
|
if (!addedRemoteStreams.has(streamRemoteId)) {
|
|
addedRemoteTracks.set(track, stream);
|
|
}
|
|
} catch (error) {
|
|
logger.error(`rtpReceiver.receive() failed:${error.message}`);
|
|
logger.error(error);
|
|
}
|
|
}
|
|
|
|
// Check track removal.
|
|
for (const ssrc of currentRemoteSsrcs) {
|
|
if (newRemoteTrackInfos.has(ssrc)) {
|
|
continue; // eslint-disable-line no-continue
|
|
}
|
|
|
|
logger.debug(`_receiveMedia() remote track removed, ssrc:${ssrc}`);
|
|
|
|
const info = this._remoteTrackInfos.get(ssrc);
|
|
const stream = info.stream;
|
|
const track = info.track;
|
|
const rtpReceiver = info.rtpReceiver;
|
|
|
|
// TODO: TMP
|
|
logger.warn(`remote track removed [track.jitsiRemoteId:${track.jitsiRemoteId}, track.id:${track.id}]`);
|
|
|
|
try {
|
|
rtpReceiver.stop();
|
|
} catch (error) {
|
|
logger.warn(`rtpReceiver.stop() failed:${error}`);
|
|
}
|
|
|
|
removedRemoteTracks.set(track, stream);
|
|
stream.removeTrack(track);
|
|
this._remoteTrackInfos.delete(ssrc);
|
|
}
|
|
|
|
// Emit MediaStream 'addtrack' for new tracks in already existing
|
|
// streams.
|
|
for (const [ track, stream ] of addedRemoteTracks) {
|
|
const event = new Event('addtrack');
|
|
|
|
event.track = track;
|
|
stream.dispatchEvent(event);
|
|
}
|
|
|
|
// Emit MediaStream 'removetrack' for removed tracks.
|
|
for (const [ track, stream ] of removedRemoteTracks) {
|
|
// TODO: TMP
|
|
logger.warn(`emit "removetrack" [stream.jitsiRemoteId:${stream.jitsiRemoteId}, track.jitsiRemoteId:${track.jitsiRemoteId}, track.id:${track.id}]`);
|
|
|
|
const event = new Event('removetrack');
|
|
|
|
event.track = track;
|
|
stream.dispatchEvent(event);
|
|
}
|
|
|
|
// Emit RTCPeerConnection 'addstream' for new remote streams.
|
|
for (const stream of addedRemoteStreams.values()) {
|
|
// Check whether at least a track was added, otherwise ignore it.
|
|
if (stream.getTracks().length === 0) {
|
|
logger.warn(
|
|
'ignoring new stream for which no track could be added');
|
|
|
|
addedRemoteStreams.delete(stream.jitsiRemoteId);
|
|
this._remoteStreams.delete(stream.jitsiRemoteId);
|
|
} else {
|
|
this._emitAddStream(stream);
|
|
}
|
|
}
|
|
|
|
// Emit RTCPeerConnection 'removestream' for removed remote streams.
|
|
for (const [ streamRemoteId, stream ] of this._remoteStreams) {
|
|
// TODO: TMP
|
|
logger.warn(`remote stream [streamRemoteId:${streamRemoteId}, jitsiRemoteId:${stream.jitsiRemoteId}, tracks:${stream.getTracks().length}]`);
|
|
|
|
if (stream.getTracks().length > 0) {
|
|
continue; // eslint-disable-line no-continue
|
|
}
|
|
|
|
this._remoteStreams.delete(streamRemoteId);
|
|
this._emitRemoveStream(stream);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Implementation for removeStream().
|
|
* @private
|
|
*/
|
|
_removeStream(stream) {
|
|
if (this._closed) {
|
|
throw new InvalidStateError('RTCPeerConnection closed');
|
|
}
|
|
|
|
// Stop and remove the RTCRtpSender associated to each track.
|
|
for (const track of stream.getTracks()) {
|
|
// Ignore if track not present.
|
|
if (!this._localTrackInfos.has(track.id)) {
|
|
continue; // eslint-disable-line no-continue
|
|
}
|
|
|
|
const rtpSender = this._localTrackInfos.get(track.id).rtpSender;
|
|
|
|
try {
|
|
rtpSender.stop();
|
|
} catch (error) {
|
|
logger.warn(`rtpSender.stop() failed:${error}`);
|
|
}
|
|
|
|
// Remove from the map.
|
|
this._localTrackInfos.delete(track.id);
|
|
}
|
|
|
|
// It may need to renegotiate.
|
|
this._emitNegotiationNeeded();
|
|
}
|
|
|
|
/**
|
|
* Start sending our media to the remote.
|
|
*/
|
|
_sendMedia() {
|
|
logger.debug('_sendMedia()');
|
|
|
|
for (const info of this._localTrackInfos.values()) {
|
|
// Ignore if already sending.
|
|
if (info.sending) {
|
|
continue; // eslint-disable-line no-continue
|
|
}
|
|
|
|
// Update sending field.
|
|
info.sending = true;
|
|
|
|
const rtpSender = info.rtpSender;
|
|
const ssrc = info.ssrc;
|
|
const rtxSsrc = info.rtxSsrc;
|
|
const track = rtpSender.track;
|
|
const kind = track.kind;
|
|
|
|
const parameters = this._getParametersForRtpSender(kind, {
|
|
ssrc,
|
|
rtxSsrc
|
|
});
|
|
|
|
logger.debug(
|
|
'calling rtpSender.send(), parameters:', parameters);
|
|
|
|
// Start rsending media.
|
|
try {
|
|
rtpSender.send(parameters);
|
|
} catch (error) {
|
|
logger.error(`rtpSender.send() failed:${error.message}`);
|
|
logger.error(error);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates the RTCDtlsTransport.
|
|
* @private
|
|
*/
|
|
_setDtlsTransport(iceTransport) {
|
|
const dtlsTransport = new RTCDtlsTransport(iceTransport);
|
|
|
|
// NOTE: Not yet implemented by Edge.
|
|
dtlsTransport.onstatechange = () => {
|
|
logger.debug(
|
|
'dtlsTransport "statechange" event, '
|
|
+ `state:${dtlsTransport.state}`);
|
|
|
|
this._emitConnectionStateChange();
|
|
};
|
|
|
|
// NOTE: Not standard, but implemented by Edge.
|
|
dtlsTransport.ondtlsstatechange = () => {
|
|
logger.debug(
|
|
'dtlsTransport "dtlsstatechange" event, '
|
|
+ `state:${dtlsTransport.state}`);
|
|
|
|
this._emitConnectionStateChange();
|
|
};
|
|
|
|
dtlsTransport.onerror = ev => {
|
|
let message;
|
|
|
|
if (ev.message) {
|
|
message = ev.message;
|
|
} else if (ev.error) {
|
|
message = ev.error.message;
|
|
}
|
|
|
|
logger.error(`dtlsTransport "error" event, message:${message}`);
|
|
|
|
// TODO: Edge does not set state to 'failed' on error. We should
|
|
// hack it.
|
|
|
|
this._emitConnectionStateChange();
|
|
};
|
|
|
|
this._dtlsTransport = dtlsTransport;
|
|
}
|
|
|
|
/**
|
|
* Creates the RTCIceGatherer.
|
|
* @private
|
|
*/
|
|
_setIceGatherer(pcConfig) {
|
|
const iceGatherOptions = {
|
|
gatherPolicy: pcConfig.iceTransportPolicy || 'all',
|
|
iceServers: pcConfig.iceServers || []
|
|
};
|
|
const iceGatherer = new RTCIceGatherer(iceGatherOptions);
|
|
|
|
// NOTE: Not yet implemented by Edge.
|
|
iceGatherer.onstatechange = () => {
|
|
logger.debug(
|
|
`iceGatherer "statechange" event, state:${iceGatherer.state}`);
|
|
|
|
this._updateAndEmitIceGatheringStateChange(iceGatherer.state);
|
|
};
|
|
|
|
iceGatherer.onlocalcandidate = ev => {
|
|
let candidate = ev.candidate;
|
|
|
|
// NOTE: Not yet implemented by Edge.
|
|
const complete = ev.complete;
|
|
|
|
logger.debug(
|
|
'iceGatherer "localcandidate" event, candidate:', candidate);
|
|
|
|
// NOTE: Instead of null candidate or complete:true, current Edge
|
|
// signals end of gathering with an empty candidate object.
|
|
if (complete
|
|
|| !candidate
|
|
|| Object.keys(candidate).length === 0) {
|
|
|
|
candidate = null;
|
|
|
|
this._updateAndEmitIceGatheringStateChange(
|
|
RTCIceGatheringState.complete);
|
|
this._emitIceCandidate(null);
|
|
} else {
|
|
this._emitIceCandidate(candidate);
|
|
}
|
|
};
|
|
|
|
iceGatherer.onerror = ev => {
|
|
const errorCode = ev.errorCode;
|
|
const errorText = ev.errorText;
|
|
|
|
logger.error(
|
|
`iceGatherer "error" event, errorCode:${errorCode}, `
|
|
+ `errorText:${errorText}`);
|
|
};
|
|
|
|
// NOTE: Not yet implemented by Edge, which starts gathering
|
|
// automatically.
|
|
try {
|
|
iceGatherer.gather();
|
|
} catch (error) {
|
|
logger.warn(`iceGatherer.gather() failed:${error}`);
|
|
}
|
|
|
|
this._iceGatherer = iceGatherer;
|
|
}
|
|
|
|
/**
|
|
* Creates the RTCIceTransport.
|
|
* @private
|
|
*/
|
|
_setIceTransport(iceGatherer) {
|
|
const iceTransport = new RTCIceTransport(iceGatherer);
|
|
|
|
// NOTE: Not yet implemented by Edge.
|
|
iceTransport.onstatechange = () => {
|
|
logger.debug(
|
|
'iceTransport "statechange" event, '
|
|
+ `state:${iceTransport.state}`);
|
|
|
|
this._emitIceConnectionStateChange();
|
|
};
|
|
|
|
// NOTE: Not standard, but implemented by Edge.
|
|
iceTransport.onicestatechange = () => {
|
|
logger.debug(
|
|
'iceTransport "icestatechange" event, '
|
|
+ `state:${iceTransport.state}`);
|
|
|
|
if (iceTransport.state === 'completed') {
|
|
logger.debug(
|
|
'nominated candidate pair:',
|
|
iceTransport.getNominatedCandidatePair());
|
|
}
|
|
|
|
this._emitIceConnectionStateChange();
|
|
};
|
|
|
|
iceTransport.oncandidatepairchange = ev => {
|
|
logger.debug(
|
|
'iceTransport "candidatepairchange" event, '
|
|
+ `pair:${ev.pair}`);
|
|
};
|
|
|
|
this._iceTransport = iceTransport;
|
|
}
|
|
|
|
/**
|
|
* Promise based implementation for setLocalDescription().
|
|
* @returns {Promise}
|
|
* @private
|
|
*/
|
|
_setLocalDescription(desc) {
|
|
if (this._closed) {
|
|
return Promise.reject(
|
|
new InvalidStateError('RTCPeerConnection closed'));
|
|
}
|
|
|
|
let localDescription;
|
|
|
|
try {
|
|
localDescription = new RTCSessionDescription(desc);
|
|
} catch (error) {
|
|
return Promise.reject(new TypeError(
|
|
`invalid RTCSessionDescriptionInit: ${error}`));
|
|
}
|
|
|
|
switch (desc.type) {
|
|
case 'offer': {
|
|
if (this.signalingState !== RTCSignalingState.stable) {
|
|
return Promise.reject(new InvalidStateError(
|
|
`invalid signalingState "${this.signalingState}"`));
|
|
}
|
|
|
|
// NOTE: P2P mode not yet supported, so createOffer() should never
|
|
// has been called, neither setLocalDescription() with an offer.
|
|
return Promise.reject(new TypeError(
|
|
'setLocalDescription() with type "offer" not supported'));
|
|
}
|
|
case 'answer': {
|
|
if (this.signalingState !== RTCSignalingState.haveRemoteOffer) {
|
|
return Promise.reject(new InvalidStateError(
|
|
`invalid signalingState "${this.signalingState}"`));
|
|
}
|
|
|
|
const isLocalInitialAnswer = Boolean(!this._localDescription);
|
|
|
|
return Promise.resolve()
|
|
.then(() => {
|
|
// Different handling for initial answer and re-answer.
|
|
if (isLocalInitialAnswer) {
|
|
return this._handleLocalInitialAnswer(localDescription);
|
|
} else { // eslint-disable-line no-else-return
|
|
return this._handleLocalReAnswer(localDescription);
|
|
}
|
|
})
|
|
.then(() => {
|
|
logger.debug('setLocalDescription() succeed');
|
|
|
|
// Update local description.
|
|
this._localDescription = localDescription;
|
|
|
|
// Update signaling state.
|
|
this._updateAndEmitSignalingStateChange(
|
|
RTCSignalingState.stable);
|
|
|
|
// If initial answer, emit buffered ICE candidates.
|
|
if (isLocalInitialAnswer) {
|
|
this._emitBufferedIceCandidates();
|
|
}
|
|
|
|
// Send our RTP.
|
|
this._sendMedia();
|
|
|
|
// Receive remote RTP.
|
|
this._receiveMedia();
|
|
})
|
|
.catch(error => {
|
|
logger.error(
|
|
`setLocalDescription() failed: ${error.message}`);
|
|
logger.error(error);
|
|
|
|
throw error;
|
|
});
|
|
}
|
|
default:
|
|
return Promise.reject(new TypeError(
|
|
`unsupported description.type "${desc.type}"`));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Promise based implementation for setRemoteDescription().
|
|
* @returns {Promise}
|
|
* @private
|
|
*/
|
|
_setRemoteDescription(desc) {
|
|
if (this._closed) {
|
|
return Promise.reject(
|
|
new InvalidStateError('RTCPeerConnection closed'));
|
|
}
|
|
|
|
let remoteDescription;
|
|
|
|
try {
|
|
remoteDescription = new RTCSessionDescription(desc);
|
|
} catch (error) {
|
|
return Promise.reject(new TypeError(
|
|
`invalid RTCSessionDescriptionInit: ${error}`));
|
|
}
|
|
|
|
switch (desc.type) {
|
|
case 'offer': {
|
|
if (this.signalingState !== RTCSignalingState.stable) {
|
|
return Promise.reject(new InvalidStateError(
|
|
`invalid signalingState "${this.signalingState}"`));
|
|
}
|
|
|
|
const isRemoteInitialOffer = Boolean(!this._remoteDescription);
|
|
|
|
return Promise.resolve()
|
|
.then(() => {
|
|
// Different handling for initial answer and re-answer.
|
|
if (isRemoteInitialOffer) {
|
|
return this._handleRemoteInitialOffer(
|
|
remoteDescription);
|
|
} else { // eslint-disable-line no-else-return
|
|
return this._handleRemoteReOffer(remoteDescription);
|
|
}
|
|
})
|
|
.then(() => {
|
|
logger.debug('setRemoteDescription() succeed');
|
|
|
|
// Update remote description.
|
|
this._remoteDescription = remoteDescription;
|
|
|
|
// Update signaling state.
|
|
this._updateAndEmitSignalingStateChange(
|
|
RTCSignalingState.haveRemoteOffer);
|
|
})
|
|
.catch(error => {
|
|
logger.error(`setRemoteDescription() failed: ${error}`);
|
|
|
|
throw error;
|
|
});
|
|
}
|
|
case 'answer': {
|
|
if (this.signalingState !== RTCSignalingState.haveLocalOffer) {
|
|
return Promise.reject(new InvalidStateError(
|
|
`invalid signalingState "${this.signalingState}"`));
|
|
}
|
|
|
|
// NOTE: P2P mode not yet supported, so createOffer() should never
|
|
// has been called, neither setRemoteDescription() with an answer.
|
|
return Promise.reject(new TypeError(
|
|
'setRemoteDescription() with type "answer" not supported'));
|
|
}
|
|
default:
|
|
return Promise.reject(new TypeError(
|
|
`unsupported description.type "${desc.type}"`));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Start ICE and DTLS connection procedures.
|
|
* @param {RTCSessionDescription} desc - Remote description.
|
|
*/
|
|
_startIceAndDtls(desc) {
|
|
const sdpObject = desc.sdpObject;
|
|
const remoteIceParameters
|
|
= ortcUtils.extractIceParameters(sdpObject);
|
|
const remoteIceCandidates
|
|
= ortcUtils.extractIceCandidates(sdpObject);
|
|
const remoteDtlsParameters
|
|
= ortcUtils.extractDtlsParameters(sdpObject);
|
|
|
|
// Start the RTCIceTransport.
|
|
switch (desc.type) {
|
|
case 'offer':
|
|
this._iceTransport.start(
|
|
this._iceGatherer, remoteIceParameters, 'controlled');
|
|
break;
|
|
case 'answer':
|
|
this._iceTransport.start(
|
|
this._iceGatherer, remoteIceParameters, 'controlling');
|
|
break;
|
|
}
|
|
|
|
// Add remote ICE candidates.
|
|
// NOTE: Remove candidates that Edge doesn't like.
|
|
for (const candidate of remoteIceCandidates) {
|
|
if (candidate.port === 0 || candidate.port === 9) {
|
|
continue; // eslint-disable-line no-continue
|
|
}
|
|
|
|
this._iceTransport.addRemoteCandidate(candidate);
|
|
}
|
|
|
|
// Also signal a 'complete' candidate as per spec.
|
|
// NOTE: It should be {complete: true} but Edge prefers {}.
|
|
// NOTE: We know that addCandidate() is never used so we need to signal
|
|
// end of candidates (otherwise the RTCIceTransport never enters the
|
|
// 'completed' state).
|
|
this._iceTransport.addRemoteCandidate({});
|
|
|
|
// Set desired remote DTLS role (as we receive the offer).
|
|
switch (desc.type) {
|
|
case 'offer':
|
|
remoteDtlsParameters.role = 'server';
|
|
break;
|
|
case 'answer':
|
|
remoteDtlsParameters.role = 'client';
|
|
break;
|
|
}
|
|
|
|
// Start RTCDtlsTransport.
|
|
this._dtlsTransport.start(remoteDtlsParameters);
|
|
}
|
|
|
|
/**
|
|
* May update iceGatheringState and emit 'icegatheringstatechange' event.
|
|
* @private
|
|
*/
|
|
_updateAndEmitIceGatheringStateChange(state) {
|
|
if (this._closed || state === this.iceGatheringState) {
|
|
return;
|
|
}
|
|
|
|
this._iceGatheringState = state;
|
|
|
|
logger.debug(
|
|
'emitting "icegatheringstatechange", iceGatheringState:',
|
|
this.iceGatheringState);
|
|
|
|
const event = new yaeti.Event('icegatheringstatechange');
|
|
|
|
this.dispatchEvent(event);
|
|
}
|
|
|
|
/**
|
|
* May update signalingState and emit 'signalingstatechange' event.
|
|
* @private
|
|
*/
|
|
_updateAndEmitSignalingStateChange(state) {
|
|
if (state === this.signalingState) {
|
|
return;
|
|
}
|
|
|
|
this._signalingState = state;
|
|
|
|
logger.debug(
|
|
'emitting "signalingstatechange", signalingState:',
|
|
this.signalingState);
|
|
|
|
const event = new yaeti.Event('signalingstatechange');
|
|
|
|
this.dispatchEvent(event);
|
|
}
|
|
}
|