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)));
}
}