src/core/Room.js
import {
CANCELED,
CLOSED,
CONNECTED,
NONE,
NOT_CONNECTED,
OPENED,
OWNER,
REJECTED,
WAS_CONNECTED
} from './util/constants';
import * as DataSync from './util/DataSync';
import cache from './util/cache';
import Participant from './Participant';
import Message from './Message';
import Local from './stream/Local';
import Invite from './Invite';
import * as Events from '../definitions/Events';
import * as Log from './util/Log';
const _joinRoom = (room, role) => {
const uid = cache.user.uid.split('/').join(':');
if (room.status !== CLOSED) {
const participant = {
status: CONNECTED,
userAgent: cache.userAgent,
_joined: DataSync.ts()
};
if (role) {
participant.role = role;
}
Log.w('Room#join', [participant, `_/rooms/${room.uid}/participants/${uid}`]);
return DataSync
.update(`_/rooms/${room.uid}/participants/${uid}`, participant)
.then(() => {
DataSync
.onDisconnect(`_/rooms/${room.uid}/participants/${uid}/status`)
.set(WAS_CONNECTED);
return room;
});
}
return Promise.reject(new Error('can\'t join a close room'));
};
/**
* Room information
* @access public
*/
export default class Room {
/**
* Create a room
* @param {Webcom/api.DataSnapshot|Object} snapData The data snapshot
* @access protected
*/
constructor(snapData, roomUid) {
let values = snapData;
if (snapData && snapData.val && typeof snapData.val === 'function') {
// values = Object.assign({}, snapData.val(), {uid: snapData.name()});
values = Object.assign({}, snapData.val(), { uid: roomUid });
}
/**
* The room unique id
* @type {string}
*/
this.uid = values.uid;
/**
* The room name
* @type {string}
*/
this.name = values.name;
/**
* The local stream of the room
* @type {Local}
*/
this.localStream = {};
/**
* The room owner uid
* @type {string}
*/
this.owner = values.owner;
/**
* The room status:
* - OPENED
* - CLOSED
* @type {string}
*/
this.status = values.status;
/**
* Indicates that the room is public so all users can join
* @type {boolean}
*/
this._public = !!values._public;
/**
* Additional room informations
* @type {Object}
*/
this.extra = values.extra;
/**
* List of declared callbacks
* @type {Object}
*/
this._callbacks = {};
}
/**
* Get the list of participants.
* This will only work if the user is either a participant or the owner of the room.
* @returns {Promise<Participant[], Error>}
*/
participants() {
return DataSync.list(`_/rooms/${this.uid}/participants`, Participant, this.uid);
}
/**
* Get the list of messages.
* This will only work if the user is either a participant or the owner of the room.
* @return {Promise<Message[], Error>}
*/
messages() {
return DataSync.list(`_/rooms/${this.uid}/messages`, Message, this.uid);
}
/**
* Get the list of streams
* @returns {Promise}
* @access private
*/
_streams(localStreams) {
if (!cache.user) {
return Promise.reject(new Error('Only an authenticated user can list a Room\'s streams.'));
}
return DataSync.get(`_/rooms/${this.uid}/streams`)
.then((snapData) => {
const values = snapData.val();
Log.d('Rooms~_streams', values);
if (values) {
return Object.keys(values).map(key => Object.assign({
uid: key,
roomId: this.uid
}, values[key]));
}
return [];
})
.then(streams => streams.filter(stream => (
localStreams === (stream.device === cache.device && stream.from === cache.user.uid))))
.then(streams => streams.map(cache.streams[`get${localStreams ? 'Shared' : 'Remote'}`].bind(cache.streams)))
.then(streams => streams.filter(stream => stream !== null));
}
/**
* Get the list of locally published streams.
* The streams published with another device won't be visible here
* This will only work if the user is either a participant or the owner of the room.
* @return {Promise<Local[], Error>}
*/
localStreams() {
return this._streams(true)
.catch(Log.r('Room~localStreams'));
}
/**
* Get the list of remotely published streams.
* This will only work if the user is either a participant or the owner of the room.
* @return {Promise<Remote[], Error>}
*/
remoteStreams() {
return this._streams(false)
.catch(Log.r('Room~remoteStreams'));
}
/**
* Invite users to the room.
* This will only work if the current User is the owner or a moderator of this Room.
* This will create the invitation and add the user to the participants list.
* @param {User[]} users the users to invite
* @param {string} [role='NONE'] the role of the invitee
* @param {string} [message] a message to add to the invite
* @return {Promise<{room: Room, invites: Invite[]}, Error>}
*/
invite(users, role = NONE, message) {
const _path = user => `_/rooms/${this.uid}/participants/${user.uid}`;
const _data = {
status: NOT_CONNECTED,
role: role || NONE
};
// Add users as participant so they can join the room
return Promise.all(users.map(user => DataSync.set(_path(user), _data)))
// Send invites
.then(() => Promise.all(users.map(user => Invite.send(user, this, message))))
.then((invites) => {
const removeParticipant = invite => DataSync.remove(`_/rooms/${invite.room}/participants/${invite.to}`);
invites.forEach((invite) => {
invite.on(REJECTED, removeParticipant);
invite.on(CANCELED, removeParticipant);
});
return { room: this, invites };
})
.catch((e) => {
Log.e('Room~invite', e);
users.forEach(user => DataSync.remove(`_/rooms/${this.uid}/participants/${user.uid}`));
return Promise.reject(e);
});
}
/**
* Register a callback for a specific event
* @param {string} event The event name ({@link Events/Room}):
* - PARTICIPANT_ADDED: a participant is added to the room
* - PARTICIPANT_CHANGED: a participant changes his status (join)
* - PARTICIPANT_REMOVED: a participant leave the room
* - MESSAGE_ADDED: new instant message
* - MESSAGE_CHANGED: an existing message has been modified (moderation)
* - MESSAGE_REMOVED: a message has been removed (moderation)
* - STREAM_PUBLISHED: a participant published a new Stream
* - STREAM_CHANGED: a participant changes his published Stream (moderation, type, mute...)
* - STREAM_UNPUBLISHED: a participant stops the publication of his Stream
* @param {function} callback
* The callback for the event, the arguments depends on the type of event:
* - PARTICIPANT_* : callback({@link Participant} p [, Error e])
* - MESSAGE_* : callback({@link Message} m [, Error e])
* - STREAM_* : callback({@link Remote} s [, Error e])
* @param {Webcom/api.Query~cancelCallback} cancelCallback The error callback for the event,
* takes an Error as only argument
*/
on(event, callback, cancelCallback) {
const
path = Events.room.toPath(event)(this);
const Obj = Events.room.toClass(event);
if (path && Obj) {
const typedCallback = (snapData) => {
if (!/^STREAM_/i.test(event) || !snapData) {
// if(/^MESSAGE_/i.test(event) || !snapData) {
Log.i(`Room~on(${event})`, snapData ? new Obj(snapData) : null);
callback(snapData ? new Obj(snapData) : null);
} else if (cache.user) {
const streamData = Object.assign({
uid: snapData.name(),
roomId: this.uid
}, snapData.val());
if (streamData.from !== cache.user.uid || streamData.device !== cache.device) {
const remoteStream = cache.streams.getRemote(streamData);
Log.i(`Room~on(${event})`, remoteStream);
callback(remoteStream);
}
}
};
DataSync.on(path, event, typedCallback, cancelCallback);
if (!this._callbacks[event]) {
this._callbacks[event] = [];
}
this._callbacks[event].push(typedCallback);
}
}
/**
* Send an instant message
* @param {string} message The message to send
* @return {Promise<Message>}
*/
sendMessage(message) {
return Message.send(this, message);
}
/**
* Publish a local stream
* @param {string} type The stream type, see {@link StreamTypes} for possible values
* @param {Element} [localStreamContainer] The element the stream is attached to.
* Can be null if already specified in {@link Config}.
* @param {MediaStreamConstraints} [constraints] The stream constraints.
* If not defined, the constraints defined in {@link Config} will be used.
* @returns {Promise<Local, Error>}
*/
share(type, localStreamContainer, constraints) {
Log.i('Room~share', { type, localStreamContainer, constraints });
// return Local.share(this.uid, type, localStreamContainer, constraints);
return Local.getLocalVideo(this.uid, type, localStreamContainer, constraints)
.then(stream => stream.publish(stream));
}
/**
* get a local stream in video tag
* @param {string} type The stream type, see {@link StreamTypes} for possible values
* @param {Element} [localStreamContainer] The element the stream is attached to.
* Can be null if already specified in {@link Config}.
* @param {MediaStreamConstraints} [constraints] The stream constraints.
* If not defined, the constraints defined in {@link Config} will be used.
* @returns {Promise<Local, Error>}
*/
getLocalVideo(type, localStreamContainer, constraints) {
Log.i('Room~getLocalVideo', { type, localStreamContainer, constraints });
return Local.getLocalVideo(this.uid, type, localStreamContainer, constraints)
.then((localStream) => {
this.localStream = localStream;
return localStream;
});
}
/**
* publish a local stream
* @returns {Local}
*/
publish() {
Log.i('Room~publish Local');
return this.localStream.publish(this.localStream);
}
/**
* Join the room. Sets the connected status of the current participant to CONNECTED.
* @return {Promise}
*/
join() {
Log.i('Room~join', this);
if (!cache.user) {
return Promise.reject(new Error('Only an authenticated user can join a Room.'));
}
return _joinRoom(this).catch(Log.r('Room~join'));
}
/**
* Leave the room. Sets the connected status of the current participant to WAS_CONNECTED,
* deletes medias and callbacks, closes WebRTC stacks in use.
* @return {Promise}
*/
leave() {
if (!cache.user) {
return Promise.reject(new Error('Only an authenticated user can leave a Room.'));
}
Log.i('Room~leave', this);
// Cancel onDisconnect
const uid = cache.user.uid.split('/').join(':');
DataSync.onDisconnect(`_/rooms/${this.uid}/participants/${uid}/status`).cancel();
// Disconnect user's callbacks
Object.keys(this._callbacks).forEach((event) => {
DataSync.off(Events.room.toPath(event)(this), event);
});
// Unpublish all published local streams
this.localStreams()
.then(localStreams => localStreams.forEach(localStream => localStream.close()));
// Unsubscribe all remote streams
this.remoteStreams()
.then(remoteStreams => remoteStreams.forEach(remoteStream => remoteStream.unSubscribe()));
// Update status
return DataSync.set(`_/rooms/${this.uid}/participants/${uid}/status`, WAS_CONNECTED)
// return DataSync.set(`_/rooms/${this.uid}/participants/${shortUserId}/status`, WAS_CONNECTED)
.catch(Log.r('Room~leave'));
}
/**
* Leaves & close the Room. Only the owner/moderator can close a room.
* @return {Promise}
*/
close() {
Log.i('Room~close', this);
this.status = CLOSED;
return this.leave()
.then(() => DataSync.update(`rooms/${this.uid}`, { status: CLOSED, _closed: DataSync.ts() }))
.then(() => DataSync.remove(`_/rooms/${this.uid}`))
.catch(Log.r('Room~close'));
}
/**
* Create a room
* @access protected
* @param {String} [name] The room name
* @param {object} [extra=null] Extra informations
* @param {boolean} [publicRoom=false] Indicates public room
* @returns {Promise<Room, Error>}
*/
static create(name, extra = null, publicRoom = false) {
if (!cache.user) {
return Promise.reject(new Error('Only an authenticated user can create a Room.'));
}
const roomMetaData = {
owner: cache.user.uid,
_public: publicRoom,
name: name || `${cache.user.name}-${Date.now()}`
};
const roomFullMetaData = Object.assign({
status: OPENED,
_created: DataSync.ts(),
extra
}, roomMetaData);
let room = null;
// Create public room infos
// return DataSync.push('rooms', roomFullMetaData)
const id1 = Math.floor(Math.random() * 1000);
const id2 = Math.floor(Math.random() * 1000);
return DataSync.push(`rooms/${id1}/${id2}`, roomFullMetaData)
// Create private room infos
.then((roomRef) => {
room = new Room(Object.assign({ uid: `${id1}/${id2}/${roomRef.name()}` }, roomFullMetaData));
cache.room = roomFullMetaData;
return DataSync.update(`_/rooms/${room.uid}/meta`, roomMetaData);
})
// Join the room
.then(() => _joinRoom(room, OWNER))
.catch(Log.r('Room#create'));
}
/**
* Get a {@link Room} from its `uid`
* @access protected
* @param uid
* @returns {Promise.<Room>}
*/
static get(uid) {
return DataSync.get(`rooms/${uid}`)
.then((snapData) => {
if (snapData.val()) {
return new Room(snapData, uid);
}
return null;
});
}
}