src/core/stream/Local.js
- import * as StreamTypes from '../../definitions/StreamTypes';
- import * as Log from '../util/Log';
- import cache from '../util/cache';
- import * as DataSync from '../util/DataSync';
- import Media from '../util/Media';
- import {
- CLOSED,
- CLOSING,
- CONNECTED,
- NONE
- } from '../util/constants';
- import * as Events from '../../definitions/Events';
-
- const _facingModes = [Media.facingMode.USER, Media.facingMode.ENVIRONMENT];
-
- const _getConstraintValue = (constraints, prop) => (
- constraints[prop].exact || constraints[prop].ideal || constraints[prop]
- );
-
- /* eslint-disable no-param-reassign */
- const _setConstrainValue = (constraints, prop, other, value) => {
- constraints[prop] = { exact: value };
- delete constraints[other];
- };
- /* eslint-enable no-param-reassign */
-
- /**
- * The local stream
- */
- export default class Local {
- /**
- * @access protected
- * @param {object} values
- */
- constructor(values) {
- /**
- * The uid of the room the stream is published in
- * @type {string}
- */
- this.roomId = values.roomId;
- /**
- * The uid of this stream
- * @type {string}
- */
- this.uid = values.uid;
- /**
- * The type of the stream
- * @type {string}
- */
- this.type = values.type;
- /**
- * Indicates if a track is muted
- * @type {{audio: boolean, video: boolean}}
- */
- this.muted = Object.assign({ audio: false, video: false }, values.muted);
- /**
- * The local DOM container element where the {@link Local~media} is displayed
- * @type {Element}
- */
- this.container = values.container || cache.config.localStreamContainer;
- /**
- * The local DOM media element where the {@link Local~media} is displayed
- * @type {Element}
- */
- this.node = null;
- /**
- * List of the PeerConnections associated to this local stream
- * @type {PeerConnection[]}
- */
- this.peerConnections = [];
- /**
- * Local stream status
- * @type {string}
- */
- this.status = NONE;
- /**
- * is the video is loaded int the local DOM media element
- * @type {boolean}
- */
- this.isVideoLoaded = false;
- /**
- * @access private
- * @type {{audio: string, video: string}}
- */
- this._inputs = {};
-
- // Set constraints
- this.constraints = values.constraints;
-
- /**
- * List of callbacks for Local
- * @type object
- * @private
- */
- this._callbacks = {};
- }
-
-
- /**
- * Register a callback for a specific event
- * @param {string} event The event name ({@link Events/Stream})
- * @param {function} callback The callback for the event
- */
- on(event, callback) {
- if (Events.local.supports(event)) {
- if (!this._callbacks[event]) {
- this._callbacks[event] = [];
- }
- this._callbacks[event].push(callback);
- }
- }
-
- /**
- * Register a callback for a specific event
- * @param {string} [event] The event name ({@link Events/Stream})
- * @param {function} [callback] The callback for the event
- */
- off(event, callback) {
- if (!event) {
- this._callbacks = {};
- } else if (Events.local.supports(event)) {
- if (!callback) {
- this._callbacks[event] = [];
- } else {
- this._callbacks[event] = this._callbacks[event].filter(cb => cb !== callback);
- }
- }
- }
-
- /**
- * The Media Constraints. (defaults to global config)
- * @param {MediaConstraints} constraints
- */
- set constraints(constraints) {
- const
- values = constraints || cache.config.constraints;
-
-
- const defaultConstraints = Media.constraints();
- ['audio', 'video'].forEach((type) => {
- if (!~this.type.indexOf(type)) { // eslint-disable-line no-bitwise
- values[type] = false;
- } else if (!values[type]) {
- values[type] = defaultConstraints[type];
- }
- if (values[type].deviceId || values[type].facingMode) {
- this._inputs[type] = _getConstraintValue(
- values[type],
- values[type].facingMode ? 'facingMode' : 'deviceId'
- );
- }
- });
- Log.d('Local~set#contraints', values);
- /**
- * @ignore
- */
- this._constraints = values;
- }
-
- /**
- * The Media Constraints. (defaults to global config)
- * @type {MediaConstraints}
- */
- get constraints() {
- return this._constraints;
- }
-
- /**
- * Updates the stream constraints and retrieve the new MediaStream
- * @param constraints
- * @returns {*|Promise.<TResult>}
- */
- updateConstraints(constraints) {
- Log.d('Local~updateConstraints', constraints);
- this.constraints = constraints;
- return navigator.mediaDevices.getUserMedia(this.constraints)
- .catch((e) => {
- (this._callbacks[Events.local.WEBRTC_ERROR] || [])
- .forEach(cb => cb(e));
- return e;
- })
- .then((media) => {
- ['audio', 'video'].forEach((kind) => {
- const constraintsValue = this.constraints[kind];
- if (constraintsValue) {
- if (constraintsValue.deviceId || constraintsValue.facingMode) {
- this._inputs[kind] = _getConstraintValue(
- constraintsValue,
- constraintsValue.facingMode ? 'facingMode' : 'deviceId'
- );
- }
- }
- });
- this.media = media;
- });
- }
-
- /**
- * The associated MediaStream
- * @type {MediaStream}
- */
- set media(mediaStream) {
- if (mediaStream) {
- if (!(mediaStream instanceof MediaStream)) {
- throw new Error('The media MUST be a MediaStream');
- }
-
- const checkDevices = {};
- mediaStream.getTracks().forEach((track) => {
- // Reset mute
- track.enabled = !this.muted[track.kind]; // eslint-disable-line no-param-reassign
- // Get device label
- if (!this._inputs[track.kind]) {
- checkDevices[track.kind] = track.label;
- }
- });
- // Try to get deviceId from label
- if (Object.keys(checkDevices).length) {
- Media.devices().then((devices) => {
- Object.keys(checkDevices).forEach((kind) => {
- if (devices[`${kind}input`]) {
- const deviceIds = devices[`${kind}input`]
- .filter(device => device.label.length && device.label === checkDevices[kind]);
- if (deviceIds.length === 1 && !this._inputs[kind]) {
- this._inputs[kind] = deviceIds[0].deviceId;
- }
- if (deviceIds.length === 0
- && devices[`${kind}input`][0].label === ''
- && !this._inputs[kind]) {
- // from a webview, the label is not delivered
- this._inputs[kind] = devices[`${kind}input`][0].deviceId;
- }
- }
- });
- });
- }
- // Display
- this.node = Media.attachStream(mediaStream, this.container, this.node, 0);
- this.node.onloadeddata = () => {
- this.isVideoLoaded = true;
- };
- this.status = CONNECTED;
- Log.d('Local~set media', { mediaStream }, this.node);
- // Renegotiate
- this.peerConnections.forEach(peerConnection => peerConnection.renegotiate(this._media,
- mediaStream));
- } else if (this.media && !mediaStream) {
- // Remove node
- this.node.srcObject = null;
- this.container.removeChild(this.node);
- this.node = null;
- // Stop stream
- this.media.getTracks().forEach(track => track.stop());
- // Close PeerConnections
- this.peerConnections.forEach(peerConnection => peerConnection.close());
- }
- // Save
- /**
- * @ignore
- */
- this._media = mediaStream;
- }
-
- /**
- * The associated MediaStream
- * @type {MediaStream}
- */
- get media() {
- return this._media;
- }
-
- /**
- * Mute a track of a Stream
- * @param {string} [track=AUDIO] The track to mute. (AUDIO, VIDEO, AUDIO_VIDEO)
- * @param {boolean} [state=true] true for mute & false for un-mute
- * @example <caption>mute video</caption>
- * stream.mute(Reach.t.VIDEO)
- * @example <caption>mute audio</caption>
- * stream.mute(Reach.t.AUDIO)
- * // or
- * stream.mute()
- */
- mute(track = StreamTypes.AUDIO, state = true) {
- Log.d('mute', track, state);
- let { audio, video } = this.muted;
- let
- tracks;
- switch (track) {
- case StreamTypes.AUDIO:
- audio = state;
- tracks = this.media.getAudioTracks();
- break;
- case StreamTypes.VIDEO:
- case StreamTypes.SCREEN_SHARING:
- video = state;
- tracks = this.media.getVideoTracks();
- break;
- case StreamTypes.AUDIO_VIDEO:
- audio = state;
- video = state;
- tracks = this.media.getTracks();
- break;
- default:
- break;
- }
- // Mute media tracks
- tracks.forEach((track) => { // eslint-disable-line no-shadow
- track.enabled = !state; // eslint-disable-line no-param-reassign
- });
- // Signal subscribers
- this.muted = { audio, video };
- DataSync.set(`_/rooms/${this.roomId}/streams/${this.uid}/muted`, this.muted);
- }
-
- /**
- * Un-mute a track of a Stream
- * @param {string} [track=AUDIO] The track to mute. (AUDIO, VIDEO, AUDIO_VIDEO)
- * @example <caption>Un-mute video</caption>
- * stream.unMute(Reach.t.VIDEO)
- * @example <caption>Un-mute audio</caption>
- * stream.unMute(Reach.t.AUDIO)
- * // or
- * stream.unMute()
- */
- unMute(track) {
- this.mute(track, false);
- }
-
- /**
- * Removes stream for published list, closes associated
- * PeerConnections and stops current MediaStream
- * @returns {Promise}
- */
- close() {
- if (!~[CLOSED, CLOSING].indexOf(this.status)) { // eslint-disable-line no-bitwise
- this.status = CLOSING;
- // Stop listening to Subscribers
- const path = `_/rooms/${this.roomId}/subscribers/${this.uid}`;
- DataSync.off(path, 'child_added');
- DataSync.off(path, 'child_removed');
- // Cancel onDisconnects
- DataSync.onDisconnect(`_/rooms/${this.roomId}/streams/${this.uid}`).cancel();
- DataSync.onDisconnect(`_/rooms/${this.roomId}/subscribers/${this.uid}`).cancel();
- // Remove subscribers
- DataSync.remove(path);
- // Remove stream
- DataSync.remove(`_/rooms/${this.roomId}/streams/${this.uid}`);
- this.media = null;
- // Close
- this.status = CLOSED;
- }
- return Promise.resolve(this.status);
- }
-
- /**
- * Switch video input device
- * @param {string} [deviceId] A video input device Id or the `facingMode` value
- * @returns {Promise<Local, Error>}
- */
- switchCamera(deviceId) {
- return this._switchDevice(StreamTypes.VIDEO, deviceId);
- }
-
- /**
- * Switch audio input device
- * @param {string} [deviceId] A audio input device Id
- * @returns {Promise<Local, Error>}
- */
- switchMicrophone(deviceId) {
- return this._switchDevice(StreamTypes.AUDIO, deviceId);
- }
-
- /**
- * Switch input device
- * @access private
- * @param {string} kind The kind of device to switch
- * @param {string} [deviceId] An input device id
- * @returns {Promise<Local, Error>}
- */
- _switchDevice(kind, deviceId) {
- Log.d('Local~_switchDevice', kind, deviceId);
- if (this.media.getTracks().some(track => track.kind === kind)) {
- let next = Promise.resolve(deviceId);
- const currentModeIdx = _facingModes.indexOf(this._inputs[kind]);
- if (!deviceId && !!~currentModeIdx) { // eslint-disable-line no-bitwise
- // Loop facingModes
- next = Promise.resolve(_facingModes[(currentModeIdx + 1) % _facingModes.length]);
- } else if (!~_facingModes.indexOf(deviceId)) { // eslint-disable-line no-bitwise
- // Loop deviceIds
- next = Media.devices()
- .then((d) => {
- // devices IDs
- const devices = d[`${kind}input`].map(mediaDevice => mediaDevice.deviceId);
- // Sort to ensure same order
- devices.sort();
- // New device
- let nextDevice = deviceId;
- if (deviceId && !devices.some(device => device === deviceId)) {
- return Promise.reject(new Error(`Unknown ${kind} device`));
- }
- if (!deviceId && devices.length > 1) {
- let idx = this._inputs[kind]
- ? devices.findIndex(v => v === this._inputs[kind], this)
- : 0;
- nextDevice = devices[++idx % devices.length]; // eslint-disable-line no-plusplus
- }
- return nextDevice;
- });
- } else {
- next = Promise.resolve(deviceId);
- }
-
- return next
- .then((device) => { // eslint-disable-line consistent-return
- if (this._inputs[kind] !== device) {
- // Update video streams
- this._inputs[kind] = device;
- // Stop tracks
- this.media.getTracks().forEach(track => track.stop());
- // Update constraints
- const constraints = Object.assign({}, this.constraints);
- let props = ['facingMode', 'deviceId'];
- if (!~_facingModes.indexOf(device)) { // eslint-disable-line no-bitwise
- props = props.reverse();
- }
- _setConstrainValue(constraints[kind], props[0], props[1], device);
- Log.d('Local~_switchDevice', kind, constraints);
- return this.updateConstraints(constraints);
- }
- })
- .then(() => this);
- }
- return Promise.reject(new Error(`Current stream does not contain a ${kind} track`));
- }
-
- /**
- * Publish a local stream
- * @access protected
- * @param {string} roomId The room Id
- * @param {string} type The stream type, see {@link StreamTypes} for possible values
- * @param {?Element} container The element the stream is attached to.
- * @param {?MediaStreamConstraints} [constraints] The stream constraints.
- * If not defined the constraints defined in ReachConfig will be used.
- * @returns {Promise<Local, Error>}
- */
- /* static share(roomId, type, container, constraints) {
- if (!cache.user) {
- return Promise.reject(new Error('Only an authenticated user can share a stream.'));
- }
- const streamMetaData = {
- from: cache.user.uid,
- device: cache.device,
- type
- };
-
-
- const sharedStream = new Local(Object.assign({ roomId, constraints, container },
- streamMetaData));
- Log.d('Local~share', { sharedStream });
- return navigator.mediaDevices.getUserMedia(sharedStream.constraints)
- .then((media) => {
- sharedStream.media = media;
- })
- // Got MediaStream, publish it
- .then(() => DataSync.push(`_/rooms/${roomId}/streams`, streamMetaData))
- .then((streamRef) => {
- sharedStream.uid = streamRef.name();
- if (/video/i.test(sharedStream.type)) {
- if (sharedStream.isVideoLoaded) {
- const streamSize = {
- height: sharedStream.node.videoHeight,
- width: sharedStream.node.videoWidth,
- };
- streamRef.update(streamSize);
- } else {
- sharedStream.node.onloadeddata = function () { // eslint-disable-line func-names
- const streamSize = {
- height: sharedStream.node.videoHeight,
- width: sharedStream.node.videoWidth,
- };
- streamRef.update(streamSize);
- };
- }
- }
- if (/video/i.test(sharedStream.type)) {
- window.addEventListener('resize', (() => {
- if (sharedStream.node != null) {
- const streamSize = {
- height: sharedStream.node.videoHeight,
- width: sharedStream.node.videoWidth,
- };
- streamRef.update(streamSize);
- }
- }));
- }
- // Save sharedStream
- cache.streams.shared[sharedStream.uid] = sharedStream;
- // Remove shared stream on Disconnect
- DataSync.onDisconnect(`_/rooms/${roomId}/streams/${sharedStream.uid}`).remove();
- // Remove shared stream on Disconnect
- DataSync.onDisconnect(`_/rooms/${roomId}/subscribers/${sharedStream.uid}`).remove();
- // Start listening to subscribers
- const
- path = `_/rooms/${sharedStream.roomId}/subscribers/${sharedStream.uid}`;
-
-
- const value = snapData => Object.assign({ device: snapData.name() }, snapData.val() || {});
- DataSync.on(path, 'child_added',
- (snapData) => {
- const subscriber = value(snapData);
- Log.d('Local~subscribed', subscriber);
- cache.peerConnections.offer(sharedStream, subscriber)
- .then((pc) => {
- (this._callbacks[Events.local.SUBSCRIBED] || [])
- .forEach(cb => cb(sharedStream, subscriber));
- return sharedStream.peerConnections.push(pc);
- });
- },
- Log.e.bind(Log));
- DataSync.on(path, 'child_removed',
- (snapData) => {
- const subscriber = value(snapData);
- Log.d('Local~un-subscribed', subscriber);
- const closedPC = cache.peerConnections.close(sharedStream.uid, subscriber.device);
- sharedStream.peerConnections = sharedStream.peerConnections
- .filter(pc => pc !== closedPC);
- },
- Log.e.bind(Log));
- Log.d('Local~shared', { sharedStream });
- return sharedStream;
- });
- } */
-
- /**
- * Get a local stream
- * @access protected
- * @param {string} roomId The room Id
- * @param {string} type The stream type, see {@link StreamTypes} for possible values
- * @param {?Element} container The element the stream is attached to.
- * @param {?MediaStreamConstraints} [constraints] The stream constraints.
- * If not defined the constraints defined in ReachConfig will be used.
- * @returns {Promise<Local, Error>}
- */
- static getLocalVideo(roomId, type, container, constraints) {
- if (!cache.user) {
- return Promise.reject(new Error('Only an authenticated user can share a stream.'));
- }
- const streamMetaData = {
- from: cache.user.uid,
- device: cache.device,
- userAgent: cache.userAgent,
- type
- };
-
-
- const sharedStream = new Local(Object.assign({ roomId, constraints, container },
- streamMetaData));
- sharedStream.streamMetaData = streamMetaData;
- Log.d('Local~getLocalVideo', { sharedStream });
- return navigator.mediaDevices.getUserMedia(sharedStream.constraints)
- .then((media) => {
- sharedStream.media = media;
- return sharedStream;
- });
- }
-
- /**
- * Publish a local stream
- * @access protected
- * @returns {Local}
- */
- publish(sharedStream) {
- Log.d('Local~publish');
- const { roomId } = sharedStream;
- return DataSync.push(`_/rooms/${roomId}/streams`, sharedStream.streamMetaData)
- .then((streamRef) => {
- sharedStream.uid = streamRef.name(); // eslint-disable-line no-param-reassign
- if (sharedStream.isVideoLoaded) {
- const streamSize = {
- height: sharedStream.node.videoHeight,
- width: sharedStream.node.videoWidth,
- };
- streamRef.update(streamSize);
- } else {
- sharedStream.node.onloadeddata = function () { // eslint-disable-line
- const streamSize = {
- height: sharedStream.node.videoHeight,
- width: sharedStream.node.videoWidth,
- };
- streamRef.update(streamSize);
- };
- }
- window.addEventListener('resize', (() => {
- if (sharedStream.node != null) {
- const streamSize = {
- height: sharedStream.node.videoHeight,
- width: sharedStream.node.videoWidth,
- };
- streamRef.update(streamSize);
- }
- }));
- // Save sharedStream
- cache.streams.shared[sharedStream.uid] = sharedStream;
- // Remove shared stream on Disconnect
- DataSync.onDisconnect(`_/rooms/${roomId}/streams/${sharedStream.uid}`).remove();
- // Remove shared stream on Disconnect
- DataSync.onDisconnect(`_/rooms/${roomId}/subscribers/${sharedStream.uid}`).remove();
- // Start listening to subscribers
- const path = `_/rooms/${sharedStream.roomId}/subscribers/${sharedStream.uid}`;
- const value = snapData => Object.assign({ device: snapData.name() }, snapData.val() || {});
-
- DataSync.on(path, 'child_added',
- (snapData) => {
- const subscriber = value(snapData);
- Log.d('Local~subscribed', subscriber);
- cache.peerConnections
- .offer(sharedStream, subscriber, this._callbacks[Events.local.WEBRTC_ERROR])
- .then((pc) => {
- (this._callbacks[Events.local.SUBSCRIBED] || [])
- .forEach(cb => cb(sharedStream, subscriber));
- return sharedStream.peerConnections.push(pc);
- });
- },
- Log.e.bind(Log), this);
- DataSync.on(path, 'child_removed',
- (snapData) => {
- const subscriber = value(snapData);
- Log.d('Local~un-subscribed', subscriber);
- const closedPC = cache.peerConnections.close(sharedStream.uid, subscriber.device);
- /* eslint-disable no-param-reassign */
- sharedStream.peerConnections = sharedStream.peerConnections
- .filter(pc => pc !== closedPC);
- /* eslint-enable no-param-reassign */
- },
- Log.e.bind(Log));
- Log.d('Local~shared', { sharedStream });
- return sharedStream;
- });
- }
- }