Home Reference Source

src/misc/Selfie.js

import {DataPipeStatus} from '../datapipe/DataPipeStatus';
import {DataPipeType} from '../datapipe/DataPipeType';
import {EventEmitter} from '../eventemitter';
import {bindMethods} from '../utils/bindMethods';

/**
 * Provides access to methods for controlling a selfie.
 * This class must not be directly instantiated. It can be obtained using
 * {@link session#createSelfie} or listening to {@link session#selfie-invite}
 * event.
 *
 * ## Events
 *
 * - **`cancel`** is emitted when the selfie has been finished.
 * - **`image`** is emitted when the image is received.
 *   - {@link string} `image`
 * - **`taking`** is emitted when the remote user is taking a picture.
 *
 * You can check EventEmitter2 documentation for additional information about
 * adding and removing listeners for this events events.
 *
 * @see https://github.com/asyncly/EventEmitter2#api
 *
 * @example <caption>How to create a Selfie</caption>
 * // 1. Agent side create a selfie using session object.
 * let selfie = session.createSelfie(['user@domain']);
 * // 2. Client listens to 'selfie-invite event'
 * session.on('selfie-invite', function(selfie){
 *  // ... do stuff with selfie.
 * })
 *
 * @example <caption>Notify the agent that user is taking a selfie</caption>
 * // Agent will receive 'taking' event.
 * selfie.on('taking', () => {...});
 * // Client just calls selfie method.
 * selfie.notifyTaking();
 *
 * @example <caption>Send the picture to the agent</caption>
 * // Agent will receive 'image' event.
 * selfie.on('image', (imageURL) => {...});
 * // Client just calls selfie method.
 * selfie.send(imageURL);
 *
 */
export default class Selfie extends EventEmitter {
	/**
	 * The version of the protocol implemented.
	 * @type {string}
	 */
	static get PROTOCOL() {
		return 'selfie/2.0';
	}

	/**
	 * Initializes a new selfie with its default values.
	 * @protected
	 */
	constructor() {
		super();

		/**
		 * The underline datapipe used for selfie communication.
		 * @type {Datapipe}
		 */
		this._pipe = null;

		bindMethods(this, [
			'_onPipeData',
		]);
	}

	get isConnected() {
		return this._pipe === null ? false : this._pipe.status === DataPipeStatus.CONNECTED;
	}

	/**
	 * Cancel the process at any time.
	 * @return {Promise} A promise that is fulfilled when the selfie is canceled.
	 */
	cancel() {
		return this._pipe.when(DataPipeStatus.CONNECTED).then(() => this._pipe.disconnect());
	}

	/**
	 * Send 'taking' event to the other side.
	 * @return {Promise} A promise that is fulfilled when the notification has been sent.
	 */
	notifyTaking() {
		return this._sendCommand('taking');
	}

	/**
	 * Send the image to the agent.
	 * This can be a regular URL or a dataUrl, with a base64 encoded image.
	 * @param {imgDataUrl} imgDataUrl The image file containing the selfie.
	 * @return {Promise} A promise that is fulfilled when the image has been sent.
	 */
	send(imgDataUrl) {
		return this._sendCommand({action: 'image', image: imgDataUrl});
	}

	/**
	 * Not implemented.
	 * @return {undefined}
	 */
	/*save() {
		throw new Error('NOT IMPLEMENTED');
	}*/

	/**
	 * Inits selfie object.
	 *  1. Sets datapipe callbacks
	 *  2. Connects the datapipe
	 *  3. If datapipe is outgoing, sends a selfie-invite command to the other peer
	 * @protected
	 * @param {Datapipe} pipe The datapipe that will be used.
	 * @return {Promise} A promise that is fulfilled when the pipe is connected
	 */
	_initDatapipe(pipe) {
		if (pipe.status !== DataPipeStatus.UNCONNECTED) {
			const expected = DataPipeStatus[DataPipeStatus.UNCONNECTED];
			const received = DataPipeStatus[pipe.status];
			return Promise.reject(Error(`Expected datapipe status "${expected}" but got "${received}" instead.`));
		}
		this._pipe = pipe;
		this._pipe.on('data', this._onPipeData);
		this._pipe.when(DataPipeStatus.DISCONNECTED).then(() => {
			this.off('data', this._onPipeData);
			this.emit('cancel');
			this._pipe = null;
		});
		return this._pipe.connect().then(() => {
			if (this._pipe.type === DataPipeType.OUTGOING) {
				return this._sendCommand('selfie-invite');
			}
		});
	}

	/**
	 * Callback executed when datapipe is connected.
	 * If we are the selfie creator, send a 'selfie-invite' to the other side.
	 * @private
	 * @param {string} data A string sent by the datapipe
	 * @return {undefined}
	 */
	_onPipeData(data) {
		const command = this._parseData(data);
		switch (command.action) {
			case 'taking':
				this.emit('taking');
				break;
			case 'image':
				this.emit('image', command.image);
				break;
			default:
		}
	}

	/**
	 * Gets a datapipe message and checks that protocol exists and is equal to Selfie.PROTOCOL
	 * otherwise throw an error.
	 * @private
	 * @param {string} data A string sent by the datapipe.
	 * @return {Object} An object with two fields: command and protocol.
	 * @property {string} command
	 * @property {string} protocol
	 */
	_parseData(data) {
		const command = JSON.parse(data);
		if (command.protocol !== Selfie.PROTOCOL) {
			throw new Error(`Expected protocol "${Selfie.PROTOCOL}" but got "${command.protocol}" instead.`);
		}
		return command;
	}

	/**
	 * Send a command through the datapipe.
	 * If command is a string it will be transformed into a object with a single command field.
	 * If command is an object, all fields will be conserved.
	 * In both cases a protocol field will be added.
	 * @private
	 * @param {string|object} command Command to send through the datapipe
	 * @return {Promise} A promise that is fulfilled when the command was sent
	 */
	_sendCommand(command) {
		if (typeof command === 'string') {
			command = {'action': command};
		}
		command.protocol = Selfie.PROTOCOL;
		return this._pipe.when(DataPipeStatus.CONNECTED).then(() => this._pipe.send(JSON.stringify(command)));
	}
}