您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

RTCPeerConnection.js 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772
  1. /* global __filename, RTCIceGatherer, RTCIceTransport */
  2. import { getLogger } from 'jitsi-meet-logger';
  3. import yaeti from 'yaeti';
  4. import { InvalidStateError } from './errors';
  5. const logger = getLogger(__filename);
  6. const RTCSignalingState = {
  7. stable: 'stable',
  8. haveLocalOffer: 'have-local-offer',
  9. haveRemoteOffer: 'have-remote-offer',
  10. closed: 'closed'
  11. };
  12. const RTCIceGatheringState = {
  13. new: 'new',
  14. gathering: 'gathering',
  15. complete: 'complete'
  16. };
  17. /**
  18. * RTCPeerConnection shim for ORTC based endpoints (such as Edge).
  19. *
  20. * The interface is based on the W3C specification of 2015, which matches
  21. * the implementation of Chrome nowadays:
  22. *
  23. * https://www.w3.org/TR/2015/WD-webrtc-20150210/
  24. */
  25. export default class ortcRTCPeerConnection extends yaeti.EventTarget {
  26. /**
  27. */
  28. constructor(pcConfig) {
  29. super();
  30. logger.debug('constructor() pcConfig:', pcConfig);
  31. // Closed flag.
  32. // @type {boolean}
  33. this._closed = false;
  34. // Create a RTCIceGatherer.
  35. // @type {RTCIceGatherer}
  36. this._iceGatherer = this._createIceGatherer(pcConfig);
  37. // RTCPeerConnection iceGatheringState.
  38. // NOTE: This should not be needed, but Edge does not implement
  39. // iceGatherer.state.
  40. // @type {RTCIceGatheringState}
  41. this._iceGatheringState = RTCIceGatheringState.new;
  42. // Create a RTCIceTransport.
  43. // @type {RTCIceTransport}
  44. this._iceTransport = this._createIceTransport(this._iceGatherer);
  45. // Local RTCSessionDescription.
  46. // @type {RTCSessionDescription}
  47. this._localDescription = null;
  48. // Set of local MediaStreams.
  49. // @type {Set<MediaStream>}
  50. this._localStreams = new Set();
  51. // Remote RTCSessionDescription.
  52. // @type {RTCSessionDescription}
  53. this._remoteDescription = null;
  54. // Set of remote MediaStreams.
  55. // @type {Set<MediaStream>}
  56. this._remoteStreams = new Set();
  57. // RTCPeerConnection signalingState.
  58. // @type {RTCSignalingState}
  59. this._signalingState = RTCSignalingState.stable;
  60. }
  61. /**
  62. * Gets the current signaling state.
  63. * @return {RTCSignalingState}
  64. */
  65. get signalingState() {
  66. return this._signalingState;
  67. }
  68. /**
  69. * Gets the current ICE gathering state.
  70. * @return {RTCIceGatheringState}
  71. */
  72. get iceGatheringState() {
  73. return this._iceGatheringState;
  74. }
  75. /**
  76. * Gets the current ICE connection state.
  77. * @return {RTCIceConnectionState}
  78. */
  79. get iceConnectionState() {
  80. return this._iceTransport.state;
  81. }
  82. /**
  83. * Gets the local description.
  84. * @return {RTCSessionDescription}
  85. */
  86. get localDescription() {
  87. return this._localDescription;
  88. }
  89. /**
  90. * Gets the remote description.
  91. * @return {RTCSessionDescription}
  92. */
  93. get remoteDescription() {
  94. return this._remoteDescription;
  95. }
  96. /**
  97. * Closes the RTCPeerConnection.
  98. */
  99. close() {
  100. if (this._closed) {
  101. return;
  102. }
  103. this._closed = true;
  104. logger.debug('close()');
  105. this._updateAndEmitSignalingStateChange(RTCSignalingState.closed);
  106. // Close iceGatherer.
  107. // NOTE: Not yet implemented by Edge.
  108. try {
  109. this._iceGatherer.close();
  110. } catch (error) {
  111. logger.warn(`iceGatherer.close() failed:${error}`);
  112. }
  113. // Close iceTransport.
  114. try {
  115. this._iceTransport.stop();
  116. } catch (error) {
  117. logger.warn(`iceTransport.stop() failed:${error}`);
  118. }
  119. // Clear local/remote streams.
  120. this._localStreams.clear();
  121. this._remoteStreams.clear();
  122. // TODO: Close and emit more stuff.
  123. }
  124. /**
  125. * Creates a local offer. Implements both the old callbacks based signature
  126. * and the new Promise based style.
  127. *
  128. * Arguments in Promise mode:
  129. * @param {RTCOfferOptions} options
  130. *
  131. * Arguments in callbacks mode:
  132. * @param {function(desc)} callback
  133. * @param {function(error)} errback
  134. * @param {MediaConstraints} constraints
  135. */
  136. createOffer(...args) {
  137. let usePromise;
  138. let options;
  139. let callback;
  140. let errback;
  141. if (args.length <= 1) {
  142. usePromise = true;
  143. options = args[0];
  144. } else {
  145. usePromise = false;
  146. callback = args[0];
  147. errback = args[1];
  148. options = args[2];
  149. if (typeof callback !== 'function') {
  150. throw new TypeError('callback missing');
  151. }
  152. if (typeof errback !== 'function') {
  153. throw new TypeError('errback missing');
  154. }
  155. }
  156. logger.debug('createOffer() options:', options);
  157. if (usePromise) {
  158. return this._createOffer(options);
  159. }
  160. this._createOffer(options)
  161. .then(desc => callback(desc))
  162. .catch(error => errback(error));
  163. }
  164. /**
  165. * Creates a local answer. Implements both the old callbacks based signature
  166. * and the new Promise based style.
  167. *
  168. * Arguments in Promise mode:
  169. * @param {RTCOfferOptions} options
  170. *
  171. * Arguments in callbacks mode:
  172. * @param {function(desc)} callback
  173. * @param {function(error)} errback
  174. * @param {MediaConstraints} constraints
  175. */
  176. createAnswer(...args) {
  177. let usePromise;
  178. let options;
  179. let callback;
  180. let errback;
  181. if (args.length <= 1) {
  182. usePromise = true;
  183. options = args[0];
  184. } else {
  185. usePromise = false;
  186. callback = args[0];
  187. errback = args[1];
  188. options = args[2];
  189. if (typeof callback !== 'function') {
  190. throw new TypeError('callback missing');
  191. }
  192. if (typeof errback !== 'function') {
  193. throw new TypeError('errback missing');
  194. }
  195. }
  196. logger.debug('createAnswer() options:', options);
  197. if (usePromise) {
  198. return this._createAnswer(options);
  199. }
  200. this._createAnswer(options)
  201. .then(desc => callback(desc))
  202. .catch(error => errback(error));
  203. }
  204. /**
  205. * Applies a local description. Implements both the old callbacks based
  206. * signature and the new Promise based style.
  207. *
  208. * Arguments in Promise mode:
  209. * @param {RTCSessionDescriptionInit} desc
  210. *
  211. * Arguments in callbacks mode:
  212. * @param {RTCSessionDescription} desc
  213. * @param {function()} callback
  214. * @param {function(error)} errback
  215. */
  216. setLocalDescription(desc, ...args) {
  217. let usePromise;
  218. let callback;
  219. let errback;
  220. if (!desc) {
  221. throw new TypeError('description missing');
  222. }
  223. if (args.length === 0) {
  224. usePromise = true;
  225. } else {
  226. usePromise = false;
  227. callback = args[0];
  228. errback = args[1];
  229. if (typeof callback !== 'function') {
  230. throw new TypeError('callback missing');
  231. }
  232. if (typeof errback !== 'function') {
  233. throw new TypeError('errback missing');
  234. }
  235. }
  236. logger.debug('setLocalDescription() desc:', desc);
  237. if (usePromise) {
  238. return this._setLocalDescription(desc);
  239. }
  240. this._setLocalDescription(desc)
  241. .then(() => callback())
  242. .catch(error => errback(error));
  243. }
  244. /**
  245. * Applies a remote description. Implements both the old callbacks based
  246. * signature and the new Promise based style.
  247. *
  248. * Arguments in Promise mode:
  249. * @param {RTCSessionDescriptionInit} desc
  250. *
  251. * Arguments in callbacks mode:
  252. * @param {RTCSessionDescription} desc
  253. * @param {function()} callback
  254. * @param {function(error)} errback
  255. */
  256. setRemoteDescription(desc, ...args) {
  257. let usePromise;
  258. let callback;
  259. let errback;
  260. if (!desc) {
  261. throw new TypeError('description missing');
  262. }
  263. if (args.length === 0) {
  264. usePromise = true;
  265. } else {
  266. usePromise = false;
  267. callback = args[0];
  268. errback = args[1];
  269. if (typeof callback !== 'function') {
  270. throw new TypeError('callback missing');
  271. }
  272. if (typeof errback !== 'function') {
  273. throw new TypeError('errback missing');
  274. }
  275. }
  276. logger.debug('setRemoteDescription() desc:', desc);
  277. if (usePromise) {
  278. return this._setRemoteDescription(desc);
  279. }
  280. this._setRemoteDescription(desc)
  281. .then(() => callback())
  282. .catch(error => errback(error));
  283. }
  284. /**
  285. * Adds a remote ICE candidate. Implements both the old callbacks based
  286. * signature and the new Promise based style.
  287. *
  288. * Arguments in Promise mode:
  289. * @param {RTCIceCandidate} candidate
  290. *
  291. * Arguments in callbacks mode:
  292. * @param {RTCIceCandidate} candidate
  293. * @param {function()} callback
  294. * @param {function(error)} errback
  295. */
  296. addIceCandidate(candidate, ...args) {
  297. let usePromise;
  298. let callback;
  299. let errback;
  300. if (!candidate) {
  301. throw new TypeError('candidate missing');
  302. }
  303. if (args.length === 0) {
  304. usePromise = true;
  305. } else {
  306. usePromise = false;
  307. callback = args[0];
  308. errback = args[1];
  309. if (typeof callback !== 'function') {
  310. throw new TypeError('callback missing');
  311. }
  312. if (typeof errback !== 'function') {
  313. throw new TypeError('errback missing');
  314. }
  315. }
  316. logger.debug('addIceCandidate() candidate:', candidate);
  317. if (usePromise) {
  318. return this._addIceCandidate(candidate);
  319. }
  320. this._addIceCandidate(candidate)
  321. .then(() => callback())
  322. .catch(error => errback(error));
  323. }
  324. /**
  325. * Adds a local MediaStream.
  326. * @param {MediaStream} stream.
  327. * NOTE: Deprecated API.
  328. */
  329. addStream(stream) {
  330. logger.debug('addStream()');
  331. this._addStream(stream);
  332. }
  333. /**
  334. * Removes a local MediaStream.
  335. * @param {MediaStream} stream.
  336. * NOTE: Deprecated API.
  337. */
  338. removeStream(stream) {
  339. logger.debug('removeStream()');
  340. this._removeStream(stream);
  341. }
  342. /**
  343. * Creates a RTCDataChannel.
  344. * TBD
  345. */
  346. createDataChannel() {
  347. logger.debug('createDataChannel()');
  348. }
  349. /**
  350. * Gets a sequence of local MediaStreams.
  351. */
  352. getLocalStreams() {
  353. return Array.from(this._localStreams);
  354. }
  355. /**
  356. * Gets a sequence of remote MediaStreams.
  357. */
  358. getRemoteStreams() {
  359. return Array.from(this._remoteStreams);
  360. }
  361. /**
  362. * TBD
  363. */
  364. getStats() {
  365. // TBD
  366. }
  367. /**
  368. * Creates and returns a RTCIceGatherer.
  369. * @return {RTCIceGatherer}
  370. * @private
  371. */
  372. _createIceGatherer(pcConfig) {
  373. const iceGatherOptions = {
  374. gatherPolicy: pcConfig.iceTransportPolicy || 'all',
  375. iceServers: pcConfig.iceServers || []
  376. };
  377. const iceGatherer = new RTCIceGatherer(iceGatherOptions);
  378. // NOTE: Not yet implemented by Edge.
  379. iceGatherer.onstatechange = () => {
  380. logger.debug(
  381. `iceGatherer "statechange" event, state:${iceGatherer.state}`);
  382. this._updateAndEmitIceGatheringStateChange(iceGatherer.state);
  383. };
  384. iceGatherer.onlocalcandidate = ev => {
  385. let candidate = ev.candidate;
  386. // NOTE: Not yet implemented by Edge.
  387. const complete = ev.complete;
  388. logger.debug(
  389. 'iceGatherer "localcandidate" event, candidate:', candidate);
  390. // NOTE: Instead of null candidate or complete:true, current Edge
  391. // signals end of gathering with an empty candidate object.
  392. if (complete
  393. || !candidate
  394. || Object.keys(candidate).length === 0) {
  395. candidate = null;
  396. this._updateAndEmitIceGatheringStateChange(
  397. RTCIceGatheringState.complete);
  398. this._emitIceCandidate(null);
  399. } else {
  400. this._emitIceCandidate(candidate);
  401. }
  402. };
  403. iceGatherer.onerror = ev => {
  404. const errorCode = ev.errorCode;
  405. const errorText = ev.errorText;
  406. logger.error(
  407. `iceGatherer "error" event, errorCode:${errorCode}, `
  408. + `errorText:${errorText}`);
  409. };
  410. // NOTE: Not yet implemented by Edge, which starts gathering
  411. // automatically.
  412. try {
  413. iceGatherer.gather();
  414. } catch (error) {
  415. logger.warn(`iceGatherer.gather() failed:${error}`);
  416. }
  417. return iceGatherer;
  418. }
  419. /**
  420. * Creates and returns a RTCIceTransport.
  421. * @return {RTCIceTransport}
  422. * @private
  423. */
  424. _createIceTransport(iceGatherer) {
  425. const iceTransport = new RTCIceTransport(iceGatherer);
  426. // NOTE: Not yet implemented by Edge.
  427. iceTransport.onstatechange = () => {
  428. logger.debug(
  429. 'iceTransport "statechange" event, '
  430. + `state:${iceTransport.state}`);
  431. this._emitIceConnectionStateChange();
  432. };
  433. // NOTE: Not standard, but implemented by Edge.
  434. iceTransport.onicestatechange = () => {
  435. logger.debug(
  436. 'iceTransport "icestatechange" event, '
  437. + `state:${iceTransport.state}`);
  438. this._emitIceConnectionStateChange();
  439. };
  440. // TODO: More stuff to be done.
  441. return iceTransport;
  442. }
  443. /**
  444. * Promise based implementation for createOffer().
  445. * @returns {Promise}
  446. * @private
  447. */
  448. _createOffer(options) { // eslint-disable-line no-unused-vars
  449. if (this._closed) {
  450. return Promise.reject(
  451. new InvalidStateError('RTCPeerConnection closed'));
  452. }
  453. if (this.signalingState !== RTCSignalingState.stable) {
  454. return Promise.reject(new InvalidStateError(
  455. `invalid signalingState "${this.signalingState}"`));
  456. }
  457. // TODO: More stuff to be done.
  458. }
  459. /**
  460. * Promise based implementation for createAnswer().
  461. * @returns {Promise}
  462. * @private
  463. */
  464. _createAnswer(options) { // eslint-disable-line no-unused-vars
  465. if (this._closed) {
  466. return Promise.reject(
  467. new InvalidStateError('RTCPeerConnection closed'));
  468. }
  469. if (this.signalingState !== RTCSignalingState.haveRemoteOffer) {
  470. return Promise.reject(new InvalidStateError(
  471. `invalid signalingState "${this.signalingState}"`));
  472. }
  473. // TODO: More stuff to be done.
  474. }
  475. /**
  476. * Promise based implementation for setLocalDescription().
  477. * @returns {Promise}
  478. * @private
  479. */
  480. _setLocalDescription(desc) {
  481. if (this._closed) {
  482. return Promise.reject(
  483. new InvalidStateError('RTCPeerConnection closed'));
  484. }
  485. switch (desc.type) {
  486. case 'offer':
  487. if (this.signalingState !== RTCSignalingState.stable) {
  488. return Promise.reject(new InvalidStateError(
  489. `invalid signalingState "${this.signalingState}"`));
  490. }
  491. break;
  492. case 'answer':
  493. if (this.signalingState !== RTCSignalingState.haveRemoteOffer) {
  494. return Promise.reject(new InvalidStateError(
  495. `invalid signalingState "${this.signalingState}"`));
  496. }
  497. break;
  498. default:
  499. throw new TypeError(`unsupported description.type "${desc.type}"`);
  500. }
  501. // TODO: More stuff to be done.
  502. }
  503. /**
  504. * Promise based implementation for setRemoteDescription().
  505. * @returns {Promise}
  506. * @private
  507. */
  508. _setRemoteDescription(desc) {
  509. if (this._closed) {
  510. return Promise.reject(
  511. new InvalidStateError('RTCPeerConnection closed'));
  512. }
  513. switch (desc.type) {
  514. case 'offer':
  515. if (this.signalingState !== RTCSignalingState.stable) {
  516. return Promise.reject(new InvalidStateError(
  517. `invalid signalingState "${this.signalingState}"`));
  518. }
  519. break;
  520. case 'answer':
  521. if (this.signalingState !== RTCSignalingState.haveLocalOffer) {
  522. return Promise.reject(new InvalidStateError(
  523. `invalid signalingState "${this.signalingState}"`));
  524. }
  525. break;
  526. default:
  527. throw new TypeError(`unsupported description.type "${desc.type}"`);
  528. }
  529. // TODO: More stuff to be done.
  530. }
  531. /**
  532. * Implementation for addStream().
  533. * @private
  534. */
  535. _addStream(stream) {
  536. if (this._closed) {
  537. throw new InvalidStateError('RTCPeerConnection closed');
  538. }
  539. if (this._localStreams.has(stream)) {
  540. return;
  541. }
  542. this._localStreams.add(stream);
  543. // It may need to renegotiate.
  544. this._emitNegotiationNeeded();
  545. }
  546. /**
  547. * Implementation for removeStream().
  548. * @private
  549. */
  550. _removeStream(stream) {
  551. if (this._closed) {
  552. throw new InvalidStateError('RTCPeerConnection closed');
  553. }
  554. if (!this._localStreams.has(stream)) {
  555. return;
  556. }
  557. this._localStreams.delete(stream);
  558. // It may need to renegotiate.
  559. this._emitNegotiationNeeded();
  560. }
  561. /**
  562. * May update signalingState and emit 'signalingstatechange' event.
  563. */
  564. _updateAndEmitSignalingStateChange(state) {
  565. if (state === this.signalingState) {
  566. return;
  567. }
  568. this._signalingState = state;
  569. logger.debug(
  570. 'emitting "signalingstatechange", signalingState:',
  571. this.signalingState);
  572. const event = new yaeti.Event('signalingstatechange');
  573. this.dispatchEvent(event);
  574. }
  575. /**
  576. * May emit 'negotiationneeded' event.
  577. */
  578. _emitNegotiationNeeded() {
  579. // Ignore if signalingState is not 'stable'.
  580. if (this.signalingState !== RTCSignalingState.stable) {
  581. return;
  582. }
  583. logger.debug('emitting "negotiationneeded"');
  584. const event = new yaeti.Event('negotiationneeded');
  585. this.dispatchEvent(event);
  586. }
  587. /**
  588. * May update iceGatheringState and emit 'icegatheringstatechange' event.
  589. */
  590. _updateAndEmitIceGatheringStateChange(state) {
  591. if (this._closed || state === this.iceGatheringState) {
  592. return;
  593. }
  594. this._iceGatheringState = state;
  595. logger.debug(
  596. 'emitting "icegatheringstatechange", iceGatheringState:',
  597. this.iceGatheringState);
  598. const event = new yaeti.Event('icegatheringstatechange');
  599. this.dispatchEvent(event);
  600. }
  601. /**
  602. * May emit 'iceconnectionstatechange' event.
  603. */
  604. _emitIceConnectionStateChange() {
  605. if (this._closed && this.iceConnectionState !== 'closed') {
  606. return;
  607. }
  608. logger.debug(
  609. 'emitting "iceconnectionstatechange", iceConnectionState:',
  610. this.iceConnectionState);
  611. const event = new yaeti.Event('iceconnectionstatechange');
  612. this.dispatchEvent(event);
  613. }
  614. /**
  615. * May emit 'icecandidate' event.
  616. */
  617. _emitIceCandidate(candidate) {
  618. if (this._closed) {
  619. return;
  620. }
  621. const event = new yaeti.Event('icecandidate');
  622. logger.debug(
  623. 'emitting "icecandidate", candidate:', candidate);
  624. event.candidate = candidate;
  625. this.dispatchEvent(event);
  626. }
  627. }