Home Manual Reference Source

src/core/stream/Local.js

  1. import * as StreamTypes from '../../definitions/StreamTypes';
  2. import * as Log from '../util/Log';
  3. import cache from '../util/cache';
  4. import * as DataSync from '../util/DataSync';
  5. import Media from '../util/Media';
  6. import {
  7. CLOSED,
  8. CLOSING,
  9. CONNECTED,
  10. NONE
  11. } from '../util/constants';
  12. import * as Events from '../../definitions/Events';
  13.  
  14. const _facingModes = [Media.facingMode.USER, Media.facingMode.ENVIRONMENT];
  15.  
  16. const _getConstraintValue = (constraints, prop) => (
  17. constraints[prop].exact || constraints[prop].ideal || constraints[prop]
  18. );
  19.  
  20. /* eslint-disable no-param-reassign */
  21. const _setConstrainValue = (constraints, prop, other, value) => {
  22. constraints[prop] = { exact: value };
  23. delete constraints[other];
  24. };
  25. /* eslint-enable no-param-reassign */
  26.  
  27. /**
  28. * The local stream
  29. */
  30. export default class Local {
  31. /**
  32. * @access protected
  33. * @param {object} values
  34. */
  35. constructor(values) {
  36. /**
  37. * The uid of the room the stream is published in
  38. * @type {string}
  39. */
  40. this.roomId = values.roomId;
  41. /**
  42. * The uid of this stream
  43. * @type {string}
  44. */
  45. this.uid = values.uid;
  46. /**
  47. * The type of the stream
  48. * @type {string}
  49. */
  50. this.type = values.type;
  51. /**
  52. * Indicates if a track is muted
  53. * @type {{audio: boolean, video: boolean}}
  54. */
  55. this.muted = Object.assign({ audio: false, video: false }, values.muted);
  56. /**
  57. * The local DOM container element where the {@link Local~media} is displayed
  58. * @type {Element}
  59. */
  60. this.container = values.container || cache.config.localStreamContainer;
  61. /**
  62. * The local DOM media element where the {@link Local~media} is displayed
  63. * @type {Element}
  64. */
  65. this.node = null;
  66. /**
  67. * List of the PeerConnections associated to this local stream
  68. * @type {PeerConnection[]}
  69. */
  70. this.peerConnections = [];
  71. /**
  72. * Local stream status
  73. * @type {string}
  74. */
  75. this.status = NONE;
  76. /**
  77. * is the video is loaded int the local DOM media element
  78. * @type {boolean}
  79. */
  80. this.isVideoLoaded = false;
  81. /**
  82. * @access private
  83. * @type {{audio: string, video: string}}
  84. */
  85. this._inputs = {};
  86.  
  87. // Set constraints
  88. this.constraints = values.constraints;
  89.  
  90. /**
  91. * List of callbacks for Local
  92. * @type object
  93. * @private
  94. */
  95. this._callbacks = {};
  96. }
  97.  
  98.  
  99. /**
  100. * Register a callback for a specific event
  101. * @param {string} event The event name ({@link Events/Stream})
  102. * @param {function} callback The callback for the event
  103. */
  104. on(event, callback) {
  105. if (Events.local.supports(event)) {
  106. if (!this._callbacks[event]) {
  107. this._callbacks[event] = [];
  108. }
  109. this._callbacks[event].push(callback);
  110. }
  111. }
  112.  
  113. /**
  114. * Register a callback for a specific event
  115. * @param {string} [event] The event name ({@link Events/Stream})
  116. * @param {function} [callback] The callback for the event
  117. */
  118. off(event, callback) {
  119. if (!event) {
  120. this._callbacks = {};
  121. } else if (Events.local.supports(event)) {
  122. if (!callback) {
  123. this._callbacks[event] = [];
  124. } else {
  125. this._callbacks[event] = this._callbacks[event].filter(cb => cb !== callback);
  126. }
  127. }
  128. }
  129.  
  130. /**
  131. * The Media Constraints. (defaults to global config)
  132. * @param {MediaConstraints} constraints
  133. */
  134. set constraints(constraints) {
  135. const
  136. values = constraints || cache.config.constraints;
  137.  
  138.  
  139. const defaultConstraints = Media.constraints();
  140. ['audio', 'video'].forEach((type) => {
  141. if (!~this.type.indexOf(type)) { // eslint-disable-line no-bitwise
  142. values[type] = false;
  143. } else if (!values[type]) {
  144. values[type] = defaultConstraints[type];
  145. }
  146. if (values[type].deviceId || values[type].facingMode) {
  147. this._inputs[type] = _getConstraintValue(
  148. values[type],
  149. values[type].facingMode ? 'facingMode' : 'deviceId'
  150. );
  151. }
  152. });
  153. Log.d('Local~set#contraints', values);
  154. /**
  155. * @ignore
  156. */
  157. this._constraints = values;
  158. }
  159.  
  160. /**
  161. * The Media Constraints. (defaults to global config)
  162. * @type {MediaConstraints}
  163. */
  164. get constraints() {
  165. return this._constraints;
  166. }
  167.  
  168. /**
  169. * Updates the stream constraints and retrieve the new MediaStream
  170. * @param constraints
  171. * @returns {*|Promise.<TResult>}
  172. */
  173. updateConstraints(constraints) {
  174. Log.d('Local~updateConstraints', constraints);
  175. this.constraints = constraints;
  176. return navigator.mediaDevices.getUserMedia(this.constraints)
  177. .catch((e) => {
  178. (this._callbacks[Events.local.WEBRTC_ERROR] || [])
  179. .forEach(cb => cb(e));
  180. return e;
  181. })
  182. .then((media) => {
  183. ['audio', 'video'].forEach((kind) => {
  184. const constraintsValue = this.constraints[kind];
  185. if (constraintsValue) {
  186. if (constraintsValue.deviceId || constraintsValue.facingMode) {
  187. this._inputs[kind] = _getConstraintValue(
  188. constraintsValue,
  189. constraintsValue.facingMode ? 'facingMode' : 'deviceId'
  190. );
  191. }
  192. }
  193. });
  194. this.media = media;
  195. });
  196. }
  197.  
  198. /**
  199. * The associated MediaStream
  200. * @type {MediaStream}
  201. */
  202. set media(mediaStream) {
  203. if (mediaStream) {
  204. if (!(mediaStream instanceof MediaStream)) {
  205. throw new Error('The media MUST be a MediaStream');
  206. }
  207.  
  208. const checkDevices = {};
  209. mediaStream.getTracks().forEach((track) => {
  210. // Reset mute
  211. track.enabled = !this.muted[track.kind]; // eslint-disable-line no-param-reassign
  212. // Get device label
  213. if (!this._inputs[track.kind]) {
  214. checkDevices[track.kind] = track.label;
  215. }
  216. });
  217. // Try to get deviceId from label
  218. if (Object.keys(checkDevices).length) {
  219. Media.devices().then((devices) => {
  220. Object.keys(checkDevices).forEach((kind) => {
  221. if (devices[`${kind}input`]) {
  222. const deviceIds = devices[`${kind}input`]
  223. .filter(device => device.label.length && device.label === checkDevices[kind]);
  224. if (deviceIds.length === 1 && !this._inputs[kind]) {
  225. this._inputs[kind] = deviceIds[0].deviceId;
  226. }
  227. if (deviceIds.length === 0
  228. && devices[`${kind}input`][0].label === ''
  229. && !this._inputs[kind]) {
  230. // from a webview, the label is not delivered
  231. this._inputs[kind] = devices[`${kind}input`][0].deviceId;
  232. }
  233. }
  234. });
  235. });
  236. }
  237. // Display
  238. this.node = Media.attachStream(mediaStream, this.container, this.node, 0);
  239. this.node.onloadeddata = () => {
  240. this.isVideoLoaded = true;
  241. };
  242. this.status = CONNECTED;
  243. Log.d('Local~set media', { mediaStream }, this.node);
  244. // Renegotiate
  245. this.peerConnections.forEach(peerConnection => peerConnection.renegotiate(this._media,
  246. mediaStream));
  247. } else if (this.media && !mediaStream) {
  248. // Remove node
  249. this.node.srcObject = null;
  250. this.container.removeChild(this.node);
  251. this.node = null;
  252. // Stop stream
  253. this.media.getTracks().forEach(track => track.stop());
  254. // Close PeerConnections
  255. this.peerConnections.forEach(peerConnection => peerConnection.close());
  256. }
  257. // Save
  258. /**
  259. * @ignore
  260. */
  261. this._media = mediaStream;
  262. }
  263.  
  264. /**
  265. * The associated MediaStream
  266. * @type {MediaStream}
  267. */
  268. get media() {
  269. return this._media;
  270. }
  271.  
  272. /**
  273. * Mute a track of a Stream
  274. * @param {string} [track=AUDIO] The track to mute. (AUDIO, VIDEO, AUDIO_VIDEO)
  275. * @param {boolean} [state=true] true for mute & false for un-mute
  276. * @example <caption>mute video</caption>
  277. * stream.mute(Reach.t.VIDEO)
  278. * @example <caption>mute audio</caption>
  279. * stream.mute(Reach.t.AUDIO)
  280. * // or
  281. * stream.mute()
  282. */
  283. mute(track = StreamTypes.AUDIO, state = true) {
  284. Log.d('mute', track, state);
  285. let { audio, video } = this.muted;
  286. let
  287. tracks;
  288. switch (track) {
  289. case StreamTypes.AUDIO:
  290. audio = state;
  291. tracks = this.media.getAudioTracks();
  292. break;
  293. case StreamTypes.VIDEO:
  294. case StreamTypes.SCREEN_SHARING:
  295. video = state;
  296. tracks = this.media.getVideoTracks();
  297. break;
  298. case StreamTypes.AUDIO_VIDEO:
  299. audio = state;
  300. video = state;
  301. tracks = this.media.getTracks();
  302. break;
  303. default:
  304. break;
  305. }
  306. // Mute media tracks
  307. tracks.forEach((track) => { // eslint-disable-line no-shadow
  308. track.enabled = !state; // eslint-disable-line no-param-reassign
  309. });
  310. // Signal subscribers
  311. this.muted = { audio, video };
  312. DataSync.set(`_/rooms/${this.roomId}/streams/${this.uid}/muted`, this.muted);
  313. }
  314.  
  315. /**
  316. * Un-mute a track of a Stream
  317. * @param {string} [track=AUDIO] The track to mute. (AUDIO, VIDEO, AUDIO_VIDEO)
  318. * @example <caption>Un-mute video</caption>
  319. * stream.unMute(Reach.t.VIDEO)
  320. * @example <caption>Un-mute audio</caption>
  321. * stream.unMute(Reach.t.AUDIO)
  322. * // or
  323. * stream.unMute()
  324. */
  325. unMute(track) {
  326. this.mute(track, false);
  327. }
  328.  
  329. /**
  330. * Removes stream for published list, closes associated
  331. * PeerConnections and stops current MediaStream
  332. * @returns {Promise}
  333. */
  334. close() {
  335. if (!~[CLOSED, CLOSING].indexOf(this.status)) { // eslint-disable-line no-bitwise
  336. this.status = CLOSING;
  337. // Stop listening to Subscribers
  338. const path = `_/rooms/${this.roomId}/subscribers/${this.uid}`;
  339. DataSync.off(path, 'child_added');
  340. DataSync.off(path, 'child_removed');
  341. // Cancel onDisconnects
  342. DataSync.onDisconnect(`_/rooms/${this.roomId}/streams/${this.uid}`).cancel();
  343. DataSync.onDisconnect(`_/rooms/${this.roomId}/subscribers/${this.uid}`).cancel();
  344. // Remove subscribers
  345. DataSync.remove(path);
  346. // Remove stream
  347. DataSync.remove(`_/rooms/${this.roomId}/streams/${this.uid}`);
  348. this.media = null;
  349. // Close
  350. this.status = CLOSED;
  351. }
  352. return Promise.resolve(this.status);
  353. }
  354.  
  355. /**
  356. * Switch video input device
  357. * @param {string} [deviceId] A video input device Id or the `facingMode` value
  358. * @returns {Promise<Local, Error>}
  359. */
  360. switchCamera(deviceId) {
  361. return this._switchDevice(StreamTypes.VIDEO, deviceId);
  362. }
  363.  
  364. /**
  365. * Switch audio input device
  366. * @param {string} [deviceId] A audio input device Id
  367. * @returns {Promise<Local, Error>}
  368. */
  369. switchMicrophone(deviceId) {
  370. return this._switchDevice(StreamTypes.AUDIO, deviceId);
  371. }
  372.  
  373. /**
  374. * Switch input device
  375. * @access private
  376. * @param {string} kind The kind of device to switch
  377. * @param {string} [deviceId] An input device id
  378. * @returns {Promise<Local, Error>}
  379. */
  380. _switchDevice(kind, deviceId) {
  381. Log.d('Local~_switchDevice', kind, deviceId);
  382. if (this.media.getTracks().some(track => track.kind === kind)) {
  383. let next = Promise.resolve(deviceId);
  384. const currentModeIdx = _facingModes.indexOf(this._inputs[kind]);
  385. if (!deviceId && !!~currentModeIdx) { // eslint-disable-line no-bitwise
  386. // Loop facingModes
  387. next = Promise.resolve(_facingModes[(currentModeIdx + 1) % _facingModes.length]);
  388. } else if (!~_facingModes.indexOf(deviceId)) { // eslint-disable-line no-bitwise
  389. // Loop deviceIds
  390. next = Media.devices()
  391. .then((d) => {
  392. // devices IDs
  393. const devices = d[`${kind}input`].map(mediaDevice => mediaDevice.deviceId);
  394. // Sort to ensure same order
  395. devices.sort();
  396. // New device
  397. let nextDevice = deviceId;
  398. if (deviceId && !devices.some(device => device === deviceId)) {
  399. return Promise.reject(new Error(`Unknown ${kind} device`));
  400. }
  401. if (!deviceId && devices.length > 1) {
  402. let idx = this._inputs[kind]
  403. ? devices.findIndex(v => v === this._inputs[kind], this)
  404. : 0;
  405. nextDevice = devices[++idx % devices.length]; // eslint-disable-line no-plusplus
  406. }
  407. return nextDevice;
  408. });
  409. } else {
  410. next = Promise.resolve(deviceId);
  411. }
  412.  
  413. return next
  414. .then((device) => { // eslint-disable-line consistent-return
  415. if (this._inputs[kind] !== device) {
  416. // Update video streams
  417. this._inputs[kind] = device;
  418. // Stop tracks
  419. this.media.getTracks().forEach(track => track.stop());
  420. // Update constraints
  421. const constraints = Object.assign({}, this.constraints);
  422. let props = ['facingMode', 'deviceId'];
  423. if (!~_facingModes.indexOf(device)) { // eslint-disable-line no-bitwise
  424. props = props.reverse();
  425. }
  426. _setConstrainValue(constraints[kind], props[0], props[1], device);
  427. Log.d('Local~_switchDevice', kind, constraints);
  428. return this.updateConstraints(constraints);
  429. }
  430. })
  431. .then(() => this);
  432. }
  433. return Promise.reject(new Error(`Current stream does not contain a ${kind} track`));
  434. }
  435.  
  436. /**
  437. * Publish a local stream
  438. * @access protected
  439. * @param {string} roomId The room Id
  440. * @param {string} type The stream type, see {@link StreamTypes} for possible values
  441. * @param {?Element} container The element the stream is attached to.
  442. * @param {?MediaStreamConstraints} [constraints] The stream constraints.
  443. * If not defined the constraints defined in ReachConfig will be used.
  444. * @returns {Promise<Local, Error>}
  445. */
  446. /* static share(roomId, type, container, constraints) {
  447. if (!cache.user) {
  448. return Promise.reject(new Error('Only an authenticated user can share a stream.'));
  449. }
  450. const streamMetaData = {
  451. from: cache.user.uid,
  452. device: cache.device,
  453. type
  454. };
  455.  
  456.  
  457. const sharedStream = new Local(Object.assign({ roomId, constraints, container },
  458. streamMetaData));
  459. Log.d('Local~share', { sharedStream });
  460. return navigator.mediaDevices.getUserMedia(sharedStream.constraints)
  461. .then((media) => {
  462. sharedStream.media = media;
  463. })
  464. // Got MediaStream, publish it
  465. .then(() => DataSync.push(`_/rooms/${roomId}/streams`, streamMetaData))
  466. .then((streamRef) => {
  467. sharedStream.uid = streamRef.name();
  468. if (/video/i.test(sharedStream.type)) {
  469. if (sharedStream.isVideoLoaded) {
  470. const streamSize = {
  471. height: sharedStream.node.videoHeight,
  472. width: sharedStream.node.videoWidth,
  473. };
  474. streamRef.update(streamSize);
  475. } else {
  476. sharedStream.node.onloadeddata = function () { // eslint-disable-line func-names
  477. const streamSize = {
  478. height: sharedStream.node.videoHeight,
  479. width: sharedStream.node.videoWidth,
  480. };
  481. streamRef.update(streamSize);
  482. };
  483. }
  484. }
  485. if (/video/i.test(sharedStream.type)) {
  486. window.addEventListener('resize', (() => {
  487. if (sharedStream.node != null) {
  488. const streamSize = {
  489. height: sharedStream.node.videoHeight,
  490. width: sharedStream.node.videoWidth,
  491. };
  492. streamRef.update(streamSize);
  493. }
  494. }));
  495. }
  496. // Save sharedStream
  497. cache.streams.shared[sharedStream.uid] = sharedStream;
  498. // Remove shared stream on Disconnect
  499. DataSync.onDisconnect(`_/rooms/${roomId}/streams/${sharedStream.uid}`).remove();
  500. // Remove shared stream on Disconnect
  501. DataSync.onDisconnect(`_/rooms/${roomId}/subscribers/${sharedStream.uid}`).remove();
  502. // Start listening to subscribers
  503. const
  504. path = `_/rooms/${sharedStream.roomId}/subscribers/${sharedStream.uid}`;
  505.  
  506.  
  507. const value = snapData => Object.assign({ device: snapData.name() }, snapData.val() || {});
  508. DataSync.on(path, 'child_added',
  509. (snapData) => {
  510. const subscriber = value(snapData);
  511. Log.d('Local~subscribed', subscriber);
  512. cache.peerConnections.offer(sharedStream, subscriber)
  513. .then((pc) => {
  514. (this._callbacks[Events.local.SUBSCRIBED] || [])
  515. .forEach(cb => cb(sharedStream, subscriber));
  516. return sharedStream.peerConnections.push(pc);
  517. });
  518. },
  519. Log.e.bind(Log));
  520. DataSync.on(path, 'child_removed',
  521. (snapData) => {
  522. const subscriber = value(snapData);
  523. Log.d('Local~un-subscribed', subscriber);
  524. const closedPC = cache.peerConnections.close(sharedStream.uid, subscriber.device);
  525. sharedStream.peerConnections = sharedStream.peerConnections
  526. .filter(pc => pc !== closedPC);
  527. },
  528. Log.e.bind(Log));
  529. Log.d('Local~shared', { sharedStream });
  530. return sharedStream;
  531. });
  532. } */
  533.  
  534. /**
  535. * Get a local stream
  536. * @access protected
  537. * @param {string} roomId The room Id
  538. * @param {string} type The stream type, see {@link StreamTypes} for possible values
  539. * @param {?Element} container The element the stream is attached to.
  540. * @param {?MediaStreamConstraints} [constraints] The stream constraints.
  541. * If not defined the constraints defined in ReachConfig will be used.
  542. * @returns {Promise<Local, Error>}
  543. */
  544. static getLocalVideo(roomId, type, container, constraints) {
  545. if (!cache.user) {
  546. return Promise.reject(new Error('Only an authenticated user can share a stream.'));
  547. }
  548. const streamMetaData = {
  549. from: cache.user.uid,
  550. device: cache.device,
  551. userAgent: cache.userAgent,
  552. type
  553. };
  554.  
  555.  
  556. const sharedStream = new Local(Object.assign({ roomId, constraints, container },
  557. streamMetaData));
  558. sharedStream.streamMetaData = streamMetaData;
  559. Log.d('Local~getLocalVideo', { sharedStream });
  560. return navigator.mediaDevices.getUserMedia(sharedStream.constraints)
  561. .then((media) => {
  562. sharedStream.media = media;
  563. return sharedStream;
  564. });
  565. }
  566.  
  567. /**
  568. * Publish a local stream
  569. * @access protected
  570. * @returns {Local}
  571. */
  572. publish(sharedStream) {
  573. Log.d('Local~publish');
  574. const { roomId } = sharedStream;
  575. return DataSync.push(`_/rooms/${roomId}/streams`, sharedStream.streamMetaData)
  576. .then((streamRef) => {
  577. sharedStream.uid = streamRef.name(); // eslint-disable-line no-param-reassign
  578. if (sharedStream.isVideoLoaded) {
  579. const streamSize = {
  580. height: sharedStream.node.videoHeight,
  581. width: sharedStream.node.videoWidth,
  582. };
  583. streamRef.update(streamSize);
  584. } else {
  585. sharedStream.node.onloadeddata = function () { // eslint-disable-line
  586. const streamSize = {
  587. height: sharedStream.node.videoHeight,
  588. width: sharedStream.node.videoWidth,
  589. };
  590. streamRef.update(streamSize);
  591. };
  592. }
  593. window.addEventListener('resize', (() => {
  594. if (sharedStream.node != null) {
  595. const streamSize = {
  596. height: sharedStream.node.videoHeight,
  597. width: sharedStream.node.videoWidth,
  598. };
  599. streamRef.update(streamSize);
  600. }
  601. }));
  602. // Save sharedStream
  603. cache.streams.shared[sharedStream.uid] = sharedStream;
  604. // Remove shared stream on Disconnect
  605. DataSync.onDisconnect(`_/rooms/${roomId}/streams/${sharedStream.uid}`).remove();
  606. // Remove shared stream on Disconnect
  607. DataSync.onDisconnect(`_/rooms/${roomId}/subscribers/${sharedStream.uid}`).remove();
  608. // Start listening to subscribers
  609. const path = `_/rooms/${sharedStream.roomId}/subscribers/${sharedStream.uid}`;
  610. const value = snapData => Object.assign({ device: snapData.name() }, snapData.val() || {});
  611.  
  612. DataSync.on(path, 'child_added',
  613. (snapData) => {
  614. const subscriber = value(snapData);
  615. Log.d('Local~subscribed', subscriber);
  616. cache.peerConnections
  617. .offer(sharedStream, subscriber, this._callbacks[Events.local.WEBRTC_ERROR])
  618. .then((pc) => {
  619. (this._callbacks[Events.local.SUBSCRIBED] || [])
  620. .forEach(cb => cb(sharedStream, subscriber));
  621. return sharedStream.peerConnections.push(pc);
  622. });
  623. },
  624. Log.e.bind(Log), this);
  625. DataSync.on(path, 'child_removed',
  626. (snapData) => {
  627. const subscriber = value(snapData);
  628. Log.d('Local~un-subscribed', subscriber);
  629. const closedPC = cache.peerConnections.close(sharedStream.uid, subscriber.device);
  630. /* eslint-disable no-param-reassign */
  631. sharedStream.peerConnections = sharedStream.peerConnections
  632. .filter(pc => pc !== closedPC);
  633. /* eslint-enable no-param-reassign */
  634. },
  635. Log.e.bind(Log));
  636. Log.d('Local~shared', { sharedStream });
  637. return sharedStream;
  638. });
  639. }
  640. }