src/core/Room.js
import {NONE, OPENED, CLOSED, CONNECTED, NOT_CONNECTED, WAS_CONNECTED, OWNER} 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';
import {REJECTED, CANCELED} from './util/constants';
const _joinRoom = (room, role) => {
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/${cache.user.uid}`]);
return DataSync
.update(`_/rooms/${room.uid}/participants/${cache.user.uid}`, participant)
.then(() => {
DataSync
.onDisconnect(`_/rooms/${room.uid}/participants/${cache.user.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) {
let values = snapData;
if(snapData && snapData.val && typeof snapData.val === 'function'){
values = Object.assign({}, snapData.val(), {uid: snapData.name()});
}
/**
* 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 => {
console.log(('on passe par ici et ça marche'));
return 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() {
console.log('on veut récupérer les remotes');
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}`,
_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),
obj = Events.room.toClass(event);
if(path && obj) {
const typedCallback = snapData => {
if(!/^STREAM_/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);
}
/**
* 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});
console.log('Room~getLocalVideo');
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 Local.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
DataSync.onDisconnect(`_/rooms/${this.uid}/participants/${cache.user.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()));
// Unpublish local stream even if not published
if (this.localStream) {
console.log('on va cloer le local');
// this.localStream.close();
console.log('ouf cest fait');
}
// Unsubscribe all remote streams
console.log('on va désouscrire les remoteStreams');
this.remoteStreams().then(remoteStreams => remoteStreams.forEach(remoteStream => remoteStream.unSubscribe()));
console.log('on a désouscrit les remoteStreams');
// Update status
return DataSync.set(`_/rooms/${this.uid}/participants/${cache.user.uid}/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(() => {
return DataSync.update(`rooms/${this.uid}`, {
status: CLOSED,
_closed: DataSync.ts()
});
})
.then(() => {
return DataSync.remove(`_/rooms/${this.uid}`);
// .catch(error => console.error(`le remove de _ rooms ne passe pas ${error}`));
})
.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()}`
},
roomFullMetaData = Object.assign({
status: OPENED,
_created: DataSync.ts(),
extra
}, roomMetaData);
let room = null;
// Create public room infos
return DataSync.push('rooms', roomFullMetaData)
// Create private room infos
.then(roomRef => {
console.log('on a créé la room dans webcom');
room = new Room(Object.assign({uid: roomRef.name()}, roomFullMetaData));
console.log('on a créé la room dans le reach');
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);
}
});
}
}