123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295 |
- /* global __filename, RTCIceGatherer, RTCIceTransport, RTCDtlsTransport,
- RTCRtpSender, RTCRtpReceiver */
-
- import { getLogger } from 'jitsi-meet-logger';
- import yaeti from 'yaeti';
-
- import RTCSessionDescription from './RTCSessionDescription';
- import * as utils from './utils';
- import { InvalidStateError } from './errors';
- import RandomUtil from '../../util/RandomUtil';
- import SDPUtil from '../../xmpp/SDPUtil';
-
- const logger = getLogger(__filename);
-
- 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-${RandomUtil.randomInt(10000, 99999)}`;
-
- /**
- * 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: SDPUtil.generateSsrc(),
- 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 (typeof args[0] === 'function') {
- usePromise = false;
- callback = args[0];
- errback = args[1];
- } else if (typeof args[1] === 'function') {
- usePromise = false;
- selector = args[0];
- callback = args[1];
- errback = args[2];
- } else {
- usePromise = true;
- selector = args[0];
- }
-
- if (!usePromise && !errback) {
- errback = error => {
- logger.error(`getStats() failed: ${error}`);
- logger.error(error.stack);
- };
- }
-
- 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
- });
- }
-
- // Check for local tracks removal.
- for (const [ trackId, info ] of this._localTrackInfos) {
- const track = info.rtpSender.track;
-
- // Check if any of the local tracks has been stopped.
- if (track.readyState === 'ended') {
- logger.warn(
- '_addStream() an already handled track was stopped, '
- + `track.id:${track.id}`);
-
- try {
- info.rtpSender.stop();
- } catch (error) {
- logger.warn(`rtpSender.stop() failed:${error}`);
- }
-
- // Remove from the map.
- this._localTrackInfos.delete(track.id);
-
- // Also, if the stream was already handled, check whether tracks
- // have been removed via stream.removeTrack() and, if so, stop
- // their RtpSenders.
- } else if (info.stream === stream
- && !stream.getTrackById(trackId)) {
- logger.warn(
- '_addStream() a track in this stream was removed, '
- + `track.id:${trackId}`);
-
- try {
- info.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();
- }
-
- /**
- * 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 = SDPUtil.generateSsrc();
- }
-
- // 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'));
- }
-
- /**
- * 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} RTCStats dictionary.
- * @private
- */
- _getStats(selector) { // eslint-disable-line no-unused-vars
- if (this._closed) {
- return Promise.reject(
- new InvalidStateError('RTCPeerConnection closed'));
- }
-
- const iceGatherer = this._iceGatherer;
- const iceTransport = this._iceTransport;
- const rtpSenders = [];
- const rtpReceivers = [];
- const promises = [];
-
- // Get RtpSenders.
- for (const info of this._localTrackInfos.values()) {
- const { rtpSender, sending } = info;
-
- if (sending) {
- rtpSenders.push(rtpSender);
- }
- }
-
- // Get RtpReceivers.
- for (const info of this._remoteTrackInfos.values()) {
- const { rtpReceiver } = info;
-
- rtpReceivers.push(rtpReceiver);
- }
-
- // Collect all the stats.
-
- if (iceGatherer) {
- promises.push(
- iceGatherer.getStats()
- .catch(() => null));
- }
-
- if (iceTransport) {
- promises.push(
- iceTransport.getStats()
- .catch(() => null));
-
- // NOTE: Proprietary stuff in Edge.
- if (typeof iceTransport.msGetStats === 'function') {
- promises.push(
- iceTransport.msGetStats()
- .catch(() => null));
- }
- }
-
- for (const rtpSender of rtpSenders) {
- const isAudio = rtpSender.track.kind === 'audio';
-
- promises.push(rtpSender.getStats()
- .then(data => {
- // Remove audioLevel from type="track" stats if this is
- // not an audio sender.
- if (!isAudio) {
- for (const key of Object.keys(data)) {
- const stat = data[key];
-
- if (stat.type === 'track') {
- delete stat.audioLevel;
- }
- }
- }
-
- return data;
- })
- .catch(() => null));
- }
-
- for (const rtpReceiver of rtpReceivers) {
- const isAudio = rtpReceiver.track.kind === 'audio';
-
- promises.push(rtpReceiver.getStats()
- .then(data => {
- // Remove audioLevel from type="track" stats if this is
- // not an audio receiver.
- if (!isAudio) {
- for (const key of Object.keys(data)) {
- const stat = data[key];
-
- if (stat.type === 'track') {
- delete stat.audioLevel;
- }
- }
- }
-
- return data;
- })
- .catch(() => null));
- }
-
- return Promise.all(promises)
- .then(datas => {
- const stats = {};
-
- for (const data of datas) {
- if (!data) {
- continue; // eslint-disable-line no-continue
- }
-
- for (const key of Object.keys(data)) {
- stats[key] = data[key];
- }
- }
-
- return stats;
- });
- }
-
- /**
- * 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 = utils.extractCapabilities(sdpObject);
-
- logger.debug('local capabilities:', this._localCapabilities);
-
- // NOTE: We assume that the answer given by the app does not change
- // SSRC or PT values. If so, things won't work as expected.
- }
-
- /**
- * 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 = utils.extractCapabilities(sdpObject);
-
- logger.debug('local capabilities:', this._localCapabilities);
-
- // NOTE: We assume that the answer given by the app does not change
- // SSRC or PT values. If so, things won't work as expected.
- }
-
- /**
- * Handles the remote initial offer.
- * @return {Promise}
- * @private
- */
- _handleRemoteInitialOffer(desc) {
- logger.debug('_handleRemoteInitialOffer(), desc:', desc);
-
- const sdpObject = desc.sdpObject;
-
- // Set MID values.
- this._mids = utils.extractMids(sdpObject);
-
- // Get remote RTP capabilities.
- const remoteCapabilities = utils.extractCapabilities(sdpObject);
-
- logger.debug('remote capabilities:', remoteCapabilities);
-
- // Get local RTP capabilities (filter them with remote capabilities).
- this._localCapabilities
- = utils.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 = utils.extractMids(sdpObject);
-
- // Get remote RTP capabilities (filter them with remote capabilities).
- const remoteCapabilities = utils.extractCapabilities(sdpObject);
-
- logger.debug('remote capabilities:', remoteCapabilities);
-
- // Update local RTP capabilities (just in case).
- this._localCapabilities
- = utils.getLocalCapabilities(remoteCapabilities);
- }
-
- /**
- * Start receiving remote media.
- */
- _receiveMedia() {
- logger.debug('_receiveMedia()');
-
- const currentRemoteSsrcs = new Set(this._remoteTrackInfos.keys());
- const newRemoteTrackInfos
- = utils.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;
-
- // 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;
-
- 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) {
- 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) {
- 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
- }
-
- 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 sending media.
- try {
- rtpSender.send(parameters);
-
- // Update sending field.
- info.sending = true;
- } 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 may
- // 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
- = utils.extractIceParameters(sdpObject);
- const remoteIceCandidates
- = utils.extractIceCandidates(sdpObject);
- const remoteDtlsParameters
- = utils.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);
- }
- }
|