123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772 |
- /* global __filename, RTCIceGatherer, RTCIceTransport */
-
- import { getLogger } from 'jitsi-meet-logger';
- import yaeti from 'yaeti';
-
- import { InvalidStateError } from './errors';
-
- 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'
- };
-
- /**
- * 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/
- */
- export default class ortcRTCPeerConnection extends yaeti.EventTarget {
- /**
- */
- constructor(pcConfig) {
- super();
-
- logger.debug('constructor() pcConfig:', pcConfig);
-
- // Closed flag.
- // @type {boolean}
- this._closed = false;
-
- // Create a RTCIceGatherer.
- // @type {RTCIceGatherer}
- this._iceGatherer = this._createIceGatherer(pcConfig);
-
- // RTCPeerConnection iceGatheringState.
- // NOTE: This should not be needed, but Edge does not implement
- // iceGatherer.state.
- // @type {RTCIceGatheringState}
- this._iceGatheringState = RTCIceGatheringState.new;
-
- // Create a RTCIceTransport.
- // @type {RTCIceTransport}
- this._iceTransport = this._createIceTransport(this._iceGatherer);
-
- // Local RTCSessionDescription.
- // @type {RTCSessionDescription}
- this._localDescription = null;
-
- // Set of local MediaStreams.
- // @type {Set<MediaStream>}
- this._localStreams = new Set();
-
- // Remote RTCSessionDescription.
- // @type {RTCSessionDescription}
- this._remoteDescription = null;
-
- // Set of remote MediaStreams.
- // @type {Set<MediaStream>}
- this._remoteStreams = new Set();
-
- // RTCPeerConnection signalingState.
- // @type {RTCSignalingState}
- this._signalingState = RTCSignalingState.stable;
- }
-
- /**
- * Gets the current signaling state.
- * @return {RTCSignalingState}
- */
- get signalingState() {
- return this._signalingState;
- }
-
- /**
- * Gets the current ICE gathering state.
- * @return {RTCIceGatheringState}
- */
- get iceGatheringState() {
- return this._iceGatheringState;
- }
-
- /**
- * Gets the current ICE connection state.
- * @return {RTCIceConnectionState}
- */
- get iceConnectionState() {
- return this._iceTransport.state;
- }
-
- /**
- * Gets the local description.
- * @return {RTCSessionDescription}
- */
- get localDescription() {
- return this._localDescription;
- }
-
- /**
- * Gets the remote description.
- * @return {RTCSessionDescription}
- */
- get remoteDescription() {
- return this._remoteDescription;
- }
-
- /**
- * Closes the RTCPeerConnection.
- */
- close() {
- if (this._closed) {
- return;
- }
-
- this._closed = true;
-
- logger.debug('close()');
-
- this._updateAndEmitSignalingStateChange(RTCSignalingState.closed);
-
- // Close iceGatherer.
- // NOTE: Not yet implemented by Edge.
- try {
- this._iceGatherer.close();
- } catch (error) {
- logger.warn(`iceGatherer.close() failed:${error}`);
- }
-
- // Close iceTransport.
- try {
- this._iceTransport.stop();
- } catch (error) {
- logger.warn(`iceTransport.stop() failed:${error}`);
- }
-
- // Clear local/remote streams.
- this._localStreams.clear();
- this._remoteStreams.clear();
-
- // TODO: Close and emit more stuff.
- }
-
- /**
- * 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));
- }
-
- /**
- * 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));
- }
-
- /**
- * 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));
- }
-
- /**
- * 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);
- }
-
- /**
- * Removes a local MediaStream.
- * @param {MediaStream} stream.
- * NOTE: Deprecated API.
- */
- removeStream(stream) {
- logger.debug('removeStream()');
-
- this._removeStream(stream);
- }
-
- /**
- * Creates a RTCDataChannel.
- * TBD
- */
- createDataChannel() {
- logger.debug('createDataChannel()');
- }
-
- /**
- * Gets a sequence of local MediaStreams.
- */
- getLocalStreams() {
- return Array.from(this._localStreams);
- }
-
- /**
- * Gets a sequence of remote MediaStreams.
- */
- getRemoteStreams() {
- return Array.from(this._remoteStreams);
- }
-
- /**
- * TBD
- */
- getStats() {
- // TBD
- }
-
- /**
- * Creates and returns a RTCIceGatherer.
- * @return {RTCIceGatherer}
- * @private
- */
- _createIceGatherer(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}`);
- }
-
- return iceGatherer;
- }
-
- /**
- * Creates and returns a RTCIceTransport.
- * @return {RTCIceTransport}
- * @private
- */
- _createIceTransport(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}`);
-
- this._emitIceConnectionStateChange();
- };
-
- // TODO: More stuff to be done.
-
- return iceTransport;
- }
-
- /**
- * 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}"`));
- }
-
- // TODO: More stuff to be done.
- }
-
- /**
- * 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}"`));
- }
-
- // TODO: More stuff to be done.
- }
-
- /**
- * Promise based implementation for setLocalDescription().
- * @returns {Promise}
- * @private
- */
- _setLocalDescription(desc) {
- if (this._closed) {
- return Promise.reject(
- new InvalidStateError('RTCPeerConnection closed'));
- }
-
- switch (desc.type) {
- case 'offer':
- if (this.signalingState !== RTCSignalingState.stable) {
- return Promise.reject(new InvalidStateError(
- `invalid signalingState "${this.signalingState}"`));
- }
-
- break;
-
- case 'answer':
- if (this.signalingState !== RTCSignalingState.haveRemoteOffer) {
- return Promise.reject(new InvalidStateError(
- `invalid signalingState "${this.signalingState}"`));
- }
-
- break;
-
- default:
- throw new TypeError(`unsupported description.type "${desc.type}"`);
- }
-
- // TODO: More stuff to be done.
- }
-
- /**
- * Promise based implementation for setRemoteDescription().
- * @returns {Promise}
- * @private
- */
- _setRemoteDescription(desc) {
- if (this._closed) {
- return Promise.reject(
- new InvalidStateError('RTCPeerConnection closed'));
- }
-
- switch (desc.type) {
- case 'offer':
- if (this.signalingState !== RTCSignalingState.stable) {
- return Promise.reject(new InvalidStateError(
- `invalid signalingState "${this.signalingState}"`));
- }
-
- break;
-
- case 'answer':
- if (this.signalingState !== RTCSignalingState.haveLocalOffer) {
- return Promise.reject(new InvalidStateError(
- `invalid signalingState "${this.signalingState}"`));
- }
-
- break;
-
- default:
- throw new TypeError(`unsupported description.type "${desc.type}"`);
- }
-
- // TODO: More stuff to be done.
- }
-
- /**
- * Implementation for addStream().
- * @private
- */
- _addStream(stream) {
- if (this._closed) {
- throw new InvalidStateError('RTCPeerConnection closed');
- }
-
- if (this._localStreams.has(stream)) {
- return;
- }
-
- this._localStreams.add(stream);
-
- // It may need to renegotiate.
- this._emitNegotiationNeeded();
- }
-
- /**
- * Implementation for removeStream().
- * @private
- */
- _removeStream(stream) {
- if (this._closed) {
- throw new InvalidStateError('RTCPeerConnection closed');
- }
-
- if (!this._localStreams.has(stream)) {
- return;
- }
-
- this._localStreams.delete(stream);
-
- // It may need to renegotiate.
- this._emitNegotiationNeeded();
- }
-
- /**
- * May update signalingState and emit 'signalingstatechange' event.
- */
- _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);
- }
-
- /**
- * May emit 'negotiationneeded' event.
- */
- _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);
- }
-
- /**
- * May update iceGatheringState and emit 'icegatheringstatechange' event.
- */
- _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 emit 'iceconnectionstatechange' event.
- */
- _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 'icecandidate' event.
- */
- _emitIceCandidate(candidate) {
- if (this._closed) {
- return;
- }
-
- const event = new yaeti.Event('icecandidate');
-
- logger.debug(
- 'emitting "icecandidate", candidate:', candidate);
-
- event.candidate = candidate;
- this.dispatchEvent(event);
- }
- }
|