Home Manual Reference Source

src/core/stream/Local.js

import 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 {NONE, CLOSED, CLOSING, CONNECTED} from '../util/constants';

const _facingModes = [Media.facingMode.USER, Media.facingMode.ENVIRONMENT];

const _getConstraintValue = (constraints, prop) => {
	return constraints[prop].exact || constraints[prop].ideal || constraints[prop];
};

const _setConstrainValue = (constraints, prop, other, value) => {
	constraints[prop] = {exact: value};
	delete constraints[other];
};

/**
 * 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;
	}

	/**
	 * The Media Constraints. (defaults to global config)
	 * @param {MediaConstraints} constraints
	 */
	set constraints(constraints) {
		const
			values = constraints || cache.config.constraints,
			defaultConstraints = Media.constraints();
		['audio', 'video'].forEach(type => {
			if(!~this.type.indexOf(type)) {
				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)
			.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];
				// 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 = this.muted.audio, video = this.muted.video, 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;
		}
		// Mute media tracks
		tracks.forEach(track => {track.enabled = !state;});
		// 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)) {
			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) {
				// Loop facingModes
				next = Promise.resolve(_facingModes[(currentModeIdx + 1) % _facingModes.length]);
			} else if(!~_facingModes.indexOf(deviceId)) {
				// 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];
						}
						return nextDevice;
					});
			} else {
				next = Promise.resolve(deviceId);
			}

			return next
				.then(device => {
					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)) {
							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
			},
			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() {
							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}`,
					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 => 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) {
		console.log('Local~share on entre ici');
		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
			},
			sharedStream = new Local(Object.assign({roomId, constraints, container}, streamMetaData));
		sharedStream.streamMetaData = streamMetaData;
		Log.d('Local~share', {sharedStream});
		console.log('Local~share', {sharedStream});
		return navigator.mediaDevices.getUserMedia(sharedStream.constraints)
			.then(media => {
				sharedStream.media = media;
				return sharedStream;
			});
	}

	/**
	 * Publish a local stream
	 * @access protected
	 * @returns {Local}
	 */
	static publish(sharedStream) {
		Log.d('Local~publish');
		const roomId = sharedStream.roomId;
		return DataSync.push(`_/rooms/${roomId}/streams`, sharedStream.streamMetaData)
			.then(streamRef => {
				sharedStream.uid = streamRef.name();
				if (sharedStream.isVideoLoaded) {
					const streamSize = {
						height: sharedStream.node.videoHeight,
						width: sharedStream.node.videoWidth,
					};
					streamRef.update(streamSize);
				} else {
					sharedStream.node.onloadeddata = function() {
						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}`,
					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 => 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;
			});
	}
}