| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381 |
- /* 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;
-
- // Use a promise to signify when the local client has finished gathering
- // ice candidates. This is needed to prevent errors on offer creation.
- this._resolveLocalIceGatheringPromise = null;
- this._iceGatheringCompletePromise = new Promise(resolve => {
- this._resolveLocalIceGatheringPromise = resolve;
- });
-
- // 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();
-
- // remoteDtlsParameters are not needed for offers.
- const remoteDtlsParameters = type === 'answer'
- ? this._dtlsTransport.getRemoteParameters() : null;
- 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}"`));
- }
-
- // Use ice gathering complete promise an invalid state error occurs if
- // creating a local description before it is complete.
- return this._iceGatheringCompletePromise
- .then(() => {
- // Stub out _mids and _localCapabilities if creating an offer
- // as they are normally set on answer and will cause errors in
- // create a local description if falsy.
- if (!this.hasAttemptedOffer) {
- if (!this._mids.size) {
- this._mids = new Map([
- [ 'audio', 'audio' ],
- [ 'video', 'video' ]
- ]);
- }
-
- if (!this._localCapabilities) {
- this._localCapabilities
- = utils.getLocalCapabilities(
- RTCRtpSender.getCapabilities());
- }
-
- this.hasAttemptedOffer = true;
- }
-
- return this._createLocalDescription('offer');
- });
- }
-
- /**
- * 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;
- }
-
- /**
- * Pares STUN and TURN servers to only one. This works around an issue on
- * edge where an error occurs if multiple are declared.
- * See https://developer.microsoft.com/en-us/microsoft-edge/platform
- * /issues/10163458
- *
- * @param {array} servers - All STUN AND TURN servers to connect to.
- * @private
- * @returns {array} An array with only one STUN server and one TURN server
- * at the most.
- */
- _filterServers(servers = []) {
- const filteredServers = [];
- const firstStun = servers.find(server => server.url.startsWith('stun'));
-
- if (firstStun) {
- filteredServers.push(firstStun);
- }
-
- const firstTurn = servers.find(server => server.url.startsWith('turn'));
-
- if (firstTurn) {
- filteredServers.push(firstTurn);
- }
-
- return filteredServers;
- }
-
- /**
- * Creates the RTCIceGatherer.
- * @private
- */
- _setIceGatherer(pcConfig) {
- const iceGatherOptions = {
- gatherPolicy: pcConfig.iceTransportPolicy || 'all',
- iceServers: this._filterServers(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._resolveLocalIceGatheringPromise();
- 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}"`));
- }
-
- return Promise.resolve()
- .then(() => {
- const sdpObject = localDescription.sdpObject;
-
- // Update local values? Needed?
- this._localCapabilities
- = utils.extractCapabilities(sdpObject);
- this._localDescription = localDescription;
-
- // Update signaling state.
- this._updateAndEmitSignalingStateChange(
- RTCSignalingState.haveLocalOffer);
- })
- .catch(error => {
- logger.error(
- `setLocalDescription() failed: ${error.message}`);
- logger.error(error);
-
- throw error;
- });
- }
- 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');
-
- this._remoteDescription = remoteDescription;
-
- 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}"`));
- }
-
- return Promise.resolve()
- .then(() => {
- // Update remote description.
- this._remoteDescription = remoteDescription;
-
- // Update signaling state.
- this._updateAndEmitSignalingStateChange(
- RTCSignalingState.stable);
- })
- .catch(error => {
- logger.error(`setRemoteDescription() failed: ${error}`);
-
- throw error;
- });
- }
- 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);
- }
- }
|