Refactors RTC module to support multiple conferences. Implements JitsiTrack interfaces.

This commit is contained in:
hristoterezov 2015-09-11 12:21:45 -05:00
parent 2b5d893ec1
commit 3281fdd523
18 changed files with 2948 additions and 2125 deletions

View File

@ -1,5 +1,8 @@
var RTC = require("./modules/RTC/RTC");
var XMPPEvents = require("./service/xmpp/XMPPEvents");
var StreamEventTypes = require("./service/RTC/StreamEventTypes");
var EventEmitter = require("events");
var JitsiConferenceEvents = require("./JitsiConferenceEvents");
/**
* Creates a JitsiConference object with the given name and properties.
@ -15,11 +18,10 @@ function JitsiConference(options) {
this.options = options;
this.connection = this.options.connection;
this.xmpp = this.connection.xmpp;
this.room = this.xmpp.createRoom(this.options.name, null, null);
this.rtc = new RTC();
this.xmpp.addListener(XMPPEvents.CALL_INCOMING,
this.rtc.onIncommingCall.bind(this.rtc));
this.eventEmitter = new EventEmitter();
this.room = this.xmpp.createRoom(this.options.name, null, null, this.options.config);
this.rtc = new RTC(this.room, options);
setupListeners(this);
}
/**
@ -27,7 +29,6 @@ function JitsiConference(options) {
* @param password {string} the password
*/
JitsiConference.prototype.join = function (password) {
this.room.join(password);
}
@ -67,7 +68,7 @@ JitsiConference.prototype.getLocalTracks = function () {
* Note: consider adding eventing functionality by extending an EventEmitter impl, instead of rolling ourselves
*/
JitsiConference.prototype.on = function (eventId, handler) {
this.room.addListener(eventId, handler);
this.eventEmitter.on(eventId, handler);
}
/**
@ -78,7 +79,7 @@ JitsiConference.prototype.on = function (eventId, handler) {
* Note: consider adding eventing functionality by extending an EventEmitter impl, instead of rolling ourselves
*/
JitsiConference.prototype.off = function (eventId, handler) {
this.room.removeListener(eventId, listener);
this.eventEmitter.removeListener(eventId, listener);
}
// Common aliases for event emitter
@ -181,5 +182,21 @@ JitsiConference.prototype.getParticipantById = function(id) {
}
function setupListeners(conference) {
conference.xmpp.addListener(XMPPEvents.CALL_INCOMING,
conference.rtc.onIncommingCall.bind(conference.rtc));
conference.room.addListener(XMPPEvents.REMOTE_STREAM_RECEIVED,
conference.rtc.createRemoteStream.bind(conference.rtc));
conference.rtc.addListener(StreamEventTypes.EVENT_TYPE_REMOTE_CREATED, function (stream) {
conference.eventEmitter.emit(JitsiConferenceEvents.TRACK_ADDED, stream);
});
conference.rtc.addListener(StreamEventTypes.EVENT_TYPE_REMOTE_CREATED, function (stream) {
conference.eventEmitter.emit(JitsiConferenceEvents.TRACK_REMOVED, stream);
})
conference.room.addListener(XMPPEvents.MUC_JOINED, function () {
conference.eventEmitter.emit(JitsiConferenceEvents.CONFERENCE_JOINED);
})
}
module.exports = JitsiConference;

View File

@ -17,6 +17,9 @@ var LibJitsiMeet = {
errors: {
conference: JitsiConferenceErrors,
connection: JitsiConnectionErrors
},
init: function (options) {
require("./modules/RTC/RTC").init(options || {});
}
}

File diff suppressed because it is too large Load Diff

View File

@ -4,155 +4,148 @@
// https://code.google.com/p/chromium/issues/detail?id=405545
var RTCEvents = require("../../service/RTC/RTCEvents");
var _dataChannels = [];
var eventEmitter = null;
/**
* Binds "ondatachannel" event listener to given PeerConnection instance.
* @param peerConnection WebRTC peer connection instance.
*/
function DataChannels(peerConnection, emitter) {
peerConnection.ondatachannel = this.onDataChannel;
this.eventEmitter = emitter;
var DataChannels = {
/**
* Callback triggered by PeerConnection when new data channel is opened
* on the bridge.
* @param event the event info object.
*/
onDataChannel: function (event) {
var dataChannel = event.channel;
this._dataChannels = [];
dataChannel.onopen = function () {
console.info("Data channel opened by the Videobridge!", dataChannel);
// Sample code for opening new data channel from Jitsi Meet to the bridge.
// Although it's not a requirement to open separate channels from both bridge
// and peer as single channel can be used for sending and receiving data.
// So either channel opened by the bridge or the one opened here is enough
// for communication with the bridge.
/*var dataChannelOptions =
{
reliable: true
};
var dataChannel
= peerConnection.createDataChannel("myChannel", dataChannelOptions);
// Code sample for sending string and/or binary data
// Sends String message to the bridge
//dataChannel.send("Hello bridge!");
// Sends 12 bytes binary message to the bridge
//dataChannel.send(new ArrayBuffer(12));
eventEmitter.emit(RTCEvents.DATA_CHANNEL_OPEN);
};
dataChannel.onerror = function (error) {
console.error("Data Channel Error:", error, dataChannel);
};
dataChannel.onmessage = function (event) {
var data = event.data;
// JSON
var obj;
try {
obj = JSON.parse(data);
}
catch (e) {
console.error(
"Failed to parse data channel message as JSON: ",
data,
dataChannel);
}
if (('undefined' !== typeof(obj)) && (null !== obj)) {
var colibriClass = obj.colibriClass;
if ("DominantSpeakerEndpointChangeEvent" === colibriClass) {
// Endpoint ID from the Videobridge.
var dominantSpeakerEndpoint = obj.dominantSpeakerEndpoint;
console.info(
"Data channel new dominant speaker event: ",
dominantSpeakerEndpoint);
eventEmitter.emit(RTCEvents.DOMINANTSPEAKER_CHANGED, dominantSpeakerEndpoint);
}
else if ("InLastNChangeEvent" === colibriClass) {
var oldValue = obj.oldValue;
var newValue = obj.newValue;
// Make sure that oldValue and newValue are of type boolean.
var type;
if ((type = typeof oldValue) !== 'boolean') {
if (type === 'string') {
oldValue = (oldValue == "true");
} else {
oldValue = new Boolean(oldValue).valueOf();
}
}
if ((type = typeof newValue) !== 'boolean') {
if (type === 'string') {
newValue = (newValue == "true");
} else {
newValue = new Boolean(newValue).valueOf();
}
}
eventEmitter.emit(RTCEvents.LASTN_CHANGED, oldValue, newValue);
}
else if ("LastNEndpointsChangeEvent" === colibriClass) {
// The new/latest list of last-n endpoint IDs.
var lastNEndpoints = obj.lastNEndpoints;
// The list of endpoint IDs which are entering the list of
// last-n at this time i.e. were not in the old list of last-n
// endpoint IDs.
var endpointsEnteringLastN = obj.endpointsEnteringLastN;
console.log(
"Data channel new last-n event: ",
lastNEndpoints, endpointsEnteringLastN, obj);
eventEmitter.emit(RTCEvents.LASTN_ENDPOINT_CHANGED,
lastNEndpoints, endpointsEnteringLastN, obj);
}
else {
console.debug("Data channel JSON-formatted message: ", obj);
}
}
};
dataChannel.onclose = function () {
console.info("The Data Channel closed", dataChannel);
var idx = _dataChannels.indexOf(dataChannel);
if (idx > -1)
_dataChannels = _dataChannels.splice(idx, 1);
};
_dataChannels.push(dataChannel);
},
/**
* Binds "ondatachannel" event listener to given PeerConnection instance.
* @param peerConnection WebRTC peer connection instance.
*/
init: function (peerConnection, emitter) {
if(!config.openSctp)
return;
peerConnection.ondatachannel = this.onDataChannel;
eventEmitter = emitter;
// Sample code for opening new data channel from Jitsi Meet to the bridge.
// Although it's not a requirement to open separate channels from both bridge
// and peer as single channel can be used for sending and receiving data.
// So either channel opened by the bridge or the one opened here is enough
// for communication with the bridge.
/*var dataChannelOptions =
{
reliable: true
};
var dataChannel
= peerConnection.createDataChannel("myChannel", dataChannelOptions);
// Can be used only when is in open state
dataChannel.onopen = function ()
{
dataChannel.send("My channel !!!");
};
dataChannel.onmessage = function (event)
{
var msgData = event.data;
console.info("Got My Data Channel Message:", msgData, dataChannel);
};*/
},
handleSelectedEndpointEvent: onSelectedEndpointChanged,
handlePinnedEndpointEvent: onPinnedEndpointChanged
// Can be used only when is in open state
dataChannel.onopen = function ()
{
dataChannel.send("My channel !!!");
};
dataChannel.onmessage = function (event)
{
var msgData = event.data;
console.info("Got My Data Channel Message:", msgData, dataChannel);
};*/
};
function onSelectedEndpointChanged(userResource) {
/**
* Callback triggered by PeerConnection when new data channel is opened
* on the bridge.
* @param event the event info object.
*/
DataChannels.prototype.onDataChannel = function (event) {
var dataChannel = event.channel;
dataChannel.onopen = function () {
console.info("Data channel opened by the Videobridge!", dataChannel);
// Code sample for sending string and/or binary data
// Sends String message to the bridge
//dataChannel.send("Hello bridge!");
// Sends 12 bytes binary message to the bridge
//dataChannel.send(new ArrayBuffer(12));
this.eventEmitter.emit(RTCEvents.DATA_CHANNEL_OPEN);
}.bind(this);
dataChannel.onerror = function (error) {
console.error("Data Channel Error:", error, dataChannel);
};
dataChannel.onmessage = function (event) {
var data = event.data;
// JSON
var obj;
try {
obj = JSON.parse(data);
}
catch (e) {
console.error(
"Failed to parse data channel message as JSON: ",
data,
dataChannel);
}
if (('undefined' !== typeof(obj)) && (null !== obj)) {
var colibriClass = obj.colibriClass;
if ("DominantSpeakerEndpointChangeEvent" === colibriClass) {
// Endpoint ID from the Videobridge.
var dominantSpeakerEndpoint = obj.dominantSpeakerEndpoint;
console.info(
"Data channel new dominant speaker event: ",
dominantSpeakerEndpoint);
this.eventEmitter.emit(RTCEvents.DOMINANTSPEAKER_CHANGED, dominantSpeakerEndpoint);
}
else if ("InLastNChangeEvent" === colibriClass) {
var oldValue = obj.oldValue;
var newValue = obj.newValue;
// Make sure that oldValue and newValue are of type boolean.
var type;
if ((type = typeof oldValue) !== 'boolean') {
if (type === 'string') {
oldValue = (oldValue == "true");
} else {
oldValue = new Boolean(oldValue).valueOf();
}
}
if ((type = typeof newValue) !== 'boolean') {
if (type === 'string') {
newValue = (newValue == "true");
} else {
newValue = new Boolean(newValue).valueOf();
}
}
this.eventEmitter.emit(RTCEvents.LASTN_CHANGED, oldValue, newValue);
}
else if ("LastNEndpointsChangeEvent" === colibriClass) {
// The new/latest list of last-n endpoint IDs.
var lastNEndpoints = obj.lastNEndpoints;
// The list of endpoint IDs which are entering the list of
// last-n at this time i.e. were not in the old list of last-n
// endpoint IDs.
var endpointsEnteringLastN = obj.endpointsEnteringLastN;
console.log(
"Data channel new last-n event: ",
lastNEndpoints, endpointsEnteringLastN, obj);
this.eventEmitter.emit(RTCEvents.LASTN_ENDPOINT_CHANGED,
lastNEndpoints, endpointsEnteringLastN, obj);
}
else {
console.debug("Data channel JSON-formatted message: ", obj);
}
}
}.bind(this);
dataChannel.onclose = function () {
console.info("The Data Channel closed", dataChannel);
var idx = this._dataChannels.indexOf(dataChannel);
if (idx > -1)
this._dataChannels = this._dataChannels.splice(idx, 1);
}.bind(this);
this._dataChannels.push(dataChannel);
};
DataChannels.prototype.handleSelectedEndpointEvent = function (userResource) {
console.log('selected endpoint changed: ', userResource);
if (_dataChannels && _dataChannels.length != 0) {
_dataChannels.some(function (dataChannel) {
if (this._dataChannels && this._dataChannels.length != 0) {
this._dataChannels.some(function (dataChannel) {
if (dataChannel.readyState == 'open') {
console.log('sending selected endpoint changed ' +
'notification to the bridge: ', userResource);
@ -169,10 +162,10 @@ function onSelectedEndpointChanged(userResource) {
}
}
function onPinnedEndpointChanged(userResource) {
DataChannels.prototype.handlePinnedEndpointEvent = function (userResource) {
console.log('pinned endpoint changed: ', userResource);
if (_dataChannels && _dataChannels.length != 0) {
_dataChannels.some(function (dataChannel) {
if (this._dataChannels && this._dataChannels.length != 0) {
this._dataChannels.some(function (dataChannel) {
if (dataChannel.readyState == 'open') {
dataChannel.send(JSON.stringify({
'colibriClass': 'PinnedEndpointChangedEvent',

View File

@ -0,0 +1,119 @@
var JitsiTrack = require("./JitsiTrack");
var StreamEventTypes = require("../../service/RTC/StreamEventTypes");
var RTCEvents = require("../../service/RTC/RTCEvents");
var RTCBrowserType = require("./RTCBrowserType");
/**
* This implements 'onended' callback normally fired by WebRTC after the stream
* is stopped. There is no such behaviour yet in FF, so we have to add it.
* @param stream original WebRTC stream object to which 'onended' handling
* will be added.
*/
function implementOnEndedHandling(stream) {
var originalStop = stream.stop;
stream.stop = function () {
originalStop.apply(stream);
if (!stream.ended) {
stream.ended = true;
stream.onended();
}
};
}
/**
* Represents a single media track (either audio or video).
* @constructor
*/
function JitsiLocalTrack(RTC, stream, eventEmitter, videoType, isGUMStream)
{
JitsiTrack.call(this, RTC, stream);
this.eventEmitter = eventEmitter;
this.videoType = videoType;
this.isGUMStream = true;
if(isGUMStream === false)
this.isGUMStream = isGUMStream;
this.stream.onended = function () {
this.eventEmitter.emit(StreamEventTypes.EVENT_TYPE_LOCAL_ENDED, this);
}.bind(this);
if (RTCBrowserType.isFirefox()) {
implementOnEndedHandling(this.stream);
}
}
JitsiLocalTrack.prototype = Object.create(JitsiTrack.prototype);
JitsiLocalTrack.prototype.constructor = JitsiLocalTrack;
/**
* Mutes / unmutes the track.
* @param mute {boolean} if true the track will be muted. Otherwise the track will be unmuted.
*/
JitsiLocalTrack.prototype._setMute = function (mute) {
var isAudio = this.type === JitsiTrack.AUDIO;
var eventType = isAudio ? RTCEvents.AUDIO_MUTE : RTCEvents.VIDEO_MUTE;
if ((window.location.protocol != "https:" && this.isGUMStream) ||
(isAudio && this.isGUMStream) || this.videoType === "screen" ||
// FIXME FF does not support 'removeStream' method used to mute
RTCBrowserType.isFirefox()) {
var tracks = this.getTracks();
for (var idx = 0; idx < tracks.length; idx++) {
tracks[idx].enabled = !mute;
}
if(isAudio)
this.rtc.room.setAudioMute(mute);
else
this.rtc.room.setVideoMute(mute);
this.eventEmitter.emit(eventType, mute);
} else {
if (mute) {
this.rtc.room.removeStream(this.stream);
this.stream.stop();
if(isAudio)
this.rtc.room.setAudioMute(mute);
else
this.rtc.room.setVideoMute(mute);
this.eventEmitter.emit(eventType, true);
} else {
var self = this;
this.rtc.obtainAudioAndVideoPermissions(
{devices: (this.isAudioStream() ? ["audio"] : ["video"])})
.then(function (stream) {
if (isAudio) {
self.rtc.changeLocalAudio(stream,
function () {
this.rtc.room.setAudioMute(mute);
self.eventEmitter.emit(eventType, false);
});
} else {
self.rtc.changeLocalVideo(stream, false,
function () {
this.rtc.room.setVideoMute(mute);
self.eventEmitter.emit(eventType, false);
});
}
});
}
}
}
/**
* Stops sending the media track. And removes it from the HTML.
* NOTE: Works for local tracks only.
*/
JitsiLocalTrack.prototype.stop = function () {
this.rtc.room.removeStream(this.stream);
this.stream.stop();
this.detach();
}
/**
* Starts sending the track.
* NOTE: Works for local tracks only.
*/
JitsiLocalTrack.prototype.start = function() {
this.rtc.room.addStream(this.stream, function () {});
}
module.exports = JitsiLocalTrack;

View File

@ -0,0 +1,31 @@
var JitsiTrack = require("./JitsiTrack");
/**
* Represents a single media track (either audio or video).
* @constructor
*/
function JitsiRemoteTrack(RTC, data, sid, ssrc, browser, eventEmitter) {
JitsiTrack.call(this, RTC, data.stream);
this.rtc = RTC;
this.sid = sid;
this.stream = data.stream;
this.peerjid = data.peerjid;
this.videoType = data.videoType;
this.ssrc = ssrc;
this.muted = false;
this.eventEmitter = eventEmitter;
}
JitsiRemoteTrack.prototype = Object.create(JitsiTrack.prototype);
JitsiRemoteTrack.prototype.constructor = JitsiRemoteTrack;
JitsiRemoteTrack.prototype._setMute = function (value) {
this.stream.muted = value;
this.muted = value;
};
delete JitsiRemoteTrack.prototype.stop;
delete JitsiRemoteTrack.prototype.start;
module.exports = JitsiRemoteTrack;

View File

@ -1,10 +1,24 @@
var RTC = require("./RTCUtils");
/**
* Represents a single media track (either audio or video).
* @constructor
*/
function JitsiTrack(stream)
function JitsiTrack(RTC, stream)
{
this.rtc = RTC;
this.stream = stream;
this.type = (this.stream.getVideoTracks().length > 0)?
JitsiTrack.VIDEO : JitsiTrack.AUDIO;
if(this.type == "audio") {
this._getTracks = function () {
return this.stream.getAudioTracks();
}.bind(this);
} else {
this._getTracks = function () {
return this.stream.getVideoTracks();
}.bind(this);
}
}
/**
@ -23,7 +37,7 @@ JitsiTrack.AUDIO = "audio";
* Returns the type (audio or video) of this track.
*/
JitsiTrack.prototype.getType = function() {
return this.stream.type;
return this.type;
};
/**
@ -37,21 +51,21 @@ JitsiTrack.prototype.getParitcipant = function() {
* Returns the RTCMediaStream from the browser (?).
*/
JitsiTrack.prototype.getOriginalStream = function() {
return this.stream.getOriginalStream();
return this.stream;
}
/**
* Mutes the track.
*/
JitsiTrack.prototype.mute = function () {
this.stream.setMute(true);
this._setMute(true);
}
/**
* Unmutes the stream.
*/
JitsiTrack.prototype.unmute = function () {
this.stream.setMute(false);
this._setMute(false);
}
/**
@ -59,7 +73,7 @@ JitsiTrack.prototype.unmute = function () {
* @param container the HTML container
*/
JitsiTrack.prototype.attach = function (container) {
RTC.attachMediaStream(container, this.stream);
}
/**
@ -67,7 +81,7 @@ JitsiTrack.prototype.attach = function (container) {
* @param container the HTML container
*/
JitsiTrack.prototype.detach = function (container) {
$(container).find(">video").remove();
}
/**
@ -76,6 +90,7 @@ JitsiTrack.prototype.detach = function (container) {
*/
JitsiTrack.prototype.stop = function () {
this.detach();
}
@ -84,6 +99,7 @@ JitsiTrack.prototype.stop = function () {
* NOTE: Works for local tracks only.
*/
JitsiTrack.prototype.start = function() {
}
/**
@ -91,6 +107,18 @@ JitsiTrack.prototype.start = function() {
* screen capture as opposed to a camera.
*/
JitsiTrack.prototype.isScreenSharing = function(){
}
/**
* Returns id of the track.
* @returns {string} id of the track or null if this is fake track.
*/
JitsiTrack.prototype.getId = function () {
var tracks = this.stream.getTracks();
if(!tracks || tracks.length === 0)
return null;
return tracks[0].id;
};
module.exports = JitsiTrack;

View File

@ -2,9 +2,9 @@
var EventEmitter = require("events");
var RTCBrowserType = require("./RTCBrowserType");
var RTCUtils = require("./RTCUtils.js");
var LocalStream = require("./LocalStream.js");
var JitsiLocalTrack = require("./JitsiLocalTrack.js");
var DataChannels = require("./DataChannels");
var MediaStream = require("./MediaStream.js");
var JitsiRemoteTrack = require("./JitsiRemoteTrack.js");
var DesktopSharingEventTypes
= require("../../service/desktopsharing/DesktopSharingEventTypes");
var MediaStreamType = require("../../service/RTC/MediaStreamTypes");
@ -40,13 +40,16 @@ function getMediaStreamUsage()
return result;
}
var rtcReady = false;
function RTC(options)
{
function RTC(room, options) {
this.devices = {
audio: true,
video: true
};
this.room = room;
this.localStreams = [];
this.remoteStreams = {};
this.localAudio = null;
@ -59,19 +62,6 @@ function RTC(options)
self.changeLocalVideo(stream, isUsingScreenStream, callback);
}, DesktopSharingEventTypes.NEW_STREAM_CREATED);
// In case of IE we continue from 'onReady' callback
// passed to RTCUtils constructor. It will be invoked by Temasys plugin
// once it is initialized.
var onReady = function () {
self.eventEmitter.emit(RTCEvents.RTC_READY, true);
};
RTCUtils.init(onReady);
// Call onReady() if Temasys plugin is not used
if (!RTCBrowserType.isTemasysPluginUsed()) {
onReady();
}
}
RTC.prototype.obtainAudioAndVideoPermissions = function (options) {
@ -80,15 +70,19 @@ RTC.prototype.obtainAudioAndVideoPermissions = function (options) {
}
RTC.prototype.onIncommingCall = function(event) {
DataChannels.init(event.peerconnection, self.eventEmitter);
if(this.options.config.openSctp)
this.dataChannels = new DataChannels(event.peerconnection, this.eventEmitter);
this.room.addLocalStreams(this.localStreams);
}
RTC.prototype.selectedEndpoint = function (id) {
DataChannels.handleSelectedEndpointEvent(id);
if(this.dataChannels)
this.dataChannels.handleSelectedEndpointEvent(id);
}
RTC.prototype.pinEndpoint = function (id) {
DataChannels.handlePinnedEndpointEvent(id);
if(this.dataChannels)
this.dataChannels.handlePinnedEndpointEvent(id);
}
RTC.prototype.addStreamListener = function (listener, eventType) {
@ -99,17 +93,50 @@ RTC.prototype.addListener = function (type, listener) {
this.eventEmitter.on(type, listener);
};
RTC.prototype.removeListener = function (listener, eventType) {
this.eventEmitter.removeListener(eventType, listener);
};
RTC.prototype.removeStreamListener = function (listener, eventType) {
if(!(eventType instanceof StreamEventTypes))
throw "Illegal argument";
this.removeListener(eventType, listener);
this.eventEmitter.removeListener(eventType, listener);
};
RTC.addRTCReadyListener = function (listener) {
RTCUtils.eventEmitter.on(RTCEvents.RTC_READY, listener);
}
RTC.removeRTCReadyListener = function (listener) {
RTCUtils.eventEmitter.removeListener(RTCEvents.RTC_READY, listener);
}
RTC.isRTCReady = function () {
return rtcReady;
}
RTC.init = function (options) {
// In case of IE we continue from 'onReady' callback
// passed to RTCUtils constructor. It will be invoked by Temasys plugin
// once it is initialized.
var onReady = function () {
rtcReady = true;
RTCUtils.eventEmitter.emit(RTCEvents.RTC_READY, true);
};
RTCUtils.init(onReady, options || {});
// Call onReady() if Temasys plugin is not used
if (!RTCBrowserType.isTemasysPluginUsed()) {
onReady();
}
}
RTC.prototype.createLocalStreams = function (streams, change) {
for (var i = 0; i < streams.length; i++) {
var localStream = new LocalStream(this, streams[i].stream,
streams[i].type, this.eventEmitter, streams[i].videoType,
var localStream = new JitsiLocalTrack(this, streams[i].stream,
this.eventEmitter, streams[i].videoType,
streams[i].isGUMStream);
this.localStreams.push(localStream);
if (streams[i].isMuted === true)
@ -139,9 +166,9 @@ RTC.prototype.removeLocalStream = function (stream) {
};
RTC.prototype.createRemoteStream = function (data, sid, thessrc) {
var remoteStream = new MediaStream(data, sid, thessrc,
var remoteStream = new JitsiRemoteTrack(this, data, sid, thessrc,
RTCBrowserType.getBrowserType(), this.eventEmitter);
if(data.peerjid)
if(!data.peerjid)
return;
var jid = data.peerjid;
if(!this.remoteStreams[jid]) {
@ -223,9 +250,9 @@ RTC.prototype.changeLocalVideo = function (stream, isUsingScreenStream, callback
if(this.localVideo.isMuted() && this.localVideo.videoType !== type) {
localCallback = function() {
APP.xmpp.setVideoMute(false, function(mute) {
self.eventEmitter.emit(RTCEvents.VIDEO_MUTE, mute);
});
this.room.setVideoMute(false, function(mute) {
this.eventEmitter.emit(RTCEvents.VIDEO_MUTE, mute);
}.bind(this));
callback();
};
@ -241,7 +268,7 @@ RTC.prototype.changeLocalVideo = function (stream, isUsingScreenStream, callback
this.switchVideoStreams(videoStream, oldStream);
APP.xmpp.switchStreams(videoStream, oldStream,localCallback);
this.room.switchStreams(videoStream, oldStream,localCallback);
};
RTC.prototype.changeLocalAudio = function (stream, callback) {
@ -250,7 +277,7 @@ RTC.prototype.changeLocalAudio = function (stream, callback) {
this.localAudio = this.createLocalStream(newStream, "audio", true);
// Stop the stream to trigger onended event for old stream
oldStream.stop();
APP.xmpp.switchStreams(newStream, oldStream, callback, true);
this.room.switchStreams(newStream, oldStream, callback, true);
};
RTC.prototype.isVideoMuted = function (jid) {
@ -279,7 +306,7 @@ RTC.prototype.setVideoMute = function (mute, callback, options) {
else
{
this.localVideo.setMute(mute);
APP.xmpp.setVideoMute(
this.room.setVideoMute(
mute,
callback,
options);

View File

@ -3,8 +3,8 @@ var RTCBrowserType = require("./RTCBrowserType");
var Resolutions = require("../../service/RTC/Resolutions");
var AdapterJS = require("./adapter.screenshare");
var SDPUtil = require("../xmpp/SDPUtil");
var EventEmitter = require("events");
var currentResolution = null;
function DummyMediaStream(id) {
this.id = id;
this.label = id;
@ -143,7 +143,8 @@ function getConstraints(um, resolution, bandwidth, fps, desktopStream, isAndroid
//Options parameter is to pass config options. Currently uses only "useIPv6".
var RTCUtils = {
init: function (onTemasysPluginReady) {
eventEmitter: new EventEmitter(),
init: function (onTemasysPluginReady, options) {
var self = this;
if (RTCBrowserType.isFirefox()) {
var FFversion = RTCBrowserType.getFirefoxVersion();
@ -217,7 +218,7 @@ var RTCUtils = {
this.pc_constraints = {'optional': [
{'DtlsSrtpKeyAgreement': 'true'}
]};
if (this.service.options.useIPv6) {
if (options.useIPv6) {
// https://code.google.com/p/webrtc/issues/detail?id=2828
this.pc_constraints.optional.push({googIPv6: true});
}
@ -351,7 +352,7 @@ var RTCUtils = {
return new Promise(function (resolve, reject) {
var successCallback = function (stream) {
resolve(self.successCallback(stream, usageOptions));
resolve(self.successCallback(RTC , stream, usageOptions));
};
if (!devices)
@ -436,13 +437,13 @@ var RTCUtils = {
}.bind(this));
},
successCallback: function (stream, usageOptions) {
successCallback: function (RTC, stream, usageOptions) {
// If this is FF or IE, the stream parameter is *not* a MediaStream object,
// it's an object with two properties: audioStream, videoStream.
if (stream && stream.getAudioTracks && stream.getVideoTracks)
console.log('got', stream, stream.getAudioTracks().length,
stream.getVideoTracks().length);
return this.handleLocalStream(stream, usageOptions);
return this.handleLocalStream(RTC, stream, usageOptions);
},
errorCallback: function (error, resolve, RTC, currentResolution) {
@ -457,7 +458,7 @@ var RTCUtils = {
&& resolution != null) {
self.getUserMediaWithConstraints(RTC, ['audio', 'video'],
function (stream) {
resolve(self.successCallback(stream));
resolve(self.successCallback(RTC, stream));
}, function (error, resolution) {
return self.errorCallback(error, resolve, RTC, resolution);
}, resolution);
@ -467,18 +468,18 @@ var RTCUtils = {
RTC,
['audio'],
function (stream) {
resolve(self.successCallback(stream));
resolve(self.successCallback(RTC, stream));
},
function (error) {
console.error('failed to obtain audio/video stream - stop',
error);
resolve(self.successCallback(null));
resolve(self.successCallback(RTC, null));
}
);
}
},
handleLocalStream: function (stream, usageOptions) {
handleLocalStream: function (service, stream, usageOptions) {
// If this is FF, the stream parameter is *not* a MediaStream object, it's
// an object with two properties: audioStream, videoStream.
if (window.webkitMediaStream) {
@ -517,7 +518,7 @@ var RTCUtils = {
var audioGUM = (!usageOptions || usageOptions.audio !== false),
videoGUM = (!usageOptions || usageOptions.video !== false);
return this.service.createLocalStreams(
return service.createLocalStreams(
[
{stream: audioStream, type: "audio", isMuted: audioMuted, isGUMStream: audioGUM, videoType: null},
{stream: videoStream, type: "video", isMuted: videoMuted, isGUMStream: videoGUM, videoType: "camera"}

577
modules/xmpp/ChatRoom.js Normal file
View File

@ -0,0 +1,577 @@
var XMPPEvents = require("../../service/xmpp/XMPPEvents");
var Moderator = require("./moderator");
var EventEmitter = require("events");
var parser = {
packet2JSON: function (packet, nodes) {
var self = this;
$(packet).children().each(function (index) {
var tagName = $(this).prop("tagName");
var node = {}
node["tagName"] = tagName;
node.attributes = {};
$($(this)[0].attributes).each(function( index, attr ) {
node.attributes[ attr.name ] = attr.value;
} );
var text = Strophe.getText($(this)[0]);
if(text)
node.value = text;
node.children = [];
nodes.push(node);
self.packet2JSON($(this), node.children);
})
},
JSON2packet: function (nodes, packet) {
for(var i = 0; i < nodes.length; i++)
{
var node = nodes[i];
if(!node || node === null){
continue;
}
packet.c(node.tagName, node.attributes);
if(node.value)
packet.t(node.value);
if(node.children)
this.JSON2packet(node.children, packet);
packet.up();
}
packet.up();
}
};
function ChatRoom(connection, jid, password, XMPP, options)
{
this.eventEmitter = new EventEmitter();
this.xmpp = XMPP;
this.connection = connection;
this.roomjid = Strophe.getBareJidFromJid(jid);
this.myroomjid = jid;
this.password = password;
console.info("Joined MUC as " + this.myroomjid);
this.members = {};
this.presMap = {};
this.presHandlers = {};
this.joined = false;
this.role = null;
this.focusMucJid = null;
this.bridgeIsDown = false;
this.options = options || {};
this.moderator = new Moderator(this.roomjid, this.xmpp, this.eventEmitter);
this.initPresenceMap();
this.session = null;
var self = this;
}
ChatRoom.prototype.initPresenceMap = function () {
this.presMap['to'] = this.myroomjid;
this.presMap['xns'] = 'http://jabber.org/protocol/muc';
this.presMap["nodes"] = [];
this.presMap["nodes"].push( {
"tagName": "user-agent",
"value": navigator.userAgent,
"attributes": {xmlns: 'http://jitsi.org/jitmeet/user-agent'}
});
};
ChatRoom.prototype.join = function (password) {
if(password)
this.password = password;
this.moderator.allocateConferenceFocus(function()
{
this.sendPresence();
}.bind(this));
}
ChatRoom.prototype.sendPresence = function () {
if (!this.presMap['to']) {
// Too early to send presence - not initialized
return;
}
var pres = $pres({to: this.presMap['to'] });
pres.c('x', {xmlns: this.presMap['xns']});
if (this.password) {
pres.c('password').t(this.password).up();
}
pres.up();
// Send XEP-0115 'c' stanza that contains our capabilities info
if (this.connection.caps) {
this.connection.caps.node = this.xmpp.options.clientNode;
pres.c('c', this.connection.caps.generateCapsAttrs()).up();
}
parser.JSON2packet(this.presMap.nodes, pres);
this.connection.send(pres);
};
ChatRoom.prototype.doLeave = function () {
console.log("do leave", this.myroomjid);
var pres = $pres({to: this.myroomjid, type: 'unavailable' });
this.presMap.length = 0;
this.connection.send(pres);
};
ChatRoom.prototype.createNonAnonymousRoom = function () {
// http://xmpp.org/extensions/xep-0045.html#createroom-reserved
var getForm = $iq({type: 'get', to: this.roomjid})
.c('query', {xmlns: 'http://jabber.org/protocol/muc#owner'})
.c('x', {xmlns: 'jabber:x:data', type: 'submit'});
var self = this;
this.connection.sendIQ(getForm, function (form) {
if (!$(form).find(
'>query>x[xmlns="jabber:x:data"]' +
'>field[var="muc#roomconfig_whois"]').length) {
console.error('non-anonymous rooms not supported');
return;
}
var formSubmit = $iq({to: this.roomjid, type: 'set'})
.c('query', {xmlns: 'http://jabber.org/protocol/muc#owner'});
formSubmit.c('x', {xmlns: 'jabber:x:data', type: 'submit'});
formSubmit.c('field', {'var': 'FORM_TYPE'})
.c('value')
.t('http://jabber.org/protocol/muc#roomconfig').up().up();
formSubmit.c('field', {'var': 'muc#roomconfig_whois'})
.c('value').t('anyone').up().up();
self.connection.sendIQ(formSubmit);
}, function (error) {
console.error("Error getting room configuration form");
});
};
ChatRoom.prototype.onPresence = function (pres) {
var from = pres.getAttribute('from');
// Parse roles.
var member = {};
member.show = $(pres).find('>show').text();
member.status = $(pres).find('>status').text();
var tmp = $(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>item');
member.affiliation = tmp.attr('affiliation');
member.role = tmp.attr('role');
// Focus recognition
member.jid = tmp.attr('jid');
member.isFocus = false;
if (member.jid
&& member.jid.indexOf(this.moderator.getFocusUserJid() + "/") == 0) {
member.isFocus = true;
}
$(pres).find(">x").remove();
var nodes = [];
parser.packet2JSON(pres, nodes);
for(var i = 0; i < nodes.length; i++)
{
var node = nodes[i];
switch(node.tagName)
{
case "nick":
member.nick = node.value;
if(!member.isFocus) {
var displayName = !this.xmpp.options.displayJids
? member.nick : Strophe.getResourceFromJid(from);
if (displayName && displayName.length > 0) {
this.eventEmitter.emit(XMPPEvents.DISPLAY_NAME_CHANGED, from, displayName);
}
console.info("Display name: " + displayName, pres);
}
break;
case "userId":
member.id = node.value;
break;
case "email":
member.email = node.value;
break;
case "bridgeIsDown":
if(!this.bridgeIsDown) {
this.bridgeIsDown = true;
this.eventEmitter.emit(XMPPEvents.BRIDGE_DOWN);
}
break;
default :
this.processNode(node, from);
}
}
if (from == this.myroomjid) {
if (member.affiliation == 'owner')
if (this.role !== member.role) {
this.role = member.role;
this.eventEmitter.emit(XMPPEvents.LOCAL_ROLE_CHANGED,
member, this.isModerator());
}
if (!this.joined) {
this.joined = true;
this.eventEmitter.emit(XMPPEvents.MUC_JOINED, from, member);
}
} else if (this.members[from] === undefined) {
// new participant
this.members[from] = member;
console.log('entered', from, member);
if (member.isFocus) {
this.focusMucJid = from;
console.info("Ignore focus: " + from + ", real JID: " + member.jid);
}
else {
this.eventEmitter.emit(XMPPEvents.MUC_MEMBER_JOINED, from, member.id || member.email, member.nick);
}
} else {
// Presence update for existing participant
// Watch role change:
if (this.members[from].role != member.role) {
this.members[from].role = member.role;
this.eventEmitter.emit(XMPPEvents.MUC_ROLE_CHANGED,
member.role, member.nick);
}
}
if(!member.isFocus)
this.eventEmitter.emit(XMPPEvents.USER_ID_CHANGED, from, member.id || member.email);
// Trigger status message update
if (member.status) {
this.eventEmitter.emit(XMPPEvents.PRESENCE_STATUS, from, member);
}
};
ChatRoom.prototype.processNode = function (node, from) {
if(this.presHandlers[node.tagName])
this.presHandlers[node.tagName](node, from);
};
ChatRoom.prototype.sendMessage = function (body, nickname) {
var msg = $msg({to: this.roomjid, type: 'groupchat'});
msg.c('body', body).up();
if (nickname) {
msg.c('nick', {xmlns: 'http://jabber.org/protocol/nick'}).t(nickname).up().up();
}
this.connection.send(msg);
this.eventEmitter.emit(XMPPEvents.SENDING_CHAT_MESSAGE, body);
};
ChatRoom.prototype.setSubject = function (subject) {
var msg = $msg({to: this.roomjid, type: 'groupchat'});
msg.c('subject', subject);
this.connection.send(msg);
console.log("topic changed to " + subject);
};
ChatRoom.prototype.onParticipantLeft = function (jid) {
this.eventEmitter.emit(XMPPEvents.MUC_MEMBER_LEFT, jid);
this.moderator.onMucMemberLeft(jid);
};
ChatRoom.prototype.onPresenceUnavailable = function (pres, from) {
// room destroyed ?
if ($(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]' +
'>destroy').length) {
var reason;
var reasonSelect = $(pres).find(
'>x[xmlns="http://jabber.org/protocol/muc#user"]' +
'>destroy>reason');
if (reasonSelect.length) {
reason = reasonSelect.text();
}
this.xmpp.disposeConference(false);
this.eventEmitter.emit(XMPPEvents.MUC_DESTROYED, reason);
delete this.connection.emuc.rooms[Strophe.getBareJidFromJid(jid)];
return true;
}
// Status code 110 indicates that this notification is "self-presence".
if (!$(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>status[code="110"]').length) {
delete this.members[from];
this.onParticipantLeft(from);
}
// If the status code is 110 this means we're leaving and we would like
// to remove everyone else from our view, so we trigger the event.
else if (Object.keys(this.members).length > 1) {
for (var i in this.members) {
var member = this.members[i];
delete this.members[i];
this.onParticipantLeft(member);
}
}
if ($(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>status[code="307"]').length) {
if (this.myroomjid === from) {
this.xmpp.disposeConference(false);
this.eventEmitter.emit(XMPPEvents.KICKED);
}
}
};
ChatRoom.prototype.onMessage = function (msg, from) {
var nick =
$(msg).find('>nick[xmlns="http://jabber.org/protocol/nick"]')
.text() ||
Strophe.getResourceFromJid(from);
var txt = $(msg).find('>body').text();
var type = msg.getAttribute("type");
if (type == "error") {
this.eventEmitter.emit(XMPPEvents.CHAT_ERROR_RECEIVED,
$(msg).find('>text').text(), txt);
return true;
}
var subject = $(msg).find('>subject');
if (subject.length) {
var subjectText = subject.text();
if (subjectText || subjectText == "") {
this.eventEmitter.emit(XMPPEvents.SUBJECT_CHANGED, subjectText);
console.log("Subject is changed to " + subjectText);
}
}
// xep-0203 delay
var stamp = $(msg).find('>delay').attr('stamp');
if (!stamp) {
// or xep-0091 delay, UTC timestamp
stamp = $(msg).find('>[xmlns="jabber:x:delay"]').attr('stamp');
if (stamp) {
// the format is CCYYMMDDThh:mm:ss
var dateParts = stamp.match(/(\d{4})(\d{2})(\d{2}T\d{2}:\d{2}:\d{2})/);
stamp = dateParts[1] + "-" + dateParts[2] + "-" + dateParts[3] + "Z";
}
}
if (txt) {
console.log('chat', nick, txt);
this.eventEmitter.emit(XMPPEvents.MESSAGE_RECEIVED,
from, nick, txt, this.myroomjid, stamp);
}
}
ChatRoom.prototype.onPresenceError = function (pres, from) {
if ($(pres).find('>error[type="auth"]>not-authorized[xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]').length) {
console.log('on password required', from);
this.eventEmitter.emit(XMPPEvents.PASSWORD_REQUIRED);
} else if ($(pres).find(
'>error[type="cancel"]>not-allowed[xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]').length) {
var toDomain = Strophe.getDomainFromJid(pres.getAttribute('to'));
if (toDomain === this.xmpp.options.hosts.anonymousdomain) {
// enter the room by replying with 'not-authorized'. This would
// result in reconnection from authorized domain.
// We're either missing Jicofo/Prosody config for anonymous
// domains or something is wrong.
this.eventEmitter.emit(XMPPEvents.ROOM_JOIN_ERROR, pres);
} else {
console.warn('onPresError ', pres);
this.eventEmitter.emit(XMPPEvents.ROOM_CONNECT_ERROR, pres);
}
} else {
console.warn('onPresError ', pres);
this.eventEmitter.emit(XMPPEvents.ROOM_CONNECT_ERROR, pres);
}
};
ChatRoom.prototype.kick = function (jid) {
var kickIQ = $iq({to: this.roomjid, type: 'set'})
.c('query', {xmlns: 'http://jabber.org/protocol/muc#admin'})
.c('item', {nick: Strophe.getResourceFromJid(jid), role: 'none'})
.c('reason').t('You have been kicked.').up().up().up();
this.connection.sendIQ(
kickIQ,
function (result) {
console.log('Kick participant with jid: ', jid, result);
},
function (error) {
console.log('Kick participant error: ', error);
});
};
ChatRoom.prototype.lockRoom = function (key, onSuccess, onError, onNotSupported) {
//http://xmpp.org/extensions/xep-0045.html#roomconfig
var ob = this;
this.connection.sendIQ($iq({to: this.roomjid, type: 'get'}).c('query', {xmlns: 'http://jabber.org/protocol/muc#owner'}),
function (res) {
if ($(res).find('>query>x[xmlns="jabber:x:data"]>field[var="muc#roomconfig_roomsecret"]').length) {
var formsubmit = $iq({to: ob.roomjid, type: 'set'}).c('query', {xmlns: 'http://jabber.org/protocol/muc#owner'});
formsubmit.c('x', {xmlns: 'jabber:x:data', type: 'submit'});
formsubmit.c('field', {'var': 'FORM_TYPE'}).c('value').t('http://jabber.org/protocol/muc#roomconfig').up().up();
formsubmit.c('field', {'var': 'muc#roomconfig_roomsecret'}).c('value').t(key).up().up();
// Fixes a bug in prosody 0.9.+ https://code.google.com/p/lxmppd/issues/detail?id=373
formsubmit.c('field', {'var': 'muc#roomconfig_whois'}).c('value').t('anyone').up().up();
// FIXME: is muc#roomconfig_passwordprotectedroom required?
ob.connection.sendIQ(formsubmit,
onSuccess,
onError);
} else {
onNotSupported();
}
}, onError);
};
ChatRoom.prototype.addToPresence = function (key, values) {
values.tagName = key;
this.presMap["nodes"].push(values);
};
ChatRoom.prototype.removeFromPresence = function (key) {
for(var i = 0; i < this.presMap.nodes.length; i++)
{
if(key === this.presMap.nodes[i].tagName)
this.presMap.nodes.splice(i, 1);
}
};
ChatRoom.prototype.addPresenceListener = function (name, handler) {
this.presHandlers[name] = handler;
}
ChatRoom.prototype.removePresenceListener = function (name) {
delete this.presHandlers[name];
}
ChatRoom.prototype.isModerator = function (jid) {
return this.role === 'moderator';
};
ChatRoom.prototype.getMemberRole = function (peerJid) {
if (this.members[peerJid]) {
return this.members[peerJid].role;
}
return null;
};
ChatRoom.prototype.setJingleSession = function(session){
this.session = session;
this.session.room = this;
};
ChatRoom.prototype.removeStream = function (stream) {
if(!this.session)
return;
this.session.peerconnection.removeStream(stream)
}
ChatRoom.prototype.switchStreams = function (stream, oldStream, callback, isAudio) {
if(this.session) {
// FIXME: will block switchInProgress on true value in case of exception
this.session.switchStreams(stream, oldStream, callback, isAudio);
} else {
// We are done immediately
console.warn("No conference handler or conference not started yet");
callback();
}
};
ChatRoom.prototype.addStream = function (stream, callback) {
if(this.session) {
// FIXME: will block switchInProgress on true value in case of exception
this.session.addStream(stream, callback);
} else {
// We are done immediately
console.warn("No conference handler or conference not started yet");
callback();
}
}
ChatRoom.prototype.setVideoMute = function (mute, callback, options) {
var self = this;
var localCallback = function (mute) {
self.sendVideoInfoPresence(mute);
return callback(mute);
};
if(this.session)
{
this.session.setVideoMute(
mute, localCallback, options);
}
else {
localCallback(mute);
}
};
ChatRoom.prototype.setAudioMute = function (mute, callback) {
//This will be for remote streams only
// if (this.forceMuted && !mute) {
// console.info("Asking focus for unmute");
// this.connection.moderate.setMute(this.connection.emuc.myroomjid, mute);
// // FIXME: wait for result before resetting muted status
// this.forceMuted = false;
// }
return this.sendAudioInfoPresence(mute, callback);;
};
ChatRoom.prototype.addAudioInfoToPresence = function (mute) {
this.addToPresence("audiomuted",
{attributes:
{"audions": "http://jitsi.org/jitmeet/audio"},
value: mute.toString()});
}
ChatRoom.prototype.sendAudioInfoPresence = function(mute, callback) {
this.addAudioInfoToPresence(mute);
if(this.connection) {
this.sendPresence();
}
callback();
};
ChatRoom.prototype.addVideoInfoToPresence = function (mute) {
this.addToPresence("videomuted",
{attributes:
{"videons": "http://jitsi.org/jitmeet/video"},
value: mute.toString()});
}
ChatRoom.prototype.sendVideoInfoPresence = function (mute) {
this.addVideoInfoToPresence(mute);
if(!this.connection)
return;
this.sendPresence();
};
ChatRoom.prototype.addListener = function(type, listener) {
this.eventEmitter.on(type, listener);
};
ChatRoom.prototype.removeListener = function (type, listener) {
this.eventEmitter.removeListener(type, listener);
};
ChatRoom.prototype.fireRemoteStreamEvent = function(data, sid, thessrc) {
this.eventEmitter.emit(XMPPEvents.REMOTE_STREAM_RECEIVED, data, sid, thessrc);
}
ChatRoom.prototype.addLocalStreams = function (localStreams) {
this.session.addLocalStreams(localStreams);
}
module.exports = ChatRoom;

View File

@ -46,6 +46,9 @@ function JingleSession(me, sid, connection, service, eventEmitter) {
// ICE servers config (RTCConfiguration?).
this.ice_config = {};
// The chat room instance associated with the session.
this.room = null;
}
/**

View File

@ -70,6 +70,7 @@ JingleSessionPC.prototype.setAnswer = function(answer) {
JingleSessionPC.prototype.updateModifySourcesQueue = function() {
var signalingState = this.peerconnection.signalingState;
var iceConnectionState = this.peerconnection.iceConnectionState;
console.debug(signalingState + " + " + iceConnectionState);
if (signalingState === 'stable' && iceConnectionState === 'connected') {
this.modifySourcesQueue.resume();
} else {
@ -108,10 +109,13 @@ JingleSessionPC.prototype.doInitialize = function () {
this.peerconnection.onremovestream = function (event) {
// Remove the stream from remoteStreams
// FIXME: remotestreamremoved.jingle not defined anywhere(unused)
$(document).trigger('remotestreamremoved.jingle', [event, self.sid]);
};
this.peerconnection.onsignalingstatechange = function (event) {
console.debug("signaling state11");
if (!(self && self.peerconnection)) return;
console.debug("signaling state222");
self.updateModifySourcesQueue();
};
/**
@ -122,7 +126,9 @@ JingleSessionPC.prototype.doInitialize = function () {
* @param event the event containing information about the change
*/
this.peerconnection.oniceconnectionstatechange = function (event) {
console.debug("ice state11");
if (!(self && self.peerconnection)) return;
console.debug("ice state222");
self.updateModifySourcesQueue();
switch (self.peerconnection.iceConnectionState) {
case 'connected':
@ -1100,6 +1106,40 @@ JingleSessionPC.prototype.switchStreams = function (new_stream, oldStream, succe
});
};
/**
* Adds streams.
* @param stream new stream that will be added.
* @param success_callback callback executed after successful stream addition.
*/
JingleSessionPC.prototype.addStream = function (stream, callback) {
// Remember SDP to figure out added/removed SSRCs
var oldSdp = null;
if(this.peerconnection) {
if(this.peerconnection.localDescription) {
oldSdp = new SDP(this.peerconnection.localDescription.sdp);
}
if(stream)
this.peerconnection.addStream(stream);
}
// Conference is not active
if(!oldSdp || !this.peerconnection) {
callback();
return;
}
self.modifySourcesQueue.push(function() {
console.log('modify sources done');
callback();
var newSdp = new SDP(this.peerconnection.localDescription.sdp);
console.log("SDPs", oldSdp, newSdp);
this.notifyMySSRCUpdate(oldSdp, newSdp);
});
}
/**
* Figures out added/removed ssrcs and send update IQs.
* @param old_sdp SDP object for old description.
@ -1430,7 +1470,7 @@ JingleSessionPC.prototype.remoteStreamAdded = function (data, times) {
}
}
RTC.createRemoteStream(data, this.sid, thessrc);
this.room.fireRemoteStreamEvent(data, this.sid, thessrc);
var isVideo = data.stream.getVideoTracks().length > 0;
// an attempt to work around https://github.com/jitsi/jitmeet/issues/32

View File

@ -5,6 +5,7 @@ var SSRCReplacement = require("./LocalSSRCReplacement");
function TraceablePeerConnection(ice_config, constraints, session) {
var self = this;
this.session = session;
var RTCPeerConnectionType = null;
if (RTCBrowserType.isFirefox()) {
RTCPeerConnectionType = mozRTCPeerConnection;
@ -374,7 +375,7 @@ TraceablePeerConnection.prototype.createOffer
offer = SSRCReplacement.mungeLocalVideoSSRC(offer);
if (config.enableSimulcast && self.simulcast.isSupported()) {
if (self.session.room.options.enableSimulcast && self.simulcast.isSupported()) {
offer = self.simulcast.mungeLocalDescription(offer);
self.trace('createOfferOnSuccess::postTransform (simulcast)', dumpSDP(offer));
}
@ -404,7 +405,7 @@ TraceablePeerConnection.prototype.createAnswer
// munge local video SSRC
answer = SSRCReplacement.mungeLocalVideoSSRC(answer);
if (config.enableSimulcast && self.simulcast.isSupported()) {
if (self.session.room.options.enableSimulcast && self.simulcast.isSupported()) {
answer = self.simulcast.mungeLocalDescription(answer);
self.trace('createAnswerOnSuccess::postTransform (simulcast)', dumpSDP(answer));
}

View File

@ -2,496 +2,7 @@
/* a simple MUC connection plugin
* can only handle a single MUC room
*/
var XMPPEvents = require("../../service/xmpp/XMPPEvents");
var Moderator = require("./moderator");
var RTC = require("../RTC/RTC");
var EventEmitter = require("events");
var parser = {
packet2JSON: function (packet, nodes) {
var self = this;
$(packet).children().each(function (index) {
var tagName = $(this).prop("tagName");
var node = {}
node["tagName"] = tagName;
node.attributes = {};
$($(this)[0].attributes).each(function( index, attr ) {
node.attributes[ attr.name ] = attr.value;
} );
var text = Strophe.getText($(this)[0]);
if(text)
node.value = text;
node.children = [];
nodes.push(node);
self.packet2JSON($(this), node.children);
})
},
JSON2packet: function (nodes, packet) {
for(var i = 0; i < nodes.length; i++)
{
var node = nodes[i];
if(!node || node === null){
continue;
}
packet.c(node.tagName, node.attributes);
if(node.value)
packet.t(node.value);
if(node.children)
this.JSON2packet(node.children, packet);
packet.up();
}
packet.up();
}
};
function ChatRoom(connection, jid, password, XMPP, eventEmitter)
{
this.eventEmitter = eventEmitter;
this.roomEmitter = new EventEmitter();
this.xmpp = XMPP;
this.connection = connection;
this.roomjid = Strophe.getBareJidFromJid(jid);
this.myroomjid = jid;
this.password = password;
console.info("Joined MUC as " + this.myroomjid);
this.members = {};
this.presMap = {};
this.presHandlers = {};
this.joined = false;
this.role = null;
this.focusMucJid = null;
this.bridgeIsDown = false;
this.moderator = new Moderator(this.roomjid, this.xmpp, eventEmitter);
this.initPresenceMap();
this.readyToJoin = false;
this.joinRequested = false;
var self = this;
this.moderator.allocateConferenceFocus(function()
{
self.readyToJoin = true;
if(self.joinRequested)
{
self.join();
}
});
}
ChatRoom.prototype.initPresenceMap = function () {
this.presMap['to'] = this.myroomjid;
this.presMap['xns'] = 'http://jabber.org/protocol/muc';
this.presMap["nodes"] = [];
if (RTC.localAudio && RTC.localAudio.isMuted()) {
this.nodes.push({
tagName: "audiomuted",
attributes: {xmlns: "http://jitsi.org/jitmeet/audio"},
value: "true"});
}
if (RTC.localVideo && RTC.localVideo.isMuted()) {
this.nodes.push({
tagName: "videomuted",
attributes: {xmlns: "http://jitsi.org/jitmeet/video"},
value: "true"});
}
this.presMap["nodes"].push( {
"tagName": "user-agent",
"value": navigator.userAgent,
"attributes": {xmlns: 'http://jitsi.org/jitmeet/user-agent'}
});
};
ChatRoom.prototype.join = function (password) {
if(password)
this.password = password;
if(!this.readyToJoin)
{
this.joinRequested = true;
return;
}
this.joinRequested = false;
this.sendPresence();
}
ChatRoom.prototype.sendPresence = function () {
if (!this.presMap['to']) {
// Too early to send presence - not initialized
return;
}
var pres = $pres({to: this.presMap['to'] });
pres.c('x', {xmlns: this.presMap['xns']});
if (this.password) {
pres.c('password').t(this.password).up();
}
pres.up();
// Send XEP-0115 'c' stanza that contains our capabilities info
if (this.connection.caps) {
this.connection.caps.node = this.xmpp.options.clientNode;
pres.c('c', this.connection.caps.generateCapsAttrs()).up();
}
parser.JSON2packet(this.presMap.nodes, pres);
this.connection.send(pres);
};
ChatRoom.prototype.doLeave = function () {
console.log("do leave", this.myroomjid);
var pres = $pres({to: this.myroomjid, type: 'unavailable' });
this.presMap.length = 0;
this.connection.send(pres);
};
ChatRoom.prototype.createNonAnonymousRoom = function () {
// http://xmpp.org/extensions/xep-0045.html#createroom-reserved
var getForm = $iq({type: 'get', to: this.roomjid})
.c('query', {xmlns: 'http://jabber.org/protocol/muc#owner'})
.c('x', {xmlns: 'jabber:x:data', type: 'submit'});
var self = this;
this.connection.sendIQ(getForm, function (form) {
if (!$(form).find(
'>query>x[xmlns="jabber:x:data"]' +
'>field[var="muc#roomconfig_whois"]').length) {
console.error('non-anonymous rooms not supported');
return;
}
var formSubmit = $iq({to: this.roomjid, type: 'set'})
.c('query', {xmlns: 'http://jabber.org/protocol/muc#owner'});
formSubmit.c('x', {xmlns: 'jabber:x:data', type: 'submit'});
formSubmit.c('field', {'var': 'FORM_TYPE'})
.c('value')
.t('http://jabber.org/protocol/muc#roomconfig').up().up();
formSubmit.c('field', {'var': 'muc#roomconfig_whois'})
.c('value').t('anyone').up().up();
self.connection.sendIQ(formSubmit);
}, function (error) {
console.error("Error getting room configuration form");
});
};
ChatRoom.prototype.onPresence = function (pres) {
var from = pres.getAttribute('from');
// Parse roles.
var member = {};
member.show = $(pres).find('>show').text();
member.status = $(pres).find('>status').text();
var tmp = $(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>item');
member.affiliation = tmp.attr('affiliation');
member.role = tmp.attr('role');
// Focus recognition
member.jid = tmp.attr('jid');
member.isFocus = false;
if (member.jid
&& member.jid.indexOf(this.moderator.getFocusUserJid() + "/") == 0) {
member.isFocus = true;
}
$(pres).find(">x").remove();
var nodes = [];
parser.packet2JSON(pres, nodes);
for(var i = 0; i < nodes.length; i++)
{
var node = nodes[i];
switch(node.tagName)
{
case "nick":
member.nick = node.value;
if(!member.isFocus) {
var displayName = !this.xmpp.options.displayJids
? member.nick : Strophe.getResourceFromJid(from);
if (displayName && displayName.length > 0) {
this.eventEmitter.emit(XMPPEvents.DISPLAY_NAME_CHANGED, from, displayName);
}
console.info("Display name: " + displayName, pres);
}
break;
case "userId":
member.id = node.value;
break;
case "email":
member.email = node.value;
break;
case "bridgeIsDown":
if(!this.bridgeIsDown) {
this.bridgeIsDown = true;
this.eventEmitter.emit(XMPPEvents.BRIDGE_DOWN);
}
break;
default :
this.processNode(node, from);
}
}
if (from == this.myroomjid) {
if (member.affiliation == 'owner')
if (this.role !== member.role) {
this.role = member.role;
this.eventEmitter.emit(XMPPEvents.LOCAL_ROLE_CHANGED,
member, this.isModerator());
}
if (!this.joined) {
this.joined = true;
this.eventEmitter.emit(XMPPEvents.MUC_JOINED, from, member);
}
} else if (this.members[from] === undefined) {
// new participant
this.members[from] = member;
console.log('entered', from, member);
if (member.isFocus) {
this.focusMucJid = from;
console.info("Ignore focus: " + from + ", real JID: " + member.jid);
}
else {
this.eventEmitter.emit(XMPPEvents.MUC_MEMBER_JOINED, from, member.id || member.email, member.nick);
}
} else {
// Presence update for existing participant
// Watch role change:
if (this.members[from].role != member.role) {
this.members[from].role = member.role;
this.eventEmitter.emit(XMPPEvents.MUC_ROLE_CHANGED,
member.role, member.nick);
}
}
if(!member.isFocus)
this.eventEmitter.emit(XMPPEvents.USER_ID_CHANGED, from, member.id || member.email);
// Trigger status message update
if (member.status) {
this.eventEmitter.emit(XMPPEvents.PRESENCE_STATUS, from, member);
}
};
ChatRoom.prototype.processNode = function (node, from) {
if(this.presHandlers[node.tagName])
this.presHandlers[node.tagName](node, from);
};
ChatRoom.prototype.sendMessage = function (body, nickname) {
var msg = $msg({to: this.roomjid, type: 'groupchat'});
msg.c('body', body).up();
if (nickname) {
msg.c('nick', {xmlns: 'http://jabber.org/protocol/nick'}).t(nickname).up().up();
}
this.connection.send(msg);
this.eventEmitter.emit(XMPPEvents.SENDING_CHAT_MESSAGE, body);
};
ChatRoom.prototype.setSubject = function (subject) {
var msg = $msg({to: this.roomjid, type: 'groupchat'});
msg.c('subject', subject);
this.connection.send(msg);
console.log("topic changed to " + subject);
};
ChatRoom.prototype.onParticipantLeft = function (jid) {
this.eventEmitter.emit(XMPPEvents.MUC_MEMBER_LEFT, jid);
this.moderator.onMucMemberLeft(jid);
};
ChatRoom.prototype.onPresenceUnavailable = function (pres, from) {
// room destroyed ?
if ($(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]' +
'>destroy').length) {
var reason;
var reasonSelect = $(pres).find(
'>x[xmlns="http://jabber.org/protocol/muc#user"]' +
'>destroy>reason');
if (reasonSelect.length) {
reason = reasonSelect.text();
}
this.xmpp.disposeConference(false);
this.eventEmitter.emit(XMPPEvents.MUC_DESTROYED, reason);
delete this.connection.emuc.rooms[Strophe.getBareJidFromJid(jid)];
return true;
}
// Status code 110 indicates that this notification is "self-presence".
if (!$(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>status[code="110"]').length) {
delete this.members[from];
this.onParticipantLeft(from);
}
// If the status code is 110 this means we're leaving and we would like
// to remove everyone else from our view, so we trigger the event.
else if (Object.keys(this.members).length > 1) {
for (var i in this.members) {
var member = this.members[i];
delete this.members[i];
this.onParticipantLeft(member);
}
}
if ($(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>status[code="307"]').length) {
if (this.myroomjid === from) {
this.xmpp.disposeConference(false);
this.eventEmitter.emit(XMPPEvents.KICKED);
}
}
};
ChatRoom.prototype.onMessage = function (msg, from) {
var nick =
$(msg).find('>nick[xmlns="http://jabber.org/protocol/nick"]')
.text() ||
Strophe.getResourceFromJid(from);
var txt = $(msg).find('>body').text();
var type = msg.getAttribute("type");
if (type == "error") {
this.eventEmitter.emit(XMPPEvents.CHAT_ERROR_RECEIVED,
$(msg).find('>text').text(), txt);
return true;
}
var subject = $(msg).find('>subject');
if (subject.length) {
var subjectText = subject.text();
if (subjectText || subjectText == "") {
this.eventEmitter.emit(XMPPEvents.SUBJECT_CHANGED, subjectText);
console.log("Subject is changed to " + subjectText);
}
}
// xep-0203 delay
var stamp = $(msg).find('>delay').attr('stamp');
if (!stamp) {
// or xep-0091 delay, UTC timestamp
stamp = $(msg).find('>[xmlns="jabber:x:delay"]').attr('stamp');
if (stamp) {
// the format is CCYYMMDDThh:mm:ss
var dateParts = stamp.match(/(\d{4})(\d{2})(\d{2}T\d{2}:\d{2}:\d{2})/);
stamp = dateParts[1] + "-" + dateParts[2] + "-" + dateParts[3] + "Z";
}
}
if (txt) {
console.log('chat', nick, txt);
this.eventEmitter.emit(XMPPEvents.MESSAGE_RECEIVED,
from, nick, txt, this.myroomjid, stamp);
}
}
ChatRoom.prototype.onPresenceError = function (pres, from) {
if ($(pres).find('>error[type="auth"]>not-authorized[xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]').length) {
console.log('on password required', from);
this.eventEmitter.emit(XMPPEvents.PASSWORD_REQUIRED);
} else if ($(pres).find(
'>error[type="cancel"]>not-allowed[xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]').length) {
var toDomain = Strophe.getDomainFromJid(pres.getAttribute('to'));
if (toDomain === this.xmpp.options.hosts.anonymousdomain) {
// enter the room by replying with 'not-authorized'. This would
// result in reconnection from authorized domain.
// We're either missing Jicofo/Prosody config for anonymous
// domains or something is wrong.
this.eventEmitter.emit(XMPPEvents.ROOM_JOIN_ERROR, pres);
} else {
console.warn('onPresError ', pres);
this.eventEmitter.emit(XMPPEvents.ROOM_CONNECT_ERROR, pres);
}
} else {
console.warn('onPresError ', pres);
this.eventEmitter.emit(XMPPEvents.ROOM_CONNECT_ERROR, pres);
}
};
ChatRoom.prototype.kick = function (jid) {
var kickIQ = $iq({to: this.roomjid, type: 'set'})
.c('query', {xmlns: 'http://jabber.org/protocol/muc#admin'})
.c('item', {nick: Strophe.getResourceFromJid(jid), role: 'none'})
.c('reason').t('You have been kicked.').up().up().up();
this.connection.sendIQ(
kickIQ,
function (result) {
console.log('Kick participant with jid: ', jid, result);
},
function (error) {
console.log('Kick participant error: ', error);
});
};
ChatRoom.prototype.lockRoom = function (key, onSuccess, onError, onNotSupported) {
//http://xmpp.org/extensions/xep-0045.html#roomconfig
var ob = this;
this.connection.sendIQ($iq({to: this.roomjid, type: 'get'}).c('query', {xmlns: 'http://jabber.org/protocol/muc#owner'}),
function (res) {
if ($(res).find('>query>x[xmlns="jabber:x:data"]>field[var="muc#roomconfig_roomsecret"]').length) {
var formsubmit = $iq({to: ob.roomjid, type: 'set'}).c('query', {xmlns: 'http://jabber.org/protocol/muc#owner'});
formsubmit.c('x', {xmlns: 'jabber:x:data', type: 'submit'});
formsubmit.c('field', {'var': 'FORM_TYPE'}).c('value').t('http://jabber.org/protocol/muc#roomconfig').up().up();
formsubmit.c('field', {'var': 'muc#roomconfig_roomsecret'}).c('value').t(key).up().up();
// Fixes a bug in prosody 0.9.+ https://code.google.com/p/lxmppd/issues/detail?id=373
formsubmit.c('field', {'var': 'muc#roomconfig_whois'}).c('value').t('anyone').up().up();
// FIXME: is muc#roomconfig_passwordprotectedroom required?
ob.connection.sendIQ(formsubmit,
onSuccess,
onError);
} else {
onNotSupported();
}
}, onError);
};
ChatRoom.prototype.addToPresence = function (key, values) {
values.tagName = key;
this.presMap["nodes"].push(values);
};
ChatRoom.prototype.removeFromPresence = function (key) {
for(var i = 0; i < this.presMap.nodes.length; i++)
{
if(key === this.presMap.nodes[i].tagName)
this.presMap.nodes.splice(i, 1);
}
};
ChatRoom.prototype.addPresenceListener = function (name, handler) {
this.presHandlers[name] = handler;
}
ChatRoom.prototype.removePresenceListener = function (name) {
delete this.presHandlers[name];
}
ChatRoom.prototype.isModerator = function (jid) {
return this.role === 'moderator';
};
ChatRoom.prototype.getMemberRole = function (peerJid) {
if (this.members[peerJid]) {
return this.members[peerJid].role;
}
return null;
};
var ChatRoom = require("./ChatRoom");
module.exports = function(XMPP) {
Strophe.addConnectionPlugin('emuc', {
@ -505,13 +16,13 @@ module.exports = function(XMPP) {
this.connection.addHandler(this.onPresenceError.bind(this), null, 'presence', 'error', null);
this.connection.addHandler(this.onMessage.bind(this), null, 'message', null, null);
},
createRoom: function (jid, password, eventEmitter) {
createRoom: function (jid, password, options) {
var roomJid = Strophe.getBareJidFromJid(jid);
if (this.rooms[roomJid]) {
console.error("You are already in the room!");
return;
}
this.rooms[roomJid] = new ChatRoom(this.connection, jid, password, XMPP, eventEmitter);
this.rooms[roomJid] = new ChatRoom(this.connection, jid, password, XMPP, options);
return this.rooms[roomJid];
},
doLeave: function (jid) {
@ -566,6 +77,14 @@ module.exports = function(XMPP) {
room.onMessage(msg, from);
return true;
},
setJingleSession: function (from, session) {
var room = this.rooms[Strophe.getBareJidFromJid(from)];
if(!room)
return;
room.setJingleSession(session);
}
});
};

View File

@ -104,10 +104,14 @@ module.exports = function(XMPP, eventEmitter) {
this.connection, XMPP, eventEmitter);
// configure session
var fromBareJid = Strophe.getBareJidFromJid(fromJid);
this.connection.emuc.setJingleSession(fromBareJid, sess);
sess.media_constraints = this.media_constraints;
sess.ice_config = this.ice_config;
sess.initialize(Strophe.getBareJidFromJid(fromJid), false);
sess.initialize(fromJid, false);
eventEmitter.emit(XMPPEvents.CALL_INCOMING, sess);
// FIXME: setRemoteDescription should only be done when this call is to be accepted
sess.setOffer($(iq).find('>jingle'));
@ -118,8 +122,6 @@ module.exports = function(XMPP, eventEmitter) {
// .sendAnswer and .accept
// or .sendTerminate -- not necessarily synchronous
eventEmitter.emit(XMPPEvents.CALL_INCOMING, sess);
sess.sendAnswer();
sess.accept();
break;

View File

@ -1,5 +1,4 @@
/* global $, APP, config, Strophe*/
var Moderator = require("./moderator");
var EventEmitter = require("events");
var Pako = require("pako");
var StreamEventTypes = require("../../service/RTC/StreamEventTypes");
@ -165,7 +164,7 @@ XMPP.prototype.connect = function (jid, password) {
return this._connect(jid, password);
};
XMPP.prototype.createRoom = function (roomName, useNicks, nick) {
XMPP.prototype.createRoom = function (roomName, options, useNicks, nick) {
var roomjid = roomName + '@' + this.options.hosts.muc;
if (useNicks) {
@ -183,7 +182,7 @@ XMPP.prototype.createRoom = function (roomName, useNicks, nick) {
roomjid += '/' + tmpJid;
}
return this.connection.emuc.createRoom(roomjid, null, this.eventEmitter);
return this.connection.emuc.createRoom(roomjid, null, options);
}
XMPP.prototype.addListener = function(type, listener) {
@ -194,6 +193,7 @@ XMPP.prototype.removeListener = function (type, listener) {
this.eventEmitter.removeListener(type, listener);
};
//FIXME: this should work with the room
XMPP.prototype.leaveRoom = function (jid) {
var handler = this.connection.jingle.jid2session[jid];
if (handler && handler.peerconnection) {
@ -213,80 +213,6 @@ XMPP.prototype.leaveRoom = function (jid) {
this.connection.emuc.doLeave(jid);
};
XMPP.prototype.isConferenceInProgress = function () {
return this.connection && this.connection.jingle.activecall &&
this.connection.jingle.activecall.peerconnection;
};
XMPP.prototype.switchStreams = function (stream, oldStream, callback, isAudio) {
if (this.isConferenceInProgress()) {
// FIXME: will block switchInProgress on true value in case of exception
this.connection.jingle.activecall.switchStreams(stream, oldStream, callback, isAudio);
} else {
// We are done immediately
console.warn("No conference handler or conference not started yet");
callback();
}
};
XMPP.prototype.sendVideoInfoPresence = function (mute) {
if(!this.connection)
return;
this.connection.emuc.addVideoInfoToPresence(mute);
this.connection.emuc.sendPresence();
};
XMPP.prototype.setVideoMute = function (mute, callback, options) {
if(!this.connection)
return;
var self = this;
var localCallback = function (mute) {
self.sendVideoInfoPresence(mute);
return callback(mute);
};
if(this.connection.jingle.activecall)
{
this.connection.jingle.activecall.setVideoMute(
mute, localCallback, options);
}
else {
localCallback(mute);
}
};
XMPP.prototype.setAudioMute = function (mute, callback) {
if (!(this.connection && RTC.localAudio)) {
return false;
}
if (this.forceMuted && !mute) {
console.info("Asking focus for unmute");
this.connection.moderate.setMute(this.connection.emuc.myroomjid, mute);
// FIXME: wait for result before resetting muted status
this.forceMuted = false;
}
if (mute == RTC.localAudio.isMuted()) {
// Nothing to do
return true;
}
RTC.localAudio.setMute(mute);
this.sendAudioInfoPresence(mute, callback);
return true;
};
XMPP.prototype.sendAudioInfoPresence = function(mute, callback) {
if(this.connection) {
this.connection.emuc.addAudioInfoToPresence(mute);
this.connection.emuc.sendPresence();
}
callback();
return true;
};
/**
* Sends 'data' as a log message to the focus. Returns true iff a message
* was sent.
@ -351,11 +277,6 @@ XMPP.prototype.getSessions = function () {
return this.connection.jingle.sessions;
};
XMPP.prototype.removeStream = function (stream) {
if (!this.isConferenceInProgress())
return;
this.connection.jingle.activecall.peerconnection.removeStream(stream);
};
XMPP.prototype.disconnect = function () {
if (this.disconnectInProgress || !this.connection || !this.connection.connected)

View File

@ -20,7 +20,7 @@
"pako": "*",
"i18next-client": "1.7.7",
"sdp-interop": "0.1.4",
"sdp-transform": "1.4.0",
"sdp-transform": "1.4.1",
"sdp-simulcast": "0.1.0",
"async": "0.9.0",
"retry": "0.6.1",

View File

@ -50,6 +50,7 @@ var XMPPEvents = {
ROOM_CONNECT_ERROR: 'xmpp.room_connect_error',
// xmpp is connected and obtained user media
READY_TO_JOIN: 'xmpp.ready_to_join',
FOCUS_LEFT: "xmpp.focus_left"
FOCUS_LEFT: "xmpp.focus_left",
REMOTE_STREAM_RECEIVED: "xmpp.remote_stream_received"
};
module.exports = XMPPEvents;