Merge pull request #441 from isymchych/jitsi-meet-new
refactoring of Etherpad and Prezi
This commit is contained in:
commit
9b7ddec703
191
app.js
191
app.js
|
@ -17,30 +17,32 @@ import URLProcessor from "./modules/config/URLProcessor";
|
|||
import RoomnameGenerator from './modules/util/RoomnameGenerator';
|
||||
import CQEvents from './service/connectionquality/CQEvents';
|
||||
import UIEvents from './service/UI/UIEvents';
|
||||
import DSEvents from './service/desktopsharing/DesktopSharingEventTypes';
|
||||
|
||||
import UI from "./modules/UI/UI";
|
||||
import statistics from "./modules/statistics/statistics";
|
||||
import settings from "./modules/settings/Settings";
|
||||
|
||||
import {openConnection} from './modules/connection';
|
||||
import AuthHandler from './modules/AuthHandler';
|
||||
|
||||
import createRoomLocker from './modules/RoomLocker';
|
||||
|
||||
const DesktopSharingEventTypes =
|
||||
require("./service/desktopsharing/DesktopSharingEventTypes");
|
||||
|
||||
const ConnectionEvents = JitsiMeetJS.events.connection;
|
||||
const ConnectionErrors = JitsiMeetJS.errors.connection;
|
||||
|
||||
const ConferenceEvents = JitsiMeetJS.events.conference;
|
||||
const ConferenceErrors = JitsiMeetJS.errors.conference;
|
||||
|
||||
const TrackEvents = JitsiMeetJS.events.track;
|
||||
const TrackErrors = JitsiMeetJS.errors.track;
|
||||
|
||||
let localVideo, localAudio;
|
||||
|
||||
const Commands = {
|
||||
CONNECTION_QUALITY: "connectionQuality",
|
||||
EMAIL: "email",
|
||||
VIDEO_TYPE: "videoType"
|
||||
VIDEO_TYPE: "videoType",
|
||||
ETHERPAD: "etherpad",
|
||||
PREZI: "prezi",
|
||||
STOP_PREZI: "stop-prezi"
|
||||
};
|
||||
|
||||
function buildRoomName () {
|
||||
|
@ -77,6 +79,22 @@ function buildRoomName () {
|
|||
|
||||
|
||||
const APP = {
|
||||
UI,
|
||||
statistics,
|
||||
settings,
|
||||
|
||||
createLocalTracks (...devices) {
|
||||
return JitsiMeetJS.createLocalTracks({
|
||||
// copy array to avoid mutations inside library
|
||||
devices: devices.slice(0),
|
||||
resolution: config.resolution
|
||||
}).catch(function (err) {
|
||||
console.error('failed to create local tracks', ...devices, err);
|
||||
APP.statistics.onGetUserMediaFailed(err);
|
||||
return [];
|
||||
});
|
||||
},
|
||||
|
||||
init () {
|
||||
let roomName = buildRoomName();
|
||||
this.conference = {
|
||||
|
@ -91,29 +109,28 @@ const APP = {
|
|||
},
|
||||
muteAudio (mute) {
|
||||
APP.UI.eventEmitter.emit(UIEvents.AUDIO_MUTED, mute);
|
||||
APP.statistics.onAudioMute(mute);
|
||||
},
|
||||
toggleAudioMuted () {
|
||||
this.muteAudio(!this.audioMuted);
|
||||
},
|
||||
muteVideo (mute) {
|
||||
APP.UI.eventEmitter.emit(UIEvents.VIDEO_MUTED, mute);
|
||||
APP.statistics.onVideoMute(mute);
|
||||
},
|
||||
toggleVideoMuted () {
|
||||
this.muteVideo(!this.videoMuted);
|
||||
}
|
||||
};
|
||||
|
||||
this.UI = require("./modules/UI/UI");
|
||||
this.API = require("./modules/API/API");
|
||||
this.connectionquality =
|
||||
require("./modules/connectionquality/connectionquality");
|
||||
this.statistics = require("./modules/statistics/statistics");
|
||||
this.desktopsharing =
|
||||
require("./modules/desktopsharing/desktopsharing");
|
||||
this.keyboardshortcut =
|
||||
require("./modules/keyboardshortcut/keyboardshortcut");
|
||||
this.translation = require("./modules/translation/translation");
|
||||
this.settings = require("./modules/settings/Settings");
|
||||
this.configFetch = require("./modules/config/HttpConfigFetch");
|
||||
}
|
||||
};
|
||||
|
@ -123,11 +140,14 @@ function initConference(localTracks, connection) {
|
|||
openSctp: config.openSctp,
|
||||
disableAudioLevels: config.disableAudioLevels
|
||||
});
|
||||
APP.conference._room = room; // FIXME do not use this
|
||||
|
||||
const addTrack = (track) => {
|
||||
room.addTrack(track);
|
||||
if(track.getType() === "audio")
|
||||
if (track.isAudioTrack()) {
|
||||
return;
|
||||
}
|
||||
|
||||
room.removeCommand(Commands.VIDEO_TYPE);
|
||||
room.sendCommand(Commands.VIDEO_TYPE, {
|
||||
value: track.videoType,
|
||||
|
@ -150,40 +170,6 @@ function initConference(localTracks, connection) {
|
|||
APP.conference.listMembersIds = function () {
|
||||
return room.getParticipants().map(p => p.getId());
|
||||
};
|
||||
/**
|
||||
* Creates video track (desktop or camera).
|
||||
* @param type "camera" or "video"
|
||||
* @param endedHandler onended function
|
||||
* @returns Promise
|
||||
*/
|
||||
APP.conference.createVideoTrack = (type, endedHandler) => {
|
||||
return JitsiMeetJS.createLocalTracks({
|
||||
devices: [type], resolution: config.resolution
|
||||
}).then((tracks) => {
|
||||
tracks[0].on(TrackEvents.TRACK_STOPPED, endedHandler);
|
||||
return tracks;
|
||||
});
|
||||
};
|
||||
|
||||
APP.conference.changeLocalVideo = (track, callback) => {
|
||||
const localCallback = (newTrack) => {
|
||||
if (newTrack.isLocal() && newTrack === localVideo) {
|
||||
if(localVideo.isMuted() &&
|
||||
localVideo.videoType !== track.videoType) {
|
||||
localVideo.mute();
|
||||
}
|
||||
callback();
|
||||
room.off(ConferenceEvents.TRACK_ADDED, localCallback);
|
||||
}
|
||||
};
|
||||
|
||||
room.on(ConferenceEvents.TRACK_ADDED, localCallback);
|
||||
|
||||
localVideo.stop();
|
||||
localVideo = track;
|
||||
addTrack(track);
|
||||
APP.UI.addLocalStream(track);
|
||||
};
|
||||
|
||||
APP.conference.sipGatewayEnabled = () => {
|
||||
return room.isSIPCallingSupported();
|
||||
|
@ -194,7 +180,7 @@ function initConference(localTracks, connection) {
|
|||
return APP.settings.getDisplayName();
|
||||
}
|
||||
|
||||
var participant = room.getParticipantById(id);
|
||||
let participant = room.getParticipantById(id);
|
||||
if (participant && participant.getDisplayName()) {
|
||||
return participant.getDisplayName();
|
||||
}
|
||||
|
@ -203,10 +189,10 @@ function initConference(localTracks, connection) {
|
|||
// add local streams when joined to the conference
|
||||
room.on(ConferenceEvents.CONFERENCE_JOINED, function () {
|
||||
localTracks.forEach(function (track) {
|
||||
if(track.getType() === "audio") {
|
||||
if(track.isAudioTrack()) {
|
||||
localAudio = track;
|
||||
}
|
||||
else if (track.getType() === "video") {
|
||||
else if (track.isVideoTrack()) {
|
||||
localVideo = track;
|
||||
}
|
||||
addTrack(track);
|
||||
|
@ -218,16 +204,14 @@ function initConference(localTracks, connection) {
|
|||
|
||||
|
||||
room.on(ConferenceEvents.USER_JOINED, function (id, user) {
|
||||
if (APP.conference.isLocalId(id)) {
|
||||
return;
|
||||
}
|
||||
console.error('USER %s connnected', id);
|
||||
console.error('USER %s connnected', id, user);
|
||||
// FIXME email???
|
||||
APP.UI.addUser(id, user.getDisplayName());
|
||||
});
|
||||
room.on(ConferenceEvents.USER_LEFT, function (id, user) {
|
||||
console.error('USER LEFT', id);
|
||||
console.error('USER %s LEFT', id, user);
|
||||
APP.UI.removeUser(id, user.getDisplayName());
|
||||
APP.UI.stopPrezi(id);
|
||||
});
|
||||
|
||||
|
||||
|
@ -237,7 +221,7 @@ function initConference(localTracks, connection) {
|
|||
APP.conference.isModerator = room.isModerator();
|
||||
APP.UI.updateLocalRole(room.isModerator());
|
||||
} else {
|
||||
var user = room.getParticipantById(id);
|
||||
let user = room.getParticipantById(id);
|
||||
if (user) {
|
||||
APP.UI.updateUserRole(user);
|
||||
}
|
||||
|
@ -348,9 +332,12 @@ function initConference(localTracks, connection) {
|
|||
room.removeCommand(Commands.CONNECTION_QUALITY);
|
||||
});
|
||||
// listen to remote stats
|
||||
room.addCommandListener(Commands.CONNECTION_QUALITY, function (data) {
|
||||
APP.connectionquality.updateRemoteStats(data.attributes.id, data.value);
|
||||
});
|
||||
room.addCommandListener(
|
||||
Commands.CONNECTION_QUALITY,
|
||||
function ({value, attributes}) {
|
||||
APP.connectionquality.updateRemoteStats(attributes.id, value);
|
||||
}
|
||||
);
|
||||
APP.connectionquality.addListener(
|
||||
CQEvents.REMOTESTATS_UPDATED,
|
||||
function (id, percent, stats) {
|
||||
|
@ -358,8 +345,39 @@ function initConference(localTracks, connection) {
|
|||
}
|
||||
);
|
||||
|
||||
room.addCommandListener(Commands.VIDEO_TYPE, (data, from) => {
|
||||
APP.UI.onPeerVideoTypeChanged(from, data.value);
|
||||
room.addCommandListener(Commands.ETHERPAD, function ({value}) {
|
||||
APP.UI.initEtherpad(value);
|
||||
});
|
||||
|
||||
|
||||
room.addCommandListener(Commands.PREZI, function ({value, attributes}) {
|
||||
APP.UI.showPrezi(attributes.id, value, attributes.slide);
|
||||
});
|
||||
room.addCommandListener(Commands.STOP_PREZI, function ({attributes}) {
|
||||
APP.UI.stopPrezi(attributes.id);
|
||||
});
|
||||
APP.UI.addListener(UIEvents.SHARE_PREZI, function (url, slide) {
|
||||
console.log('Sharing Prezi %s slide %s', url, slide);
|
||||
room.removeCommand(Commands.PREZI);
|
||||
room.sendCommand(Commands.PREZI, {
|
||||
value: url,
|
||||
attributes: {
|
||||
id: room.myUserId(),
|
||||
slide
|
||||
}
|
||||
});
|
||||
});
|
||||
APP.UI.addListener(UIEvents.STOP_SHARING_PREZI, function () {
|
||||
room.removeCommand(Commands.PREZI);
|
||||
room.sendCommandOnce(Commands.STOP_PREZI, {
|
||||
attributes: {
|
||||
id: room.myUserId()
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
room.addCommandListener(Commands.VIDEO_TYPE, ({value}, from) => {
|
||||
APP.UI.onPeerVideoTypeChanged(from, value);
|
||||
});
|
||||
|
||||
|
||||
|
@ -373,7 +391,7 @@ function initConference(localTracks, connection) {
|
|||
});
|
||||
}
|
||||
|
||||
var email = APP.settings.getEmail();
|
||||
let email = APP.settings.getEmail();
|
||||
email && sendEmail(email);
|
||||
APP.UI.addListener(UIEvents.EMAIL_CHANGED, function (email) {
|
||||
APP.settings.setEmail(email);
|
||||
|
@ -429,6 +447,8 @@ function initConference(localTracks, connection) {
|
|||
window.location.pathname = "/";
|
||||
}, 3000);
|
||||
}
|
||||
}, function (err) {
|
||||
console.error(err);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -489,6 +509,35 @@ function initConference(localTracks, connection) {
|
|||
APP.UI.updateDTMFSupport(isDTMFSupported);
|
||||
});
|
||||
|
||||
APP.UI.addListener(UIEvents.TOGGLE_SCREENSHARING, function () {
|
||||
APP.desktopsharing.toggleScreenSharing();
|
||||
});
|
||||
APP.UI.addListener(DSEvents.SWITCHING_DONE, function (isSharingScreen) {
|
||||
APP.UI.updateDesktopSharingButtons(isSharingScreen);
|
||||
});
|
||||
APP.desktopsharing.addListener(
|
||||
DSEvents.NEW_STREAM_CREATED,
|
||||
(track, callback) => {
|
||||
const localCallback = (newTrack) => {
|
||||
if (newTrack.isLocal() && newTrack === localVideo) {
|
||||
if(localVideo.isMuted() &&
|
||||
localVideo.videoType !== track.videoType) {
|
||||
localVideo.mute();
|
||||
}
|
||||
callback();
|
||||
room.off(ConferenceEvents.TRACK_ADDED, localCallback);
|
||||
}
|
||||
};
|
||||
|
||||
room.on(ConferenceEvents.TRACK_ADDED, localCallback);
|
||||
|
||||
localVideo.stop();
|
||||
localVideo = track;
|
||||
addTrack(track);
|
||||
APP.UI.addLocalStream(track);
|
||||
}
|
||||
);
|
||||
|
||||
$(window).bind('beforeunload', function () {
|
||||
room.leave();
|
||||
});
|
||||
|
@ -558,15 +607,6 @@ function initConference(localTracks, connection) {
|
|||
});
|
||||
}
|
||||
|
||||
function createLocalTracks () {
|
||||
return JitsiMeetJS.createLocalTracks({
|
||||
devices: ['audio', 'video']
|
||||
}).catch(function (err) {
|
||||
console.error('failed to create local tracks', err);
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
function connect() {
|
||||
return openConnection({retry: true}).catch(function (err) {
|
||||
if (err === ConnectionErrors.PASSWORD_REQUIRED) {
|
||||
|
@ -584,7 +624,10 @@ function init() {
|
|||
JitsiMeetJS.setLogLevel(JitsiMeetJS.logLevels.TRACE);
|
||||
|
||||
JitsiMeetJS.init(config).then(function () {
|
||||
return Promise.all([createLocalTracks(), connect()]);
|
||||
return Promise.all([
|
||||
APP.createLocalTracks('audio', 'video'),
|
||||
connect()
|
||||
]);
|
||||
}).then(function ([tracks, connection]) {
|
||||
console.log('initialized with %s local tracks', tracks.length);
|
||||
return initConference(tracks, connection);
|
||||
|
@ -596,12 +639,6 @@ function init() {
|
|||
APP.settings.setLanguage(language);
|
||||
});
|
||||
|
||||
APP.desktopsharing.addListener(
|
||||
DesktopSharingEventTypes.NEW_STREAM_CREATED,
|
||||
(stream, callback) => {
|
||||
APP.conference.changeLocalVideo(stream,
|
||||
callback);
|
||||
});
|
||||
APP.desktopsharing.init(JitsiMeetJS.isDesktopSharingEnabled());
|
||||
APP.statistics.start();
|
||||
APP.connectionquality.init();
|
||||
|
@ -653,7 +690,7 @@ $(document).ready(function () {
|
|||
URLProcessor.setConfigParametersFromUrl();
|
||||
APP.init();
|
||||
|
||||
APP.translation.init();
|
||||
APP.translation.init(settings.getLanguage());
|
||||
|
||||
if (APP.API.isEnabled()) {
|
||||
APP.API.init();
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
}
|
||||
|
||||
#remoteVideos .videocontainer {
|
||||
display: inline-block;
|
||||
display: none;
|
||||
background-color: black;
|
||||
background-size: contain;
|
||||
border-radius:8px;
|
||||
|
@ -378,7 +378,7 @@
|
|||
padding-right:2px;
|
||||
height:38px;
|
||||
width:auto;
|
||||
background-color: rgba(0,0,0,0.8);
|
||||
background-color: rgba(0,0,0,0.8);
|
||||
border: 1px solid rgba(256, 256, 256, 0.2);
|
||||
border-radius: 6px;
|
||||
pointer-events: auto;
|
||||
|
|
19
index.html
19
index.html
|
@ -140,6 +140,25 @@
|
|||
</div>
|
||||
<div id="reloadPresentation"><a id="reloadPresentationLink"><i title="Reload Prezi" class="fa fa-repeat fa-lg"></i></a></div>
|
||||
<div id="videospace">
|
||||
|
||||
<div id="largeVideoContainer" class="videocontainer">
|
||||
<div id="presentation"></div>
|
||||
<div id="etherpad"></div>
|
||||
<a target="_new"><div class="watermark leftwatermark"></div></a>
|
||||
<a target="_new"><div class="watermark rightwatermark"></div></a>
|
||||
<a class="poweredby" href="http://jitsi.org" target="_new">
|
||||
<span data-i18n="poweredby"></span> jitsi.org
|
||||
</a>
|
||||
<div id="activeSpeaker">
|
||||
<img id="activeSpeakerAvatar" src=""/>
|
||||
<canvas id="activeSpeakerAudioLevel"></canvas>
|
||||
</div>
|
||||
<div id="largeVideoWrapper">
|
||||
<video id="largeVideo" muted="true" autoplay oncontextmenu="return false;"></video>
|
||||
</div>
|
||||
<span id="videoConnectionMessage"></span>
|
||||
</div>
|
||||
|
||||
<div id="remoteVideos">
|
||||
<span id="localVideoContainer" class="videocontainer">
|
||||
<span id="localNick" class="nick"></span>
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,4 +1,4 @@
|
|||
/* global JitsiMeetJS */
|
||||
/* global JitsiMeetJS, APP */
|
||||
|
||||
import LoginDialog from './UI/authentication/LoginDialog';
|
||||
import UIEvents from '../service/UI/UIEvents';
|
||||
|
@ -44,14 +44,16 @@ function doXmppAuth (room, lockPassword) {
|
|||
// open room
|
||||
let newRoom = connection.initJitsiConference(room.getName());
|
||||
|
||||
newRoom.on(ConferenceEvents.CONFERENCE_FAILED, function (err) {
|
||||
connection.disconnect();
|
||||
loginDialog.displayError(err);
|
||||
});
|
||||
loginDialog.displayConnectionStatus(
|
||||
APP.translation.translateString('connection.FETCH_SESSION_ID')
|
||||
);
|
||||
|
||||
newRoom.room.moderator.allocateConferenceFocus(function () {
|
||||
newRoom.room.moderator.authenticate().then(function () {
|
||||
connection.disconnect();
|
||||
loginDialog.close();
|
||||
|
||||
loginDialog.displayConnectionStatus(
|
||||
APP.translation.translateString('connection.GOT_SESSION_ID')
|
||||
);
|
||||
|
||||
if (room.isJoined()) {
|
||||
// just reallocate focus if already joined
|
||||
|
@ -60,8 +62,19 @@ function doXmppAuth (room, lockPassword) {
|
|||
// or join
|
||||
room.join(lockPassword);
|
||||
}
|
||||
});
|
||||
|
||||
loginDialog.close();
|
||||
}).catch(function (error, code) {
|
||||
connection.disconnect();
|
||||
|
||||
console.error('Auth on the fly failed', error);
|
||||
|
||||
let errorMsg = APP.translation.translateString(
|
||||
'connection.GET_SESSION_ID_ERROR'
|
||||
);
|
||||
|
||||
loginDialog.displayError(errorMsg + code);
|
||||
});
|
||||
}, function (err) {
|
||||
loginDialog.displayError(err);
|
||||
});
|
||||
|
|
|
@ -1,211 +0,0 @@
|
|||
/* global config, APP, Strophe */
|
||||
/* jshint -W101 */
|
||||
|
||||
// cache datachannels to avoid garbage collection
|
||||
// https://code.google.com/p/chromium/issues/detail?id=405545
|
||||
var RTCEvents = require("../../service/RTC/RTCEvents");
|
||||
|
||||
var _dataChannels = [];
|
||||
var eventEmitter = null;
|
||||
|
||||
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;
|
||||
|
||||
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));
|
||||
|
||||
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 = Boolean(oldValue).valueOf();
|
||||
}
|
||||
}
|
||||
if ((type = typeof newValue) !== 'boolean') {
|
||||
if (type === 'string') {
|
||||
newValue = (newValue == "true");
|
||||
} else {
|
||||
newValue = 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.info(
|
||||
"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);
|
||||
// The received message appears to be appropriately
|
||||
// formatted (i.e. is a JSON object which assigns a value to
|
||||
// the mandatory property colibriClass) so don't just
|
||||
// swallow it, expose it to public consumption.
|
||||
eventEmitter.emit("rtc.datachannel." + colibriClass, 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: function (userResource) {
|
||||
onXXXEndpointChanged("selected", userResource);
|
||||
},
|
||||
handlePinnedEndpointEvent: function (userResource) {
|
||||
onXXXEndpointChanged("pinned", userResource);
|
||||
},
|
||||
|
||||
some: function (callback, thisArg) {
|
||||
if (_dataChannels && _dataChannels.length !== 0) {
|
||||
if (thisArg)
|
||||
return _dataChannels.some(callback, thisArg);
|
||||
else
|
||||
return _dataChannels.some(callback);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Notifies Videobridge about a change in the value of a specific
|
||||
* endpoint-related property such as selected endpoint and pinnned endpoint.
|
||||
*
|
||||
* @param xxx the name of the endpoint-related property whose value changed
|
||||
* @param userResource the new value of the endpoint-related property after the
|
||||
* change
|
||||
*/
|
||||
function onXXXEndpointChanged(xxx, userResource) {
|
||||
// Derive the correct words from xxx such as selected and Selected, pinned
|
||||
// and Pinned.
|
||||
var head = xxx.charAt(0);
|
||||
var tail = xxx.substring(1);
|
||||
var lower = head.toLowerCase() + tail;
|
||||
var upper = head.toUpperCase() + tail;
|
||||
|
||||
// Notify Videobridge about the specified endpoint change.
|
||||
console.log(lower + ' endpoint changed: ', userResource);
|
||||
DataChannels.some(function (dataChannel) {
|
||||
if (dataChannel.readyState == 'open') {
|
||||
console.log(
|
||||
'sending ' + lower
|
||||
+ ' endpoint changed notification to the bridge: ',
|
||||
userResource);
|
||||
|
||||
var jsonObject = {};
|
||||
|
||||
jsonObject.colibriClass = (upper + 'EndpointChangedEvent');
|
||||
jsonObject[lower + "Endpoint"]
|
||||
= (userResource ? userResource : null);
|
||||
dataChannel.send(JSON.stringify(jsonObject));
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = DataChannels;
|
||||
|
|
@ -1,145 +0,0 @@
|
|||
/* global APP */
|
||||
var MediaStreamType = require("../../service/RTC/MediaStreamTypes");
|
||||
var RTCEvents = require("../../service/RTC/RTCEvents");
|
||||
var RTCBrowserType = require("./RTCBrowserType");
|
||||
var StreamEventTypes = require("../../service/RTC/StreamEventTypes.js");
|
||||
|
||||
/**
|
||||
* 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(localStream) {
|
||||
var stream = localStream.getOriginalStream();
|
||||
var originalStop = stream.stop;
|
||||
stream.stop = function () {
|
||||
originalStop.apply(stream);
|
||||
if (localStream.isActive()) {
|
||||
stream.onended();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function LocalStream(stream, type, eventEmitter, videoType, isGUMStream) {
|
||||
this.stream = stream;
|
||||
this.eventEmitter = eventEmitter;
|
||||
this.type = type;
|
||||
this.videoType = videoType;
|
||||
this.isGUMStream = true;
|
||||
if(isGUMStream === false)
|
||||
this.isGUMStream = isGUMStream;
|
||||
var self = this;
|
||||
if (MediaStreamType.AUDIO_TYPE === type) {
|
||||
this.getTracks = function () {
|
||||
return self.stream.getAudioTracks();
|
||||
};
|
||||
} else {
|
||||
this.getTracks = function () {
|
||||
return self.stream.getVideoTracks();
|
||||
};
|
||||
}
|
||||
|
||||
APP.RTC.addMediaStreamInactiveHandler(
|
||||
this.stream,
|
||||
function () {
|
||||
self.streamEnded();
|
||||
});
|
||||
|
||||
if (RTCBrowserType.isFirefox()) {
|
||||
implementOnEndedHandling(this);
|
||||
}
|
||||
}
|
||||
|
||||
LocalStream.prototype.streamEnded = function () {
|
||||
this.eventEmitter.emit(StreamEventTypes.EVENT_TYPE_LOCAL_ENDED, this);
|
||||
};
|
||||
|
||||
LocalStream.prototype.getOriginalStream = function()
|
||||
{
|
||||
return this.stream;
|
||||
};
|
||||
|
||||
LocalStream.prototype.isAudioStream = function () {
|
||||
return MediaStreamType.AUDIO_TYPE === this.type;
|
||||
};
|
||||
|
||||
LocalStream.prototype.isVideoStream = function () {
|
||||
return MediaStreamType.VIDEO_TYPE === this.type;
|
||||
};
|
||||
|
||||
LocalStream.prototype.setMute = function (mute)
|
||||
{
|
||||
var isAudio = this.isAudioStream();
|
||||
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;
|
||||
}
|
||||
this.eventEmitter.emit(eventType, mute);
|
||||
} else {
|
||||
if (mute) {
|
||||
APP.xmpp.removeStream(this.stream);
|
||||
APP.RTC.stopMediaStream(this.stream);
|
||||
this.eventEmitter.emit(eventType, true);
|
||||
} else {
|
||||
var self = this;
|
||||
APP.RTC.rtcUtils.obtainAudioAndVideoPermissions(
|
||||
(this.isAudioStream() ? ["audio"] : ["video"]),
|
||||
function (stream) {
|
||||
if (isAudio) {
|
||||
APP.RTC.changeLocalAudio(stream,
|
||||
function () {
|
||||
self.eventEmitter.emit(eventType, false);
|
||||
});
|
||||
} else {
|
||||
APP.RTC.changeLocalVideo(stream, false,
|
||||
function () {
|
||||
self.eventEmitter.emit(eventType, false);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
LocalStream.prototype.isMuted = function () {
|
||||
var tracks = [];
|
||||
if (this.isAudioStream()) {
|
||||
tracks = this.stream.getAudioTracks();
|
||||
} else {
|
||||
if (!this.isActive())
|
||||
return true;
|
||||
tracks = this.stream.getVideoTracks();
|
||||
}
|
||||
for (var idx = 0; idx < tracks.length; idx++) {
|
||||
if(tracks[idx].enabled)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
LocalStream.prototype.getId = function () {
|
||||
return this.stream.getTracks()[0].id;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks whether the MediaStream is avtive/not ended.
|
||||
* When there is no check for active we don't have information and so
|
||||
* will return that stream is active (in case of FF).
|
||||
* @returns {boolean} whether MediaStream is active.
|
||||
*/
|
||||
LocalStream.prototype.isActive = function () {
|
||||
if((typeof this.stream.active !== "undefined"))
|
||||
return this.stream.active;
|
||||
else
|
||||
return true;
|
||||
};
|
||||
|
||||
module.exports = LocalStream;
|
|
@ -1,57 +0,0 @@
|
|||
var MediaStreamType = require("../../service/RTC/MediaStreamTypes");
|
||||
|
||||
/**
|
||||
* Creates a MediaStream object for the given data, session id and ssrc.
|
||||
* It is a wrapper class for the MediaStream.
|
||||
*
|
||||
* @param data the data object from which we obtain the stream,
|
||||
* the peerjid, etc.
|
||||
* @param ssrc the ssrc corresponding to this MediaStream
|
||||
* @param mute the whether this MediaStream is muted
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
function MediaStream(data, ssrc, browser, eventEmitter, muted, type) {
|
||||
|
||||
// XXX(gp) to minimize headaches in the future, we should build our
|
||||
// abstractions around tracks and not streams. ORTC is track based API.
|
||||
// Mozilla expects m-lines to represent media tracks.
|
||||
//
|
||||
// Practically, what I'm saying is that we should have a MediaTrack class
|
||||
// and not a MediaStream class.
|
||||
//
|
||||
// Also, we should be able to associate multiple SSRCs with a MediaTrack as
|
||||
// a track might have an associated RTX and FEC sources.
|
||||
|
||||
if (!type) {
|
||||
console.log("Errrm...some code needs an update...");
|
||||
}
|
||||
|
||||
this.stream = data.stream;
|
||||
this.peerjid = data.peerjid;
|
||||
this.videoType = data.videoType;
|
||||
this.ssrc = ssrc;
|
||||
this.type = type;
|
||||
this.muted = muted;
|
||||
this.eventEmitter = eventEmitter;
|
||||
}
|
||||
|
||||
// FIXME duplicated with LocalStream methods - extract base class
|
||||
MediaStream.prototype.isAudioStream = function () {
|
||||
return MediaStreamType.AUDIO_TYPE === this.type;
|
||||
};
|
||||
|
||||
MediaStream.prototype.isVideoStream = function () {
|
||||
return MediaStreamType.VIDEO_TYPE === this.type;
|
||||
};
|
||||
|
||||
MediaStream.prototype.getOriginalStream = function () {
|
||||
return this.stream;
|
||||
};
|
||||
|
||||
MediaStream.prototype.setMute = function (value) {
|
||||
this.stream.muted = value;
|
||||
this.muted = value;
|
||||
};
|
||||
|
||||
module.exports = MediaStream;
|
|
@ -1,335 +0,0 @@
|
|||
/* global APP */
|
||||
var EventEmitter = require("events");
|
||||
var RTCBrowserType = require("./RTCBrowserType");
|
||||
var RTCUtils = require("./RTCUtils.js");
|
||||
var LocalStream = require("./LocalStream.js");
|
||||
var DataChannels = require("./DataChannels");
|
||||
var MediaStream = require("./MediaStream.js");
|
||||
var DesktopSharingEventTypes
|
||||
= require("../../service/desktopsharing/DesktopSharingEventTypes");
|
||||
var MediaStreamType = require("../../service/RTC/MediaStreamTypes");
|
||||
var StreamEventTypes = require("../../service/RTC/StreamEventTypes.js");
|
||||
var RTCEvents = require("../../service/RTC/RTCEvents.js");
|
||||
var XMPPEvents = require("../../service/xmpp/XMPPEvents");
|
||||
var UIEvents = require("../../service/UI/UIEvents");
|
||||
|
||||
var eventEmitter = new EventEmitter();
|
||||
|
||||
|
||||
function getMediaStreamUsage()
|
||||
{
|
||||
var result = {
|
||||
audio: true,
|
||||
video: true
|
||||
};
|
||||
|
||||
/** There are some issues with the desktop sharing
|
||||
* when this property is enabled.
|
||||
* WARNING: We must change the implementation to start video/audio if we
|
||||
* receive from the focus that the peer is not muted.
|
||||
|
||||
var isSecureConnection = window.location.protocol == "https:";
|
||||
|
||||
if(config.disableEarlyMediaPermissionRequests || !isSecureConnection)
|
||||
{
|
||||
result = {
|
||||
audio: false,
|
||||
video: false
|
||||
};
|
||||
|
||||
}
|
||||
**/
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
var RTC = {
|
||||
// Exposes DataChannels to public consumption (e.g. jitsi-meet-torture)
|
||||
// without the necessity to require the module.
|
||||
"DataChannels": DataChannels,
|
||||
|
||||
rtcUtils: null,
|
||||
devices: {
|
||||
audio: true,
|
||||
video: true
|
||||
},
|
||||
remoteStreams: {},
|
||||
localAudio: null,
|
||||
localVideo: null,
|
||||
addStreamListener: function (listener, eventType) {
|
||||
eventEmitter.on(eventType, listener);
|
||||
},
|
||||
addListener: function (type, listener) {
|
||||
eventEmitter.on(type, listener);
|
||||
},
|
||||
removeStreamListener: function (listener, eventType) {
|
||||
if(!(eventType instanceof StreamEventTypes))
|
||||
throw "Illegal argument";
|
||||
|
||||
eventEmitter.removeListener(eventType, listener);
|
||||
},
|
||||
createLocalStream: function (stream, type, change, videoType,
|
||||
isMuted, isGUMStream) {
|
||||
|
||||
var localStream =
|
||||
new LocalStream(stream, type, eventEmitter, videoType, isGUMStream);
|
||||
if(isMuted === true)
|
||||
localStream.setMute(true);
|
||||
|
||||
if (MediaStreamType.AUDIO_TYPE === type) {
|
||||
this.localAudio = localStream;
|
||||
} else {
|
||||
this.localVideo = localStream;
|
||||
}
|
||||
var eventType = StreamEventTypes.EVENT_TYPE_LOCAL_CREATED;
|
||||
if(change)
|
||||
eventType = StreamEventTypes.EVENT_TYPE_LOCAL_CHANGED;
|
||||
|
||||
eventEmitter.emit(eventType, localStream, isMuted);
|
||||
return localStream;
|
||||
},
|
||||
createRemoteStream: function (data, ssrc) {
|
||||
var jid = data.peerjid || APP.xmpp.myJid();
|
||||
|
||||
// check the video muted state from last stored presence if any
|
||||
var muted = false;
|
||||
var pres = APP.xmpp.getLastPresence(jid);
|
||||
if (pres && pres.videoMuted) {
|
||||
muted = pres.videoMuted;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
[MediaStreamType.AUDIO_TYPE, MediaStreamType.VIDEO_TYPE].forEach(
|
||||
function (type) {
|
||||
var tracks =
|
||||
type == MediaStreamType.AUDIO_TYPE
|
||||
? data.stream.getAudioTracks() : data.stream.getVideoTracks();
|
||||
if (!tracks || !Array.isArray(tracks) || !tracks.length) {
|
||||
console.log("Not creating a(n) " + type + " stream: no tracks");
|
||||
return;
|
||||
}
|
||||
|
||||
var remoteStream = new MediaStream(data, ssrc,
|
||||
RTCBrowserType.getBrowserType(), eventEmitter, muted, type);
|
||||
|
||||
if (!self.remoteStreams[jid]) {
|
||||
self.remoteStreams[jid] = {};
|
||||
}
|
||||
self.remoteStreams[jid][type] = remoteStream;
|
||||
eventEmitter.emit(StreamEventTypes.EVENT_TYPE_REMOTE_CREATED,
|
||||
remoteStream);
|
||||
});
|
||||
},
|
||||
getPCConstraints: function () {
|
||||
return this.rtcUtils.pc_constraints;
|
||||
},
|
||||
getUserMediaWithConstraints:function(um, success_callback,
|
||||
failure_callback, resolution,
|
||||
bandwidth, fps, desktopStream)
|
||||
{
|
||||
return this.rtcUtils.getUserMediaWithConstraints(um, success_callback,
|
||||
failure_callback, resolution, bandwidth, fps, desktopStream);
|
||||
},
|
||||
attachMediaStream: function (elSelector, stream) {
|
||||
this.rtcUtils.attachMediaStream(elSelector, stream);
|
||||
},
|
||||
getStreamID: function (stream) {
|
||||
return this.rtcUtils.getStreamID(stream);
|
||||
},
|
||||
getVideoSrc: function (element) {
|
||||
return this.rtcUtils.getVideoSrc(element);
|
||||
},
|
||||
setVideoSrc: function (element, src) {
|
||||
this.rtcUtils.setVideoSrc(element, src);
|
||||
},
|
||||
getVideoElementName: function () {
|
||||
return RTCBrowserType.isTemasysPluginUsed() ? 'object' : 'video';
|
||||
},
|
||||
dispose: function() {
|
||||
if (this.rtcUtils) {
|
||||
this.rtcUtils = null;
|
||||
}
|
||||
},
|
||||
stop: function () {
|
||||
this.dispose();
|
||||
},
|
||||
start: function () {
|
||||
var self = this;
|
||||
APP.desktopsharing.addListener(
|
||||
DesktopSharingEventTypes.NEW_STREAM_CREATED,
|
||||
function (stream, isUsingScreenStream, callback) {
|
||||
self.changeLocalVideo(stream, isUsingScreenStream, callback);
|
||||
});
|
||||
APP.xmpp.addListener(XMPPEvents.CALL_INCOMING, function(event) {
|
||||
DataChannels.init(event.peerconnection, eventEmitter);
|
||||
});
|
||||
APP.UI.addListener(UIEvents.SELECTED_ENDPOINT,
|
||||
DataChannels.handleSelectedEndpointEvent);
|
||||
APP.UI.addListener(UIEvents.PINNED_ENDPOINT,
|
||||
DataChannels.handlePinnedEndpointEvent);
|
||||
|
||||
// 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 () {
|
||||
eventEmitter.emit(RTCEvents.RTC_READY, true);
|
||||
self.rtcUtils.obtainAudioAndVideoPermissions(
|
||||
null, null, getMediaStreamUsage());
|
||||
};
|
||||
|
||||
this.rtcUtils = new RTCUtils(this, eventEmitter, onReady);
|
||||
|
||||
// Call onReady() if Temasys plugin is not used
|
||||
if (!RTCBrowserType.isTemasysPluginUsed()) {
|
||||
onReady();
|
||||
}
|
||||
},
|
||||
muteRemoteVideoStream: function (jid, value) {
|
||||
var stream;
|
||||
|
||||
if(this.remoteStreams[jid] &&
|
||||
this.remoteStreams[jid][MediaStreamType.VIDEO_TYPE]) {
|
||||
stream = this.remoteStreams[jid][MediaStreamType.VIDEO_TYPE];
|
||||
}
|
||||
|
||||
if(!stream)
|
||||
return true;
|
||||
|
||||
if (value != stream.muted) {
|
||||
stream.setMute(value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
changeLocalVideo: function (stream, isUsingScreenStream, callback) {
|
||||
var oldStream = this.localVideo.getOriginalStream();
|
||||
var type = (isUsingScreenStream ? "screen" : "camera");
|
||||
var localCallback = callback;
|
||||
if(this.localVideo.isMuted() && this.localVideo.videoType !== type) {
|
||||
localCallback = function() {
|
||||
APP.xmpp.setVideoMute(false, function(mute) {
|
||||
eventEmitter.emit(RTCEvents.VIDEO_MUTE, mute);
|
||||
});
|
||||
|
||||
callback();
|
||||
};
|
||||
}
|
||||
// FIXME: Workaround for FF/IE/Safari
|
||||
if (stream && stream.videoStream) {
|
||||
stream = stream.videoStream;
|
||||
}
|
||||
var videoStream = this.rtcUtils.createStream(stream, true);
|
||||
this.localVideo =
|
||||
this.createLocalStream(videoStream, "video", true, type);
|
||||
// Stop the stream
|
||||
this.stopMediaStream(oldStream);
|
||||
|
||||
APP.xmpp.switchStreams(videoStream, oldStream,localCallback);
|
||||
},
|
||||
changeLocalAudio: function (stream, callback) {
|
||||
var oldStream = this.localAudio.getOriginalStream();
|
||||
var newStream = this.rtcUtils.createStream(stream);
|
||||
this.localAudio
|
||||
= this.createLocalStream(
|
||||
newStream, MediaStreamType.AUDIO_TYPE, true);
|
||||
// Stop the stream
|
||||
this.stopMediaStream(oldStream);
|
||||
APP.xmpp.switchStreams(newStream, oldStream, callback, true);
|
||||
},
|
||||
isVideoMuted: function (jid) {
|
||||
if (jid === APP.xmpp.myJid()) {
|
||||
var localVideo = APP.RTC.localVideo;
|
||||
return (!localVideo || localVideo.isMuted());
|
||||
} else {
|
||||
if (!APP.RTC.remoteStreams[jid] ||
|
||||
!APP.RTC.remoteStreams[jid][MediaStreamType.VIDEO_TYPE]) {
|
||||
return null;
|
||||
}
|
||||
return APP.RTC.remoteStreams[jid][MediaStreamType.VIDEO_TYPE].muted;
|
||||
}
|
||||
},
|
||||
setVideoMute: function (mute, callback, options) {
|
||||
if (!this.localVideo)
|
||||
return;
|
||||
|
||||
if (mute == APP.RTC.localVideo.isMuted())
|
||||
{
|
||||
APP.xmpp.sendVideoInfoPresence(mute);
|
||||
if (callback)
|
||||
callback(mute);
|
||||
}
|
||||
else
|
||||
{
|
||||
APP.RTC.localVideo.setMute(mute);
|
||||
APP.xmpp.setVideoMute(
|
||||
mute,
|
||||
callback,
|
||||
options);
|
||||
}
|
||||
},
|
||||
setDeviceAvailability: function (devices) {
|
||||
if(!devices)
|
||||
return;
|
||||
if(devices.audio === true || devices.audio === false)
|
||||
this.devices.audio = devices.audio;
|
||||
if(devices.video === true || devices.video === false)
|
||||
this.devices.video = devices.video;
|
||||
eventEmitter.emit(RTCEvents.AVAILABLE_DEVICES_CHANGED, this.devices);
|
||||
},
|
||||
/**
|
||||
* A method to handle stopping of the stream.
|
||||
* One point to handle the differences in various implementations.
|
||||
* @param mediaStream MediaStream object to stop.
|
||||
*/
|
||||
stopMediaStream: function (mediaStream) {
|
||||
mediaStream.getTracks().forEach(function (track) {
|
||||
// stop() not supported with IE
|
||||
if (track.stop) {
|
||||
track.stop();
|
||||
}
|
||||
});
|
||||
|
||||
// leave stop for implementation still using it
|
||||
if (mediaStream.stop) {
|
||||
mediaStream.stop();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Adds onended/inactive handler to a MediaStream.
|
||||
* @param mediaStream a MediaStream to attach onended/inactive handler
|
||||
* @param handler the handler
|
||||
*/
|
||||
addMediaStreamInactiveHandler: function (mediaStream, handler) {
|
||||
if (mediaStream.addEventListener) {
|
||||
// chrome
|
||||
if(typeof mediaStream.active !== "undefined")
|
||||
mediaStream.oninactive = handler;
|
||||
else
|
||||
mediaStream.onended = handler;
|
||||
} else {
|
||||
// themasys
|
||||
mediaStream.attachEvent('ended', function () {
|
||||
handler(mediaStream);
|
||||
});
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Removes onended/inactive handler.
|
||||
* @param mediaStream the MediaStream to remove the handler from.
|
||||
* @param handler the handler to remove.
|
||||
*/
|
||||
removeMediaStreamInactiveHandler: function (mediaStream, handler) {
|
||||
if (mediaStream.removeEventListener) {
|
||||
// chrome
|
||||
if(typeof mediaStream.active !== "undefined")
|
||||
mediaStream.oninactive = null;
|
||||
else
|
||||
mediaStream.onended = null;
|
||||
} else {
|
||||
// themasys
|
||||
mediaStream.detachEvent('ended', handler);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = RTC;
|
|
@ -1,574 +0,0 @@
|
|||
/* global APP, config, require, attachMediaStream, getUserMedia,
|
||||
RTCPeerConnection, webkitMediaStream, webkitURL, webkitRTCPeerConnection,
|
||||
mozRTCIceCandidate, mozRTCSessionDescription, mozRTCPeerConnection */
|
||||
/* jshint -W101 */
|
||||
var MediaStreamType = require("../../service/RTC/MediaStreamTypes");
|
||||
var RTCBrowserType = require("./RTCBrowserType");
|
||||
var Resolutions = require("../../service/RTC/Resolutions");
|
||||
var RTCEvents = require("../../service/RTC/RTCEvents");
|
||||
var AdapterJS = require("./adapter.screenshare");
|
||||
|
||||
var currentResolution = null;
|
||||
|
||||
function getPreviousResolution(resolution) {
|
||||
if(!Resolutions[resolution])
|
||||
return null;
|
||||
var order = Resolutions[resolution].order;
|
||||
var res = null;
|
||||
var resName = null;
|
||||
for(var i in Resolutions) {
|
||||
var tmp = Resolutions[i];
|
||||
if (!res || (res.order < tmp.order && tmp.order < order)) {
|
||||
resName = i;
|
||||
res = tmp;
|
||||
}
|
||||
}
|
||||
return resName;
|
||||
}
|
||||
|
||||
function setResolutionConstraints(constraints, resolution) {
|
||||
var isAndroid = RTCBrowserType.isAndroid();
|
||||
|
||||
if (Resolutions[resolution]) {
|
||||
constraints.video.mandatory.minWidth = Resolutions[resolution].width;
|
||||
constraints.video.mandatory.minHeight = Resolutions[resolution].height;
|
||||
}
|
||||
else if (isAndroid) {
|
||||
// FIXME can't remember if the purpose of this was to always request
|
||||
// low resolution on Android ? if yes it should be moved up front
|
||||
constraints.video.mandatory.minWidth = 320;
|
||||
constraints.video.mandatory.minHeight = 240;
|
||||
constraints.video.mandatory.maxFrameRate = 15;
|
||||
}
|
||||
|
||||
if (constraints.video.mandatory.minWidth)
|
||||
constraints.video.mandatory.maxWidth =
|
||||
constraints.video.mandatory.minWidth;
|
||||
if (constraints.video.mandatory.minHeight)
|
||||
constraints.video.mandatory.maxHeight =
|
||||
constraints.video.mandatory.minHeight;
|
||||
}
|
||||
|
||||
function getConstraints(um, resolution, bandwidth, fps, desktopStream) {
|
||||
var constraints = {audio: false, video: false};
|
||||
|
||||
if (um.indexOf('video') >= 0) {
|
||||
// same behaviour as true
|
||||
constraints.video = { mandatory: {}, optional: [] };
|
||||
|
||||
constraints.video.optional.push({ googLeakyBucket: true });
|
||||
|
||||
setResolutionConstraints(constraints, resolution);
|
||||
}
|
||||
if (um.indexOf('audio') >= 0) {
|
||||
if (!RTCBrowserType.isFirefox()) {
|
||||
// same behaviour as true
|
||||
constraints.audio = { mandatory: {}, optional: []};
|
||||
// if it is good enough for hangouts...
|
||||
constraints.audio.optional.push(
|
||||
{googEchoCancellation: true},
|
||||
{googAutoGainControl: true},
|
||||
{googNoiseSupression: true},
|
||||
{googHighpassFilter: true},
|
||||
{googNoisesuppression2: true},
|
||||
{googEchoCancellation2: true},
|
||||
{googAutoGainControl2: true}
|
||||
);
|
||||
} else {
|
||||
constraints.audio = true;
|
||||
}
|
||||
}
|
||||
if (um.indexOf('screen') >= 0) {
|
||||
if (RTCBrowserType.isChrome()) {
|
||||
constraints.video = {
|
||||
mandatory: {
|
||||
chromeMediaSource: "screen",
|
||||
googLeakyBucket: true,
|
||||
maxWidth: window.screen.width,
|
||||
maxHeight: window.screen.height,
|
||||
maxFrameRate: 3
|
||||
},
|
||||
optional: []
|
||||
};
|
||||
} else if (RTCBrowserType.isTemasysPluginUsed()) {
|
||||
constraints.video = {
|
||||
optional: [
|
||||
{
|
||||
sourceId: AdapterJS.WebRTCPlugin.plugin.screensharingKey
|
||||
}
|
||||
]
|
||||
};
|
||||
} else if (RTCBrowserType.isFirefox()) {
|
||||
constraints.video = {
|
||||
mozMediaSource: "window",
|
||||
mediaSource: "window"
|
||||
};
|
||||
|
||||
} else {
|
||||
console.error(
|
||||
"'screen' WebRTC media source is supported only in Chrome" +
|
||||
" and with Temasys plugin");
|
||||
}
|
||||
}
|
||||
if (um.indexOf('desktop') >= 0) {
|
||||
constraints.video = {
|
||||
mandatory: {
|
||||
chromeMediaSource: "desktop",
|
||||
chromeMediaSourceId: desktopStream,
|
||||
googLeakyBucket: true,
|
||||
maxWidth: window.screen.width,
|
||||
maxHeight: window.screen.height,
|
||||
maxFrameRate: 3
|
||||
},
|
||||
optional: []
|
||||
};
|
||||
}
|
||||
|
||||
if (bandwidth) {
|
||||
if (!constraints.video) {
|
||||
//same behaviour as true
|
||||
constraints.video = {mandatory: {}, optional: []};
|
||||
}
|
||||
constraints.video.optional.push({bandwidth: bandwidth});
|
||||
}
|
||||
if (fps) {
|
||||
// for some cameras it might be necessary to request 30fps
|
||||
// so they choose 30fps mjpg over 10fps yuy2
|
||||
if (!constraints.video) {
|
||||
// same behaviour as true;
|
||||
constraints.video = {mandatory: {}, optional: []};
|
||||
}
|
||||
constraints.video.mandatory.minFrameRate = fps;
|
||||
}
|
||||
|
||||
// we turn audio for both audio and video tracks, the fake audio & video seems to work
|
||||
// only when enabled in one getUserMedia call, we cannot get fake audio separate by fake video
|
||||
// this later can be a problem with some of the tests
|
||||
if(RTCBrowserType.isFirefox() && config.firefox_fake_device)
|
||||
{
|
||||
constraints.audio = true;
|
||||
constraints.fake = true;
|
||||
}
|
||||
|
||||
return constraints;
|
||||
}
|
||||
|
||||
|
||||
function RTCUtils(RTCService, eventEmitter, onTemasysPluginReady)
|
||||
{
|
||||
var self = this;
|
||||
this.service = RTCService;
|
||||
this.eventEmitter = eventEmitter;
|
||||
if (RTCBrowserType.isFirefox()) {
|
||||
var FFversion = RTCBrowserType.getFirefoxVersion();
|
||||
if (FFversion >= 40) {
|
||||
this.peerconnection = mozRTCPeerConnection;
|
||||
this.getUserMedia = navigator.mozGetUserMedia.bind(navigator);
|
||||
this.pc_constraints = {};
|
||||
this.attachMediaStream = function (element, stream) {
|
||||
// srcObject is being standardized and FF will eventually
|
||||
// support that unprefixed. FF also supports the
|
||||
// "element.src = URL.createObjectURL(...)" combo, but that
|
||||
// will be deprecated in favour of srcObject.
|
||||
//
|
||||
// https://groups.google.com/forum/#!topic/mozilla.dev.media/pKOiioXonJg
|
||||
// https://github.com/webrtc/samples/issues/302
|
||||
if(!element[0])
|
||||
return;
|
||||
element[0].mozSrcObject = stream;
|
||||
element[0].play();
|
||||
};
|
||||
this.getStreamID = function (stream) {
|
||||
var id = stream.id;
|
||||
if (!id) {
|
||||
var tracks = stream.getVideoTracks();
|
||||
if (!tracks || tracks.length === 0) {
|
||||
tracks = stream.getAudioTracks();
|
||||
}
|
||||
id = tracks[0].id;
|
||||
}
|
||||
return APP.xmpp.filter_special_chars(id);
|
||||
};
|
||||
this.getVideoSrc = function (element) {
|
||||
if(!element)
|
||||
return null;
|
||||
return element.mozSrcObject;
|
||||
};
|
||||
this.setVideoSrc = function (element, src) {
|
||||
if(element)
|
||||
element.mozSrcObject = src;
|
||||
};
|
||||
window.RTCSessionDescription = mozRTCSessionDescription;
|
||||
window.RTCIceCandidate = mozRTCIceCandidate;
|
||||
} else {
|
||||
console.error(
|
||||
"Firefox version too old: " + FFversion + ". Required >= 40.");
|
||||
window.location.href = 'unsupported_browser.html';
|
||||
return;
|
||||
}
|
||||
|
||||
} else if (RTCBrowserType.isChrome() || RTCBrowserType.isOpera()) {
|
||||
this.peerconnection = webkitRTCPeerConnection;
|
||||
this.getUserMedia = navigator.webkitGetUserMedia.bind(navigator);
|
||||
this.attachMediaStream = function (element, stream) {
|
||||
element.attr('src', webkitURL.createObjectURL(stream));
|
||||
};
|
||||
this.getStreamID = function (stream) {
|
||||
// streams from FF endpoints have the characters '{' and '}'
|
||||
// that make jQuery choke.
|
||||
return APP.xmpp.filter_special_chars(stream.id);
|
||||
};
|
||||
this.getVideoSrc = function (element) {
|
||||
if(!element)
|
||||
return null;
|
||||
return element.getAttribute("src");
|
||||
};
|
||||
this.setVideoSrc = function (element, src) {
|
||||
if(element)
|
||||
element.setAttribute("src", src);
|
||||
};
|
||||
// DTLS should now be enabled by default but..
|
||||
this.pc_constraints = {'optional': [{'DtlsSrtpKeyAgreement': 'true'}]};
|
||||
if (RTCBrowserType.isAndroid()) {
|
||||
this.pc_constraints = {}; // disable DTLS on Android
|
||||
}
|
||||
if (!webkitMediaStream.prototype.getVideoTracks) {
|
||||
webkitMediaStream.prototype.getVideoTracks = function () {
|
||||
return this.videoTracks;
|
||||
};
|
||||
}
|
||||
if (!webkitMediaStream.prototype.getAudioTracks) {
|
||||
webkitMediaStream.prototype.getAudioTracks = function () {
|
||||
return this.audioTracks;
|
||||
};
|
||||
}
|
||||
}
|
||||
// Detect IE/Safari
|
||||
else if (RTCBrowserType.isTemasysPluginUsed()) {
|
||||
|
||||
//AdapterJS.WebRTCPlugin.setLogLevel(
|
||||
// AdapterJS.WebRTCPlugin.PLUGIN_LOG_LEVELS.VERBOSE);
|
||||
|
||||
AdapterJS.webRTCReady(function (isPlugin) {
|
||||
|
||||
self.peerconnection = RTCPeerConnection;
|
||||
self.getUserMedia = getUserMedia;
|
||||
self.attachMediaStream = function (elSel, stream) {
|
||||
|
||||
if (stream.id === "dummyAudio" || stream.id === "dummyVideo") {
|
||||
return;
|
||||
}
|
||||
|
||||
attachMediaStream(elSel[0], stream);
|
||||
};
|
||||
self.getStreamID = function (stream) {
|
||||
return APP.xmpp.filter_special_chars(stream.label);
|
||||
};
|
||||
self.getVideoSrc = function (element) {
|
||||
if (!element) {
|
||||
console.warn("Attempt to get video SRC of null element");
|
||||
return null;
|
||||
}
|
||||
var children = element.children;
|
||||
for (var i = 0; i !== children.length; ++i) {
|
||||
if (children[i].name === 'streamId') {
|
||||
return children[i].value;
|
||||
}
|
||||
}
|
||||
//console.info(element.id + " SRC: " + src);
|
||||
return null;
|
||||
};
|
||||
self.setVideoSrc = function (element, src) {
|
||||
//console.info("Set video src: ", element, src);
|
||||
if (!src) {
|
||||
console.warn("Not attaching video stream, 'src' is null");
|
||||
return;
|
||||
}
|
||||
AdapterJS.WebRTCPlugin.WaitForPluginReady();
|
||||
var stream = AdapterJS.WebRTCPlugin.plugin
|
||||
.getStreamWithId(AdapterJS.WebRTCPlugin.pageId, src);
|
||||
attachMediaStream(element, stream);
|
||||
};
|
||||
|
||||
onTemasysPluginReady(isPlugin);
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
console.log('Browser does not appear to be WebRTC-capable');
|
||||
} catch (e) { }
|
||||
window.location.href = 'unsupported_browser.html';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
RTCUtils.prototype.getUserMediaWithConstraints = function(
|
||||
um, success_callback, failure_callback, resolution,bandwidth, fps,
|
||||
desktopStream) {
|
||||
currentResolution = resolution;
|
||||
|
||||
var constraints = getConstraints(
|
||||
um, resolution, bandwidth, fps, desktopStream);
|
||||
|
||||
console.info("Get media constraints", constraints);
|
||||
|
||||
var self = this;
|
||||
|
||||
try {
|
||||
this.getUserMedia(constraints,
|
||||
function (stream) {
|
||||
console.log('onUserMediaSuccess');
|
||||
self.setAvailableDevices(um, true);
|
||||
success_callback(stream);
|
||||
},
|
||||
function (error) {
|
||||
self.setAvailableDevices(um, false);
|
||||
console.warn('Failed to get access to local media. Error ',
|
||||
error, constraints);
|
||||
self.eventEmitter.emit(RTCEvents.GET_USER_MEDIA_FAILED, error);
|
||||
if (failure_callback) {
|
||||
failure_callback(error);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('GUM failed: ', e);
|
||||
self.eventEmitter.emit(RTCEvents.GET_USER_MEDIA_FAILED, e);
|
||||
if(failure_callback) {
|
||||
failure_callback(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
RTCUtils.prototype.setAvailableDevices = function (um, available) {
|
||||
var devices = {};
|
||||
if(um.indexOf("video") != -1) {
|
||||
devices.video = available;
|
||||
}
|
||||
if(um.indexOf("audio") != -1) {
|
||||
devices.audio = available;
|
||||
}
|
||||
this.service.setDeviceAvailability(devices);
|
||||
};
|
||||
|
||||
/**
|
||||
* We ask for audio and video combined stream in order to get permissions and
|
||||
* not to ask twice.
|
||||
*/
|
||||
RTCUtils.prototype.obtainAudioAndVideoPermissions =
|
||||
function(devices, callback, usageOptions)
|
||||
{
|
||||
var self = this;
|
||||
// Get AV
|
||||
|
||||
var successCallback = function (stream) {
|
||||
if(callback)
|
||||
callback(stream, usageOptions);
|
||||
else
|
||||
self.successCallback(stream, usageOptions);
|
||||
};
|
||||
|
||||
if(!devices)
|
||||
devices = ['audio', 'video'];
|
||||
|
||||
var newDevices = [];
|
||||
|
||||
|
||||
if(usageOptions)
|
||||
for(var i = 0; i < devices.length; i++) {
|
||||
var device = devices[i];
|
||||
if(usageOptions[device] === true)
|
||||
newDevices.push(device);
|
||||
}
|
||||
else
|
||||
newDevices = devices;
|
||||
|
||||
if(newDevices.length === 0) {
|
||||
successCallback();
|
||||
return;
|
||||
}
|
||||
|
||||
if (RTCBrowserType.isFirefox() || RTCBrowserType.isTemasysPluginUsed()) {
|
||||
|
||||
// With FF/IE we can't split the stream into audio and video because FF
|
||||
// doesn't support media stream constructors. So, we need to get the
|
||||
// audio stream separately from the video stream using two distinct GUM
|
||||
// calls. Not very user friendly :-( but we don't have many other
|
||||
// options neither.
|
||||
//
|
||||
// Note that we pack those 2 streams in a single object and pass it to
|
||||
// the successCallback method.
|
||||
var obtainVideo = function (audioStream) {
|
||||
self.getUserMediaWithConstraints(
|
||||
['video'],
|
||||
function (videoStream) {
|
||||
return successCallback({
|
||||
audioStream: audioStream,
|
||||
videoStream: videoStream
|
||||
});
|
||||
},
|
||||
function (error) {
|
||||
console.error(
|
||||
'failed to obtain video stream - stop', error);
|
||||
self.errorCallback(error);
|
||||
},
|
||||
config.resolution || '360');
|
||||
};
|
||||
var obtainAudio = function () {
|
||||
self.getUserMediaWithConstraints(
|
||||
['audio'],
|
||||
function (audioStream) {
|
||||
if (newDevices.indexOf('video') !== -1)
|
||||
obtainVideo(audioStream);
|
||||
},
|
||||
function (error) {
|
||||
console.error(
|
||||
'failed to obtain audio stream - stop', error);
|
||||
self.errorCallback(error);
|
||||
}
|
||||
);
|
||||
};
|
||||
if (newDevices.indexOf('audio') !== -1) {
|
||||
obtainAudio();
|
||||
} else {
|
||||
obtainVideo(null);
|
||||
}
|
||||
} else {
|
||||
this.getUserMediaWithConstraints(
|
||||
newDevices,
|
||||
function (stream) {
|
||||
successCallback(stream);
|
||||
},
|
||||
function (error) {
|
||||
self.errorCallback(error);
|
||||
},
|
||||
config.resolution || '360');
|
||||
}
|
||||
};
|
||||
|
||||
RTCUtils.prototype.successCallback = function (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);
|
||||
this.handleLocalStream(stream, usageOptions);
|
||||
};
|
||||
|
||||
RTCUtils.prototype.errorCallback = function (error) {
|
||||
var self = this;
|
||||
console.error('failed to obtain audio/video stream - trying audio only', error);
|
||||
var resolution = getPreviousResolution(currentResolution);
|
||||
if(typeof error == "object" && error.constraintName && error.name
|
||||
&& (error.name == "ConstraintNotSatisfiedError" ||
|
||||
error.name == "OverconstrainedError") &&
|
||||
(error.constraintName == "minWidth" || error.constraintName == "maxWidth" ||
|
||||
error.constraintName == "minHeight" || error.constraintName == "maxHeight")
|
||||
&& resolution)
|
||||
{
|
||||
self.getUserMediaWithConstraints(['audio', 'video'],
|
||||
function (stream) {
|
||||
return self.successCallback(stream);
|
||||
}, function (error) {
|
||||
return self.errorCallback(error);
|
||||
}, resolution);
|
||||
}
|
||||
else {
|
||||
self.getUserMediaWithConstraints(
|
||||
['audio'],
|
||||
function (stream) {
|
||||
return self.successCallback(stream);
|
||||
},
|
||||
function (error) {
|
||||
console.error('failed to obtain audio/video stream - stop',
|
||||
error);
|
||||
return self.successCallback(null);
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
RTCUtils.prototype.handleLocalStream = function(stream, usageOptions) {
|
||||
// If this is FF, the stream parameter is *not* a MediaStream object, it's
|
||||
// an object with two properties: audioStream, videoStream.
|
||||
var audioStream, videoStream;
|
||||
if(window.webkitMediaStream)
|
||||
{
|
||||
audioStream = new webkitMediaStream();
|
||||
videoStream = new webkitMediaStream();
|
||||
if(stream) {
|
||||
var audioTracks = stream.getAudioTracks();
|
||||
|
||||
for (var i = 0; i < audioTracks.length; i++) {
|
||||
audioStream.addTrack(audioTracks[i]);
|
||||
}
|
||||
|
||||
var videoTracks = stream.getVideoTracks();
|
||||
|
||||
for (i = 0; i < videoTracks.length; i++) {
|
||||
videoStream.addTrack(videoTracks[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (RTCBrowserType.isFirefox() || RTCBrowserType.isTemasysPluginUsed())
|
||||
{ // Firefox and Temasys plugin
|
||||
if (stream && stream.audioStream)
|
||||
audioStream = stream.audioStream;
|
||||
else
|
||||
audioStream = new DummyMediaStream("dummyAudio");
|
||||
|
||||
if (stream && stream.videoStream)
|
||||
videoStream = stream.videoStream;
|
||||
else
|
||||
videoStream = new DummyMediaStream("dummyVideo");
|
||||
}
|
||||
|
||||
var audioMuted = (usageOptions && usageOptions.audio === false),
|
||||
videoMuted = (usageOptions && usageOptions.video === false);
|
||||
|
||||
var audioGUM = (!usageOptions || usageOptions.audio !== false),
|
||||
videoGUM = (!usageOptions || usageOptions.video !== false);
|
||||
|
||||
|
||||
this.service.createLocalStream(
|
||||
audioStream, MediaStreamType.AUDIO_TYPE, null, null,
|
||||
audioMuted, audioGUM);
|
||||
|
||||
this.service.createLocalStream(
|
||||
videoStream, MediaStreamType.VIDEO_TYPE, null, 'camera',
|
||||
videoMuted, videoGUM);
|
||||
};
|
||||
|
||||
function DummyMediaStream(id) {
|
||||
this.id = id;
|
||||
this.label = id;
|
||||
this.stop = function() { };
|
||||
this.getAudioTracks = function() { return []; };
|
||||
this.getVideoTracks = function() { return []; };
|
||||
}
|
||||
|
||||
RTCUtils.prototype.createStream = function(stream, isVideo) {
|
||||
var newStream = null;
|
||||
if (window.webkitMediaStream) {
|
||||
newStream = new webkitMediaStream();
|
||||
if (newStream) {
|
||||
var tracks = (isVideo ? stream.getVideoTracks() : stream.getAudioTracks());
|
||||
|
||||
for (var i = 0; i < tracks.length; i++) {
|
||||
newStream.addTrack(tracks[i]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
else {
|
||||
// FIXME: this is duplicated with 'handleLocalStream' !!!
|
||||
if (stream) {
|
||||
newStream = stream;
|
||||
} else {
|
||||
newStream =
|
||||
new DummyMediaStream(isVideo ? "dummyVideo" : "dummyAudio");
|
||||
}
|
||||
}
|
||||
|
||||
return newStream;
|
||||
};
|
||||
|
||||
module.exports = RTCUtils;
|
File diff suppressed because it is too large
Load Diff
|
@ -92,6 +92,7 @@ export default function createRoomLocker (room) {
|
|||
return room.lock(newPass).then(function () {
|
||||
password = newPass;
|
||||
}).catch(function (err) {
|
||||
console.error(err);
|
||||
if (err === ConferenceErrors.PASSWORD_NOT_SUPPORTED) {
|
||||
notifyPasswordNotSupported();
|
||||
} else {
|
||||
|
@ -111,7 +112,7 @@ export default function createRoomLocker (room) {
|
|||
},
|
||||
|
||||
askToUnlock () {
|
||||
askToUnlock().then(function () {
|
||||
return askToUnlock().then(function () {
|
||||
return lock();
|
||||
}).then(function () {
|
||||
AnalyticsAdapter.sendEvent('toolbar.lock.disabled');
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
/* global $, config, interfaceConfig */
|
||||
/* global $, APP, config, interfaceConfig */
|
||||
|
||||
/*
|
||||
* Created by Yana Stamcheva on 2/10/15.
|
||||
*/
|
||||
var messageHandler = require("./util/MessageHandler");
|
||||
var callStats = require("../statistics/CallStats");
|
||||
var APP = require("../../app");
|
||||
|
||||
/**
|
||||
* Constructs the html for the overall feedback window.
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
/* jshint -W101 */
|
||||
var UI = {};
|
||||
|
||||
import AudioLevels from './audio_levels/AudioLevels';
|
||||
import Chat from "./side_pannels/chat/Chat";
|
||||
import Toolbar from "./toolbars/Toolbar";
|
||||
import ToolbarToggler from "./toolbars/ToolbarToggler";
|
||||
|
@ -12,26 +11,26 @@ import Avatar from "./avatar/Avatar";
|
|||
import PanelToggler from "./side_pannels/SidePanelToggler";
|
||||
import UIUtil from "./util/UIUtil";
|
||||
import UIEvents from "../../service/UI/UIEvents";
|
||||
import CQEvents from '../../service/connectionquality/CQEvents';
|
||||
import PreziManager from './prezi/Prezi';
|
||||
import EtherpadManager from './etherpad/Etherpad';
|
||||
|
||||
import VideoLayout from "./videolayout/VideoLayout";
|
||||
import SettingsMenu from "./side_pannels/settings/SettingsMenu";
|
||||
import Settings from "./../settings/Settings";
|
||||
|
||||
var Prezi = require("./prezi/Prezi");
|
||||
var Etherpad = require("./etherpad/Etherpad");
|
||||
var EventEmitter = require("events");
|
||||
var Settings = require("./../settings/Settings");
|
||||
UI.messageHandler = require("./util/MessageHandler");
|
||||
var messageHandler = UI.messageHandler;
|
||||
var JitsiPopover = require("./util/JitsiPopover");
|
||||
var CQEvents = require("../../service/connectionquality/CQEvents");
|
||||
var DesktopSharingEventTypes
|
||||
= require("../../service/desktopsharing/DesktopSharingEventTypes");
|
||||
var StatisticsEvents = require("../../service/statistics/Events");
|
||||
var Feedback = require("./Feedback");
|
||||
|
||||
var eventEmitter = new EventEmitter();
|
||||
UI.eventEmitter = eventEmitter;
|
||||
|
||||
let preziManager;
|
||||
let etherpadManager;
|
||||
|
||||
function promptDisplayName() {
|
||||
let nickRequiredMsg = APP.translation.translateString("dialog.displayNameRequired");
|
||||
let defaultNickMsg = APP.translation.translateString(
|
||||
|
@ -77,12 +76,6 @@ function promptDisplayName() {
|
|||
);
|
||||
}
|
||||
|
||||
function setupPrezi() {
|
||||
$("#reloadPresentationLink").click(function() {
|
||||
Prezi.reloadPresentation();
|
||||
});
|
||||
}
|
||||
|
||||
function setupChat() {
|
||||
Chat.init(eventEmitter);
|
||||
$("#toggle_smileys").click(function() {
|
||||
|
@ -93,7 +86,7 @@ function setupChat() {
|
|||
function setupToolbars() {
|
||||
Toolbar.init(eventEmitter);
|
||||
Toolbar.setupButtonsFromConfig();
|
||||
BottomToolbar.init(eventEmitter);
|
||||
BottomToolbar.setupListeners(eventEmitter);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -189,20 +182,18 @@ UI.initConference = function () {
|
|||
};
|
||||
|
||||
function registerListeners() {
|
||||
UI.addListener(UIEvents.LARGEVIDEO_INIT, function () {
|
||||
AudioLevels.init();
|
||||
});
|
||||
|
||||
UI.addListener(UIEvents.EMAIL_CHANGED, function (email) {
|
||||
UI.setUserAvatar(APP.conference.localId, email);
|
||||
});
|
||||
|
||||
UI.addListener(UIEvents.PREZI_CLICKED, function () {
|
||||
Prezi.openPreziDialog();
|
||||
preziManager.handlePreziButtonClicked();
|
||||
});
|
||||
|
||||
UI.addListener(UIEvents.ETHERPAD_CLICKED, function () {
|
||||
Etherpad.toggleEtherpad(0);
|
||||
if (etherpadManager) {
|
||||
etherpadManager.toggleEtherpad();
|
||||
}
|
||||
});
|
||||
|
||||
UI.addListener(UIEvents.FULLSCREEN_TOGGLE, toggleFullScreen);
|
||||
|
@ -221,7 +212,7 @@ function registerListeners() {
|
|||
function bindEvents() {
|
||||
function onResize() {
|
||||
PanelToggler.resizeChat();
|
||||
VideoLayout.resizeLargeVideoContainer();
|
||||
VideoLayout.resizeLargeVideoContainer(PanelToggler.isVisible());
|
||||
}
|
||||
|
||||
// Resize and reposition videos in full screen mode.
|
||||
|
@ -253,12 +244,20 @@ UI.start = function () {
|
|||
|
||||
registerListeners();
|
||||
|
||||
BottomToolbar.init();
|
||||
|
||||
VideoLayout.init(eventEmitter);
|
||||
if (!interfaceConfig.filmStripOnly) {
|
||||
VideoLayout.initLargeVideo(PanelToggler.isVisible());
|
||||
}
|
||||
VideoLayout.resizeLargeVideoContainer(PanelToggler.isVisible());
|
||||
|
||||
ContactList.init(eventEmitter);
|
||||
|
||||
bindEvents();
|
||||
setupPrezi();
|
||||
preziManager = new PreziManager(eventEmitter);
|
||||
if (!interfaceConfig.filmStripOnly) {
|
||||
|
||||
$("#videospace").mousemove(function () {
|
||||
return ToolbarToggler.showToolbar();
|
||||
});
|
||||
|
@ -278,8 +277,7 @@ UI.start = function () {
|
|||
$("#header").css("display", "none");
|
||||
$("#bottomToolbar").css("display", "none");
|
||||
$("#downloadlog").css("display", "none");
|
||||
$("#remoteVideos").css("padding", "0px 0px 18px 0px");
|
||||
$("#remoteVideos").css("right", "0px");
|
||||
BottomToolbar.setupFilmStripOnly();
|
||||
messageHandler.disableNotifications();
|
||||
$('body').popover("disable");
|
||||
JitsiPopover.enabled = false;
|
||||
|
@ -350,9 +348,14 @@ UI.setSubject = function (subject) {
|
|||
Chat.setSubject(subject);
|
||||
};
|
||||
|
||||
function initEtherpad(name) {
|
||||
Etherpad.init(name);
|
||||
}
|
||||
UI.initEtherpad = function (name) {
|
||||
if (etherpadManager) {
|
||||
return;
|
||||
}
|
||||
console.log('Etherpad is enabled');
|
||||
etherpadManager = new EtherpadManager(config.etherpad_base, name);
|
||||
Toolbar.showEtherpadButton();
|
||||
};
|
||||
|
||||
UI.addUser = function (id, displayName) {
|
||||
ContactList.addContact(id);
|
||||
|
@ -391,8 +394,8 @@ UI.removeUser = function (id, displayName) {
|
|||
// VideoLayout.setPresenceStatus(Strophe.getResourceFromJid(jid), info.status);
|
||||
// }
|
||||
|
||||
UI.onPeerVideoTypeChanged = (resourceJid, newVideoType) => {
|
||||
VideoLayout.onVideoTypeChanged(resourceJid, newVideoType);
|
||||
UI.onPeerVideoTypeChanged = (id, newVideoType) => {
|
||||
VideoLayout.onVideoTypeChanged(id, newVideoType);
|
||||
};
|
||||
|
||||
UI.updateLocalRole = function (isModerator) {
|
||||
|
@ -443,7 +446,6 @@ UI.getSettings = function () {
|
|||
|
||||
UI.toggleFilmStrip = function () {
|
||||
BottomToolbar.toggleFilmStrip();
|
||||
VideoLayout.updateLargeVideoSize();
|
||||
};
|
||||
|
||||
UI.toggleChat = function () {
|
||||
|
@ -592,13 +594,11 @@ UI.handleLastNEndpoints = function (ids) {
|
|||
};
|
||||
|
||||
UI.setAudioLevel = function (id, lvl) {
|
||||
AudioLevels.updateAudioLevel(
|
||||
id, lvl, VideoLayout.getLargeVideoId()
|
||||
);
|
||||
VideoLayout.setAudioLevel(id, lvl);
|
||||
};
|
||||
|
||||
UI.updateDesktopSharingButtons = function () {
|
||||
Toolbar.changeDesktopSharingButtonState();
|
||||
UI.updateDesktopSharingButtons = function (isSharingScreen) {
|
||||
Toolbar.changeDesktopSharingButtonState(isSharingScreen);
|
||||
};
|
||||
|
||||
UI.hideStats = function () {
|
||||
|
@ -609,8 +609,8 @@ UI.updateLocalStats = function (percent, stats) {
|
|||
VideoLayout.updateLocalConnectionStats(percent, stats);
|
||||
};
|
||||
|
||||
UI.updateRemoteStats = function (jid, percent, stats) {
|
||||
VideoLayout.updateConnectionStats(jid, percent, stats);
|
||||
UI.updateRemoteStats = function (id, percent, stats) {
|
||||
VideoLayout.updateConnectionStats(id, percent, stats);
|
||||
};
|
||||
|
||||
UI.markVideoInterrupted = function (interrupted) {
|
||||
|
@ -750,4 +750,14 @@ UI.updateAuthInfo = function (isAuthEnabled, login) {
|
|||
}
|
||||
};
|
||||
|
||||
UI.showPrezi = function (userId, url, slide) {
|
||||
preziManager.showPrezi(userId, url, slide);
|
||||
};
|
||||
|
||||
UI.stopPrezi = function (userId) {
|
||||
if (preziManager.isSharing(userId)) {
|
||||
preziManager.removePrezi(userId);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = UI;
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
/* jshint -W101 */
|
||||
|
||||
import CanvasUtil from './CanvasUtils';
|
||||
import BottomToolbar from '../toolbars/BottomToolbar';
|
||||
|
||||
const LOCAL_LEVEL = 'local';
|
||||
|
||||
|
@ -112,33 +113,6 @@ function getVideoSpanId(id) {
|
|||
return videoSpanId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that the remote video has been resized.
|
||||
*/
|
||||
$(document).bind('remotevideo.resized', function (event, width, height) {
|
||||
let resized = false;
|
||||
|
||||
$('#remoteVideos>span>canvas').each(function() {
|
||||
let canvas = $(this).get(0);
|
||||
if (canvas.width !== width + interfaceConfig.CANVAS_EXTRA) {
|
||||
canvas.width = width + interfaceConfig.CANVAS_EXTRA;
|
||||
resized = true;
|
||||
}
|
||||
|
||||
if (canvas.height !== height + interfaceConfig.CANVAS_EXTRA) {
|
||||
canvas.height = height + interfaceConfig.CANVAS_EXTRA;
|
||||
resized = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (resized) {
|
||||
Object.keys(audioLevelCanvasCache).forEach(function (id) {
|
||||
audioLevelCanvasCache[id].width = width + interfaceConfig.CANVAS_EXTRA;
|
||||
audioLevelCanvasCache[id].height = height + interfaceConfig.CANVAS_EXTRA;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* The audio Levels plugin.
|
||||
*/
|
||||
|
@ -153,7 +127,7 @@ const AudioLevels = {
|
|||
* Updates the audio level canvas for the given id. If the canvas
|
||||
* didn't exist we create it.
|
||||
*/
|
||||
updateAudioLevelCanvas (id, VideoLayout) {
|
||||
updateAudioLevelCanvas (id, thumbWidth, thumbHeight) {
|
||||
let videoSpanId = 'localVideoContainer';
|
||||
if (id) {
|
||||
videoSpanId = `participant_${id}`;
|
||||
|
@ -172,24 +146,19 @@ const AudioLevels = {
|
|||
|
||||
let audioLevelCanvas = $(`#${videoSpanId}>canvas`);
|
||||
|
||||
let videoSpaceWidth = $('#remoteVideos').width();
|
||||
let thumbnailSize = VideoLayout.calculateThumbnailSize(videoSpaceWidth);
|
||||
let thumbnailWidth = thumbnailSize[0];
|
||||
let thumbnailHeight = thumbnailSize[1];
|
||||
|
||||
if (!audioLevelCanvas || audioLevelCanvas.length === 0) {
|
||||
|
||||
audioLevelCanvas = document.createElement('canvas');
|
||||
audioLevelCanvas.className = "audiolevel";
|
||||
audioLevelCanvas.style.bottom = `-${interfaceConfig.CANVAS_EXTRA/2}px`;
|
||||
audioLevelCanvas.style.left = `-${interfaceConfig.CANVAS_EXTRA/2}px`;
|
||||
resizeAudioLevelCanvas(audioLevelCanvas, thumbnailWidth, thumbnailHeight);
|
||||
resizeAudioLevelCanvas(audioLevelCanvas, thumbWidth, thumbHeight);
|
||||
|
||||
videoSpan.appendChild(audioLevelCanvas);
|
||||
} else {
|
||||
audioLevelCanvas = audioLevelCanvas.get(0);
|
||||
|
||||
resizeAudioLevelCanvas(audioLevelCanvas, thumbnailWidth, thumbnailHeight);
|
||||
resizeAudioLevelCanvas(audioLevelCanvas, thumbWidth, thumbHeight);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -248,6 +217,18 @@ const AudioLevels = {
|
|||
|
||||
// Fill the shape.
|
||||
ASDrawContext.fill();
|
||||
},
|
||||
|
||||
updateCanvasSize (thumbWidth, thumbHeight) {
|
||||
let canvasWidth = thumbWidth + interfaceConfig.CANVAS_EXTRA;
|
||||
let canvasHeight = thumbHeight + interfaceConfig.CANVAS_EXTRA;
|
||||
|
||||
BottomToolbar.getThumbs().children('canvas').width(canvasWidth).height(canvasHeight);
|
||||
|
||||
Object.keys(audioLevelCanvasCache).forEach(function (id) {
|
||||
audioLevelCanvasCache[id].width = canvasWidth;
|
||||
audioLevelCanvasCache[id].height = canvasHeight;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -110,14 +110,21 @@ function Dialog(successCallback, cancelCallback) {
|
|||
*/
|
||||
this.displayError = function (message) {
|
||||
|
||||
var finishedState = connDialog.getState('finished');
|
||||
let finishedState = connDialog.getState('finished');
|
||||
|
||||
var errorMessageElem = finishedState.find('#errorMessage');
|
||||
let errorMessageElem = finishedState.find('#errorMessage');
|
||||
errorMessageElem.text(message);
|
||||
|
||||
connDialog.goToState('finished');
|
||||
};
|
||||
|
||||
this.displayConnectionStatus = function (message) {
|
||||
let connectingState = connDialog.getState('connecting');
|
||||
|
||||
let connectionStatus = connectingState.find('#connectionStatus');
|
||||
connectionStatus.text(message);
|
||||
};
|
||||
|
||||
/**
|
||||
* Closes LoginDialog.
|
||||
*/
|
||||
|
|
|
@ -1,61 +1,17 @@
|
|||
/* global $, config,
|
||||
setLargeVideoVisible, Util */
|
||||
/* global $ */
|
||||
|
||||
var VideoLayout = require("../videolayout/VideoLayout");
|
||||
var Prezi = require("../prezi/Prezi");
|
||||
var UIUtil = require("../util/UIUtil");
|
||||
import VideoLayout from "../videolayout/VideoLayout";
|
||||
import LargeContainer from '../videolayout/LargeContainer';
|
||||
import UIUtil from "../util/UIUtil";
|
||||
import SidePanelToggler from "../side_pannels/SidePanelToggler";
|
||||
import BottomToolbar from '../toolbars/BottomToolbar';
|
||||
|
||||
var etherpadName = null;
|
||||
var etherpadIFrame = null;
|
||||
var domain = null;
|
||||
var options = "?showControls=true&showChat=false&showLineNumbers=true" +
|
||||
"&useMonospaceFont=false";
|
||||
|
||||
|
||||
/**
|
||||
* Resizes the etherpad.
|
||||
*/
|
||||
function resize() {
|
||||
if ($('#etherpad>iframe').length) {
|
||||
var remoteVideos = $('#remoteVideos');
|
||||
var availableHeight
|
||||
= window.innerHeight - remoteVideos.outerHeight();
|
||||
var availableWidth = UIUtil.getAvailableVideoWidth();
|
||||
|
||||
$('#etherpad>iframe').width(availableWidth);
|
||||
$('#etherpad>iframe').height(availableHeight);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the Etherpad button and adds it to the toolbar.
|
||||
*/
|
||||
function enableEtherpadButton() {
|
||||
if (!$('#toolbar_button_etherpad').is(":visible"))
|
||||
$('#toolbar_button_etherpad').css({display: 'inline-block'});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the IFrame for the etherpad.
|
||||
*/
|
||||
function createIFrame() {
|
||||
etherpadIFrame = VideoLayout.createEtherpadIframe(
|
||||
domain + etherpadName + options, function() {
|
||||
|
||||
document.domain = document.domain;
|
||||
bubbleIframeMouseMove(etherpadIFrame);
|
||||
setTimeout(function() {
|
||||
// the iframes inside of the etherpad are
|
||||
// not yet loaded when the etherpad iframe is loaded
|
||||
var outer = etherpadIFrame.
|
||||
contentDocument.getElementsByName("ace_outer")[0];
|
||||
bubbleIframeMouseMove(outer);
|
||||
var inner = outer.
|
||||
contentDocument.getElementsByName("ace_inner")[0];
|
||||
bubbleIframeMouseMove(inner);
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
const options = $.param({
|
||||
showControns: true,
|
||||
showChat: false,
|
||||
showLineNumbers: true,
|
||||
useMonospaceFont: false
|
||||
});
|
||||
|
||||
function bubbleIframeMouseMove(iframe){
|
||||
var existingOnMouseMove = iframe.contentWindow.onmousemove;
|
||||
|
@ -71,8 +27,8 @@ function bubbleIframeMouseMove(iframe){
|
|||
e.detail,
|
||||
e.screenX,
|
||||
e.screenY,
|
||||
e.clientX + boundingClientRect.left,
|
||||
e.clientY + boundingClientRect.top,
|
||||
e.clientX + boundingClientRect.left,
|
||||
e.clientY + boundingClientRect.top,
|
||||
e.ctrlKey,
|
||||
e.altKey,
|
||||
e.shiftKey,
|
||||
|
@ -84,48 +40,121 @@ function bubbleIframeMouseMove(iframe){
|
|||
};
|
||||
}
|
||||
|
||||
const DEFAULT_WIDTH = 640;
|
||||
const DEFAULT_HEIGHT = 480;
|
||||
|
||||
var Etherpad = {
|
||||
/**
|
||||
* Initializes the etherpad.
|
||||
*/
|
||||
init: function (name) {
|
||||
const EtherpadContainerType = "etherpad";
|
||||
|
||||
if (config.etherpad_base && !etherpadName && name) {
|
||||
class Etherpad extends LargeContainer {
|
||||
constructor (domain, name) {
|
||||
super();
|
||||
|
||||
domain = config.etherpad_base;
|
||||
const iframe = document.createElement('iframe');
|
||||
|
||||
etherpadName = name;
|
||||
iframe.src = domain + name + '?' + options;
|
||||
iframe.frameBorder = 0;
|
||||
iframe.scrolling = "no";
|
||||
iframe.width = DEFAULT_WIDTH;
|
||||
iframe.height = DEFAULT_HEIGHT;
|
||||
iframe.setAttribute('style', 'visibility: hidden;');
|
||||
|
||||
enableEtherpadButton();
|
||||
this.container.appendChild(iframe);
|
||||
|
||||
/**
|
||||
* Resizes the etherpad, when the window is resized.
|
||||
*/
|
||||
$(window).resize(function () {
|
||||
resize();
|
||||
});
|
||||
}
|
||||
},
|
||||
iframe.onload = function() {
|
||||
document.domain = document.domain;
|
||||
bubbleIframeMouseMove(iframe);
|
||||
|
||||
/**
|
||||
* Opens/hides the Etherpad.
|
||||
*/
|
||||
toggleEtherpad: function (isPresentation) {
|
||||
if (!etherpadIFrame)
|
||||
createIFrame();
|
||||
setTimeout(function() {
|
||||
const doc = iframe.contentDocument;
|
||||
|
||||
// the iframes inside of the etherpad are
|
||||
// not yet loaded when the etherpad iframe is loaded
|
||||
const outer = doc.getElementsByName("ace_outer")[0];
|
||||
bubbleIframeMouseMove(outer);
|
||||
|
||||
if(VideoLayout.getLargeVideoState() === "etherpad")
|
||||
{
|
||||
VideoLayout.setLargeVideoState("video");
|
||||
}
|
||||
else
|
||||
{
|
||||
VideoLayout.setLargeVideoState("etherpad");
|
||||
}
|
||||
resize();
|
||||
const inner = doc.getElementsByName("ace_inner")[0];
|
||||
bubbleIframeMouseMove(inner);
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
this.iframe = iframe;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = Etherpad;
|
||||
get isOpen () {
|
||||
return !!this.iframe;
|
||||
}
|
||||
|
||||
get container () {
|
||||
return document.getElementById('etherpad');
|
||||
}
|
||||
|
||||
resize (containerWidth, containerHeight, animate) {
|
||||
let height = containerHeight - BottomToolbar.getFilmStripHeight();
|
||||
let width = containerWidth;
|
||||
|
||||
$(this.iframe).width(width).height(height);
|
||||
}
|
||||
|
||||
show () {
|
||||
const $iframe = $(this.iframe);
|
||||
const $container = $(this.container);
|
||||
|
||||
return new Promise(resolve => {
|
||||
$iframe.fadeIn(300, function () {
|
||||
document.body.style.background = '#eeeeee';
|
||||
$iframe.css({visibility: 'visible'});
|
||||
$container.css({zIndex: 2});
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
hide () {
|
||||
const $iframe = $(this.iframe);
|
||||
const $container = $(this.container);
|
||||
|
||||
return new Promise(resolve => {
|
||||
$iframe.fadeOut(300, function () {
|
||||
$iframe.css({visibility: 'hidden'});
|
||||
$container.css({zIndex: 0});
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default class EtherpadManager {
|
||||
constructor (domain, name) {
|
||||
if (!domain || !name) {
|
||||
throw new Error("missing domain or name");
|
||||
}
|
||||
|
||||
this.domain = domain;
|
||||
this.name = name;
|
||||
this.etherpad = null;
|
||||
}
|
||||
|
||||
get isOpen () {
|
||||
return !!this.etherpad;
|
||||
}
|
||||
|
||||
openEtherpad () {
|
||||
this.etherpad = new Etherpad(this.domain, this.name);
|
||||
VideoLayout.addLargeVideoContainer(
|
||||
EtherpadContainerType,
|
||||
this.etherpad
|
||||
);
|
||||
}
|
||||
|
||||
toggleEtherpad () {
|
||||
if (!this.isOpen) {
|
||||
this.openEtherpad();
|
||||
}
|
||||
|
||||
let isVisible = VideoLayout.isLargeContainerTypeVisible(
|
||||
EtherpadContainerType
|
||||
);
|
||||
|
||||
VideoLayout.showLargeVideoContainer(EtherpadContainerType, !isVisible);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,268 +1,22 @@
|
|||
/* global $, APP */
|
||||
/* jshint -W101 */
|
||||
import UIUtil from "../util/UIUtil";
|
||||
|
||||
import VideoLayout from "../videolayout/VideoLayout";
|
||||
import LargeContainer from '../videolayout/LargeContainer';
|
||||
import PreziPlayer from './PreziPlayer';
|
||||
import UIUtil from '../util/UIUtil';
|
||||
import UIEvents from '../../../service/UI/UIEvents';
|
||||
import messageHandler from '../util/MessageHandler';
|
||||
import ToolbarToggler from "../toolbars/ToolbarToggler";
|
||||
import SidePanelToggler from "../side_pannels/SidePanelToggler";
|
||||
import BottomToolbar from '../toolbars/BottomToolbar';
|
||||
|
||||
var messageHandler = require("../util/MessageHandler");
|
||||
var PreziPlayer = require("./PreziPlayer");
|
||||
const defaultPreziLink = "http://prezi.com/wz7vhjycl7e6/my-prezi";
|
||||
const alphanumRegex = /^[a-z0-9-_\/&\?=;]+$/i;
|
||||
const aspectRatio = 16.0 / 9.0;
|
||||
|
||||
var preziPlayer = null;
|
||||
|
||||
|
||||
/**
|
||||
* Shows/hides a presentation.
|
||||
*/
|
||||
function setPresentationVisible(visible) {
|
||||
|
||||
if (visible) {
|
||||
VideoLayout.setLargeVideoState("prezi");
|
||||
}
|
||||
else {
|
||||
VideoLayout.setLargeVideoState("video");
|
||||
}
|
||||
}
|
||||
|
||||
var Prezi = {
|
||||
|
||||
|
||||
/**
|
||||
* Reloads the current presentation.
|
||||
*/
|
||||
reloadPresentation: function() {
|
||||
var iframe = document.getElementById(preziPlayer.options.preziId);
|
||||
iframe.src = iframe.src;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns <tt>true</tt> if the presentation is visible, <tt>false</tt> -
|
||||
* otherwise.
|
||||
*/
|
||||
isPresentationVisible: function () {
|
||||
return ($('#presentation>iframe') != null
|
||||
&& $('#presentation>iframe').css('opacity') == 1);
|
||||
},
|
||||
|
||||
/**
|
||||
* Opens the Prezi dialog, from which the user could choose a presentation
|
||||
* to load.
|
||||
*/
|
||||
openPreziDialog: function() {
|
||||
var myprezi = APP.xmpp.getPrezi();
|
||||
if (myprezi) {
|
||||
messageHandler.openTwoButtonDialog("dialog.removePreziTitle",
|
||||
null,
|
||||
"dialog.removePreziMsg",
|
||||
null,
|
||||
false,
|
||||
"dialog.Remove",
|
||||
function(e,v,m,f) {
|
||||
if(v) {
|
||||
APP.xmpp.removePreziFromPresence();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
else if (preziPlayer != null) {
|
||||
messageHandler.openTwoButtonDialog("dialog.sharePreziTitle",
|
||||
null, "dialog.sharePreziMsg",
|
||||
null,
|
||||
false,
|
||||
"dialog.Ok",
|
||||
function(e,v,m,f) {
|
||||
$.prompt.close();
|
||||
}
|
||||
);
|
||||
}
|
||||
else {
|
||||
var html = APP.translation.generateTranslationHTML(
|
||||
"dialog.sharePreziTitle");
|
||||
var cancelButton = APP.translation.generateTranslationHTML(
|
||||
"dialog.Cancel");
|
||||
var shareButton = APP.translation.generateTranslationHTML(
|
||||
"dialog.Share");
|
||||
var backButton = APP.translation.generateTranslationHTML(
|
||||
"dialog.Back");
|
||||
var buttons = [];
|
||||
var buttons1 = [];
|
||||
// Cancel button to both states
|
||||
buttons.push({title: cancelButton, value: false});
|
||||
buttons1.push({title: cancelButton, value: false});
|
||||
// Share button
|
||||
buttons.push({title: shareButton, value: true});
|
||||
// Back button
|
||||
buttons1.push({title: backButton, value: true});
|
||||
var linkError = APP.translation.generateTranslationHTML(
|
||||
"dialog.preziLinkError");
|
||||
var defaultUrl = APP.translation.translateString("defaultPreziLink",
|
||||
{url: "http://prezi.com/wz7vhjycl7e6/my-prezi"});
|
||||
var openPreziState = {
|
||||
state0: {
|
||||
html: '<h2>' + html + '</h2>' +
|
||||
'<input name="preziUrl" type="text" ' +
|
||||
'data-i18n="[placeholder]defaultPreziLink" data-i18n-options=\'' +
|
||||
JSON.stringify({"url": "http://prezi.com/wz7vhjycl7e6/my-prezi"}) +
|
||||
'\' placeholder="' + defaultUrl + '" autofocus>',
|
||||
persistent: false,
|
||||
buttons: buttons,
|
||||
focus: ':input:first',
|
||||
defaultButton: 0,
|
||||
submit: function (e, v, m, f) {
|
||||
e.preventDefault();
|
||||
if(v)
|
||||
{
|
||||
var preziUrl = f.preziUrl;
|
||||
|
||||
if (preziUrl)
|
||||
{
|
||||
var urlValue
|
||||
= encodeURI(UIUtil.escapeHtml(preziUrl));
|
||||
|
||||
if (urlValue.indexOf('http://prezi.com/') != 0
|
||||
&& urlValue.indexOf('https://prezi.com/') != 0)
|
||||
{
|
||||
$.prompt.goToState('state1');
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
var presIdTmp = urlValue.substring(
|
||||
urlValue.indexOf("prezi.com/") + 10);
|
||||
if (!isAlphanumeric(presIdTmp)
|
||||
|| presIdTmp.indexOf('/') < 2) {
|
||||
$.prompt.goToState('state1');
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
APP.xmpp.addToPresence("prezi", urlValue);
|
||||
$.prompt.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
$.prompt.close();
|
||||
}
|
||||
},
|
||||
state1: {
|
||||
html: '<h2>' + html + '</h2>' +
|
||||
linkError,
|
||||
persistent: false,
|
||||
buttons: buttons1,
|
||||
focus: ':input:first',
|
||||
defaultButton: 1,
|
||||
submit: function (e, v, m, f) {
|
||||
e.preventDefault();
|
||||
if (v === 0)
|
||||
$.prompt.close();
|
||||
else
|
||||
$.prompt.goToState('state0');
|
||||
}
|
||||
}
|
||||
};
|
||||
messageHandler.openDialogWithStates(openPreziState);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* A new presentation has been added.
|
||||
*
|
||||
* @param event the event indicating the add of a presentation
|
||||
* @param jid the jid from which the presentation was added
|
||||
* @param presUrl url of the presentation
|
||||
* @param currentSlide the current slide to which we should move
|
||||
*/
|
||||
function presentationAdded(event, jid, presUrl, currentSlide) {
|
||||
console.log("presentation added", presUrl);
|
||||
|
||||
var presId = getPresentationId(presUrl);
|
||||
|
||||
var elementId = 'participant_'
|
||||
+ Strophe.getResourceFromJid(jid)
|
||||
+ '_' + presId;
|
||||
|
||||
VideoLayout.addPreziContainer(elementId);
|
||||
|
||||
var controlsEnabled = false;
|
||||
if (jid === APP.xmpp.myJid())
|
||||
controlsEnabled = true;
|
||||
|
||||
setPresentationVisible(true);
|
||||
VideoLayout.setLargeVideoHover(
|
||||
function (event) {
|
||||
if (Prezi.isPresentationVisible()) {
|
||||
var reloadButtonRight = window.innerWidth
|
||||
- $('#presentation>iframe').offset().left
|
||||
- $('#presentation>iframe').width();
|
||||
|
||||
$('#reloadPresentation').css({ right: reloadButtonRight,
|
||||
display:'inline-block'});
|
||||
}
|
||||
},
|
||||
function (event) {
|
||||
if (!Prezi.isPresentationVisible())
|
||||
$('#reloadPresentation').css({display:'none'});
|
||||
else {
|
||||
var e = event.toElement || event.relatedTarget;
|
||||
|
||||
if (e && e.id != 'reloadPresentation' && e.id != 'header')
|
||||
$('#reloadPresentation').css({display:'none'});
|
||||
}
|
||||
});
|
||||
|
||||
preziPlayer = new PreziPlayer(
|
||||
'presentation',
|
||||
{preziId: presId,
|
||||
width: getPresentationWidth(),
|
||||
height: getPresentationHeihgt(),
|
||||
controls: controlsEnabled,
|
||||
debug: true
|
||||
});
|
||||
|
||||
$('#presentation>iframe').attr('id', preziPlayer.options.preziId);
|
||||
|
||||
preziPlayer.on(PreziPlayer.EVENT_STATUS, function(event) {
|
||||
console.log("prezi status", event.value);
|
||||
if (event.value == PreziPlayer.STATUS_CONTENT_READY) {
|
||||
if (jid != APP.xmpp.myJid())
|
||||
preziPlayer.flyToStep(currentSlide);
|
||||
}
|
||||
});
|
||||
|
||||
preziPlayer.on(PreziPlayer.EVENT_CURRENT_STEP, function(event) {
|
||||
console.log("event value", event.value);
|
||||
APP.xmpp.addToPresence("preziSlide", event.value);
|
||||
});
|
||||
|
||||
$("#" + elementId).css( 'background-image',
|
||||
'url(../images/avatarprezi.png)');
|
||||
$("#" + elementId).click(
|
||||
function () {
|
||||
setPresentationVisible(true);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* A presentation has been removed.
|
||||
*
|
||||
* @param event the event indicating the remove of a presentation
|
||||
* @param jid the jid for which the presentation was removed
|
||||
* @param the url of the presentation
|
||||
*/
|
||||
function presentationRemoved(event, jid, presUrl) {
|
||||
console.log('presentation removed', presUrl);
|
||||
var presId = getPresentationId(presUrl);
|
||||
setPresentationVisible(false);
|
||||
$('#participant_'
|
||||
+ Strophe.getResourceFromJid(jid)
|
||||
+ '_' + presId).remove();
|
||||
$('#presentation>iframe').remove();
|
||||
if (preziPlayer != null) {
|
||||
preziPlayer.destroy();
|
||||
preziPlayer = null;
|
||||
}
|
||||
};
|
||||
const DEFAULT_WIDTH = 640;
|
||||
const DEFAULT_HEIGHT = 480;
|
||||
|
||||
/**
|
||||
* Indicates if the given string is an alphanumeric string.
|
||||
|
@ -270,76 +24,354 @@ function presentationRemoved(event, jid, presUrl) {
|
|||
* purpose of checking URIs.
|
||||
*/
|
||||
function isAlphanumeric(unsafeText) {
|
||||
var regex = /^[a-z0-9-_\/&\?=;]+$/i;
|
||||
return regex.test(unsafeText);
|
||||
return alphanumRegex.test(unsafeText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the presentation id from the given url.
|
||||
*/
|
||||
function getPresentationId (presUrl) {
|
||||
var presIdTmp = presUrl.substring(presUrl.indexOf("prezi.com/") + 10);
|
||||
return presIdTmp.substring(0, presIdTmp.indexOf('/'));
|
||||
function getPresentationId (url) {
|
||||
let presId = url.substring(url.indexOf("prezi.com/") + 10);
|
||||
return presId.substring(0, presId.indexOf('/'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the presentation width.
|
||||
*/
|
||||
function getPresentationWidth() {
|
||||
var availableWidth = UIUtil.getAvailableVideoWidth();
|
||||
var availableHeight = getPresentationHeihgt();
|
||||
|
||||
var aspectRatio = 16.0 / 9.0;
|
||||
if (availableHeight < availableWidth / aspectRatio) {
|
||||
availableWidth = Math.floor(availableHeight * aspectRatio);
|
||||
function isPreziLink(url) {
|
||||
if (url.indexOf('http://prezi.com/') !== 0 && url.indexOf('https://prezi.com/') !== 0) {
|
||||
return false;
|
||||
}
|
||||
return availableWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the presentation height.
|
||||
*/
|
||||
function getPresentationHeihgt() {
|
||||
var remoteVideos = $('#remoteVideos');
|
||||
return window.innerHeight - remoteVideos.outerHeight();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resizes the presentation iframe.
|
||||
*/
|
||||
function resize() {
|
||||
if ($('#presentation>iframe')) {
|
||||
$('#presentation>iframe').width(getPresentationWidth());
|
||||
$('#presentation>iframe').height(getPresentationHeihgt());
|
||||
let presId = url.substring(url.indexOf("prezi.com/") + 10);
|
||||
if (!isAlphanumeric(presId) || presId.indexOf('/') < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Presentation has been removed.
|
||||
*/
|
||||
$(document).bind('presentationremoved.muc', presentationRemoved);
|
||||
function notifyOtherIsSharingPrezi() {
|
||||
messageHandler.openMessageDialog(
|
||||
"dialog.sharePreziTitle",
|
||||
"dialog.sharePreziMsg"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Presentation has been added.
|
||||
*/
|
||||
$(document).bind('presentationadded.muc', presentationAdded);
|
||||
function proposeToClosePrezi() {
|
||||
return new Promise(function (resolve, reject) {
|
||||
messageHandler.openTwoButtonDialog(
|
||||
"dialog.removePreziTitle",
|
||||
null,
|
||||
"dialog.removePreziMsg",
|
||||
null,
|
||||
false,
|
||||
"dialog.Remove",
|
||||
function(e,v,m,f) {
|
||||
if (v) {
|
||||
resolve();
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/*
|
||||
* Indicates presentation slide change.
|
||||
*/
|
||||
$(document).bind('gotoslide.muc', function (event, jid, presUrl, current) {
|
||||
if (preziPlayer && preziPlayer.getCurrentStep() != current) {
|
||||
preziPlayer.flyToStep(current);
|
||||
});
|
||||
}
|
||||
|
||||
var animationStepsArray = preziPlayer.getAnimationCountOnSteps();
|
||||
for (var i = 0; i < parseInt(animationStepsArray[current]); i++) {
|
||||
preziPlayer.flyToStep(current, i);
|
||||
function requestPreziLink() {
|
||||
const title = APP.translation.generateTranslationHTML("dialog.sharePreziTitle");
|
||||
const cancelButton = APP.translation.generateTranslationHTML("dialog.Cancel");
|
||||
const shareButton = APP.translation.generateTranslationHTML("dialog.Share");
|
||||
const backButton = APP.translation.generateTranslationHTML("dialog.Back");
|
||||
const linkError = APP.translation.generateTranslationHTML("dialog.preziLinkError");
|
||||
const i18nOptions = {url: defaultPreziLink};
|
||||
const defaultUrl = APP.translation.translateString(
|
||||
"defaultPreziLink", i18nOptions
|
||||
);
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
let dialog = messageHandler.openDialogWithStates({
|
||||
state0: {
|
||||
html: `
|
||||
<h2>${title}</h2>
|
||||
<input name="preziUrl" type="text"
|
||||
data-i18n="[placeholder]defaultPreziLink"
|
||||
data-i18n-options="${JSON.stringify(i18nOptions)}"
|
||||
placeholder="${defaultUrl}" autofocus>`,
|
||||
persistent: false,
|
||||
buttons: [
|
||||
{title: cancelButton, value: false},
|
||||
{title: shareButton, value: true}
|
||||
],
|
||||
focus: ':input:first',
|
||||
defaultButton: 1,
|
||||
submit: function (e, v, m, f) {
|
||||
e.preventDefault();
|
||||
if (!v) {
|
||||
reject('cancelled');
|
||||
dialog.close();
|
||||
return;
|
||||
}
|
||||
|
||||
let preziUrl = f.preziUrl;
|
||||
if (!preziUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
let urlValue = encodeURI(UIUtil.escapeHtml(preziUrl));
|
||||
|
||||
if (!isPreziLink(urlValue)) {
|
||||
dialog.goToState('state1');
|
||||
return false;
|
||||
}
|
||||
|
||||
resolve(urlValue);
|
||||
dialog.close();
|
||||
}
|
||||
},
|
||||
|
||||
state1: {
|
||||
html: `<h2>${title}</h2> ${linkError}`,
|
||||
persistent: false,
|
||||
buttons: [
|
||||
{title: cancelButton, value: false},
|
||||
{title: backButton, value: true}
|
||||
],
|
||||
focus: ':input:first',
|
||||
defaultButton: 1,
|
||||
submit: function (e, v, m, f) {
|
||||
e.preventDefault();
|
||||
if (v === 0) {
|
||||
reject();
|
||||
dialog.close();
|
||||
} else {
|
||||
dialog.goToState('state0');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
export const PreziContainerType = "prezi";
|
||||
|
||||
class PreziContainer extends LargeContainer {
|
||||
|
||||
constructor ({preziId, isMy, slide, onSlideChanged}) {
|
||||
super();
|
||||
this.reloadBtn = $('#reloadPresentation');
|
||||
|
||||
let preziPlayer = new PreziPlayer(
|
||||
'presentation', {
|
||||
preziId,
|
||||
width: DEFAULT_WIDTH,
|
||||
height: DEFAULT_HEIGHT,
|
||||
controls: isMy,
|
||||
debug: true
|
||||
}
|
||||
);
|
||||
this.preziPlayer = preziPlayer;
|
||||
this.$iframe = $(preziPlayer.iframe);
|
||||
|
||||
this.$iframe.attr('id', preziId);
|
||||
|
||||
preziPlayer.on(PreziPlayer.EVENT_STATUS, function({value}) {
|
||||
console.log("prezi status", value);
|
||||
if (value == PreziPlayer.STATUS_CONTENT_READY && !isMy) {
|
||||
preziPlayer.flyToStep(slide);
|
||||
}
|
||||
});
|
||||
|
||||
preziPlayer.on(PreziPlayer.EVENT_CURRENT_STEP, function({value}) {
|
||||
console.log("event value", value);
|
||||
onSlideChanged(value);
|
||||
});
|
||||
}
|
||||
|
||||
goToSlide (slide) {
|
||||
if (this.preziPlayer.getCurrentStep() === slide) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.preziPlayer.flyToStep(slide);
|
||||
|
||||
let animationStepsArray = this.preziPlayer.getAnimationCountOnSteps();
|
||||
if (!animationStepsArray) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < parseInt(animationStepsArray[slide]); i += 1) {
|
||||
this.preziPlayer.flyToStep(slide, i);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$(window).resize(function () {
|
||||
resize();
|
||||
});
|
||||
showReloadBtn (show) {
|
||||
this.reloadBtn.css('display', show ? 'inline-block' : 'none');
|
||||
}
|
||||
|
||||
module.exports = Prezi;
|
||||
show () {
|
||||
return new Promise(resolve => {
|
||||
this.$iframe.fadeIn(300, () => {
|
||||
this.$iframe.css({opacity: 1});
|
||||
ToolbarToggler.dockToolbar(true);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
hide () {
|
||||
return new Promise(resolve => {
|
||||
this.$iframe.fadeOut(300, () => {
|
||||
this.$iframe.css({opacity: 0});
|
||||
this.showReloadBtn(false);
|
||||
ToolbarToggler.dockToolbar(false);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onHoverIn () {
|
||||
let rightOffset = window.innerWidth - this.$iframe.offset().left - this.$iframe.width();
|
||||
|
||||
this.showReloadBtn(true);
|
||||
this.reloadBtn.css('right', rightOffset);
|
||||
}
|
||||
|
||||
onHoverOut (event) {
|
||||
let e = event.toElement || event.relatedTarget;
|
||||
|
||||
if (e && e.id != 'reloadPresentation' && e.id != 'header') {
|
||||
this.showReloadBtn(false);
|
||||
}
|
||||
}
|
||||
|
||||
resize (containerWidth, containerHeight) {
|
||||
let height = containerHeight - BottomToolbar.getFilmStripHeight();
|
||||
|
||||
let width = containerWidth;
|
||||
|
||||
if (height < width / aspectRatio) {
|
||||
width = Math.floor(height * aspectRatio);
|
||||
}
|
||||
|
||||
this.$iframe.width(width).height(height);
|
||||
}
|
||||
|
||||
close () {
|
||||
this.showReloadBtn(false);
|
||||
this.preziPlayer.destroy();
|
||||
this.$iframe.remove();
|
||||
}
|
||||
}
|
||||
|
||||
export default class PreziManager {
|
||||
constructor (emitter) {
|
||||
this.emitter = emitter;
|
||||
|
||||
this.userId = null;
|
||||
this.url = null;
|
||||
this.prezi = null;
|
||||
|
||||
$("#reloadPresentationLink").click(this.reloadPresentation.bind(this));
|
||||
}
|
||||
|
||||
get isPresenting () {
|
||||
return !!this.userId;
|
||||
}
|
||||
|
||||
get isMyPrezi () {
|
||||
return this.userId === APP.conference.localId;
|
||||
}
|
||||
|
||||
isSharing (id) {
|
||||
return this.userId === id;
|
||||
}
|
||||
|
||||
handlePreziButtonClicked () {
|
||||
if (!this.isPresenting) {
|
||||
requestPreziLink().then(
|
||||
url => this.emitter.emit(UIEvents.SHARE_PREZI, url, 0),
|
||||
err => console.error('PREZI CANCELED', err)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isMyPrezi) {
|
||||
proposeToClosePrezi().then(() => this.emitter.emit(UIEvents.STOP_SHARING_PREZI));
|
||||
} else {
|
||||
notifyOtherIsSharingPrezi();
|
||||
}
|
||||
}
|
||||
|
||||
reloadPresentation () {
|
||||
if (!this.prezi) {
|
||||
return;
|
||||
}
|
||||
let iframe = this.prezi.$iframe[0];
|
||||
iframe.src = iframe.src;
|
||||
}
|
||||
|
||||
showPrezi (id, url, slide) {
|
||||
if (!this.isPresenting) {
|
||||
this.createPrezi(id, url, slide);
|
||||
}
|
||||
|
||||
if (this.userId === id && this.url === url) {
|
||||
this.prezi.goToSlide(slide);
|
||||
} else {
|
||||
console.error(this.userId, id);
|
||||
console.error(this.url, url);
|
||||
throw new Error("unexpected presentation change");
|
||||
}
|
||||
}
|
||||
|
||||
createPrezi (id, url, slide) {
|
||||
console.log("presentation added", url);
|
||||
|
||||
this.userId = id;
|
||||
this.url = url;
|
||||
|
||||
let preziId = getPresentationId(url);
|
||||
let elementId = `participant_${id}_${preziId}`;
|
||||
|
||||
this.$thumb = $(VideoLayout.addRemoteVideoContainer(elementId));
|
||||
VideoLayout.resizeThumbnails();
|
||||
this.$thumb.css({
|
||||
'background-image': 'url(../images/avatarprezi.png)'
|
||||
}).click(() => VideoLayout.showLargeVideoContainer(PreziContainerType, true));
|
||||
|
||||
this.prezi = new PreziContainer({
|
||||
preziId,
|
||||
isMy: this.isMyPrezi,
|
||||
slide,
|
||||
onSlideChanged: newSlide => {
|
||||
if (this.isMyPrezi) {
|
||||
this.emitter.emit(UIEvents.SHARE_PREZI, url, newSlide);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
VideoLayout.addLargeVideoContainer(PreziContainerType, this.prezi);
|
||||
VideoLayout.showLargeVideoContainer(PreziContainerType, true);
|
||||
}
|
||||
|
||||
removePrezi (id) {
|
||||
if (this.userId !== id) {
|
||||
throw new Error(`cannot close presentation from ${this.userId} instead of ${id}`);
|
||||
}
|
||||
|
||||
this.$thumb.remove();
|
||||
this.$thumb = null;
|
||||
|
||||
// wait until Prezi is hidden, then remove it
|
||||
VideoLayout.showLargeVideoContainer(PreziContainerType, false).then(() => {
|
||||
console.log("presentation removed", this.url);
|
||||
|
||||
VideoLayout.removeLargeVideoContainer(PreziContainerType);
|
||||
|
||||
this.userId = null;
|
||||
this.url = null;
|
||||
this.prezi.close();
|
||||
this.prezi = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,298 +1,290 @@
|
|||
/* global PreziPlayer */
|
||||
/* jshint -W101 */
|
||||
(function() {
|
||||
"use strict";
|
||||
var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
|
||||
|
||||
window.PreziPlayer = (function() {
|
||||
var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
|
||||
|
||||
PreziPlayer.API_VERSION = 1;
|
||||
PreziPlayer.CURRENT_STEP = 'currentStep';
|
||||
PreziPlayer.CURRENT_ANIMATION_STEP = 'currentAnimationStep';
|
||||
PreziPlayer.CURRENT_OBJECT = 'currentObject';
|
||||
PreziPlayer.STATUS_LOADING = 'loading';
|
||||
PreziPlayer.STATUS_READY = 'ready';
|
||||
PreziPlayer.STATUS_CONTENT_READY = 'contentready';
|
||||
PreziPlayer.EVENT_CURRENT_STEP = "currentStepChange";
|
||||
PreziPlayer.EVENT_CURRENT_ANIMATION_STEP = "currentAnimationStepChange";
|
||||
PreziPlayer.EVENT_CURRENT_OBJECT = "currentObjectChange";
|
||||
PreziPlayer.EVENT_STATUS = "statusChange";
|
||||
PreziPlayer.EVENT_PLAYING = "isAutoPlayingChange";
|
||||
PreziPlayer.EVENT_IS_MOVING = "isMovingChange";
|
||||
PreziPlayer.domain = "https://prezi.com";
|
||||
PreziPlayer.path = "/player/";
|
||||
PreziPlayer.players = {};
|
||||
PreziPlayer.binded_methods = ['changesHandler'];
|
||||
PreziPlayer.API_VERSION = 1;
|
||||
PreziPlayer.CURRENT_STEP = 'currentStep';
|
||||
PreziPlayer.CURRENT_ANIMATION_STEP = 'currentAnimationStep';
|
||||
PreziPlayer.CURRENT_OBJECT = 'currentObject';
|
||||
PreziPlayer.STATUS_LOADING = 'loading';
|
||||
PreziPlayer.STATUS_READY = 'ready';
|
||||
PreziPlayer.STATUS_CONTENT_READY = 'contentready';
|
||||
PreziPlayer.EVENT_CURRENT_STEP = "currentStepChange";
|
||||
PreziPlayer.EVENT_CURRENT_ANIMATION_STEP = "currentAnimationStepChange";
|
||||
PreziPlayer.EVENT_CURRENT_OBJECT = "currentObjectChange";
|
||||
PreziPlayer.EVENT_STATUS = "statusChange";
|
||||
PreziPlayer.EVENT_PLAYING = "isAutoPlayingChange";
|
||||
PreziPlayer.EVENT_IS_MOVING = "isMovingChange";
|
||||
PreziPlayer.domain = "https://prezi.com";
|
||||
PreziPlayer.path = "/player/";
|
||||
PreziPlayer.players = {};
|
||||
PreziPlayer.binded_methods = ['changesHandler'];
|
||||
|
||||
PreziPlayer.createMultiplePlayers = function(optionArray){
|
||||
for(var i=0; i<optionArray.length; i++) {
|
||||
var optionSet = optionArray[i];
|
||||
new PreziPlayer(optionSet.id, optionSet);
|
||||
PreziPlayer.createMultiplePlayers = function(optionArray){
|
||||
for(var i=0; i<optionArray.length; i++) {
|
||||
var optionSet = optionArray[i];
|
||||
new PreziPlayer(optionSet.id, optionSet);
|
||||
}
|
||||
};
|
||||
|
||||
PreziPlayer.messageReceived = function(event){
|
||||
var message, item, player;
|
||||
try {
|
||||
message = JSON.parse(event.data);
|
||||
if (message.id && (player = PreziPlayer.players[message.id])) {
|
||||
if (player.options.debug === true) {
|
||||
if (console && console.log)
|
||||
console.log('received', message);
|
||||
}
|
||||
};
|
||||
|
||||
PreziPlayer.messageReceived = function(event){
|
||||
var message, item, player;
|
||||
try {
|
||||
message = JSON.parse(event.data);
|
||||
if (message.id && (player = PreziPlayer.players[message.id])) {
|
||||
if (player.options.debug === true) {
|
||||
if (console && console.log)
|
||||
console.log('received', message);
|
||||
}
|
||||
if (message.type === "changes") {
|
||||
player.changesHandler(message);
|
||||
}
|
||||
for (var i = 0; i < player.callbacks.length; i++) {
|
||||
item = player.callbacks[i];
|
||||
if (item && message.type === item.event) {
|
||||
item.callback(message);
|
||||
}
|
||||
}
|
||||
if (message.type === "changes") {
|
||||
player.changesHandler(message);
|
||||
}
|
||||
for (var i = 0; i < player.callbacks.length; i++) {
|
||||
item = player.callbacks[i];
|
||||
if (item && message.type === item.event) {
|
||||
item.callback(message);
|
||||
}
|
||||
} catch (e) { }
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (e) { }
|
||||
};
|
||||
|
||||
/*jshint -W004 */
|
||||
function PreziPlayer(id, options) {
|
||||
/*jshint +W004 */
|
||||
var params, paramString = "", _this = this;
|
||||
if (PreziPlayer.players[id]){
|
||||
PreziPlayer.players[id].destroy();
|
||||
}
|
||||
for(var i=0; i<PreziPlayer.binded_methods.length; i++) {
|
||||
var method_name = PreziPlayer.binded_methods[i];
|
||||
_this[method_name] = __bind(_this[method_name], _this);
|
||||
}
|
||||
options = options || {};
|
||||
this.options = options;
|
||||
this.values = {'status': PreziPlayer.STATUS_LOADING};
|
||||
this.values[PreziPlayer.CURRENT_STEP] = 0;
|
||||
this.values[PreziPlayer.CURRENT_ANIMATION_STEP] = 0;
|
||||
this.values[PreziPlayer.CURRENT_OBJECT] = null;
|
||||
this.callbacks = [];
|
||||
this.id = id;
|
||||
this.embedTo = document.getElementById(id);
|
||||
if (!this.embedTo) {
|
||||
throw "The element id is not available.";
|
||||
}
|
||||
this.iframe = document.createElement('iframe');
|
||||
params = [
|
||||
{ name: 'oid', value: options.preziId },
|
||||
{ name: 'explorable', value: options.explorable ? 1 : 0 },
|
||||
{ name: 'controls', value: options.controls ? 1 : 0 }
|
||||
];
|
||||
for (i=0; i<params.length; i++) {
|
||||
var param = params[i];
|
||||
paramString += (i===0 ? "?" : "&") + param.name + "=" + param.value;
|
||||
}
|
||||
this.iframe.src = PreziPlayer.domain + PreziPlayer.path + paramString;
|
||||
this.iframe.frameBorder = 0;
|
||||
this.iframe.scrolling = "no";
|
||||
this.iframe.width = options.width || 640;
|
||||
this.iframe.height = options.height || 480;
|
||||
this.embedTo.innerHTML = '';
|
||||
// JITSI: IN CASE SOMETHING GOES WRONG.
|
||||
try {
|
||||
this.embedTo.appendChild(this.iframe);
|
||||
}
|
||||
catch (err) {
|
||||
console.log("CATCH ERROR");
|
||||
}
|
||||
function PreziPlayer(id, options) {
|
||||
/*jshint +W004 */
|
||||
var params, paramString = "", _this = this;
|
||||
if (PreziPlayer.players[id]){
|
||||
PreziPlayer.players[id].destroy();
|
||||
}
|
||||
for(var i=0; i<PreziPlayer.binded_methods.length; i++) {
|
||||
var method_name = PreziPlayer.binded_methods[i];
|
||||
_this[method_name] = __bind(_this[method_name], _this);
|
||||
}
|
||||
options = options || {};
|
||||
this.options = options;
|
||||
this.values = {'status': PreziPlayer.STATUS_LOADING};
|
||||
this.values[PreziPlayer.CURRENT_STEP] = 0;
|
||||
this.values[PreziPlayer.CURRENT_ANIMATION_STEP] = 0;
|
||||
this.values[PreziPlayer.CURRENT_OBJECT] = null;
|
||||
this.callbacks = [];
|
||||
this.id = id;
|
||||
this.embedTo = document.getElementById(id);
|
||||
if (!this.embedTo) {
|
||||
throw "The element id is not available.";
|
||||
}
|
||||
this.iframe = document.createElement('iframe');
|
||||
params = [
|
||||
{ name: 'oid', value: options.preziId },
|
||||
{ name: 'explorable', value: options.explorable ? 1 : 0 },
|
||||
{ name: 'controls', value: options.controls ? 1 : 0 }
|
||||
];
|
||||
for (i=0; i<params.length; i++) {
|
||||
var param = params[i];
|
||||
paramString += (i===0 ? "?" : "&") + param.name + "=" + param.value;
|
||||
}
|
||||
this.iframe.src = PreziPlayer.domain + PreziPlayer.path + paramString;
|
||||
this.iframe.frameBorder = 0;
|
||||
this.iframe.scrolling = "no";
|
||||
this.iframe.width = options.width || 640;
|
||||
this.iframe.height = options.height || 480;
|
||||
this.embedTo.innerHTML = '';
|
||||
// JITSI: IN CASE SOMETHING GOES WRONG.
|
||||
try {
|
||||
this.embedTo.appendChild(this.iframe);
|
||||
}
|
||||
catch (err) {
|
||||
console.log("CATCH ERROR");
|
||||
}
|
||||
|
||||
// JITSI: Increase interval from 200 to 500, which fixes prezi
|
||||
// crashes for us.
|
||||
this.initPollInterval = setInterval(function(){
|
||||
_this.sendMessage({'action': 'init'});
|
||||
}, 500);
|
||||
PreziPlayer.players[id] = this;
|
||||
}
|
||||
// JITSI: Increase interval from 200 to 500, which fixes prezi
|
||||
// crashes for us.
|
||||
this.initPollInterval = setInterval(function(){
|
||||
_this.sendMessage({'action': 'init'});
|
||||
}, 500);
|
||||
PreziPlayer.players[id] = this;
|
||||
}
|
||||
|
||||
PreziPlayer.prototype.changesHandler = function(message) {
|
||||
var key, value, j, item;
|
||||
if (this.initPollInterval) {
|
||||
clearInterval(this.initPollInterval);
|
||||
this.initPollInterval = false;
|
||||
}
|
||||
for (key in message.data) {
|
||||
if (message.data.hasOwnProperty(key)){
|
||||
value = message.data[key];
|
||||
this.values[key] = value;
|
||||
for (j=0; j<this.callbacks.length; j++) {
|
||||
item = this.callbacks[j];
|
||||
if (item && item.event === key + "Change"){
|
||||
item.callback({type: item.event, value: value});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.destroy = function() {
|
||||
if (this.initPollInterval) {
|
||||
clearInterval(this.initPollInterval);
|
||||
this.initPollInterval = false;
|
||||
}
|
||||
this.embedTo.innerHTML = '';
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.sendMessage = function(message) {
|
||||
if (this.options.debug === true) {
|
||||
if (console && console.log) console.log('sent', message);
|
||||
}
|
||||
message.version = PreziPlayer.API_VERSION;
|
||||
message.id = this.id;
|
||||
return this.iframe.contentWindow.postMessage(JSON.stringify(message), '*');
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.nextStep = /* nextStep is DEPRECATED */
|
||||
PreziPlayer.prototype.flyToNextStep = function() {
|
||||
return this.sendMessage({
|
||||
'action': 'present',
|
||||
'data': ['moveToNextStep']
|
||||
});
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.previousStep = /* previousStep is DEPRECATED */
|
||||
PreziPlayer.prototype.flyToPreviousStep = function() {
|
||||
return this.sendMessage({
|
||||
'action': 'present',
|
||||
'data': ['moveToPrevStep']
|
||||
});
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.toStep = /* toStep is DEPRECATED */
|
||||
PreziPlayer.prototype.flyToStep = function(step, animation_step) {
|
||||
var obj = this;
|
||||
// check animation_step
|
||||
if (animation_step > 0 &&
|
||||
obj.values.animationCountOnSteps &&
|
||||
obj.values.animationCountOnSteps[step] <= animation_step) {
|
||||
animation_step = obj.values.animationCountOnSteps[step];
|
||||
}
|
||||
// jump to animation steps by calling flyToNextStep()
|
||||
function doAnimationSteps() {
|
||||
if (obj.values.isMoving) {
|
||||
setTimeout(doAnimationSteps, 100); // wait until the flight ends
|
||||
return;
|
||||
}
|
||||
while (animation_step-- > 0) {
|
||||
obj.flyToNextStep(); // do the animation steps
|
||||
}
|
||||
}
|
||||
setTimeout(doAnimationSteps, 200); // 200ms is the internal "reporting" time
|
||||
// jump to the step
|
||||
return this.sendMessage({
|
||||
'action': 'present',
|
||||
'data': ['moveToStep', step]
|
||||
});
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.toObject = /* toObject is DEPRECATED */
|
||||
PreziPlayer.prototype.flyToObject = function(objectId) {
|
||||
return this.sendMessage({
|
||||
'action': 'present',
|
||||
'data': ['moveToObject', objectId]
|
||||
});
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.play = function(defaultDelay) {
|
||||
return this.sendMessage({
|
||||
'action': 'present',
|
||||
'data': ['startAutoPlay', defaultDelay]
|
||||
});
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.stop = function() {
|
||||
return this.sendMessage({
|
||||
'action': 'present',
|
||||
'data': ['stopAutoPlay']
|
||||
});
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.pause = function(defaultDelay) {
|
||||
return this.sendMessage({
|
||||
'action': 'present',
|
||||
'data': ['pauseAutoPlay', defaultDelay]
|
||||
});
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.getCurrentStep = function() {
|
||||
return this.values.currentStep;
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.getCurrentAnimationStep = function() {
|
||||
return this.values.currentAnimationStep;
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.getCurrentObject = function() {
|
||||
return this.values.currentObject;
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.getStatus = function() {
|
||||
return this.values.status;
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.isPlaying = function() {
|
||||
return this.values.isAutoPlaying;
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.getStepCount = function() {
|
||||
return this.values.stepCount;
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.getAnimationCountOnSteps = function() {
|
||||
return this.values.animationCountOnSteps;
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.getTitle = function() {
|
||||
return this.values.title;
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.setDimensions = function(dims) {
|
||||
for (var parameter in dims) {
|
||||
this.iframe[parameter] = dims[parameter];
|
||||
}
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.getDimensions = function() {
|
||||
return {
|
||||
width: parseInt(this.iframe.width, 10),
|
||||
height: parseInt(this.iframe.height, 10)
|
||||
};
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.on = function(event, callback) {
|
||||
this.callbacks.push({
|
||||
event: event,
|
||||
callback: callback
|
||||
});
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.off = function(event, callback) {
|
||||
var j, item;
|
||||
if (event === undefined) {
|
||||
this.callbacks = [];
|
||||
}
|
||||
j = this.callbacks.length;
|
||||
while (j--) {
|
||||
PreziPlayer.prototype.changesHandler = function(message) {
|
||||
var key, value, j, item;
|
||||
if (this.initPollInterval) {
|
||||
clearInterval(this.initPollInterval);
|
||||
this.initPollInterval = false;
|
||||
}
|
||||
for (key in message.data) {
|
||||
if (message.data.hasOwnProperty(key)){
|
||||
value = message.data[key];
|
||||
this.values[key] = value;
|
||||
for (j=0; j<this.callbacks.length; j++) {
|
||||
item = this.callbacks[j];
|
||||
if (item && item.event === event && (callback === undefined || item.callback === callback)){
|
||||
this.callbacks.splice(j, 1);
|
||||
if (item && item.event === key + "Change"){
|
||||
item.callback({type: item.event, value: value});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (window.addEventListener) {
|
||||
window.addEventListener('message', PreziPlayer.messageReceived, false);
|
||||
} else {
|
||||
window.attachEvent('onmessage', PreziPlayer.messageReceived);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return PreziPlayer;
|
||||
PreziPlayer.prototype.destroy = function() {
|
||||
if (this.initPollInterval) {
|
||||
clearInterval(this.initPollInterval);
|
||||
this.initPollInterval = false;
|
||||
}
|
||||
this.embedTo.innerHTML = '';
|
||||
};
|
||||
|
||||
})();
|
||||
PreziPlayer.prototype.sendMessage = function(message) {
|
||||
if (this.options.debug === true) {
|
||||
if (console && console.log) console.log('sent', message);
|
||||
}
|
||||
message.version = PreziPlayer.API_VERSION;
|
||||
message.id = this.id;
|
||||
return this.iframe.contentWindow.postMessage(JSON.stringify(message), '*');
|
||||
};
|
||||
|
||||
})();
|
||||
PreziPlayer.prototype.nextStep = /* nextStep is DEPRECATED */
|
||||
PreziPlayer.prototype.flyToNextStep = function() {
|
||||
return this.sendMessage({
|
||||
'action': 'present',
|
||||
'data': ['moveToNextStep']
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = PreziPlayer;
|
||||
PreziPlayer.prototype.previousStep = /* previousStep is DEPRECATED */
|
||||
PreziPlayer.prototype.flyToPreviousStep = function() {
|
||||
return this.sendMessage({
|
||||
'action': 'present',
|
||||
'data': ['moveToPrevStep']
|
||||
});
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.toStep = /* toStep is DEPRECATED */
|
||||
PreziPlayer.prototype.flyToStep = function(step, animation_step) {
|
||||
var obj = this;
|
||||
// check animation_step
|
||||
if (animation_step > 0 &&
|
||||
obj.values.animationCountOnSteps &&
|
||||
obj.values.animationCountOnSteps[step] <= animation_step) {
|
||||
animation_step = obj.values.animationCountOnSteps[step];
|
||||
}
|
||||
// jump to animation steps by calling flyToNextStep()
|
||||
function doAnimationSteps() {
|
||||
if (obj.values.isMoving) {
|
||||
setTimeout(doAnimationSteps, 100); // wait until the flight ends
|
||||
return;
|
||||
}
|
||||
while (animation_step-- > 0) {
|
||||
obj.flyToNextStep(); // do the animation steps
|
||||
}
|
||||
}
|
||||
setTimeout(doAnimationSteps, 200); // 200ms is the internal "reporting" time
|
||||
// jump to the step
|
||||
return this.sendMessage({
|
||||
'action': 'present',
|
||||
'data': ['moveToStep', step]
|
||||
});
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.toObject = /* toObject is DEPRECATED */
|
||||
PreziPlayer.prototype.flyToObject = function(objectId) {
|
||||
return this.sendMessage({
|
||||
'action': 'present',
|
||||
'data': ['moveToObject', objectId]
|
||||
});
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.play = function(defaultDelay) {
|
||||
return this.sendMessage({
|
||||
'action': 'present',
|
||||
'data': ['startAutoPlay', defaultDelay]
|
||||
});
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.stop = function() {
|
||||
return this.sendMessage({
|
||||
'action': 'present',
|
||||
'data': ['stopAutoPlay']
|
||||
});
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.pause = function(defaultDelay) {
|
||||
return this.sendMessage({
|
||||
'action': 'present',
|
||||
'data': ['pauseAutoPlay', defaultDelay]
|
||||
});
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.getCurrentStep = function() {
|
||||
return this.values.currentStep;
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.getCurrentAnimationStep = function() {
|
||||
return this.values.currentAnimationStep;
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.getCurrentObject = function() {
|
||||
return this.values.currentObject;
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.getStatus = function() {
|
||||
return this.values.status;
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.isPlaying = function() {
|
||||
return this.values.isAutoPlaying;
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.getStepCount = function() {
|
||||
return this.values.stepCount;
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.getAnimationCountOnSteps = function() {
|
||||
return this.values.animationCountOnSteps;
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.getTitle = function() {
|
||||
return this.values.title;
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.setDimensions = function(dims) {
|
||||
for (var parameter in dims) {
|
||||
this.iframe[parameter] = dims[parameter];
|
||||
}
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.getDimensions = function() {
|
||||
return {
|
||||
width: parseInt(this.iframe.width, 10),
|
||||
height: parseInt(this.iframe.height, 10)
|
||||
};
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.on = function(event, callback) {
|
||||
this.callbacks.push({
|
||||
event: event,
|
||||
callback: callback
|
||||
});
|
||||
};
|
||||
|
||||
PreziPlayer.prototype.off = function(event, callback) {
|
||||
var j, item;
|
||||
if (event === undefined) {
|
||||
this.callbacks = [];
|
||||
}
|
||||
j = this.callbacks.length;
|
||||
while (j--) {
|
||||
item = this.callbacks[j];
|
||||
if (item && item.event === event && (callback === undefined || item.callback === callback)){
|
||||
this.callbacks.splice(j, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (window.addEventListener) {
|
||||
window.addEventListener('message', PreziPlayer.messageReceived, false);
|
||||
} else {
|
||||
window.attachEvent('onmessage', PreziPlayer.messageReceived);
|
||||
}
|
||||
|
||||
window.PreziPlayer = PreziPlayer;
|
||||
|
||||
export default PreziPlayer;
|
||||
|
|
|
@ -6,7 +6,6 @@ import SettingsMenu from "./settings/SettingsMenu";
|
|||
import VideoLayout from "../videolayout/VideoLayout";
|
||||
import ToolbarToggler from "../toolbars/ToolbarToggler";
|
||||
import UIUtil from "../util/UIUtil";
|
||||
import LargeVideo from "../videolayout/LargeVideo";
|
||||
|
||||
const buttons = {
|
||||
'#chatspace': '#chatBottomButton',
|
||||
|
@ -47,7 +46,7 @@ function toggle (object, selector, onOpenComplete, onOpen, onClose) {
|
|||
} else {
|
||||
// Undock the toolbar when the chat is shown and if we're in a
|
||||
// video mode.
|
||||
if (LargeVideo.isLargeVideoVisible()) {
|
||||
if (VideoLayout.isLargeVideoVisible()) {
|
||||
ToolbarToggler.dockToolbar(false);
|
||||
}
|
||||
|
||||
|
@ -62,7 +61,7 @@ function toggle (object, selector, onOpenComplete, onOpen, onClose) {
|
|||
}
|
||||
|
||||
$("#toast-container").animate({
|
||||
right: (PanelToggler.getPanelSize()[0] + 5)
|
||||
right: (UIUtil.getSidePanelSize()[0] + 5)
|
||||
}, {
|
||||
queue: false,
|
||||
duration: 500
|
||||
|
@ -116,7 +115,7 @@ var PanelToggler = {
|
|||
},
|
||||
|
||||
resizeChat () {
|
||||
let [width, height] = this.getPanelSize();
|
||||
let [width, height] = UIUtil.getSidePanelSize();
|
||||
Chat.resizeChat(width, height);
|
||||
},
|
||||
|
||||
|
@ -156,21 +155,6 @@ var PanelToggler = {
|
|||
null);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the size of the side panel.
|
||||
*/
|
||||
getPanelSize () {
|
||||
var availableHeight = window.innerHeight;
|
||||
var availableWidth = window.innerWidth;
|
||||
|
||||
var panelWidth = 200;
|
||||
if (availableWidth * 0.2 < 200) {
|
||||
panelWidth = availableWidth * 0.2;
|
||||
}
|
||||
|
||||
return [panelWidth, availableHeight];
|
||||
},
|
||||
|
||||
isVisible () {
|
||||
return (Chat.isVisible() ||
|
||||
ContactList.isVisible() ||
|
||||
|
|
|
@ -9,15 +9,13 @@ const defaultBottomToolbarButtons = {
|
|||
'filmstrip': '#bottom_toolbar_film_strip'
|
||||
};
|
||||
|
||||
$(document).bind("remotevideo.resized", function (event, width, height) {
|
||||
let toolbar = $('#bottomToolbar');
|
||||
let bottom = (height - toolbar.outerHeight())/2 + 18;
|
||||
|
||||
toolbar.css({bottom});
|
||||
});
|
||||
|
||||
const BottomToolbar = {
|
||||
init (emitter) {
|
||||
init () {
|
||||
this.filmStrip = $('#remoteVideos');
|
||||
this.toolbar = $('#bottomToolbar');
|
||||
},
|
||||
|
||||
setupListeners (emitter) {
|
||||
UIUtil.hideDisabledButtons(defaultBottomToolbarButtons);
|
||||
|
||||
const buttonHandlers = {
|
||||
|
@ -41,7 +39,69 @@ const BottomToolbar = {
|
|||
},
|
||||
|
||||
toggleFilmStrip () {
|
||||
$("#remoteVideos").toggleClass("hidden");
|
||||
this.filmStrip.toggleClass("hidden");
|
||||
},
|
||||
|
||||
isFilmStripVisible () {
|
||||
return !this.filmStrip.hasClass('hidden');
|
||||
},
|
||||
|
||||
setupFilmStripOnly () {
|
||||
this.filmStrip.css({
|
||||
padding: "0px 0px 18px 0px",
|
||||
right: 0
|
||||
});
|
||||
},
|
||||
|
||||
getFilmStripHeight () {
|
||||
if (this.isFilmStripVisible()) {
|
||||
return this.filmStrip.outerHeight();
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
},
|
||||
|
||||
getFilmStripWidth () {
|
||||
return this.filmStrip.width();
|
||||
},
|
||||
|
||||
resizeThumbnails (thumbWidth, thumbHeight, animate = false) {
|
||||
return new Promise(resolve => {
|
||||
this.filmStrip.animate({
|
||||
// adds 2 px because of small video 1px border
|
||||
height: thumbHeight + 2
|
||||
}, {
|
||||
queue: false,
|
||||
duration: animate ? 500 : 0
|
||||
});
|
||||
|
||||
this.getThumbs().animate({
|
||||
height: thumbHeight,
|
||||
width: thumbWidth
|
||||
}, {
|
||||
queue: false,
|
||||
duration: animate ? 500 : 0,
|
||||
complete: resolve
|
||||
});
|
||||
|
||||
if (!animate) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
resizeToolbar (thumbWidth, thumbHeight) {
|
||||
let bottom = (thumbHeight - this.toolbar.outerHeight())/2 + 18;
|
||||
this.toolbar.css({bottom});
|
||||
},
|
||||
|
||||
getThumbs (visible = false) {
|
||||
let selector = 'span';
|
||||
if (visible) {
|
||||
selector += ':visible';
|
||||
}
|
||||
|
||||
return this.filmStrip.children(selector);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -136,7 +136,7 @@ const buttonHandlers = {
|
|||
} else {
|
||||
AnalyticsAdapter.sendEvent('toolbar.screen.enabled');
|
||||
}
|
||||
APP.desktopsharing.toggleScreenSharing();
|
||||
emitter.emit(UIEvents.TOGGLE_SCREENSHARING);
|
||||
},
|
||||
"toolbar_button_fullScreen": function() {
|
||||
AnalyticsAdapter.sendEvent('toolbar.fullscreen.enabled');
|
||||
|
@ -182,17 +182,17 @@ const buttonHandlers = {
|
|||
}
|
||||
};
|
||||
const defaultToolbarButtons = {
|
||||
'microphone': '#toolbar_button_mute',
|
||||
'camera': '#toolbar_button_camera',
|
||||
'desktop': '#toolbar_button_desktopsharing',
|
||||
'security': '#toolbar_button_security',
|
||||
'invite': '#toolbar_button_link',
|
||||
'chat': '#toolbar_button_chat',
|
||||
'prezi': '#toolbar_button_prezi',
|
||||
'etherpad': '#toolbar_button_etherpad',
|
||||
'fullscreen': '#toolbar_button_fullScreen',
|
||||
'settings': '#toolbar_button_settings',
|
||||
'hangup': '#toolbar_button_hangup'
|
||||
'microphone': '#toolbar_button_mute',
|
||||
'camera': '#toolbar_button_camera',
|
||||
'desktop': '#toolbar_button_desktopsharing',
|
||||
'security': '#toolbar_button_security',
|
||||
'invite': '#toolbar_button_link',
|
||||
'chat': '#toolbar_button_chat',
|
||||
'prezi': '#toolbar_button_prezi',
|
||||
'etherpad': '#toolbar_button_etherpad',
|
||||
'fullscreen': '#toolbar_button_fullScreen',
|
||||
'settings': '#toolbar_button_settings',
|
||||
'hangup': '#toolbar_button_hangup'
|
||||
};
|
||||
|
||||
function dialpadButtonClicked() {
|
||||
|
@ -208,7 +208,7 @@ function showSipNumberInput () {
|
|||
messageHandler.openTwoButtonDialog(
|
||||
null, null, null,
|
||||
`<h2>${sipMsg}</h2>
|
||||
<input name="sipNumber" type="text" value="${defaultNumber}" autofocus>`,
|
||||
<input name="sipNumber" type="text" value="${defaultNumber}" autofocus>`,
|
||||
false, "dialog.Dial",
|
||||
function (e, v, m, f) {
|
||||
if (v && f.sipNumber) {
|
||||
|
@ -250,7 +250,7 @@ const Toolbar = {
|
|||
* Disables and enables some of the buttons.
|
||||
*/
|
||||
setupButtonsFromConfig () {
|
||||
if (UIUtil.isButtonEnabled('prezi')) {
|
||||
if (!UIUtil.isButtonEnabled('prezi')) {
|
||||
$("#toolbar_button_prezi").css({display: "none"});
|
||||
}
|
||||
},
|
||||
|
@ -283,6 +283,12 @@ const Toolbar = {
|
|||
}
|
||||
},
|
||||
|
||||
showEtherpadButton () {
|
||||
if (!$('#toolbar_button_etherpad').is(":visible")) {
|
||||
$('#toolbar_button_etherpad').css({display: 'inline-block'});
|
||||
}
|
||||
},
|
||||
|
||||
// Shows or hides the 'recording' button.
|
||||
showRecordingButton (show) {
|
||||
if (UIUtil.isButtonEnabled('recording') && show) {
|
||||
|
@ -303,9 +309,8 @@ const Toolbar = {
|
|||
// checks whether desktop sharing is enabled and whether
|
||||
// we have params to start automatically sharing
|
||||
checkAutoEnableDesktopSharing () {
|
||||
if (UIUtil.isButtonEnabled('desktop')
|
||||
&& config.autoEnableDesktopSharing) {
|
||||
APP.desktopsharing.toggleScreenSharing();
|
||||
if (UIUtil.isButtonEnabled('desktop') && config.autoEnableDesktopSharing) {
|
||||
emitter.emit(UIEvents.TOGGLE_SCREENSHARING);
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/* global APP, config, $, interfaceConfig */
|
||||
|
||||
import UIUtil from '../util/UIUtil';
|
||||
import BottomToolbar from './BottomToolbar';
|
||||
|
||||
let toolbarTimeoutObject;
|
||||
let toolbarTimeout = interfaceConfig.INITIAL_TOOLBAR_TIMEOUT;
|
||||
|
@ -47,7 +48,7 @@ function hideToolbar() {
|
|||
} else {
|
||||
header.hide("slide", { direction: "up", duration: 300});
|
||||
$('#subject').animate({top: "-=40"}, 300);
|
||||
if ($("#remoteVideos").hasClass("hidden")) {
|
||||
if (!BottomToolbar.isFilmStripVisible()) {
|
||||
bottomToolbar.hide(
|
||||
"slide", {direction: "right", duration: 300}
|
||||
);
|
||||
|
|
|
@ -1,19 +1,34 @@
|
|||
/* global $, config, interfaceConfig */
|
||||
|
||||
import PanelToggler from "../side_pannels/SidePanelToggler";
|
||||
|
||||
/**
|
||||
* Created by hristo on 12/22/14.
|
||||
*/
|
||||
var UIUtil = {
|
||||
|
||||
/**
|
||||
* Returns the size of the side panel.
|
||||
*/
|
||||
getSidePanelSize () {
|
||||
var availableHeight = window.innerHeight;
|
||||
var availableWidth = window.innerWidth;
|
||||
|
||||
var panelWidth = 200;
|
||||
if (availableWidth * 0.2 < 200) {
|
||||
panelWidth = availableWidth * 0.2;
|
||||
}
|
||||
|
||||
return [panelWidth, availableHeight];
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the available video width.
|
||||
*/
|
||||
getAvailableVideoWidth: function (isVisible) {
|
||||
if(typeof isVisible === "undefined" || isVisible === null)
|
||||
isVisible = PanelToggler.isVisible();
|
||||
var rightPanelWidth
|
||||
= isVisible ? PanelToggler.getPanelSize()[0] : 0;
|
||||
getAvailableVideoWidth: function (isSidePanelVisible) {
|
||||
let rightPanelWidth = 0;
|
||||
|
||||
if (isSidePanelVisible) {
|
||||
rightPanelWidth = UIUtil.getSidePanelSize()[0];
|
||||
}
|
||||
|
||||
return window.innerWidth - rightPanelWidth;
|
||||
},
|
||||
|
@ -118,6 +133,12 @@ import PanelToggler from "../side_pannels/SidePanelToggler";
|
|||
|
||||
redirect (url) {
|
||||
window.location.href = url;
|
||||
},
|
||||
|
||||
isFullScreen () {
|
||||
return document.fullScreen
|
||||
|| document.mozFullScreen
|
||||
|| document.webkitIsFullScreen;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
|
||||
export default class LargeContainer {
|
||||
|
||||
/**
|
||||
* @returns Promise
|
||||
*/
|
||||
show () {
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns Promise
|
||||
*/
|
||||
hide () {
|
||||
}
|
||||
|
||||
resize (containerWidth, containerHeight, animate) {
|
||||
}
|
||||
|
||||
onHoverIn (e) {
|
||||
}
|
||||
|
||||
onHoverOut (e) {
|
||||
}
|
||||
}
|
|
@ -1,98 +1,21 @@
|
|||
/* global $, APP, interfaceConfig */
|
||||
/* jshint -W101 */
|
||||
import Avatar from "../avatar/Avatar";
|
||||
import ToolbarToggler from "../toolbars/ToolbarToggler";
|
||||
|
||||
import UIUtil from "../util/UIUtil";
|
||||
import UIEvents from "../../../service/UI/UIEvents";
|
||||
import LargeContainer from './LargeContainer';
|
||||
import BottomToolbar from '../toolbars/BottomToolbar';
|
||||
|
||||
var RTCBrowserType = require("../../RTC/RTCBrowserType");
|
||||
const RTCBrowserType = require("../../RTC/RTCBrowserType");
|
||||
|
||||
// FIXME: With Temasys we have to re-select everytime
|
||||
//var video = $('#largeVideo');
|
||||
const avatarSize = interfaceConfig.ACTIVE_SPEAKER_AVATAR_SIZE;
|
||||
|
||||
var currentVideoWidth = null;
|
||||
var currentVideoHeight = null;
|
||||
// By default we use camera
|
||||
var getVideoSize = getCameraVideoSize;
|
||||
var getVideoPosition = getCameraVideoPosition;
|
||||
/**
|
||||
* The small video instance that is displayed in the large video
|
||||
* @type {SmallVideo}
|
||||
*/
|
||||
var currentSmallVideo = null;
|
||||
/**
|
||||
* Indicates whether the large video is enabled.
|
||||
* @type {boolean}
|
||||
*/
|
||||
var isEnabled = true;
|
||||
/**
|
||||
* Current large video state.
|
||||
* Possible values - video, prezi or etherpad.
|
||||
* @type {string}
|
||||
*/
|
||||
var state = "video";
|
||||
|
||||
/**
|
||||
* Returns the html element associated with the passed state of large video
|
||||
* @param state the state.
|
||||
* @returns {JQuery|*|jQuery|HTMLElement} the container.
|
||||
*/
|
||||
function getContainerByState(state) {
|
||||
var selector = null;
|
||||
switch (state) {
|
||||
case "video":
|
||||
selector = "#largeVideoWrapper";
|
||||
break;
|
||||
case "etherpad":
|
||||
selector = "#etherpad>iframe";
|
||||
break;
|
||||
case "prezi":
|
||||
selector = "#presentation>iframe";
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
return $(selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the size and position of the given video element.
|
||||
*
|
||||
* @param video the video element to position
|
||||
* @param width the desired video width
|
||||
* @param height the desired video height
|
||||
* @param horizontalIndent the left and right indent
|
||||
* @param verticalIndent the top and bottom indent
|
||||
*/
|
||||
function positionVideo(video,
|
||||
width,
|
||||
height,
|
||||
horizontalIndent,
|
||||
verticalIndent,
|
||||
animate) {
|
||||
if (animate) {
|
||||
video.animate({
|
||||
width: width,
|
||||
height: height,
|
||||
top: verticalIndent,
|
||||
bottom: verticalIndent,
|
||||
left: horizontalIndent,
|
||||
right: horizontalIndent
|
||||
}, {
|
||||
queue: false,
|
||||
duration: 500
|
||||
});
|
||||
function getStreamId(stream) {
|
||||
if (stream.isLocal()) {
|
||||
return APP.conference.localId;
|
||||
} else {
|
||||
video.width(width);
|
||||
video.height(height);
|
||||
video.css({
|
||||
top: verticalIndent,
|
||||
bottom: verticalIndent,
|
||||
left: horizontalIndent,
|
||||
right: horizontalIndent
|
||||
});
|
||||
return stream.getParticipantId();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -106,80 +29,25 @@ function getDesktopVideoSize(videoWidth,
|
|||
videoHeight,
|
||||
videoSpaceWidth,
|
||||
videoSpaceHeight) {
|
||||
if (!videoWidth)
|
||||
videoWidth = currentVideoWidth;
|
||||
if (!videoHeight)
|
||||
videoHeight = currentVideoHeight;
|
||||
|
||||
var aspectRatio = videoWidth / videoHeight;
|
||||
let aspectRatio = videoWidth / videoHeight;
|
||||
|
||||
var availableWidth = Math.max(videoWidth, videoSpaceWidth);
|
||||
var availableHeight = Math.max(videoHeight, videoSpaceHeight);
|
||||
let availableWidth = Math.max(videoWidth, videoSpaceWidth);
|
||||
let availableHeight = Math.max(videoHeight, videoSpaceHeight);
|
||||
|
||||
var filmstrip = $("#remoteVideos");
|
||||
videoSpaceHeight -= BottomToolbar.getFilmStripHeight();
|
||||
|
||||
if (!filmstrip.hasClass("hidden"))
|
||||
videoSpaceHeight -= filmstrip.outerHeight();
|
||||
|
||||
if (availableWidth / aspectRatio >= videoSpaceHeight)
|
||||
{
|
||||
if (availableWidth / aspectRatio >= videoSpaceHeight) {
|
||||
availableHeight = videoSpaceHeight;
|
||||
availableWidth = availableHeight * aspectRatio;
|
||||
}
|
||||
|
||||
if (availableHeight * aspectRatio >= videoSpaceWidth)
|
||||
{
|
||||
if (availableHeight * aspectRatio >= videoSpaceWidth) {
|
||||
availableWidth = videoSpaceWidth;
|
||||
availableHeight = availableWidth / aspectRatio;
|
||||
}
|
||||
|
||||
return [availableWidth, availableHeight];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns an array of the video horizontal and vertical indents,
|
||||
* so that if fits its parent.
|
||||
*
|
||||
* @return an array with 2 elements, the horizontal indent and the vertical
|
||||
* indent
|
||||
*/
|
||||
function getCameraVideoPosition(videoWidth,
|
||||
videoHeight,
|
||||
videoSpaceWidth,
|
||||
videoSpaceHeight) {
|
||||
// Parent height isn't completely calculated when we position the video in
|
||||
// full screen mode and this is why we use the screen height in this case.
|
||||
// Need to think it further at some point and implement it properly.
|
||||
var isFullScreen = document.fullScreen ||
|
||||
document.mozFullScreen ||
|
||||
document.webkitIsFullScreen;
|
||||
if (isFullScreen)
|
||||
videoSpaceHeight = window.innerHeight;
|
||||
|
||||
var horizontalIndent = (videoSpaceWidth - videoWidth) / 2;
|
||||
var verticalIndent = (videoSpaceHeight - videoHeight) / 2;
|
||||
|
||||
return [horizontalIndent, verticalIndent];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of the video horizontal and vertical indents.
|
||||
* Centers horizontally and top aligns vertically.
|
||||
*
|
||||
* @return an array with 2 elements, the horizontal indent and the vertical
|
||||
* indent
|
||||
*/
|
||||
function getDesktopVideoPosition(videoWidth,
|
||||
videoHeight,
|
||||
videoSpaceWidth,
|
||||
videoSpaceHeight) {
|
||||
|
||||
var horizontalIndent = (videoSpaceWidth - videoWidth) / 2;
|
||||
|
||||
var verticalIndent = 0;// Top aligned
|
||||
|
||||
return [horizontalIndent, verticalIndent];
|
||||
return { availableWidth, availableHeight };
|
||||
}
|
||||
|
||||
|
||||
|
@ -199,15 +67,10 @@ function getCameraVideoSize(videoWidth,
|
|||
videoSpaceWidth,
|
||||
videoSpaceHeight) {
|
||||
|
||||
if (!videoWidth)
|
||||
videoWidth = currentVideoWidth;
|
||||
if (!videoHeight)
|
||||
videoHeight = currentVideoHeight;
|
||||
let aspectRatio = videoWidth / videoHeight;
|
||||
|
||||
var aspectRatio = videoWidth / videoHeight;
|
||||
|
||||
var availableWidth = videoWidth;
|
||||
var availableHeight = videoHeight;
|
||||
let availableWidth = videoWidth;
|
||||
let availableHeight = videoHeight;
|
||||
|
||||
if (interfaceConfig.VIDEO_LAYOUT_FIT == 'height') {
|
||||
availableHeight = videoSpaceHeight;
|
||||
|
@ -233,487 +96,347 @@ function getCameraVideoSize(videoWidth,
|
|||
}
|
||||
|
||||
|
||||
return [availableWidth, availableHeight];
|
||||
return { availableWidth, availableHeight };
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the src of the active speaker avatar
|
||||
* Returns an array of the video horizontal and vertical indents,
|
||||
* so that if fits its parent.
|
||||
*
|
||||
* @return an array with 2 elements, the horizontal indent and the vertical
|
||||
* indent
|
||||
*/
|
||||
function updateActiveSpeakerAvatarSrc() {
|
||||
let avatar = $("#activeSpeakerAvatar");
|
||||
let id = currentSmallVideo.id;
|
||||
let url = Avatar.getActiveSpeakerUrl(id);
|
||||
if (id && avatar.attr('src') !== url) {
|
||||
avatar.attr('src', url);
|
||||
currentSmallVideo.showAvatar();
|
||||
function getCameraVideoPosition(videoWidth,
|
||||
videoHeight,
|
||||
videoSpaceWidth,
|
||||
videoSpaceHeight) {
|
||||
// Parent height isn't completely calculated when we position the video in
|
||||
// full screen mode and this is why we use the screen height in this case.
|
||||
// Need to think it further at some point and implement it properly.
|
||||
if (UIUtil.isFullScreen()) {
|
||||
videoSpaceHeight = window.innerHeight;
|
||||
}
|
||||
|
||||
let horizontalIndent = (videoSpaceWidth - videoWidth) / 2;
|
||||
let verticalIndent = (videoSpaceHeight - videoHeight) / 2;
|
||||
|
||||
return { horizontalIndent, verticalIndent };
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the video source of the large video.
|
||||
* @param isVisible
|
||||
* Returns an array of the video horizontal and vertical indents.
|
||||
* Centers horizontally and top aligns vertically.
|
||||
*
|
||||
* @return an array with 2 elements, the horizontal indent and the vertical
|
||||
* indent
|
||||
*/
|
||||
function changeVideo(isVisible) {
|
||||
function getDesktopVideoPosition(videoWidth,
|
||||
videoHeight,
|
||||
videoSpaceWidth,
|
||||
videoSpaceHeight) {
|
||||
|
||||
if (!currentSmallVideo) {
|
||||
console.error("Unable to change large video - no 'currentSmallVideo'");
|
||||
return;
|
||||
}
|
||||
let horizontalIndent = (videoSpaceWidth - videoWidth) / 2;
|
||||
|
||||
updateActiveSpeakerAvatarSrc();
|
||||
let largeVideoElement = $('#largeVideo');
|
||||
let verticalIndent = 0;// Top aligned
|
||||
|
||||
currentSmallVideo.stream.attach(largeVideoElement);
|
||||
|
||||
let flipX = currentSmallVideo.flipX;
|
||||
|
||||
largeVideoElement.css({
|
||||
transform: flipX ? "scaleX(-1)" : "none"
|
||||
});
|
||||
|
||||
LargeVideo.updateVideoSizeAndPosition(currentSmallVideo.getVideoType());
|
||||
|
||||
// Only if the large video is currently visible.
|
||||
if (isVisible) {
|
||||
LargeVideo.VideoLayout.largeVideoUpdated(currentSmallVideo);
|
||||
|
||||
$('#largeVideoWrapper').fadeTo(300, 1);
|
||||
}
|
||||
return { horizontalIndent, verticalIndent };
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the html elements for the large video.
|
||||
*/
|
||||
function createLargeVideoHTML()
|
||||
{
|
||||
var html = '<div id="largeVideoContainer" class="videocontainer">';
|
||||
html += '<div id="presentation"></div>' +
|
||||
'<div id="etherpad"></div>' +
|
||||
'<a target="_new"><div class="watermark leftwatermark"></div></a>' +
|
||||
'<a target="_new"><div class="watermark rightwatermark"></div></a>' +
|
||||
'<a class="poweredby" href="http://jitsi.org" target="_new" >' +
|
||||
'<span data-i18n="poweredby"></span> jitsi.org' +
|
||||
'</a>'+
|
||||
'<div id="activeSpeaker">' +
|
||||
'<img id="activeSpeakerAvatar" src=""/>' +
|
||||
'<canvas id="activeSpeakerAudioLevel"></canvas>' +
|
||||
'</div>' +
|
||||
'<div id="largeVideoWrapper">' +
|
||||
'<video id="largeVideo" muted="true"' +
|
||||
'autoplay oncontextmenu="return false;"></video>' +
|
||||
'</div id="largeVideoWrapper">' +
|
||||
'<span id="videoConnectionMessage"></span>';
|
||||
html += '</div>';
|
||||
$(html).prependTo("#videospace");
|
||||
export const VideoContainerType = "video";
|
||||
|
||||
if (interfaceConfig.SHOW_JITSI_WATERMARK) {
|
||||
var leftWatermarkDiv
|
||||
= $("#largeVideoContainer div[class='watermark leftwatermark']");
|
||||
|
||||
leftWatermarkDiv.css({display: 'block'});
|
||||
leftWatermarkDiv.parent().get(0).href
|
||||
= interfaceConfig.JITSI_WATERMARK_LINK;
|
||||
class VideoContainer extends LargeContainer {
|
||||
// FIXME: With Temasys we have to re-select everytime
|
||||
get $video () {
|
||||
return $('#largeVideo');
|
||||
}
|
||||
|
||||
if (interfaceConfig.SHOW_BRAND_WATERMARK) {
|
||||
var rightWatermarkDiv
|
||||
= $("#largeVideoContainer div[class='watermark rightwatermark']");
|
||||
|
||||
rightWatermarkDiv.css({display: 'block'});
|
||||
rightWatermarkDiv.parent().get(0).href
|
||||
= interfaceConfig.BRAND_WATERMARK_LINK;
|
||||
rightWatermarkDiv.get(0).style.backgroundImage
|
||||
= "url(images/rightwatermark.png)";
|
||||
get id () {
|
||||
if (this.stream) {
|
||||
return getStreamId(this.stream);
|
||||
}
|
||||
}
|
||||
|
||||
if (interfaceConfig.SHOW_POWERED_BY) {
|
||||
$("#largeVideoContainer>a[class='poweredby']").css({display: 'block'});
|
||||
constructor (onPlay) {
|
||||
super();
|
||||
this.stream = null;
|
||||
this.videoType = null;
|
||||
|
||||
this.$avatar = $('#activeSpeaker');
|
||||
this.$wrapper = $('#largeVideoWrapper');
|
||||
|
||||
if (!RTCBrowserType.isIExplorer()) {
|
||||
this.$video.volume = 0;
|
||||
}
|
||||
|
||||
this.$video.on('play', onPlay);
|
||||
}
|
||||
|
||||
if (!RTCBrowserType.isIExplorer()) {
|
||||
$('#largeVideo').volume = 0;
|
||||
}
|
||||
}
|
||||
|
||||
var LargeVideo = {
|
||||
|
||||
init: function (VideoLayout, emitter) {
|
||||
if(!isEnabled)
|
||||
return;
|
||||
createLargeVideoHTML();
|
||||
|
||||
this.VideoLayout = VideoLayout;
|
||||
this.eventEmitter = emitter;
|
||||
this.eventEmitter.emit(UIEvents.LARGEVIDEO_INIT);
|
||||
var self = this;
|
||||
// Listen for large video size updates
|
||||
var largeVideo = $('#largeVideo')[0];
|
||||
var onplaying = function (arg1, arg2, arg3) {
|
||||
// re-select
|
||||
if (RTCBrowserType.isTemasysPluginUsed())
|
||||
largeVideo = $('#largeVideo')[0];
|
||||
currentVideoWidth = largeVideo.videoWidth;
|
||||
currentVideoHeight = largeVideo.videoHeight;
|
||||
self.position(currentVideoWidth, currentVideoHeight);
|
||||
getStreamSize () {
|
||||
let video = this.$video[0];
|
||||
return {
|
||||
width: video.videoWidth,
|
||||
height: video.videoHeight
|
||||
};
|
||||
largeVideo.onplaying = onplaying;
|
||||
},
|
||||
/**
|
||||
* Indicates if the large video is currently visible.
|
||||
*
|
||||
* @return <tt>true</tt> if visible, <tt>false</tt> - otherwise
|
||||
*/
|
||||
isLargeVideoVisible: function() {
|
||||
return $('#largeVideoWrapper').is(':visible');
|
||||
},
|
||||
/**
|
||||
* Returns <tt>true</tt> if the user is currently displayed on large video.
|
||||
*/
|
||||
isCurrentlyOnLarge: function (id) {
|
||||
return id && id === this.getId();
|
||||
},
|
||||
/**
|
||||
* Updates the large video with the given new video source.
|
||||
*/
|
||||
updateLargeVideo: function (id, forceUpdate) {
|
||||
if(!isEnabled) {
|
||||
return;
|
||||
}
|
||||
let newSmallVideo = this.VideoLayout.getSmallVideo(id);
|
||||
console.info(`hover in ${id} , video: `, newSmallVideo);
|
||||
}
|
||||
|
||||
if (!newSmallVideo) {
|
||||
console.error("Small video not found for: " + id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!LargeVideo.isCurrentlyOnLarge(id) || forceUpdate) {
|
||||
$('#activeSpeaker').css('visibility', 'hidden');
|
||||
|
||||
let oldId = this.getId();
|
||||
|
||||
currentSmallVideo = newSmallVideo;
|
||||
|
||||
if (oldId !== id) {
|
||||
// we want the notification to trigger even if id is undefined,
|
||||
// or null.
|
||||
this.eventEmitter.emit(UIEvents.SELECTED_ENDPOINT, id);
|
||||
}
|
||||
// We are doing fadeOut/fadeIn animations on parent div which wraps
|
||||
// largeVideo, because when Temasys plugin is in use it replaces
|
||||
// <video> elements with plugin <object> tag. In Safari jQuery is
|
||||
// unable to store values on this plugin object which breaks all
|
||||
// animation effects performed on it directly.
|
||||
//
|
||||
// If for any reason large video was hidden before calling fadeOut
|
||||
// changeVideo will never be called, so we call show() in chain just
|
||||
// to be sure
|
||||
$('#largeVideoWrapper').show().fadeTo(300, 0,
|
||||
changeVideo.bind($('#largeVideo'), this.isLargeVideoVisible()));
|
||||
getVideoSize (containerWidth, containerHeight) {
|
||||
let { width, height } = this.getStreamSize();
|
||||
if (this.stream && this.isScreenSharing()) {
|
||||
return getDesktopVideoSize(width, height, containerWidth, containerHeight);
|
||||
} else {
|
||||
if (currentSmallVideo) {
|
||||
currentSmallVideo.showAvatar();
|
||||
}
|
||||
return getCameraVideoSize(width, height, containerWidth, containerHeight);
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Shows/hides the large video.
|
||||
*/
|
||||
setLargeVideoVisible: function(isVisible) {
|
||||
if(!isEnabled)
|
||||
return;
|
||||
if (isVisible) {
|
||||
$('#largeVideoWrapper').css({visibility: 'visible'});
|
||||
$('.watermark').css({visibility: 'visible'});
|
||||
if(currentSmallVideo)
|
||||
currentSmallVideo.enableDominantSpeaker(true);
|
||||
}
|
||||
else {
|
||||
$('#largeVideoWrapper').css({visibility: 'hidden'});
|
||||
$('#activeSpeaker').css('visibility', 'hidden');
|
||||
$('.watermark').css({visibility: 'hidden'});
|
||||
if(currentSmallVideo)
|
||||
currentSmallVideo.enableDominantSpeaker(false);
|
||||
}
|
||||
},
|
||||
onVideoTypeChanged: function (id, newVideoType) {
|
||||
if (!isEnabled)
|
||||
return;
|
||||
if (LargeVideo.isCurrentlyOnLarge(id)) {
|
||||
LargeVideo.updateVideoSizeAndPosition(newVideoType);
|
||||
|
||||
this.position(null, null, null, null, true);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Positions the large video.
|
||||
*
|
||||
* @param videoWidth the stream video width
|
||||
* @param videoHeight the stream video height
|
||||
*/
|
||||
position: function (videoWidth, videoHeight,
|
||||
videoSpaceWidth, videoSpaceHeight, animate) {
|
||||
if(!isEnabled)
|
||||
return;
|
||||
if(!videoSpaceWidth)
|
||||
videoSpaceWidth = $('#videospace').width();
|
||||
if(!videoSpaceHeight)
|
||||
videoSpaceHeight = window.innerHeight;
|
||||
|
||||
var videoSize = getVideoSize(videoWidth,
|
||||
videoHeight,
|
||||
videoSpaceWidth,
|
||||
videoSpaceHeight);
|
||||
|
||||
var largeVideoWidth = videoSize[0];
|
||||
var largeVideoHeight = videoSize[1];
|
||||
|
||||
var videoPosition = getVideoPosition(largeVideoWidth,
|
||||
largeVideoHeight,
|
||||
videoSpaceWidth,
|
||||
videoSpaceHeight);
|
||||
|
||||
var horizontalIndent = videoPosition[0];
|
||||
var verticalIndent = videoPosition[1];
|
||||
|
||||
positionVideo($('#largeVideoWrapper'),
|
||||
largeVideoWidth,
|
||||
largeVideoHeight,
|
||||
horizontalIndent, verticalIndent, animate);
|
||||
},
|
||||
/**
|
||||
* Resizes the large html elements.
|
||||
*
|
||||
* @param animate boolean property that indicates whether the resize should
|
||||
* be animated or not.
|
||||
* @param isSideBarVisible boolean property that indicates whether the chat
|
||||
* area is displayed or not.
|
||||
* If that parameter is null the method will check the chat panel
|
||||
* visibility.
|
||||
* @param completeFunction a function to be called when the video space is
|
||||
* resized
|
||||
* @returns {*[]} array with the current width and height values of the
|
||||
* largeVideo html element.
|
||||
*/
|
||||
resize: function (animate, isSideBarVisible, completeFunction) {
|
||||
if(!isEnabled)
|
||||
return;
|
||||
var availableHeight = window.innerHeight;
|
||||
var availableWidth = UIUtil.getAvailableVideoWidth(isSideBarVisible);
|
||||
|
||||
if (availableWidth < 0 || availableHeight < 0) return;
|
||||
|
||||
var avatarSize = interfaceConfig.ACTIVE_SPEAKER_AVATAR_SIZE;
|
||||
var top = availableHeight / 2 - avatarSize / 4 * 3;
|
||||
$('#activeSpeaker').css('top', top);
|
||||
|
||||
this.VideoLayout
|
||||
.resizeVideoSpace(animate, isSideBarVisible, completeFunction);
|
||||
if(animate) {
|
||||
$('#largeVideoContainer').animate({
|
||||
width: availableWidth,
|
||||
height: availableHeight
|
||||
},
|
||||
{
|
||||
queue: false,
|
||||
duration: 500
|
||||
});
|
||||
getVideoPosition (width, height, containerWidth, containerHeight) {
|
||||
if (this.stream && this.isScreenSharing()) {
|
||||
return getDesktopVideoPosition(width, height, containerWidth, containerHeight);
|
||||
} else {
|
||||
$('#largeVideoContainer').width(availableWidth);
|
||||
$('#largeVideoContainer').height(availableHeight);
|
||||
return getCameraVideoPosition(width, height, containerWidth, containerHeight);
|
||||
}
|
||||
return [availableWidth, availableHeight];
|
||||
},
|
||||
/**
|
||||
* Resizes the large video.
|
||||
*
|
||||
* @param isSideBarVisible indicating if the side bar is visible
|
||||
* @param completeFunction the callback function to be executed after the
|
||||
* resize
|
||||
*/
|
||||
resizeVideoAreaAnimated: function (isSideBarVisible, completeFunction) {
|
||||
if(!isEnabled)
|
||||
return;
|
||||
var size = this.resize(true, isSideBarVisible, completeFunction);
|
||||
this.position(null, null, size[0], size[1], true);
|
||||
},
|
||||
/**
|
||||
* Updates the video size and position.
|
||||
*
|
||||
* @param videoType the video type indicating if the stream is of type
|
||||
* desktop or web cam
|
||||
*/
|
||||
updateVideoSizeAndPosition: function (videoType) {
|
||||
if (!videoType)
|
||||
videoType = currentSmallVideo.getVideoType();
|
||||
}
|
||||
|
||||
var isDesktop = videoType === 'desktop';
|
||||
resize (containerWidth, containerHeight, animate = false) {
|
||||
let { width, height } = this.getVideoSize(containerWidth, containerHeight);
|
||||
let { horizontalIndent, verticalIndent } = this.getVideoPosition(width, height, containerWidth, containerHeight);
|
||||
|
||||
// Change the way we'll be measuring and positioning large video
|
||||
getVideoSize = isDesktop ? getDesktopVideoSize : getCameraVideoSize;
|
||||
getVideoPosition = isDesktop ? getDesktopVideoPosition :
|
||||
getCameraVideoPosition;
|
||||
},
|
||||
getId: function () {
|
||||
return currentSmallVideo ? currentSmallVideo.id : null;
|
||||
},
|
||||
updateAvatar: function (id) {
|
||||
if (!isEnabled) {
|
||||
return;
|
||||
}
|
||||
if (id === this.getId()) {
|
||||
updateActiveSpeakerAvatarSrc();
|
||||
}
|
||||
},
|
||||
showAvatar: function (id, show) {
|
||||
if (!isEnabled) {
|
||||
return;
|
||||
}
|
||||
if (this.getId() === id && state === "video") {
|
||||
$("#largeVideoWrapper").css("visibility", show ? "hidden" : "visible");
|
||||
$('#activeSpeaker').css("visibility", show ? "visible" : "hidden");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
/**
|
||||
* Disables the large video
|
||||
*/
|
||||
disable: function () {
|
||||
isEnabled = false;
|
||||
},
|
||||
/**
|
||||
* Enables the large video
|
||||
*/
|
||||
enable: function () {
|
||||
isEnabled = true;
|
||||
},
|
||||
/**
|
||||
* Returns true if the video is enabled.
|
||||
*/
|
||||
isEnabled: function () {
|
||||
return isEnabled;
|
||||
},
|
||||
/**
|
||||
* Creates the iframe used by the etherpad
|
||||
* @param src the value for src attribute
|
||||
* @param onloadHandler handler executed when the iframe loads it content
|
||||
* @returns {HTMLElement} the iframe
|
||||
*/
|
||||
createEtherpadIframe: function (src, onloadHandler) {
|
||||
if(!isEnabled)
|
||||
return;
|
||||
// update avatar position
|
||||
let top = this.containerHeight / 2 - avatarSize / 4 * 3;
|
||||
this.$avatar.css('top', top);
|
||||
|
||||
var etherpadIFrame = document.createElement('iframe');
|
||||
etherpadIFrame.src = src;
|
||||
etherpadIFrame.frameBorder = 0;
|
||||
etherpadIFrame.scrolling = "no";
|
||||
etherpadIFrame.width = $('#largeVideoContainer').width() || 640;
|
||||
etherpadIFrame.height = $('#largeVideoContainer').height() || 480;
|
||||
etherpadIFrame.setAttribute('style', 'visibility: hidden;');
|
||||
this.$wrapper.animate({
|
||||
width,
|
||||
height,
|
||||
|
||||
document.getElementById('etherpad').appendChild(etherpadIFrame);
|
||||
top: verticalIndent,
|
||||
bottom: verticalIndent,
|
||||
|
||||
etherpadIFrame.onload = onloadHandler;
|
||||
left: horizontalIndent,
|
||||
right: horizontalIndent
|
||||
}, {
|
||||
queue: false,
|
||||
duration: animate ? 500 : 0
|
||||
});
|
||||
}
|
||||
|
||||
return etherpadIFrame;
|
||||
},
|
||||
/**
|
||||
* Changes the state of the large video.
|
||||
* Possible values - video, prezi, etherpad.
|
||||
* @param newState - the new state
|
||||
*/
|
||||
setState: function (newState) {
|
||||
if(state === newState)
|
||||
return;
|
||||
var currentContainer = getContainerByState(state);
|
||||
if(!currentContainer)
|
||||
return;
|
||||
setStream (stream, videoType) {
|
||||
this.stream = stream;
|
||||
this.videoType = videoType;
|
||||
|
||||
var self = this;
|
||||
var oldState = state;
|
||||
stream.attach(this.$video);
|
||||
|
||||
switch (newState)
|
||||
{
|
||||
case "etherpad":
|
||||
$('#activeSpeaker').css('visibility', 'hidden');
|
||||
currentContainer.fadeOut(300, function () {
|
||||
if (oldState === "prezi") {
|
||||
currentContainer.css({opacity: '0'});
|
||||
$('#reloadPresentation').css({display: 'none'});
|
||||
}
|
||||
else {
|
||||
self.setLargeVideoVisible(false);
|
||||
}
|
||||
});
|
||||
let flipX = stream.isLocal() && !this.isScreenSharing();
|
||||
this.$video.css({
|
||||
transform: flipX ? 'scaleX(-1)' : 'none'
|
||||
});
|
||||
}
|
||||
|
||||
$('#etherpad>iframe').fadeIn(300, function () {
|
||||
document.body.style.background = '#eeeeee';
|
||||
$('#etherpad>iframe').css({visibility: 'visible'});
|
||||
$('#etherpad').css({zIndex: 2});
|
||||
});
|
||||
break;
|
||||
case "prezi":
|
||||
var prezi = $('#presentation>iframe');
|
||||
currentContainer.fadeOut(300, function() {
|
||||
document.body.style.background = 'black';
|
||||
});
|
||||
prezi.fadeIn(300, function() {
|
||||
prezi.css({opacity:'1'});
|
||||
ToolbarToggler.dockToolbar(true);//fix that
|
||||
self.setLargeVideoVisible(false);
|
||||
$('#etherpad>iframe').css({visibility: 'hidden'});
|
||||
$('#etherpad').css({zIndex: 0});
|
||||
});
|
||||
$('#activeSpeaker').css('visibility', 'hidden');
|
||||
break;
|
||||
isScreenSharing () {
|
||||
return this.videoType === 'desktop';
|
||||
}
|
||||
|
||||
case "video":
|
||||
currentContainer.fadeOut(300, function () {
|
||||
$('#presentation>iframe').css({opacity:'0'});
|
||||
$('#reloadPresentation').css({display:'none'});
|
||||
$('#etherpad>iframe').css({visibility: 'hidden'});
|
||||
$('#etherpad').css({zIndex: 0});
|
||||
document.body.style.background = 'black';
|
||||
ToolbarToggler.dockToolbar(false);//fix that
|
||||
});
|
||||
$('#largeVideoWrapper').fadeIn(300, function () {
|
||||
self.setLargeVideoVisible(true);
|
||||
});
|
||||
break;
|
||||
showAvatar (show) {
|
||||
this.$avatar.css("visibility", show ? "visible" : "hidden");
|
||||
}
|
||||
|
||||
// We are doing fadeOut/fadeIn animations on parent div which wraps
|
||||
// largeVideo, because when Temasys plugin is in use it replaces
|
||||
// <video> elements with plugin <object> tag. In Safari jQuery is
|
||||
// unable to store values on this plugin object which breaks all
|
||||
// animation effects performed on it directly.
|
||||
|
||||
show () {
|
||||
let $wrapper = this.$wrapper;
|
||||
return new Promise(resolve => {
|
||||
$wrapper.fadeIn(300, function () {
|
||||
$wrapper.css({visibility: 'visible'});
|
||||
$('.watermark').css({visibility: 'visible'});
|
||||
});
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
hide () {
|
||||
this.showAvatar(false);
|
||||
|
||||
let $wrapper = this.$wrapper;
|
||||
|
||||
return new Promise(resolve => {
|
||||
$wrapper.fadeOut(300, function () {
|
||||
$wrapper.css({visibility: 'hidden'});
|
||||
$('.watermark').css({visibility: 'hidden'});
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default class LargeVideoManager {
|
||||
constructor () {
|
||||
this.containers = {};
|
||||
|
||||
this.state = VideoContainerType;
|
||||
this.videoContainer = new VideoContainer(() => this.resizeContainer(VideoContainerType));
|
||||
this.addContainer(VideoContainerType, this.videoContainer);
|
||||
|
||||
this.width = 0;
|
||||
this.height = 0;
|
||||
|
||||
this.$container = $('#largeVideoContainer');
|
||||
|
||||
this.$container.css({
|
||||
display: 'inline-block'
|
||||
});
|
||||
|
||||
if (interfaceConfig.SHOW_JITSI_WATERMARK) {
|
||||
let leftWatermarkDiv = this.$container.find("div.watermark.leftwatermark");
|
||||
|
||||
leftWatermarkDiv.css({display: 'block'});
|
||||
|
||||
leftWatermarkDiv.parent().attr('href', interfaceConfig.JITSI_WATERMARK_LINK);
|
||||
}
|
||||
|
||||
state = newState;
|
||||
if (interfaceConfig.SHOW_BRAND_WATERMARK) {
|
||||
let rightWatermarkDiv = this.$container.find("div.watermark.rightwatermark");
|
||||
|
||||
},
|
||||
/**
|
||||
* Returns the current state of the large video.
|
||||
* @returns {string} the current state - video, prezi or etherpad.
|
||||
*/
|
||||
getState: function () {
|
||||
return state;
|
||||
},
|
||||
/**
|
||||
* Sets hover handlers for the large video container div.
|
||||
*
|
||||
* @param inHandler
|
||||
* @param outHandler
|
||||
*/
|
||||
setHover: function(inHandler, outHandler)
|
||||
{
|
||||
$('#largeVideoContainer').hover(inHandler, outHandler);
|
||||
},
|
||||
rightWatermarkDiv.css({
|
||||
display: 'block',
|
||||
backgroundImage: 'url(images/rightwatermark.png)'
|
||||
});
|
||||
|
||||
rightWatermarkDiv.parent().attr('href', interfaceConfig.BRAND_WATERMARK_LINK);
|
||||
}
|
||||
|
||||
if (interfaceConfig.SHOW_POWERED_BY) {
|
||||
this.$container.children("a.poweredby").css({display: 'block'});
|
||||
}
|
||||
|
||||
this.$container.hover(
|
||||
e => this.onHoverIn(e),
|
||||
e => this.onHoverOut(e)
|
||||
);
|
||||
}
|
||||
|
||||
onHoverIn (e) {
|
||||
if (!this.state) {
|
||||
return;
|
||||
}
|
||||
let container = this.getContainer(this.state);
|
||||
container.onHoverIn(e);
|
||||
}
|
||||
|
||||
onHoverOut (e) {
|
||||
if (!this.state) {
|
||||
return;
|
||||
}
|
||||
let container = this.getContainer(this.state);
|
||||
container.onHoverOut(e);
|
||||
}
|
||||
|
||||
get id () {
|
||||
return this.videoContainer.id;
|
||||
}
|
||||
|
||||
updateLargeVideo (stream, videoType) {
|
||||
let id = getStreamId(stream);
|
||||
|
||||
let container = this.getContainer(this.state);
|
||||
|
||||
container.hide().then(() => {
|
||||
console.info("hover in %s", id);
|
||||
this.state = VideoContainerType;
|
||||
this.videoContainer.setStream(stream, videoType);
|
||||
this.videoContainer.show();
|
||||
});
|
||||
}
|
||||
|
||||
updateContainerSize (isSideBarVisible) {
|
||||
this.width = UIUtil.getAvailableVideoWidth(isSideBarVisible);
|
||||
this.height = window.innerHeight;
|
||||
}
|
||||
|
||||
resizeContainer (type, animate = false) {
|
||||
let container = this.getContainer(type);
|
||||
container.resize(this.width, this.height, animate);
|
||||
}
|
||||
|
||||
resize (animate) {
|
||||
// resize all containers
|
||||
Object.keys(this.containers).forEach(type => this.resizeContainer(type, animate));
|
||||
|
||||
this.$container.animate({
|
||||
width: this.width,
|
||||
height: this.height
|
||||
}, {
|
||||
queue: false,
|
||||
duration: animate ? 500 : 0
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables/disables the filter indicating a video problem to the user.
|
||||
*
|
||||
* @param enable <tt>true</tt> to enable, <tt>false</tt> to disable
|
||||
*/
|
||||
enableVideoProblemFilter: function (enable) {
|
||||
$("#largeVideo").toggleClass("videoProblemFilter", enable);
|
||||
enableVideoProblemFilter (enable) {
|
||||
this.videoContainer.$video.toggleClass("videoProblemFilter", enable);
|
||||
}
|
||||
};
|
||||
|
||||
export default LargeVideo;
|
||||
/**
|
||||
* Updates the src of the active speaker avatar
|
||||
*/
|
||||
updateAvatar (thumbUrl) {
|
||||
$("#activeSpeakerAvatar").attr('src', thumbUrl);
|
||||
}
|
||||
|
||||
showAvatar (show) {
|
||||
this.videoContainer.showAvatar(show);
|
||||
}
|
||||
|
||||
addContainer (type, container) {
|
||||
if (this.containers[type]) {
|
||||
throw new Error(`container of type ${type} already exist`);
|
||||
}
|
||||
|
||||
this.containers[type] = container;
|
||||
this.resizeContainer(type);
|
||||
}
|
||||
|
||||
getContainer (type) {
|
||||
let container = this.containers[type];
|
||||
|
||||
if (!container) {
|
||||
throw new Error(`container of type ${type} doesn't exist`);
|
||||
}
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
removeContainer (type) {
|
||||
if (!this.containers[type]) {
|
||||
throw new Error(`container of type ${type} doesn't exist`);
|
||||
}
|
||||
|
||||
delete this.containers[type];
|
||||
}
|
||||
|
||||
showContainer (type) {
|
||||
if (this.state === type) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
let container = this.getContainer(type);
|
||||
|
||||
if (this.state) {
|
||||
let oldContainer = this.containers[this.state];
|
||||
if (oldContainer) {
|
||||
oldContainer.hide();
|
||||
}
|
||||
}
|
||||
|
||||
this.state = type;
|
||||
|
||||
return container.show();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,7 +34,8 @@ RemoteVideo.prototype.addRemoteVideoContainer = function() {
|
|||
if (APP.conference.isModerator) {
|
||||
this.addRemoteVideoMenu();
|
||||
}
|
||||
AudioLevels.updateAudioLevelCanvas(this.id, this.VideoLayout);
|
||||
let {thumbWidth, thumbHeight} = this.VideoLayout.calculateThumbnailSize();
|
||||
AudioLevels.updateAudioLevelCanvas(this.id, thumbWidth, thumbHeight);
|
||||
|
||||
return this.container;
|
||||
};
|
||||
|
@ -296,6 +297,12 @@ RemoteVideo.prototype.showPeerContainer = function (state) {
|
|||
|
||||
};
|
||||
|
||||
RemoteVideo.prototype.updateResolution = function (resolution) {
|
||||
if (this.connectionIndicator) {
|
||||
this.connectionIndicator.updateResolution(resolution);
|
||||
}
|
||||
};
|
||||
|
||||
RemoteVideo.prototype.removeConnectionIndicator = function () {
|
||||
if (this.connectionIndicator)
|
||||
this.connectionIndicator.remove();
|
||||
|
|
|
@ -145,8 +145,8 @@ SmallVideo.prototype.bindHoverHandler = function () {
|
|||
function () {
|
||||
// If the video has been "pinned" by the user we want to
|
||||
// keep the display name on place.
|
||||
if (!LargeVideo.isLargeVideoVisible() ||
|
||||
!LargeVideo.isCurrentlyOnLarge(self.id))
|
||||
if (!self.VideoLayout.isLargeVideoVisible() ||
|
||||
!self.VideoLayout.isCurrentlyOnLarge(self.id))
|
||||
self.showDisplayName(false);
|
||||
}
|
||||
);
|
||||
|
@ -254,7 +254,7 @@ SmallVideo.prototype.enableDominantSpeaker = function (isEnable) {
|
|||
}
|
||||
|
||||
if (isEnable) {
|
||||
this.showDisplayName(LargeVideo.getState() === "video");
|
||||
this.showDisplayName(this.VideoLayout.isLargeVideoVisible());
|
||||
|
||||
if (!this.container.classList.contains("dominantspeaker"))
|
||||
this.container.classList.add("dominantspeaker");
|
||||
|
@ -388,7 +388,10 @@ SmallVideo.prototype.showAvatar = function (show) {
|
|||
}
|
||||
}
|
||||
|
||||
if (LargeVideo.showAvatar(this.id, show)) {
|
||||
if (this.VideoLayout.isCurrentlyOnLarge(this.id)
|
||||
&& this.VideoLayout.isLargeVideoVisible()) {
|
||||
|
||||
this.VideoLayout.showLargeVideoAvatar(show);
|
||||
setVisibility(avatar, false);
|
||||
setVisibility(video, false);
|
||||
} else {
|
||||
|
|
|
@ -2,15 +2,16 @@
|
|||
/* jshint -W101 */
|
||||
|
||||
import AudioLevels from "../audio_levels/AudioLevels";
|
||||
import BottomToolbar from "../toolbars/BottomToolbar";
|
||||
|
||||
import UIEvents from "../../../service/UI/UIEvents";
|
||||
import UIUtil from "../util/UIUtil";
|
||||
|
||||
import RemoteVideo from "./RemoteVideo";
|
||||
import LargeVideo from "./LargeVideo";
|
||||
import LargeVideoManager, {VideoContainerType} from "./LargeVideo";
|
||||
import {PreziContainerType} from '../prezi/Prezi';
|
||||
import LocalVideo from "./LocalVideo";
|
||||
|
||||
var MediaStreamType = require("../../../service/RTC/MediaStreamTypes");
|
||||
var RTCBrowserType = require('../../RTC/RTCBrowserType');
|
||||
|
||||
var remoteVideos = {};
|
||||
|
@ -32,6 +33,8 @@ var eventEmitter = null;
|
|||
*/
|
||||
var focusedVideoResourceJid = null;
|
||||
|
||||
const thumbAspectRatio = 16.0 / 9.0;
|
||||
|
||||
/**
|
||||
* On contact list item clicked.
|
||||
*/
|
||||
|
@ -88,22 +91,31 @@ function getPeerContainerResourceId (containerElement) {
|
|||
}
|
||||
}
|
||||
|
||||
let largeVideo;
|
||||
|
||||
var VideoLayout = {
|
||||
init (emitter) {
|
||||
eventEmitter = emitter;
|
||||
localVideoThumbnail = new LocalVideo(VideoLayout, emitter);
|
||||
if (interfaceConfig.filmStripOnly) {
|
||||
LargeVideo.disable();
|
||||
} else {
|
||||
LargeVideo.init(VideoLayout, emitter);
|
||||
}
|
||||
|
||||
VideoLayout.resizeLargeVideoContainer();
|
||||
|
||||
emitter.addListener(UIEvents.CONTACT_CLICKED, onContactClicked);
|
||||
},
|
||||
|
||||
initLargeVideo (isSideBarVisible) {
|
||||
largeVideo = new LargeVideoManager();
|
||||
largeVideo.updateContainerSize(isSideBarVisible);
|
||||
AudioLevels.init();
|
||||
},
|
||||
|
||||
setAudioLevel(id, lvl) {
|
||||
if (!largeVideo) {
|
||||
return;
|
||||
}
|
||||
AudioLevels.updateAudioLevel(
|
||||
id, lvl, largeVideo.id
|
||||
);
|
||||
},
|
||||
|
||||
isInLastN (resource) {
|
||||
return lastNCount < 0 || // lastN is disabled
|
||||
// lastNEndpoints cache not built yet
|
||||
|
@ -140,15 +152,16 @@ var VideoLayout = {
|
|||
localVideoThumbnail.createConnectionIndicator();
|
||||
|
||||
let localId = APP.conference.localId;
|
||||
this.onVideoTypeChanged(localId, stream.getType());
|
||||
this.onVideoTypeChanged(localId, stream.videoType);
|
||||
|
||||
AudioLevels.updateAudioLevelCanvas(null, VideoLayout);
|
||||
let {thumbWidth, thumbHeight} = this.calculateThumbnailSize();
|
||||
AudioLevels.updateAudioLevelCanvas(null, thumbWidth, thumbHeight);
|
||||
|
||||
localVideoThumbnail.changeVideo(stream);
|
||||
|
||||
/* force update if we're currently being displayed */
|
||||
if (LargeVideo.isCurrentlyOnLarge(localId)) {
|
||||
LargeVideo.updateLargeVideo(localId, true);
|
||||
if (this.isCurrentlyOnLarge(localId)) {
|
||||
this.updateLargeVideo(localId, true);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -156,8 +169,8 @@ var VideoLayout = {
|
|||
let id = APP.conference.localId;
|
||||
localVideoThumbnail.joined(id);
|
||||
|
||||
if (!LargeVideo.id) {
|
||||
LargeVideo.updateLargeVideo(id, true);
|
||||
if (largeVideo && !largeVideo.id) {
|
||||
this.updateLargeVideo(id, true);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -183,7 +196,7 @@ var VideoLayout = {
|
|||
* another one instead.
|
||||
*/
|
||||
updateRemovedVideo (id) {
|
||||
if (id !== LargeVideo.getId()) {
|
||||
if (!this.isCurrentlyOnLarge(id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -198,15 +211,17 @@ var VideoLayout = {
|
|||
newId = this.electLastVisibleVideo();
|
||||
}
|
||||
|
||||
LargeVideo.updateLargeVideo(newId);
|
||||
this.updateLargeVideo(newId);
|
||||
},
|
||||
|
||||
electLastVisibleVideo () {
|
||||
// pick the last visible video in the row
|
||||
// if nobody else is left, this picks the local video
|
||||
let pick = $('#remoteVideos>span[id!="mixedstream"]:visible:last');
|
||||
if (pick.length) {
|
||||
let id = getPeerContainerResourceId(pick[0]);
|
||||
let thumbs = BottomToolbar.getThumbs(true).filter('[id!="mixedstream"]');
|
||||
|
||||
let lastVisible = thumbs.filter(':visible:last');
|
||||
if (lastVisible.length) {
|
||||
let id = getPeerContainerResourceId(lastVisible[0]);
|
||||
if (remoteVideos[id]) {
|
||||
console.info("electLastVisibleVideo: " + id);
|
||||
return id;
|
||||
|
@ -216,9 +231,9 @@ var VideoLayout = {
|
|||
}
|
||||
|
||||
console.info("Last visible video no longer exists");
|
||||
pick = $('#remoteVideos>span[id!="mixedstream"]');
|
||||
if (pick.length) {
|
||||
let id = getPeerContainerResourceId(pick[0]);
|
||||
thumbs = BottomToolbar.getThumbs();
|
||||
if (thumbs.length) {
|
||||
let id = getPeerContainerResourceId(thumbs[0]);
|
||||
if (remoteVideos[id]) {
|
||||
console.info("electLastVisibleVideo: " + id);
|
||||
return id;
|
||||
|
@ -242,10 +257,6 @@ var VideoLayout = {
|
|||
remoteVideos[id].addRemoteStreamElement(stream);
|
||||
},
|
||||
|
||||
getLargeVideoId () {
|
||||
return LargeVideo.getId();
|
||||
},
|
||||
|
||||
/**
|
||||
* Return the type of the remote video.
|
||||
* @param id the id for the remote video
|
||||
|
@ -255,25 +266,6 @@ var VideoLayout = {
|
|||
return remoteVideoTypes[id];
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when large video update is finished
|
||||
* @param currentSmallVideo small video currently displayed on large video
|
||||
*/
|
||||
largeVideoUpdated (currentSmallVideo) {
|
||||
// Makes sure that dominant speaker UI
|
||||
// is enabled only on current small video
|
||||
localVideoThumbnail.enableDominantSpeaker(localVideoThumbnail === currentSmallVideo);
|
||||
Object.keys(remoteVideos).forEach(
|
||||
function (resourceJid) {
|
||||
var remoteVideo = remoteVideos[resourceJid];
|
||||
if (remoteVideo) {
|
||||
remoteVideo.enableDominantSpeaker(
|
||||
remoteVideo === currentSmallVideo);
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
handleVideoThumbClicked (noPinnedEndpointChangedEvent,
|
||||
resourceJid) {
|
||||
if(focusedVideoResourceJid) {
|
||||
|
@ -291,7 +283,7 @@ var VideoLayout = {
|
|||
// Enable the currently set dominant speaker.
|
||||
if (currentDominantSpeaker) {
|
||||
if(smallVideo && smallVideo.hasVideo()) {
|
||||
LargeVideo.updateLargeVideo(currentDominantSpeaker);
|
||||
this.updateLargeVideo(currentDominantSpeaker);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -314,9 +306,7 @@ var VideoLayout = {
|
|||
}
|
||||
}
|
||||
|
||||
LargeVideo.setState("video");
|
||||
|
||||
LargeVideo.updateLargeVideo(resourceJid);
|
||||
this.updateLargeVideo(resourceJid);
|
||||
|
||||
// Writing volume not allowed in IE
|
||||
if (!RTCBrowserType.isIExplorer()) {
|
||||
|
@ -346,7 +336,7 @@ var VideoLayout = {
|
|||
|
||||
// In case this is not currently in the last n we don't show it.
|
||||
if (localLastNCount && localLastNCount > 0 &&
|
||||
$('#remoteVideos>span').length >= localLastNCount + 2) {
|
||||
BottomToolbar.getThumbs().length >= localLastNCount + 2) {
|
||||
remoteVideo.showPeerContainer('hide');
|
||||
} else {
|
||||
VideoLayout.resizeThumbnails();
|
||||
|
@ -370,11 +360,11 @@ var VideoLayout = {
|
|||
// the current dominant speaker.
|
||||
if ((!focusedVideoResourceJid &&
|
||||
!currentDominantSpeaker &&
|
||||
!require("../prezi/Prezi").isPresentationVisible()) ||
|
||||
!this.isLargeContainerTypeVisible(PreziContainerType)) ||
|
||||
focusedVideoResourceJid === resourceJid ||
|
||||
(resourceJid &&
|
||||
currentDominantSpeaker === resourceJid)) {
|
||||
LargeVideo.updateLargeVideo(resourceJid, true);
|
||||
this.updateLargeVideo(resourceJid, true);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -419,100 +409,67 @@ var VideoLayout = {
|
|||
/**
|
||||
* Resizes the large video container.
|
||||
*/
|
||||
resizeLargeVideoContainer () {
|
||||
if(LargeVideo.isEnabled()) {
|
||||
LargeVideo.resize();
|
||||
resizeLargeVideoContainer (isSideBarVisible) {
|
||||
if (largeVideo) {
|
||||
largeVideo.updateContainerSize(isSideBarVisible);
|
||||
largeVideo.resize(false);
|
||||
} else {
|
||||
VideoLayout.resizeVideoSpace();
|
||||
this.resizeVideoSpace(false, isSideBarVisible);
|
||||
}
|
||||
VideoLayout.resizeThumbnails();
|
||||
LargeVideo.position();
|
||||
this.resizeThumbnails(false);
|
||||
},
|
||||
|
||||
/**
|
||||
* Resizes thumbnails.
|
||||
*/
|
||||
resizeThumbnails (animate) {
|
||||
var videoSpaceWidth = $('#remoteVideos').width();
|
||||
resizeThumbnails (animate = false) {
|
||||
let {thumbWidth, thumbHeight} = this.calculateThumbnailSize();
|
||||
|
||||
var thumbnailSize = VideoLayout.calculateThumbnailSize(videoSpaceWidth);
|
||||
var width = thumbnailSize[0];
|
||||
var height = thumbnailSize[1];
|
||||
$('.userAvatar').css('left', (thumbWidth - thumbHeight) / 2);
|
||||
|
||||
$('.userAvatar').css('left', (width - height) / 2);
|
||||
|
||||
if(animate) {
|
||||
$('#remoteVideos').animate({
|
||||
// adds 2 px because of small video 1px border
|
||||
height: height + 2
|
||||
},
|
||||
{
|
||||
queue: false,
|
||||
duration: 500
|
||||
});
|
||||
|
||||
$('#remoteVideos>span').animate({
|
||||
height: height,
|
||||
width: width
|
||||
},
|
||||
{
|
||||
queue: false,
|
||||
duration: 500,
|
||||
complete: function () {
|
||||
$(document).trigger(
|
||||
"remotevideo.resized",
|
||||
[width,
|
||||
height]);
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
// size videos so that while keeping AR and max height, we have a
|
||||
// nice fit
|
||||
// adds 2 px because of small video 1px border
|
||||
$('#remoteVideos').height(height + 2);
|
||||
$('#remoteVideos>span').width(width);
|
||||
$('#remoteVideos>span').height(height);
|
||||
|
||||
$(document).trigger("remotevideo.resized", [width, height]);
|
||||
}
|
||||
BottomToolbar.resizeThumbnails(thumbWidth, thumbHeight, animate).then(function () {
|
||||
BottomToolbar.resizeToolbar(thumbWidth, thumbHeight);
|
||||
AudioLevels.updateCanvasSize(thumbWidth, thumbHeight);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Calculates the thumbnail size.
|
||||
*
|
||||
* @param videoSpaceWidth the width of the video space
|
||||
*/
|
||||
calculateThumbnailSize (videoSpaceWidth) {
|
||||
calculateThumbnailSize () {
|
||||
let videoSpaceWidth = BottomToolbar.getFilmStripWidth();
|
||||
// Calculate the available height, which is the inner window height
|
||||
// minus 39px for the header minus 2px for the delimiter lines on the
|
||||
// top and bottom of the large video, minus the 36px space inside the
|
||||
// remoteVideos container used for highlighting shadow.
|
||||
var availableHeight = 100;
|
||||
let availableHeight = 100;
|
||||
|
||||
var numvids = $('#remoteVideos>span:visible').length;
|
||||
let numvids = BottomToolbar.getThumbs().length;
|
||||
if (localLastNCount && localLastNCount > 0) {
|
||||
numvids = Math.min(localLastNCount + 1, numvids);
|
||||
}
|
||||
|
||||
// Remove the 3px borders arround videos and border around the remote
|
||||
// videos area and the 4 pixels between the local video and the others
|
||||
//TODO: Find out where the 4 pixels come from and remove them
|
||||
var availableWinWidth = videoSpaceWidth - 2 * 3 * numvids - 70 - 4;
|
||||
// Remove the 3px borders arround videos and border around the remote
|
||||
// videos area and the 4 pixels between the local video and the others
|
||||
//TODO: Find out where the 4 pixels come from and remove them
|
||||
let availableWinWidth = videoSpaceWidth - 2 * 3 * numvids - 70 - 4;
|
||||
|
||||
var availableWidth = availableWinWidth / numvids;
|
||||
var aspectRatio = 16.0 / 9.0;
|
||||
var maxHeight = Math.min(160, availableHeight);
|
||||
availableHeight
|
||||
= Math.min( maxHeight,
|
||||
availableWidth / aspectRatio,
|
||||
window.innerHeight - 18);
|
||||
let availableWidth = availableWinWidth / numvids;
|
||||
let maxHeight = Math.min(160, availableHeight);
|
||||
availableHeight
|
||||
= Math.min( maxHeight,
|
||||
availableWidth / thumbAspectRatio,
|
||||
window.innerHeight - 18);
|
||||
|
||||
if (availableHeight < availableWidth / aspectRatio) {
|
||||
availableWidth = Math.floor(availableHeight * aspectRatio);
|
||||
}
|
||||
if (availableHeight < availableWidth / thumbAspectRatio) {
|
||||
availableWidth = Math.floor(availableHeight * thumbAspectRatio);
|
||||
}
|
||||
|
||||
return [availableWidth, availableHeight];
|
||||
return {
|
||||
thumbWidth: availableWidth,
|
||||
thumbHeight: availableHeight
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -604,7 +561,7 @@ var VideoLayout = {
|
|||
// Update the large video if the video source is already available,
|
||||
// otherwise wait for the "videoactive.jingle" event.
|
||||
if (videoSel[0].currentTime > 0) {
|
||||
LargeVideo.updateLargeVideo(id);
|
||||
this.updateLargeVideo(id);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -656,7 +613,7 @@ var VideoLayout = {
|
|||
var updateLargeVideo = false;
|
||||
|
||||
// Handle LastN/local LastN changes.
|
||||
$('#remoteVideos>span').each(function( index, element ) {
|
||||
BottomToolbar.getThumbs().each(function( index, element ) {
|
||||
var resourceJid = getPeerContainerResourceId(element);
|
||||
|
||||
// We do not want to process any logic for our own(local) video
|
||||
|
@ -696,7 +653,7 @@ var VideoLayout = {
|
|||
// displayed in the large video we have to switch to another
|
||||
// user.
|
||||
if (!updateLargeVideo &&
|
||||
resourceJid === LargeVideo.getId()) {
|
||||
this.isCurrentlyOnLarge(resourceJid)) {
|
||||
updateLargeVideo = true;
|
||||
}
|
||||
}
|
||||
|
@ -716,7 +673,7 @@ var VideoLayout = {
|
|||
|
||||
var jid = APP.xmpp.findJidFromResource(resourceJid);
|
||||
var mediaStream =
|
||||
APP.RTC.remoteStreams[jid][MediaStreamType.VIDEO_TYPE];
|
||||
APP.RTC.remoteStreams[jid]['video'];
|
||||
var sel = remoteVideo.selectVideoElement();
|
||||
|
||||
APP.RTC.attachMediaStream(sel, mediaStream.stream);
|
||||
|
@ -754,7 +711,7 @@ var VideoLayout = {
|
|||
continue;
|
||||
|
||||
// videoSrcToSsrc needs to be update for this call to succeed.
|
||||
LargeVideo.updateLargeVideo(resource);
|
||||
this.updateLargeVideo(resource);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -766,23 +723,25 @@ var VideoLayout = {
|
|||
* @param object
|
||||
*/
|
||||
updateLocalConnectionStats (percent, object) {
|
||||
var resolution = null;
|
||||
let resolutions = {};
|
||||
if (object.resolution !== null) {
|
||||
resolution = object.resolution;
|
||||
object.resolution = resolution[APP.xmpp.myJid()];
|
||||
delete resolution[APP.xmpp.myJid()];
|
||||
resolutions = object.resolution;
|
||||
object.resolution = resolutions[APP.conference.localId];
|
||||
}
|
||||
localVideoThumbnail.updateStatsIndicator(percent, object);
|
||||
for (var jid in resolution) {
|
||||
if (resolution[jid] === null)
|
||||
continue;
|
||||
var resourceJid = Strophe.getResourceFromJid(jid);
|
||||
if (remoteVideos[resourceJid] &&
|
||||
remoteVideos[resourceJid].connectionIndicator) {
|
||||
remoteVideos[resourceJid].connectionIndicator.
|
||||
updateResolution(resolution[jid]);
|
||||
|
||||
Object.keys(resolutions).forEach(function (id) {
|
||||
if (APP.conference.isLocalId(id)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let resolution = resolutions[id];
|
||||
let remoteVideo = remoteVideos[id];
|
||||
|
||||
if (resolution && remoteVideo) {
|
||||
remoteVideo.updateResolution(resolution);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -862,15 +821,9 @@ var VideoLayout = {
|
|||
}
|
||||
|
||||
smallVideo.setVideoType(newVideoType);
|
||||
LargeVideo.onVideoTypeChanged(id, newVideoType);
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates the video size and position.
|
||||
*/
|
||||
updateLargeVideoSize () {
|
||||
LargeVideo.updateVideoSizeAndPosition();
|
||||
LargeVideo.position(null, null, null, null, true);
|
||||
if (this.isCurrentlyOnLarge(id)) {
|
||||
this.updateLargeVideo(id, true);
|
||||
}
|
||||
},
|
||||
|
||||
showMore (jid) {
|
||||
|
@ -886,22 +839,8 @@ var VideoLayout = {
|
|||
}
|
||||
},
|
||||
|
||||
addPreziContainer (id) {
|
||||
var container = RemoteVideo.createContainer(id);
|
||||
VideoLayout.resizeThumbnails();
|
||||
return container;
|
||||
},
|
||||
|
||||
setLargeVideoVisible (isVisible) {
|
||||
LargeVideo.setLargeVideoVisible(isVisible);
|
||||
if(!isVisible && focusedVideoResourceJid) {
|
||||
var smallVideo = VideoLayout.getSmallVideo(focusedVideoResourceJid);
|
||||
if(smallVideo) {
|
||||
smallVideo.focus(false);
|
||||
smallVideo.showAvatar();
|
||||
}
|
||||
focusedVideoResourceJid = null;
|
||||
}
|
||||
addRemoteVideoContainer (id) {
|
||||
return RemoteVideo.createContainer(id);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -912,8 +851,15 @@ var VideoLayout = {
|
|||
* resized.
|
||||
*/
|
||||
resizeVideoArea (isSideBarVisible, callback) {
|
||||
LargeVideo.resizeVideoAreaAnimated(isSideBarVisible, callback);
|
||||
VideoLayout.resizeThumbnails(true);
|
||||
let animate = true;
|
||||
|
||||
if (largeVideo) {
|
||||
largeVideo.updateContainerSize(isSideBarVisible);
|
||||
largeVideo.resize(animate);
|
||||
this.resizeVideoSpace(animate, isSideBarVisible, callback);
|
||||
}
|
||||
|
||||
VideoLayout.resizeThumbnails(animate);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -945,8 +891,7 @@ var VideoLayout = {
|
|||
complete: completeFunction
|
||||
});
|
||||
} else {
|
||||
$('#videospace').width(availableWidth);
|
||||
$('#videospace').height(availableHeight);
|
||||
$('#videospace').width(availableWidth).height(availableHeight);
|
||||
}
|
||||
|
||||
},
|
||||
|
@ -968,43 +913,106 @@ var VideoLayout = {
|
|||
"Missed avatar update - no small video yet for " + id
|
||||
);
|
||||
}
|
||||
LargeVideo.updateAvatar(id, thumbUrl);
|
||||
},
|
||||
|
||||
createEtherpadIframe (src, onloadHandler) {
|
||||
return LargeVideo.createEtherpadIframe(src, onloadHandler);
|
||||
},
|
||||
|
||||
setLargeVideoState (state) {
|
||||
LargeVideo.setState(state);
|
||||
},
|
||||
|
||||
getLargeVideoState () {
|
||||
return LargeVideo.getState();
|
||||
},
|
||||
|
||||
setLargeVideoHover (inHandler, outHandler) {
|
||||
LargeVideo.setHover(inHandler, outHandler);
|
||||
if (this.isCurrentlyOnLarge(id)) {
|
||||
largeVideo.updateAvatar(thumbUrl);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Indicates that the video has been interrupted.
|
||||
*/
|
||||
onVideoInterrupted () {
|
||||
LargeVideo.enableVideoProblemFilter(true);
|
||||
var reconnectingKey = "connection.RECONNECTING";
|
||||
$('#videoConnectionMessage').attr("data-i18n", reconnectingKey);
|
||||
this.enableVideoProblemFilter(true);
|
||||
let reconnectingKey = "connection.RECONNECTING";
|
||||
$('#videoConnectionMessage')
|
||||
.text(APP.translation.translateString(reconnectingKey));
|
||||
$('#videoConnectionMessage').css({display: "block"});
|
||||
.attr("data-i18n", reconnectingKey)
|
||||
.text(APP.translation.translateString(reconnectingKey))
|
||||
.css({display: "block"});
|
||||
},
|
||||
|
||||
/**
|
||||
* Indicates that the video has been restored.
|
||||
*/
|
||||
onVideoRestored () {
|
||||
LargeVideo.enableVideoProblemFilter(false);
|
||||
this.enableVideoProblemFilter(false);
|
||||
$('#videoConnectionMessage').css({display: "none"});
|
||||
},
|
||||
|
||||
enableVideoProblemFilter (enable) {
|
||||
if (!largeVideo) {
|
||||
return;
|
||||
}
|
||||
|
||||
largeVideo.enableVideoProblemFilter(enable);
|
||||
},
|
||||
|
||||
isLargeVideoVisible () {
|
||||
return this.isLargeContainerTypeVisible(VideoContainerType);
|
||||
},
|
||||
|
||||
isCurrentlyOnLarge (id) {
|
||||
return largeVideo && largeVideo.id === id;
|
||||
},
|
||||
|
||||
updateLargeVideo (id, forceUpdate) {
|
||||
if (!largeVideo) {
|
||||
return;
|
||||
}
|
||||
let isOnLarge = this.isCurrentlyOnLarge(id);
|
||||
let currentId = largeVideo.id;
|
||||
|
||||
if (!isOnLarge || forceUpdate) {
|
||||
if (id !== currentId) {
|
||||
eventEmitter.emit(UIEvents.SELECTED_ENDPOINT, id);
|
||||
}
|
||||
if (currentId) {
|
||||
let currentSmallVideo = this.getSmallVideo(currentId);
|
||||
currentSmallVideo && currentSmallVideo.enableDominantSpeaker(false);
|
||||
}
|
||||
|
||||
let smallVideo = this.getSmallVideo(id);
|
||||
|
||||
let videoType = this.getRemoteVideoType(id);
|
||||
largeVideo.updateLargeVideo(smallVideo.stream, videoType);
|
||||
|
||||
smallVideo.enableDominantSpeaker(true);
|
||||
} else if (currentId) {
|
||||
let currentSmallVideo = this.getSmallVideo(currentId);
|
||||
currentSmallVideo.showAvatar();
|
||||
}
|
||||
},
|
||||
|
||||
showLargeVideoAvatar (show) {
|
||||
largeVideo && largeVideo.showAvatar(show);
|
||||
},
|
||||
|
||||
addLargeVideoContainer (type, container) {
|
||||
largeVideo && largeVideo.addContainer(type, container);
|
||||
},
|
||||
|
||||
removeLargeVideoContainer (type) {
|
||||
largeVideo && largeVideo.removeContainer(type);
|
||||
},
|
||||
|
||||
/**
|
||||
* @returns Promise
|
||||
*/
|
||||
showLargeVideoContainer (type, show) {
|
||||
if (!largeVideo) {
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
let isVisible = this.isLargeContainerTypeVisible(type);
|
||||
if (isVisible === show) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// if !show then use default type - large video
|
||||
return largeVideo.showContainer(show ? type : VideoContainerType);
|
||||
},
|
||||
|
||||
isLargeContainerTypeVisible (type) {
|
||||
return largeVideo && largeVideo.state === type;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
/* global APP, config */
|
||||
/* global APP, JitsiMeetJS, config */
|
||||
var EventEmitter = require("events");
|
||||
var DesktopSharingEventTypes
|
||||
= require("../../service/desktopsharing/DesktopSharingEventTypes");
|
||||
import DSEvents from '../../service/desktopsharing/DesktopSharingEventTypes';
|
||||
|
||||
const TrackEvents = JitsiMeetJS.events.track;
|
||||
|
||||
/**
|
||||
* Indicates that desktop stream is currently in use (for toggle purpose).
|
||||
|
@ -25,25 +26,22 @@ var eventEmitter = new EventEmitter();
|
|||
|
||||
function streamSwitchDone() {
|
||||
switchInProgress = false;
|
||||
eventEmitter.emit(
|
||||
DesktopSharingEventTypes.SWITCHING_DONE,
|
||||
isUsingScreenStream);
|
||||
eventEmitter.emit(DSEvents.SWITCHING_DONE, isUsingScreenStream);
|
||||
}
|
||||
|
||||
function newStreamCreated(track) {
|
||||
eventEmitter.emit(DesktopSharingEventTypes.NEW_STREAM_CREATED,
|
||||
track, streamSwitchDone);
|
||||
eventEmitter.emit(DSEvents.NEW_STREAM_CREATED, track, streamSwitchDone);
|
||||
}
|
||||
|
||||
function getVideoStreamFailed(error) {
|
||||
console.error("Failed to obtain the stream to switch to", error);
|
||||
function getVideoStreamFailed() {
|
||||
console.error("Failed to obtain the stream to switch to");
|
||||
switchInProgress = false;
|
||||
isUsingScreenStream = false;
|
||||
newStreamCreated(null);
|
||||
}
|
||||
|
||||
function getDesktopStreamFailed(error) {
|
||||
console.error("Failed to obtain the stream to switch to", error);
|
||||
function getDesktopStreamFailed() {
|
||||
console.error("Failed to obtain the stream to switch to");
|
||||
switchInProgress = false;
|
||||
}
|
||||
|
||||
|
@ -92,21 +90,34 @@ module.exports = {
|
|||
return;
|
||||
}
|
||||
switchInProgress = true;
|
||||
let type, handler;
|
||||
let type;
|
||||
if (!isUsingScreenStream) {
|
||||
// Switch to desktop stream
|
||||
handler = onEndedHandler;
|
||||
type = "desktop";
|
||||
} else {
|
||||
handler = () => {};
|
||||
type = "video";
|
||||
}
|
||||
APP.conference.createVideoTrack(type, handler).then(
|
||||
(tracks) => {
|
||||
// We now use screen stream
|
||||
isUsingScreenStream = type === "desktop";
|
||||
newStreamCreated(tracks[0]);
|
||||
}).catch(getDesktopStreamFailed);
|
||||
APP.createLocalTracks(type).then(function (tracks) {
|
||||
if (!tracks.length) {
|
||||
if (type === 'desktop') {
|
||||
getDesktopStreamFailed();
|
||||
} else {
|
||||
getVideoStreamFailed();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let stream = tracks[0];
|
||||
|
||||
// We now use screen stream
|
||||
isUsingScreenStream = type === "desktop";
|
||||
if (isUsingScreenStream) {
|
||||
stream.on(TrackEvents.TRACK_STOPPED, onEndedHandler);
|
||||
}
|
||||
|
||||
newStreamCreated(stream);
|
||||
});
|
||||
},
|
||||
/*
|
||||
* Exports the event emitter to allow use by ScreenObtainer. Not for outside
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
var UsernameGenerator = require('../util/UsernameGenerator');
|
||||
import {generateUsername} from '../util/UsernameGenerator';
|
||||
|
||||
var email = '';
|
||||
var displayName = '';
|
||||
|
@ -32,7 +32,7 @@ if (supportsLocalStorage()) {
|
|||
|
||||
if (!window.localStorage.callStatsUserName) {
|
||||
window.localStorage.callStatsUserName
|
||||
= UsernameGenerator.generateUsername();
|
||||
= generateUsername();
|
||||
console.log('generated callstats uid',
|
||||
window.localStorage.callStatsUserName);
|
||||
|
||||
|
@ -45,10 +45,10 @@ if (supportsLocalStorage()) {
|
|||
} else {
|
||||
console.log("local storage is not supported");
|
||||
userId = generateUniqueId();
|
||||
callStatsUserName = UsernameGenerator.generateUsername();
|
||||
callStatsUserName = generateUsername();
|
||||
}
|
||||
|
||||
var Settings = {
|
||||
export default {
|
||||
|
||||
/**
|
||||
* Sets the local user display name and saves it to local storage
|
||||
|
@ -99,10 +99,11 @@ var Settings = {
|
|||
language: language
|
||||
};
|
||||
},
|
||||
getLanguage () {
|
||||
return language;
|
||||
},
|
||||
setLanguage: function (lang) {
|
||||
language = lang;
|
||||
window.localStorage.language = lang;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = Settings;
|
||||
|
|
|
@ -1,15 +1,19 @@
|
|||
function NoopAnalytics() {}
|
||||
NoopAnalytics.prototype.sendEvent = function () {};
|
||||
|
||||
function AnalyticsAdapter() {
|
||||
var AnalyticsImpl = window.Analytics || NoopAnalytics;
|
||||
this.analytics = new AnalyticsImpl();
|
||||
class NoopAnalytics {
|
||||
sendEvent () {}
|
||||
}
|
||||
|
||||
AnalyticsAdapter.prototype.sendEvent = function (action, data) {
|
||||
try {
|
||||
this.analytics.sendEvent.apply(this.analytics, arguments);
|
||||
} catch (ignored) {}
|
||||
};
|
||||
const AnalyticsImpl = window.Analytics || NoopAnalytics;
|
||||
|
||||
module.exports = new AnalyticsAdapter();
|
||||
class AnalyticsAdapter {
|
||||
constructor () {
|
||||
this.analytics = new AnalyticsImpl();
|
||||
}
|
||||
|
||||
sendEvent (...args) {
|
||||
try {
|
||||
this.analytics.sendEvent(...args);
|
||||
} catch (ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
export default new AnalyticsAdapter();
|
||||
|
|
|
@ -1,128 +0,0 @@
|
|||
/* global config, AudioContext */
|
||||
/**
|
||||
* Provides statistics for the local stream.
|
||||
*/
|
||||
|
||||
var RTCBrowserType = require('../RTC/RTCBrowserType');
|
||||
var StatisticsEvents = require('../../service/statistics/Events');
|
||||
|
||||
/**
|
||||
* Size of the webaudio analyzer buffer.
|
||||
* @type {number}
|
||||
*/
|
||||
var WEBAUDIO_ANALYZER_FFT_SIZE = 2048;
|
||||
|
||||
/**
|
||||
* Value of the webaudio analyzer smoothing time parameter.
|
||||
* @type {number}
|
||||
*/
|
||||
var WEBAUDIO_ANALYZER_SMOOTING_TIME = 0.8;
|
||||
|
||||
/**
|
||||
* Converts time domain data array to audio level.
|
||||
* @param samples the time domain data array.
|
||||
* @returns {number} the audio level
|
||||
*/
|
||||
function timeDomainDataToAudioLevel(samples) {
|
||||
|
||||
var maxVolume = 0;
|
||||
|
||||
var length = samples.length;
|
||||
|
||||
for (var i = 0; i < length; i++) {
|
||||
if (maxVolume < samples[i])
|
||||
maxVolume = samples[i];
|
||||
}
|
||||
|
||||
return parseFloat(((maxVolume - 127) / 128).toFixed(3));
|
||||
}
|
||||
|
||||
/**
|
||||
* Animates audio level change
|
||||
* @param newLevel the new audio level
|
||||
* @param lastLevel the last audio level
|
||||
* @returns {Number} the audio level to be set
|
||||
*/
|
||||
function animateLevel(newLevel, lastLevel) {
|
||||
var value = 0;
|
||||
var diff = lastLevel - newLevel;
|
||||
if(diff > 0.2) {
|
||||
value = lastLevel - 0.2;
|
||||
}
|
||||
else if(diff < -0.4) {
|
||||
value = lastLevel + 0.4;
|
||||
}
|
||||
else {
|
||||
value = newLevel;
|
||||
}
|
||||
|
||||
return parseFloat(value.toFixed(3));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* <tt>LocalStatsCollector</tt> calculates statistics for the local stream.
|
||||
*
|
||||
* @param stream the local stream
|
||||
* @param interval stats refresh interval given in ms.
|
||||
* @constructor
|
||||
*/
|
||||
function LocalStatsCollector(stream, interval,
|
||||
statisticsService, eventEmitter) {
|
||||
window.AudioContext = window.AudioContext || window.webkitAudioContext;
|
||||
this.stream = stream;
|
||||
this.intervalId = null;
|
||||
this.intervalMilis = interval;
|
||||
this.eventEmitter = eventEmitter;
|
||||
this.audioLevel = 0;
|
||||
this.statisticsService = statisticsService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the collecting the statistics.
|
||||
*/
|
||||
LocalStatsCollector.prototype.start = function () {
|
||||
if (config.disableAudioLevels || !window.AudioContext ||
|
||||
RTCBrowserType.isTemasysPluginUsed())
|
||||
return;
|
||||
|
||||
var context = new AudioContext();
|
||||
var analyser = context.createAnalyser();
|
||||
analyser.smoothingTimeConstant = WEBAUDIO_ANALYZER_SMOOTING_TIME;
|
||||
analyser.fftSize = WEBAUDIO_ANALYZER_FFT_SIZE;
|
||||
|
||||
|
||||
var source = context.createMediaStreamSource(this.stream);
|
||||
source.connect(analyser);
|
||||
|
||||
|
||||
var self = this;
|
||||
|
||||
this.intervalId = setInterval(
|
||||
function () {
|
||||
var array = new Uint8Array(analyser.frequencyBinCount);
|
||||
analyser.getByteTimeDomainData(array);
|
||||
var audioLevel = timeDomainDataToAudioLevel(array);
|
||||
if (audioLevel != self.audioLevel) {
|
||||
self.audioLevel = animateLevel(audioLevel, self.audioLevel);
|
||||
self.eventEmitter.emit(
|
||||
StatisticsEvents.AUDIO_LEVEL,
|
||||
self.statisticsService.LOCAL_JID,
|
||||
self.audioLevel);
|
||||
}
|
||||
},
|
||||
this.intervalMilis
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Stops collecting the statistics.
|
||||
*/
|
||||
LocalStatsCollector.prototype.stop = function () {
|
||||
if (this.intervalId) {
|
||||
clearInterval(this.intervalId);
|
||||
this.intervalId = null;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = LocalStatsCollector;
|
|
@ -385,7 +385,7 @@ StatsCollector.prototype.addStatsToBeLogged = function (reports) {
|
|||
|
||||
StatsCollector.prototype.logStats = function () {
|
||||
|
||||
if(!APP.xmpp.sendLogs(this.statsToBeLogged))
|
||||
if(!APP.conference._room.xmpp.sendLogs(this.statsToBeLogged))
|
||||
return;
|
||||
// Reset the stats
|
||||
this.statsToBeLogged.stats = {};
|
||||
|
@ -501,7 +501,7 @@ StatsCollector.prototype.processStatsReport = function () {
|
|||
var ssrc = getStatValue(now, 'ssrc');
|
||||
if(!ssrc)
|
||||
continue;
|
||||
var jid = APP.xmpp.getJidFromSSRC(ssrc);
|
||||
var jid = APP.conference._room.room.getJidBySSRC(ssrc);
|
||||
if (!jid && (Date.now() - now.timestamp) < 3000) {
|
||||
console.warn("No jid for ssrc: " + ssrc);
|
||||
continue;
|
||||
|
@ -647,12 +647,20 @@ StatsCollector.prototype.processStatsReport = function () {
|
|||
upload:
|
||||
calculatePacketLoss(lostPackets.upload, totalPackets.upload)
|
||||
};
|
||||
|
||||
let idResolution = {};
|
||||
if (resolutions) { // use id instead of jid
|
||||
Object.keys(resolutions).forEach(function (jid) {
|
||||
let id = Strophe.getResourceFromJid(jid);
|
||||
resolution[id] = resolutions[id];
|
||||
});
|
||||
}
|
||||
this.eventEmitter.emit(StatisticsEvents.CONNECTION_STATS,
|
||||
{
|
||||
"bitrate": PeerStats.bitrate,
|
||||
"packetLoss": PeerStats.packetLoss,
|
||||
"bandwidth": PeerStats.bandwidth,
|
||||
"resolution": resolutions,
|
||||
"resolution": idResolution,
|
||||
"transport": PeerStats.transport
|
||||
});
|
||||
PeerStats.transport = [];
|
||||
|
@ -681,7 +689,7 @@ StatsCollector.prototype.processAudioLevelReport = function () {
|
|||
}
|
||||
|
||||
var ssrc = getStatValue(now, 'ssrc');
|
||||
var jid = APP.xmpp.getJidFromSSRC(ssrc);
|
||||
var jid = APP.conference._room.room.getJidBySSRC(ssrc);
|
||||
if (!jid) {
|
||||
if((Date.now() - now.timestamp) < 3000)
|
||||
console.warn("No jid for ssrc: " + ssrc);
|
||||
|
@ -713,7 +721,7 @@ StatsCollector.prototype.processAudioLevelReport = function () {
|
|||
// but it seems to vary between 0 and around 32k.
|
||||
audioLevel = audioLevel / 32767;
|
||||
jidStats.setSsrcAudioLevel(ssrc, audioLevel);
|
||||
if (jid != APP.xmpp.myJid()) {
|
||||
if (jid != APP.conference._room.room.myroomjid) {
|
||||
this.eventEmitter.emit(
|
||||
StatisticsEvents.AUDIO_LEVEL, jid, audioLevel);
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
/**
|
||||
* Created by hristo on 8/4/14.
|
||||
*/
|
||||
var LocalStats = require("./LocalStatsCollector.js");
|
||||
var RTPStats = require("./RTPStatsCollector.js");
|
||||
var EventEmitter = require("events");
|
||||
var StreamEventTypes = require("../../service/RTC/StreamEventTypes.js");
|
||||
|
@ -13,17 +12,8 @@ var StatisticsEvents = require("../../service/statistics/Events");
|
|||
|
||||
var eventEmitter = new EventEmitter();
|
||||
|
||||
var localStats = null;
|
||||
|
||||
var rtpStats = null;
|
||||
|
||||
function stopLocal() {
|
||||
if (localStats) {
|
||||
localStats.stop();
|
||||
localStats = null;
|
||||
}
|
||||
}
|
||||
|
||||
function stopRemote() {
|
||||
if (rtpStats) {
|
||||
rtpStats.stop();
|
||||
|
@ -41,26 +31,15 @@ function startRemoteStats (peerconnection) {
|
|||
rtpStats.start();
|
||||
}
|
||||
|
||||
function onStreamCreated(stream) {
|
||||
if(stream.getOriginalStream().getAudioTracks().length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
localStats = new LocalStats(stream.getOriginalStream(), 200, statistics,
|
||||
eventEmitter);
|
||||
localStats.start();
|
||||
}
|
||||
|
||||
function onDisposeConference(onUnload) {
|
||||
CallStats.sendTerminateEvent();
|
||||
stopRemote();
|
||||
if(onUnload) {
|
||||
stopLocal();
|
||||
if (onUnload) {
|
||||
eventEmitter.removeAllListeners();
|
||||
}
|
||||
}
|
||||
|
||||
var statistics = {
|
||||
export default {
|
||||
/**
|
||||
* Indicates that this audio level is for local jid.
|
||||
* @type {string}
|
||||
|
@ -74,65 +53,61 @@ var statistics = {
|
|||
eventEmitter.removeListener(type, listener);
|
||||
},
|
||||
stop: function () {
|
||||
stopLocal();
|
||||
stopRemote();
|
||||
if(eventEmitter)
|
||||
{
|
||||
if (eventEmitter) {
|
||||
eventEmitter.removeAllListeners();
|
||||
}
|
||||
},
|
||||
stopRemoteStatistics: function()
|
||||
{
|
||||
stopRemote();
|
||||
onAudioMute (mute) {
|
||||
CallStats.sendMuteEvent(mute, "audio");
|
||||
},
|
||||
onVideoMute (mute) {
|
||||
CallStats.sendMuteEvent(mute, "video");
|
||||
},
|
||||
onGetUserMediaFailed (e) {
|
||||
CallStats.sendGetUserMediaFailed(e);
|
||||
},
|
||||
start: function () {
|
||||
return;
|
||||
APP.RTC.addStreamListener(onStreamCreated,
|
||||
StreamEventTypes.EVENT_TYPE_LOCAL_CREATED);
|
||||
APP.xmpp.addListener(XMPPEvents.DISPOSE_CONFERENCE,
|
||||
onDisposeConference);
|
||||
const xmpp = APP.conference._room.xmpp;
|
||||
xmpp.addListener(
|
||||
XMPPEvents.DISPOSE_CONFERENCE,
|
||||
onDisposeConference
|
||||
);
|
||||
//FIXME: we may want to change CALL INCOMING event to
|
||||
// onnegotiationneeded
|
||||
APP.xmpp.addListener(XMPPEvents.CALL_INCOMING, function (event) {
|
||||
xmpp.addListener(XMPPEvents.CALL_INCOMING, function (event) {
|
||||
startRemoteStats(event.peerconnection);
|
||||
// CallStats.init(event);
|
||||
// CallStats.init(event);
|
||||
});
|
||||
APP.xmpp.addListener(XMPPEvents.PEERCONNECTION_READY,
|
||||
xmpp.addListener(
|
||||
XMPPEvents.PEERCONNECTION_READY,
|
||||
function (session) {
|
||||
CallStats.init(session);
|
||||
});
|
||||
APP.RTC.addListener(RTCEvents.AUDIO_MUTE, function (mute) {
|
||||
CallStats.sendMuteEvent(mute, "audio");
|
||||
});
|
||||
APP.xmpp.addListener(XMPPEvents.CONFERENCE_SETUP_FAILED, function () {
|
||||
CallStats.init(session);
|
||||
}
|
||||
);
|
||||
xmpp.addListener(XMPPEvents.CONFERENCE_SETUP_FAILED, function () {
|
||||
CallStats.sendSetupFailedEvent();
|
||||
});
|
||||
APP.RTC.addListener(RTCEvents.VIDEO_MUTE, function (mute) {
|
||||
CallStats.sendMuteEvent(mute, "video");
|
||||
});
|
||||
|
||||
APP.RTC.addListener(RTCEvents.GET_USER_MEDIA_FAILED, function (e) {
|
||||
CallStats.sendGetUserMediaFailed(e);
|
||||
});
|
||||
APP.xmpp.addListener(RTCEvents.CREATE_OFFER_FAILED, function (e, pc) {
|
||||
xmpp.addListener(RTCEvents.CREATE_OFFER_FAILED, function (e, pc) {
|
||||
CallStats.sendCreateOfferFailed(e, pc);
|
||||
});
|
||||
APP.xmpp.addListener(RTCEvents.CREATE_ANSWER_FAILED, function (e, pc) {
|
||||
xmpp.addListener(RTCEvents.CREATE_ANSWER_FAILED, function (e, pc) {
|
||||
CallStats.sendCreateAnswerFailed(e, pc);
|
||||
});
|
||||
APP.xmpp.addListener(
|
||||
xmpp.addListener(
|
||||
RTCEvents.SET_LOCAL_DESCRIPTION_FAILED,
|
||||
function (e, pc) {
|
||||
CallStats.sendSetLocalDescFailed(e, pc);
|
||||
}
|
||||
);
|
||||
APP.xmpp.addListener(
|
||||
xmpp.addListener(
|
||||
RTCEvents.SET_REMOTE_DESCRIPTION_FAILED,
|
||||
function (e, pc) {
|
||||
CallStats.sendSetRemoteDescFailed(e, pc);
|
||||
}
|
||||
);
|
||||
APP.xmpp.addListener(
|
||||
xmpp.addListener(
|
||||
RTCEvents.ADD_ICE_CANDIDATE_FAILED,
|
||||
function (e, pc) {
|
||||
CallStats.sendAddIceCandidateFailed(e, pc);
|
||||
|
@ -140,8 +115,3 @@ var statistics = {
|
|||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
module.exports = statistics;
|
|
@ -1,7 +1,6 @@
|
|||
/* global $, require, config, interfaceConfig */
|
||||
var i18n = require("i18next-client");
|
||||
var languages = require("../../service/translation/languages");
|
||||
var Settings = require("../settings/Settings");
|
||||
var DEFAULT_LANG = languages.EN;
|
||||
|
||||
i18n.addPostProcessor("resolveAppName", function(value, key, options) {
|
||||
|
@ -68,7 +67,7 @@ function initCompleted(t) {
|
|||
$("[data-i18n]").i18n();
|
||||
}
|
||||
|
||||
function checkForParameter() {
|
||||
function getLangFromQuery() {
|
||||
var query = window.location.search.substring(1);
|
||||
var vars = query.split("&");
|
||||
for (var i=0;i<vars.length;i++) {
|
||||
|
@ -82,27 +81,11 @@ function checkForParameter() {
|
|||
}
|
||||
|
||||
module.exports = {
|
||||
init: function (lang) {
|
||||
var options = defaultOptions;
|
||||
init: function (settingsLang) {
|
||||
let options = defaultOptions;
|
||||
|
||||
|
||||
if(!lang)
|
||||
{
|
||||
lang = checkForParameter();
|
||||
if(!lang)
|
||||
{
|
||||
var settings = Settings.getSettings();
|
||||
if(settings)
|
||||
lang = settings.language;
|
||||
|
||||
if(!lang && config.defaultLanguage)
|
||||
{
|
||||
lang = config.defaultLanguage;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(lang) {
|
||||
let lang = getLangFromQuery() || settingsLang || config.defaultLanguage;
|
||||
if (lang) {
|
||||
options.lng = lang;
|
||||
}
|
||||
|
||||
|
@ -124,8 +107,7 @@ module.exports = {
|
|||
},
|
||||
generateTranslationHTML: function (key, options) {
|
||||
var str = "<span data-i18n=\"" + key + "\"";
|
||||
if(options)
|
||||
{
|
||||
if (options) {
|
||||
str += " data-i18n-options=\"" + JSON.stringify(options) + "\"";
|
||||
}
|
||||
str += ">";
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
var RandomUtil = require('./RandomUtil');
|
||||
import RandomUtil from './RandomUtil';
|
||||
|
||||
/**
|
||||
* from faker.js - Copyright (c) 2014-2015 Matthew Bergman & Marak Squires
|
||||
|
@ -417,13 +417,9 @@ var names = [
|
|||
* Generate random username.
|
||||
* @returns {string} random username
|
||||
*/
|
||||
function generateUsername () {
|
||||
export function generateUsername () {
|
||||
var name = RandomUtil.randomElement(names);
|
||||
var suffix = RandomUtil.randomAlphanumStr(3);
|
||||
|
||||
return name + '-' + suffix;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
generateUsername: generateUsername
|
||||
};
|
||||
|
|
|
@ -1,127 +0,0 @@
|
|||
/*
|
||||
* JingleSession provides an API to manage a single Jingle session. We will
|
||||
* have different implementations depending on the underlying interface used
|
||||
* (i.e. WebRTC and ORTC) and here we hold the code common to all of them.
|
||||
*/
|
||||
function JingleSession(me, sid, connection, service, eventEmitter) {
|
||||
/**
|
||||
* Our JID.
|
||||
*/
|
||||
this.me = me;
|
||||
|
||||
/**
|
||||
* The Jingle session identifier.
|
||||
*/
|
||||
this.sid = sid;
|
||||
|
||||
/**
|
||||
* The XMPP connection.
|
||||
*/
|
||||
this.connection = connection;
|
||||
|
||||
/**
|
||||
* The XMPP service.
|
||||
*/
|
||||
this.service = service;
|
||||
|
||||
/**
|
||||
* The event emitter.
|
||||
*/
|
||||
this.eventEmitter = eventEmitter;
|
||||
|
||||
/**
|
||||
* Whether to use dripping or not. Dripping is sending trickle candidates
|
||||
* not one-by-one.
|
||||
* Note: currently we do not support 'false'.
|
||||
*/
|
||||
this.usedrip = true;
|
||||
|
||||
/**
|
||||
* When dripping is used, stores ICE candidates which are to be sent.
|
||||
*/
|
||||
this.drip_container = [];
|
||||
|
||||
// Media constraints. Is this WebRTC only?
|
||||
this.media_constraints = null;
|
||||
|
||||
// ICE servers config (RTCConfiguration?).
|
||||
this.ice_config = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares this object to initiate a session.
|
||||
* @param peerjid the JID of the remote peer.
|
||||
* @param isInitiator whether we will be the Jingle initiator.
|
||||
* @param media_constraints
|
||||
* @param ice_config
|
||||
*/
|
||||
JingleSession.prototype.initialize = function(peerjid, isInitiator,
|
||||
media_constraints, ice_config) {
|
||||
this.media_constraints = media_constraints;
|
||||
this.ice_config = ice_config;
|
||||
|
||||
if (this.state !== null) {
|
||||
console.error('attempt to initiate on session ' + this.sid +
|
||||
'in state ' + this.state);
|
||||
return;
|
||||
}
|
||||
this.state = 'pending';
|
||||
this.initiator = isInitiator ? this.me : peerjid;
|
||||
this.responder = !isInitiator ? this.me : peerjid;
|
||||
this.peerjid = peerjid;
|
||||
|
||||
this.doInitialize();
|
||||
};
|
||||
|
||||
/**
|
||||
* Finishes initialization.
|
||||
*/
|
||||
JingleSession.prototype.doInitialize = function() {};
|
||||
|
||||
/**
|
||||
* Adds the ICE candidates found in the 'contents' array as remote candidates?
|
||||
* Note: currently only used on transport-info
|
||||
*/
|
||||
JingleSession.prototype.addIceCandidates = function(contents) {};
|
||||
|
||||
/**
|
||||
* Handles an 'add-source' event.
|
||||
*
|
||||
* @param contents an array of Jingle 'content' elements.
|
||||
*/
|
||||
JingleSession.prototype.addSources = function(contents) {};
|
||||
|
||||
/**
|
||||
* Handles a 'remove-source' event.
|
||||
*
|
||||
* @param contents an array of Jingle 'content' elements.
|
||||
*/
|
||||
JingleSession.prototype.removeSources = function(contents) {};
|
||||
|
||||
/**
|
||||
* Terminates this Jingle session (stops sending media and closes the streams?)
|
||||
*/
|
||||
JingleSession.prototype.terminate = function() {};
|
||||
|
||||
/**
|
||||
* Sends a Jingle session-terminate message to the peer and terminates the
|
||||
* session.
|
||||
* @param reason
|
||||
* @param text
|
||||
*/
|
||||
JingleSession.prototype.sendTerminate = function(reason, text) {};
|
||||
|
||||
/**
|
||||
* Handles an offer from the remote peer (prepares to accept a session).
|
||||
* @param jingle the 'jingle' XML element.
|
||||
*/
|
||||
JingleSession.prototype.setOffer = function(jingle) {};
|
||||
|
||||
/**
|
||||
* Handles an answer from the remote peer (prepares to accept a session).
|
||||
* @param jingle the 'jingle' XML element.
|
||||
*/
|
||||
JingleSession.prototype.setAnswer = function(jingle) {};
|
||||
|
||||
|
||||
module.exports = JingleSession;
|
File diff suppressed because it is too large
Load Diff
|
@ -1,268 +0,0 @@
|
|||
/* global $ */
|
||||
|
||||
/*
|
||||
Here we do modifications of local video SSRCs. There are 2 situations we have
|
||||
to handle:
|
||||
|
||||
1. We generate SSRC for local recvonly video stream. This is the case when we
|
||||
have no local camera and it is not generated automatically, but SSRC=1 is
|
||||
used implicitly. If that happens RTCP packets will be dropped by the JVB
|
||||
and we won't be able to request video key frames correctly.
|
||||
|
||||
2. A hack to re-use SSRC of the first video stream for any new stream created
|
||||
in future. It turned out that Chrome may keep on using the SSRC of removed
|
||||
video stream in RTCP even though a new one has been created. So we just
|
||||
want to avoid that by re-using it. Jingle 'source-remove'/'source-add'
|
||||
notifications are blocked once first video SSRC is advertised to the focus.
|
||||
|
||||
What this hack does:
|
||||
|
||||
1. Stores the SSRC of the first video stream created by
|
||||
a) scanning Jingle session-accept/session-invite for existing video SSRC
|
||||
b) watching for 'source-add' for new video stream if it has not been
|
||||
created in step a)
|
||||
2. Exposes method 'mungeLocalVideoSSRC' which replaces any new video SSRC with
|
||||
the stored one. It is called by 'TracablePeerConnection' before local SDP is
|
||||
returned to the other parts of the application.
|
||||
3. Scans 'source-remove'/'source-add' notifications for stored video SSRC and
|
||||
blocks those notifications. This makes Jicofo and all participants think
|
||||
that it exists all the time even if the video stream has been removed or
|
||||
replaced locally. Thanks to that there is no additional signaling activity
|
||||
on video mute or when switching to the desktop stream.
|
||||
*/
|
||||
|
||||
var SDP = require('./SDP');
|
||||
var RTCBrowserType = require('../RTC/RTCBrowserType');
|
||||
|
||||
/**
|
||||
* The hack is enabled on all browsers except FF by default
|
||||
* FIXME finish the hack once removeStream method is implemented in FF
|
||||
* @type {boolean}
|
||||
*/
|
||||
var isEnabled = !RTCBrowserType.isFirefox();
|
||||
|
||||
/**
|
||||
* Stored SSRC of local video stream.
|
||||
*/
|
||||
var localVideoSSRC;
|
||||
|
||||
/**
|
||||
* SSRC used for recvonly video stream when we have no local camera.
|
||||
* This is in order to tell Chrome what SSRC should be used in RTCP requests
|
||||
* instead of 1.
|
||||
*/
|
||||
var localRecvOnlySSRC;
|
||||
|
||||
/**
|
||||
* cname for <tt>localRecvOnlySSRC</tt>
|
||||
*/
|
||||
var localRecvOnlyCName;
|
||||
|
||||
/**
|
||||
* Method removes <source> element which describes <tt>localVideoSSRC</tt>
|
||||
* from given Jingle IQ.
|
||||
* @param modifyIq 'source-add' or 'source-remove' Jingle IQ.
|
||||
* @param actionName display name of the action which will be printed in log
|
||||
* messages.
|
||||
* @returns {*} modified Jingle IQ, so that it does not contain <source> element
|
||||
* corresponding to <tt>localVideoSSRC</tt> or <tt>null</tt> if no
|
||||
* other SSRCs left to be signaled after removing it.
|
||||
*/
|
||||
var filterOutSource = function (modifyIq, actionName) {
|
||||
var modifyIqTree = $(modifyIq.tree());
|
||||
|
||||
if (!localVideoSSRC)
|
||||
return modifyIqTree[0];
|
||||
|
||||
var videoSSRC = modifyIqTree.find(
|
||||
'>jingle>content[name="video"]' +
|
||||
'>description>source[ssrc="' + localVideoSSRC + '"]');
|
||||
|
||||
if (!videoSSRC.length) {
|
||||
return modifyIqTree[0];
|
||||
}
|
||||
|
||||
console.info(
|
||||
'Blocking ' + actionName + ' for local video SSRC: ' + localVideoSSRC);
|
||||
|
||||
videoSSRC.remove();
|
||||
|
||||
// Check if any sources still left to be added/removed
|
||||
if (modifyIqTree.find('>jingle>content>description>source').length) {
|
||||
return modifyIqTree[0];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Scans given Jingle IQ for video SSRC and stores it.
|
||||
* @param jingleIq the Jingle IQ to be scanned for video SSRC.
|
||||
*/
|
||||
var storeLocalVideoSSRC = function (jingleIq) {
|
||||
var videoSSRCs =
|
||||
$(jingleIq.tree())
|
||||
.find('>jingle>content[name="video"]>description>source');
|
||||
|
||||
videoSSRCs.each(function (idx, ssrcElem) {
|
||||
if (localVideoSSRC)
|
||||
return;
|
||||
// We consider SSRC real only if it has msid attribute
|
||||
// recvonly streams in FF do not have it as well as local SSRCs
|
||||
// we generate for recvonly streams in Chrome
|
||||
var ssrSel = $(ssrcElem);
|
||||
var msid = ssrSel.find('>parameter[name="msid"]');
|
||||
if (msid.length) {
|
||||
var ssrcVal = ssrSel.attr('ssrc');
|
||||
if (ssrcVal) {
|
||||
localVideoSSRC = ssrcVal;
|
||||
console.info('Stored local video SSRC' +
|
||||
' for future re-use: ' + localVideoSSRC);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates new SSRC for local video recvonly stream.
|
||||
* FIXME what about eventual SSRC collision ?
|
||||
*/
|
||||
function generateRecvonlySSRC() {
|
||||
//
|
||||
localRecvOnlySSRC =
|
||||
Math.random().toString(10).substring(2, 11);
|
||||
localRecvOnlyCName =
|
||||
Math.random().toString(36).substring(2);
|
||||
console.info(
|
||||
"Generated local recvonly SSRC: " + localRecvOnlySSRC +
|
||||
", cname: " + localRecvOnlyCName);
|
||||
}
|
||||
|
||||
var LocalSSRCReplacement = {
|
||||
/**
|
||||
* Method must be called before 'session-initiate' or 'session-invite' is
|
||||
* sent. Scans the IQ for local video SSRC and stores it if detected.
|
||||
*
|
||||
* @param sessionInit our 'session-initiate' or 'session-accept' Jingle IQ
|
||||
* which will be scanned for local video SSRC.
|
||||
*/
|
||||
processSessionInit: function (sessionInit) {
|
||||
if (!isEnabled)
|
||||
return;
|
||||
|
||||
if (localVideoSSRC) {
|
||||
console.error("Local SSRC stored already: " + localVideoSSRC);
|
||||
return;
|
||||
}
|
||||
storeLocalVideoSSRC(sessionInit);
|
||||
},
|
||||
/**
|
||||
* If we have local video SSRC stored searched given
|
||||
* <tt>localDescription</tt> for video SSRC and makes sure it is replaced
|
||||
* with the stored one.
|
||||
* @param localDescription local description object that will have local
|
||||
* video SSRC replaced with the stored one
|
||||
* @returns modified <tt>localDescription</tt> object.
|
||||
*/
|
||||
mungeLocalVideoSSRC: function (localDescription) {
|
||||
if (!isEnabled)
|
||||
return localDescription;
|
||||
|
||||
if (!localDescription) {
|
||||
console.warn("localDescription is null or undefined");
|
||||
return localDescription;
|
||||
}
|
||||
|
||||
// IF we have local video SSRC stored make sure it is replaced
|
||||
// with old SSRC
|
||||
if (localVideoSSRC) {
|
||||
var newSdp = new SDP(localDescription.sdp);
|
||||
if (newSdp.media[1].indexOf("a=ssrc:") !== -1 &&
|
||||
!newSdp.containsSSRC(localVideoSSRC)) {
|
||||
// Get new video SSRC
|
||||
var map = newSdp.getMediaSsrcMap();
|
||||
var videoPart = map[1];
|
||||
var videoSSRCs = videoPart.ssrcs;
|
||||
var newSSRC = Object.keys(videoSSRCs)[0];
|
||||
|
||||
console.info(
|
||||
"Replacing new video SSRC: " + newSSRC +
|
||||
" with " + localVideoSSRC);
|
||||
|
||||
localDescription.sdp =
|
||||
newSdp.raw.replace(
|
||||
new RegExp('a=ssrc:' + newSSRC, 'g'),
|
||||
'a=ssrc:' + localVideoSSRC);
|
||||
}
|
||||
} else {
|
||||
// Make sure we have any SSRC for recvonly video stream
|
||||
var sdp = new SDP(localDescription.sdp);
|
||||
|
||||
if (sdp.media[1] && sdp.media[1].indexOf('a=ssrc:') === -1 &&
|
||||
sdp.media[1].indexOf('a=recvonly') !== -1) {
|
||||
|
||||
if (!localRecvOnlySSRC) {
|
||||
generateRecvonlySSRC();
|
||||
}
|
||||
|
||||
console.info('No SSRC in video recvonly stream' +
|
||||
' - adding SSRC: ' + localRecvOnlySSRC);
|
||||
|
||||
sdp.media[1] += 'a=ssrc:' + localRecvOnlySSRC +
|
||||
' cname:' + localRecvOnlyCName + '\r\n';
|
||||
|
||||
localDescription.sdp = sdp.session + sdp.media.join('');
|
||||
}
|
||||
}
|
||||
return localDescription;
|
||||
},
|
||||
/**
|
||||
* Method must be called before 'source-add' notification is sent. In case
|
||||
* we have local video SSRC advertised already it will be removed from the
|
||||
* notification. If no other SSRCs are described by given IQ null will be
|
||||
* returned which means that there is no point in sending the notification.
|
||||
* @param sourceAdd 'source-add' Jingle IQ to be processed
|
||||
* @returns modified 'source-add' IQ which can be sent to the focus or
|
||||
* <tt>null</tt> if no notification shall be sent. It is no longer
|
||||
* a Strophe IQ Builder instance, but DOM element tree.
|
||||
*/
|
||||
processSourceAdd: function (sourceAdd) {
|
||||
if (!isEnabled)
|
||||
return sourceAdd;
|
||||
|
||||
if (!localVideoSSRC) {
|
||||
// Store local SSRC if available
|
||||
storeLocalVideoSSRC(sourceAdd);
|
||||
return sourceAdd;
|
||||
} else {
|
||||
return filterOutSource(sourceAdd, 'source-add');
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Method must be called before 'source-remove' notification is sent.
|
||||
* Removes local video SSRC from the notification. If there are no other
|
||||
* SSRCs described in the given IQ <tt>null</tt> will be returned which
|
||||
* means that there is no point in sending the notification.
|
||||
* @param sourceRemove 'source-remove' Jingle IQ to be processed
|
||||
* @returns modified 'source-remove' IQ which can be sent to the focus or
|
||||
* <tt>null</tt> if no notification shall be sent. It is no longer
|
||||
* a Strophe IQ Builder instance, but DOM element tree.
|
||||
*/
|
||||
processSourceRemove: function (sourceRemove) {
|
||||
if (!isEnabled)
|
||||
return sourceRemove;
|
||||
|
||||
return filterOutSource(sourceRemove, 'source-remove');
|
||||
},
|
||||
|
||||
/**
|
||||
* Turns the hack on or off
|
||||
* @param enabled <tt>true</tt> to enable the hack or <tt>false</tt>
|
||||
* to disable it
|
||||
*/
|
||||
setEnabled: function (enabled) {
|
||||
isEnabled = enabled;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = LocalSSRCReplacement;
|
|
@ -1,645 +0,0 @@
|
|||
/* jshint -W101 */
|
||||
/* jshint -W117 */
|
||||
var SDPUtil = require("./SDPUtil");
|
||||
|
||||
// SDP STUFF
|
||||
function SDP(sdp) {
|
||||
/**
|
||||
* Whether or not to remove TCP ice candidates when translating from/to jingle.
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.removeTcpCandidates = false;
|
||||
|
||||
/**
|
||||
* Whether or not to remove UDP ice candidates when translating from/to jingle.
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.removeUdpCandidates = false;
|
||||
|
||||
this.media = sdp.split('\r\nm=');
|
||||
for (var i = 1; i < this.media.length; i++) {
|
||||
this.media[i] = 'm=' + this.media[i];
|
||||
if (i != this.media.length - 1) {
|
||||
this.media[i] += '\r\n';
|
||||
}
|
||||
}
|
||||
this.session = this.media.shift() + '\r\n';
|
||||
this.raw = this.session + this.media.join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns map of MediaChannel mapped per channel idx.
|
||||
*/
|
||||
SDP.prototype.getMediaSsrcMap = function() {
|
||||
var self = this;
|
||||
var media_ssrcs = {};
|
||||
var tmp;
|
||||
for (var mediaindex = 0; mediaindex < self.media.length; mediaindex++) {
|
||||
tmp = SDPUtil.find_lines(self.media[mediaindex], 'a=ssrc:');
|
||||
var mid = SDPUtil.parse_mid(SDPUtil.find_line(self.media[mediaindex], 'a=mid:'));
|
||||
var media = {
|
||||
mediaindex: mediaindex,
|
||||
mid: mid,
|
||||
ssrcs: {},
|
||||
ssrcGroups: []
|
||||
};
|
||||
media_ssrcs[mediaindex] = media;
|
||||
tmp.forEach(function (line) {
|
||||
var linessrc = line.substring(7).split(' ')[0];
|
||||
// allocate new ChannelSsrc
|
||||
if(!media.ssrcs[linessrc]) {
|
||||
media.ssrcs[linessrc] = {
|
||||
ssrc: linessrc,
|
||||
lines: []
|
||||
};
|
||||
}
|
||||
media.ssrcs[linessrc].lines.push(line);
|
||||
});
|
||||
tmp = SDPUtil.find_lines(self.media[mediaindex], 'a=ssrc-group:');
|
||||
tmp.forEach(function(line){
|
||||
var idx = line.indexOf(' ');
|
||||
var semantics = line.substr(0, idx).substr(13);
|
||||
var ssrcs = line.substr(14 + semantics.length).split(' ');
|
||||
if (ssrcs.length) {
|
||||
media.ssrcGroups.push({
|
||||
semantics: semantics,
|
||||
ssrcs: ssrcs
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
return media_ssrcs;
|
||||
};
|
||||
/**
|
||||
* Returns <tt>true</tt> if this SDP contains given SSRC.
|
||||
* @param ssrc the ssrc to check.
|
||||
* @returns {boolean} <tt>true</tt> if this SDP contains given SSRC.
|
||||
*/
|
||||
SDP.prototype.containsSSRC = function(ssrc) {
|
||||
var medias = this.getMediaSsrcMap();
|
||||
Object.keys(medias).forEach(function(mediaindex){
|
||||
var media = medias[mediaindex];
|
||||
//console.log("Check", channel, ssrc);
|
||||
if(Object.keys(media.ssrcs).indexOf(ssrc) != -1){
|
||||
return true;
|
||||
}
|
||||
});
|
||||
return false;
|
||||
};
|
||||
|
||||
// remove iSAC and CN from SDP
|
||||
SDP.prototype.mangle = function () {
|
||||
var i, j, mline, lines, rtpmap, newdesc;
|
||||
for (i = 0; i < this.media.length; i++) {
|
||||
lines = this.media[i].split('\r\n');
|
||||
lines.pop(); // remove empty last element
|
||||
mline = SDPUtil.parse_mline(lines.shift());
|
||||
if (mline.media != 'audio')
|
||||
continue;
|
||||
newdesc = '';
|
||||
mline.fmt.length = 0;
|
||||
for (j = 0; j < lines.length; j++) {
|
||||
if (lines[j].substr(0, 9) == 'a=rtpmap:') {
|
||||
rtpmap = SDPUtil.parse_rtpmap(lines[j]);
|
||||
if (rtpmap.name == 'CN' || rtpmap.name == 'ISAC')
|
||||
continue;
|
||||
mline.fmt.push(rtpmap.id);
|
||||
newdesc += lines[j] + '\r\n';
|
||||
} else {
|
||||
newdesc += lines[j] + '\r\n';
|
||||
}
|
||||
}
|
||||
this.media[i] = SDPUtil.build_mline(mline) + '\r\n';
|
||||
this.media[i] += newdesc;
|
||||
}
|
||||
this.raw = this.session + this.media.join('');
|
||||
};
|
||||
|
||||
// remove lines matching prefix from session section
|
||||
SDP.prototype.removeSessionLines = function(prefix) {
|
||||
var self = this;
|
||||
var lines = SDPUtil.find_lines(this.session, prefix);
|
||||
lines.forEach(function(line) {
|
||||
self.session = self.session.replace(line + '\r\n', '');
|
||||
});
|
||||
this.raw = this.session + this.media.join('');
|
||||
return lines;
|
||||
};
|
||||
|
||||
// remove lines matching prefix from a media section specified by mediaindex
|
||||
// TODO: non-numeric mediaindex could match mid
|
||||
SDP.prototype.removeMediaLines = function(mediaindex, prefix) {
|
||||
var self = this;
|
||||
var lines = SDPUtil.find_lines(this.media[mediaindex], prefix);
|
||||
lines.forEach(function(line) {
|
||||
self.media[mediaindex] = self.media[mediaindex].replace(line + '\r\n', '');
|
||||
});
|
||||
this.raw = this.session + this.media.join('');
|
||||
return lines;
|
||||
};
|
||||
|
||||
// add content's to a jingle element
|
||||
SDP.prototype.toJingle = function (elem, thecreator, ssrcs) {
|
||||
// console.log("SSRC" + ssrcs["audio"] + " - " + ssrcs["video"]);
|
||||
var i, j, k, mline, ssrc, rtpmap, tmp, lines;
|
||||
// new bundle plan
|
||||
if (SDPUtil.find_line(this.session, 'a=group:')) {
|
||||
lines = SDPUtil.find_lines(this.session, 'a=group:');
|
||||
for (i = 0; i < lines.length; i++) {
|
||||
tmp = lines[i].split(' ');
|
||||
var semantics = tmp.shift().substr(8);
|
||||
elem.c('group', {xmlns: 'urn:xmpp:jingle:apps:grouping:0', semantics:semantics});
|
||||
for (j = 0; j < tmp.length; j++) {
|
||||
elem.c('content', {name: tmp[j]}).up();
|
||||
}
|
||||
elem.up();
|
||||
}
|
||||
}
|
||||
for (i = 0; i < this.media.length; i++) {
|
||||
mline = SDPUtil.parse_mline(this.media[i].split('\r\n')[0]);
|
||||
if (!(mline.media === 'audio' ||
|
||||
mline.media === 'video' ||
|
||||
mline.media === 'application'))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (SDPUtil.find_line(this.media[i], 'a=ssrc:')) {
|
||||
ssrc = SDPUtil.find_line(this.media[i], 'a=ssrc:').substring(7).split(' ')[0]; // take the first
|
||||
} else {
|
||||
if(ssrcs && ssrcs[mline.media]) {
|
||||
ssrc = ssrcs[mline.media];
|
||||
} else {
|
||||
ssrc = false;
|
||||
}
|
||||
}
|
||||
|
||||
elem.c('content', {creator: thecreator, name: mline.media});
|
||||
if (SDPUtil.find_line(this.media[i], 'a=mid:')) {
|
||||
// prefer identifier from a=mid if present
|
||||
var mid = SDPUtil.parse_mid(SDPUtil.find_line(this.media[i], 'a=mid:'));
|
||||
elem.attrs({ name: mid });
|
||||
}
|
||||
|
||||
if (SDPUtil.find_line(this.media[i], 'a=rtpmap:').length) {
|
||||
elem.c('description',
|
||||
{xmlns: 'urn:xmpp:jingle:apps:rtp:1',
|
||||
media: mline.media });
|
||||
if (ssrc) {
|
||||
elem.attrs({ssrc: ssrc});
|
||||
}
|
||||
for (j = 0; j < mline.fmt.length; j++) {
|
||||
rtpmap = SDPUtil.find_line(this.media[i], 'a=rtpmap:' + mline.fmt[j]);
|
||||
elem.c('payload-type', SDPUtil.parse_rtpmap(rtpmap));
|
||||
// put any 'a=fmtp:' + mline.fmt[j] lines into <param name=foo value=bar/>
|
||||
if (SDPUtil.find_line(this.media[i], 'a=fmtp:' + mline.fmt[j])) {
|
||||
tmp = SDPUtil.parse_fmtp(SDPUtil.find_line(this.media[i], 'a=fmtp:' + mline.fmt[j]));
|
||||
for (k = 0; k < tmp.length; k++) {
|
||||
elem.c('parameter', tmp[k]).up();
|
||||
}
|
||||
}
|
||||
this.rtcpFbToJingle(i, elem, mline.fmt[j]); // XEP-0293 -- map a=rtcp-fb
|
||||
|
||||
elem.up();
|
||||
}
|
||||
if (SDPUtil.find_line(this.media[i], 'a=crypto:', this.session)) {
|
||||
elem.c('encryption', {required: 1});
|
||||
var crypto = SDPUtil.find_lines(this.media[i], 'a=crypto:', this.session);
|
||||
crypto.forEach(function(line) {
|
||||
elem.c('crypto', SDPUtil.parse_crypto(line)).up();
|
||||
});
|
||||
elem.up(); // end of encryption
|
||||
}
|
||||
|
||||
if (ssrc) {
|
||||
// new style mapping
|
||||
elem.c('source', { ssrc: ssrc, xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
|
||||
// FIXME: group by ssrc and support multiple different ssrcs
|
||||
var ssrclines = SDPUtil.find_lines(this.media[i], 'a=ssrc:');
|
||||
if(ssrclines.length > 0) {
|
||||
ssrclines.forEach(function (line) {
|
||||
var idx = line.indexOf(' ');
|
||||
var linessrc = line.substr(0, idx).substr(7);
|
||||
if (linessrc != ssrc) {
|
||||
elem.up();
|
||||
ssrc = linessrc;
|
||||
elem.c('source', { ssrc: ssrc, xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
|
||||
}
|
||||
var kv = line.substr(idx + 1);
|
||||
elem.c('parameter');
|
||||
if (kv.indexOf(':') == -1) {
|
||||
elem.attrs({ name: kv });
|
||||
} else {
|
||||
var k = kv.split(':', 2)[0];
|
||||
elem.attrs({ name: k });
|
||||
|
||||
var v = kv.split(':', 2)[1];
|
||||
v = SDPUtil.filter_special_chars(v);
|
||||
elem.attrs({ value: v });
|
||||
}
|
||||
elem.up();
|
||||
});
|
||||
} else {
|
||||
elem.up();
|
||||
elem.c('source', { ssrc: ssrc, xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
|
||||
elem.c('parameter');
|
||||
elem.attrs({name: "cname", value:Math.random().toString(36).substring(7)});
|
||||
elem.up();
|
||||
var msid = null;
|
||||
if(mline.media == "audio") {
|
||||
msid = APP.RTC.localAudio.getId();
|
||||
} else {
|
||||
msid = APP.RTC.localVideo.getId();
|
||||
}
|
||||
if(msid !== null) {
|
||||
msid = SDPUtil.filter_special_chars(msid);
|
||||
elem.c('parameter');
|
||||
elem.attrs({name: "msid", value:msid});
|
||||
elem.up();
|
||||
elem.c('parameter');
|
||||
elem.attrs({name: "mslabel", value:msid});
|
||||
elem.up();
|
||||
elem.c('parameter');
|
||||
elem.attrs({name: "label", value:msid});
|
||||
elem.up();
|
||||
}
|
||||
}
|
||||
elem.up();
|
||||
|
||||
// XEP-0339 handle ssrc-group attributes
|
||||
var ssrc_group_lines = SDPUtil.find_lines(this.media[i], 'a=ssrc-group:');
|
||||
ssrc_group_lines.forEach(function(line) {
|
||||
var idx = line.indexOf(' ');
|
||||
var semantics = line.substr(0, idx).substr(13);
|
||||
var ssrcs = line.substr(14 + semantics.length).split(' ');
|
||||
if (ssrcs.length) {
|
||||
elem.c('ssrc-group', { semantics: semantics, xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
|
||||
ssrcs.forEach(function(ssrc) {
|
||||
elem.c('source', { ssrc: ssrc })
|
||||
.up();
|
||||
});
|
||||
elem.up();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (SDPUtil.find_line(this.media[i], 'a=rtcp-mux')) {
|
||||
elem.c('rtcp-mux').up();
|
||||
}
|
||||
|
||||
// XEP-0293 -- map a=rtcp-fb:*
|
||||
this.rtcpFbToJingle(i, elem, '*');
|
||||
|
||||
// XEP-0294
|
||||
if (SDPUtil.find_line(this.media[i], 'a=extmap:')) {
|
||||
lines = SDPUtil.find_lines(this.media[i], 'a=extmap:');
|
||||
for (j = 0; j < lines.length; j++) {
|
||||
tmp = SDPUtil.parse_extmap(lines[j]);
|
||||
elem.c('rtp-hdrext', { xmlns: 'urn:xmpp:jingle:apps:rtp:rtp-hdrext:0',
|
||||
uri: tmp.uri,
|
||||
id: tmp.value });
|
||||
if (tmp.hasOwnProperty('direction')) {
|
||||
switch (tmp.direction) {
|
||||
case 'sendonly':
|
||||
elem.attrs({senders: 'responder'});
|
||||
break;
|
||||
case 'recvonly':
|
||||
elem.attrs({senders: 'initiator'});
|
||||
break;
|
||||
case 'sendrecv':
|
||||
elem.attrs({senders: 'both'});
|
||||
break;
|
||||
case 'inactive':
|
||||
elem.attrs({senders: 'none'});
|
||||
break;
|
||||
}
|
||||
}
|
||||
// TODO: handle params
|
||||
elem.up();
|
||||
}
|
||||
}
|
||||
elem.up(); // end of description
|
||||
}
|
||||
|
||||
// map ice-ufrag/pwd, dtls fingerprint, candidates
|
||||
this.transportToJingle(i, elem);
|
||||
|
||||
if (SDPUtil.find_line(this.media[i], 'a=sendrecv', this.session)) {
|
||||
elem.attrs({senders: 'both'});
|
||||
} else if (SDPUtil.find_line(this.media[i], 'a=sendonly', this.session)) {
|
||||
elem.attrs({senders: 'initiator'});
|
||||
} else if (SDPUtil.find_line(this.media[i], 'a=recvonly', this.session)) {
|
||||
elem.attrs({senders: 'responder'});
|
||||
} else if (SDPUtil.find_line(this.media[i], 'a=inactive', this.session)) {
|
||||
elem.attrs({senders: 'none'});
|
||||
}
|
||||
if (mline.port == '0') {
|
||||
// estos hack to reject an m-line
|
||||
elem.attrs({senders: 'rejected'});
|
||||
}
|
||||
elem.up(); // end of content
|
||||
}
|
||||
elem.up();
|
||||
return elem;
|
||||
};
|
||||
|
||||
SDP.prototype.transportToJingle = function (mediaindex, elem) {
|
||||
var tmp, sctpmap, sctpAttrs, fingerprints;
|
||||
var self = this;
|
||||
elem.c('transport');
|
||||
|
||||
// XEP-0343 DTLS/SCTP
|
||||
if (SDPUtil.find_line(this.media[mediaindex], 'a=sctpmap:').length)
|
||||
{
|
||||
sctpmap = SDPUtil.find_line(
|
||||
this.media[mediaindex], 'a=sctpmap:', self.session);
|
||||
if (sctpmap)
|
||||
{
|
||||
sctpAttrs = SDPUtil.parse_sctpmap(sctpmap);
|
||||
elem.c('sctpmap',
|
||||
{
|
||||
xmlns: 'urn:xmpp:jingle:transports:dtls-sctp:1',
|
||||
number: sctpAttrs[0], /* SCTP port */
|
||||
protocol: sctpAttrs[1] /* protocol */
|
||||
});
|
||||
// Optional stream count attribute
|
||||
if (sctpAttrs.length > 2)
|
||||
elem.attrs({ streams: sctpAttrs[2]});
|
||||
elem.up();
|
||||
}
|
||||
}
|
||||
// XEP-0320
|
||||
fingerprints = SDPUtil.find_lines(this.media[mediaindex], 'a=fingerprint:', this.session);
|
||||
fingerprints.forEach(function(line) {
|
||||
tmp = SDPUtil.parse_fingerprint(line);
|
||||
tmp.xmlns = 'urn:xmpp:jingle:apps:dtls:0';
|
||||
elem.c('fingerprint').t(tmp.fingerprint);
|
||||
delete tmp.fingerprint;
|
||||
line = SDPUtil.find_line(self.media[mediaindex], 'a=setup:', self.session);
|
||||
if (line) {
|
||||
tmp.setup = line.substr(8);
|
||||
}
|
||||
elem.attrs(tmp);
|
||||
elem.up(); // end of fingerprint
|
||||
});
|
||||
tmp = SDPUtil.iceparams(this.media[mediaindex], this.session);
|
||||
if (tmp) {
|
||||
tmp.xmlns = 'urn:xmpp:jingle:transports:ice-udp:1';
|
||||
elem.attrs(tmp);
|
||||
// XEP-0176
|
||||
if (SDPUtil.find_line(this.media[mediaindex], 'a=candidate:', this.session)) { // add any a=candidate lines
|
||||
var lines = SDPUtil.find_lines(this.media[mediaindex], 'a=candidate:', this.session);
|
||||
lines.forEach(function (line) {
|
||||
var candidate = SDPUtil.candidateToJingle(line);
|
||||
var protocol = (candidate &&
|
||||
typeof candidate.protocol === 'string')
|
||||
? candidate.protocol.toLowerCase() : '';
|
||||
if ((self.removeTcpCandidates && protocol === 'tcp') ||
|
||||
(self.removeUdpCandidates && protocol === 'udp')) {
|
||||
return;
|
||||
}
|
||||
elem.c('candidate', candidate).up();
|
||||
});
|
||||
}
|
||||
}
|
||||
elem.up(); // end of transport
|
||||
};
|
||||
|
||||
SDP.prototype.rtcpFbToJingle = function (mediaindex, elem, payloadtype) { // XEP-0293
|
||||
var lines = SDPUtil.find_lines(this.media[mediaindex], 'a=rtcp-fb:' + payloadtype);
|
||||
lines.forEach(function (line) {
|
||||
var tmp = SDPUtil.parse_rtcpfb(line);
|
||||
if (tmp.type == 'trr-int') {
|
||||
elem.c('rtcp-fb-trr-int', {xmlns: 'urn:xmpp:jingle:apps:rtp:rtcp-fb:0', value: tmp.params[0]});
|
||||
elem.up();
|
||||
} else {
|
||||
elem.c('rtcp-fb', {xmlns: 'urn:xmpp:jingle:apps:rtp:rtcp-fb:0', type: tmp.type});
|
||||
if (tmp.params.length > 0) {
|
||||
elem.attrs({'subtype': tmp.params[0]});
|
||||
}
|
||||
elem.up();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
SDP.prototype.rtcpFbFromJingle = function (elem, payloadtype) { // XEP-0293
|
||||
var media = '';
|
||||
var tmp = elem.find('>rtcp-fb-trr-int[xmlns="urn:xmpp:jingle:apps:rtp:rtcp-fb:0"]');
|
||||
if (tmp.length) {
|
||||
media += 'a=rtcp-fb:' + '*' + ' ' + 'trr-int' + ' ';
|
||||
if (tmp.attr('value')) {
|
||||
media += tmp.attr('value');
|
||||
} else {
|
||||
media += '0';
|
||||
}
|
||||
media += '\r\n';
|
||||
}
|
||||
tmp = elem.find('>rtcp-fb[xmlns="urn:xmpp:jingle:apps:rtp:rtcp-fb:0"]');
|
||||
tmp.each(function () {
|
||||
media += 'a=rtcp-fb:' + payloadtype + ' ' + $(this).attr('type');
|
||||
if ($(this).attr('subtype')) {
|
||||
media += ' ' + $(this).attr('subtype');
|
||||
}
|
||||
media += '\r\n';
|
||||
});
|
||||
return media;
|
||||
};
|
||||
|
||||
// construct an SDP from a jingle stanza
|
||||
SDP.prototype.fromJingle = function (jingle) {
|
||||
var self = this;
|
||||
this.raw = 'v=0\r\n' +
|
||||
'o=- 1923518516 2 IN IP4 0.0.0.0\r\n' +// FIXME
|
||||
's=-\r\n' +
|
||||
't=0 0\r\n';
|
||||
// http://tools.ietf.org/html/draft-ietf-mmusic-sdp-bundle-negotiation-04#section-8
|
||||
if ($(jingle).find('>group[xmlns="urn:xmpp:jingle:apps:grouping:0"]').length) {
|
||||
$(jingle).find('>group[xmlns="urn:xmpp:jingle:apps:grouping:0"]').each(function (idx, group) {
|
||||
var contents = $(group).find('>content').map(function (idx, content) {
|
||||
return content.getAttribute('name');
|
||||
}).get();
|
||||
if (contents.length > 0) {
|
||||
self.raw += 'a=group:' + (group.getAttribute('semantics') || group.getAttribute('type')) + ' ' + contents.join(' ') + '\r\n';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.session = this.raw;
|
||||
jingle.find('>content').each(function () {
|
||||
var m = self.jingle2media($(this));
|
||||
self.media.push(m);
|
||||
});
|
||||
|
||||
// reconstruct msid-semantic -- apparently not necessary
|
||||
/*
|
||||
var msid = SDPUtil.parse_ssrc(this.raw);
|
||||
if (msid.hasOwnProperty('mslabel')) {
|
||||
this.session += "a=msid-semantic: WMS " + msid.mslabel + "\r\n";
|
||||
}
|
||||
*/
|
||||
|
||||
this.raw = this.session + this.media.join('');
|
||||
};
|
||||
|
||||
// translate a jingle content element into an an SDP media part
|
||||
SDP.prototype.jingle2media = function (content) {
|
||||
var media = '',
|
||||
desc = content.find('description'),
|
||||
ssrc = desc.attr('ssrc'),
|
||||
self = this,
|
||||
tmp;
|
||||
var sctp = content.find(
|
||||
'>transport>sctpmap[xmlns="urn:xmpp:jingle:transports:dtls-sctp:1"]');
|
||||
|
||||
tmp = { media: desc.attr('media') };
|
||||
tmp.port = '1';
|
||||
if (content.attr('senders') == 'rejected') {
|
||||
// estos hack to reject an m-line.
|
||||
tmp.port = '0';
|
||||
}
|
||||
if (content.find('>transport>fingerprint').length || desc.find('encryption').length) {
|
||||
if (sctp.length)
|
||||
tmp.proto = 'DTLS/SCTP';
|
||||
else
|
||||
tmp.proto = 'RTP/SAVPF';
|
||||
} else {
|
||||
tmp.proto = 'RTP/AVPF';
|
||||
}
|
||||
if (!sctp.length) {
|
||||
tmp.fmt = desc.find('payload-type').map(
|
||||
function () { return this.getAttribute('id'); }).get();
|
||||
media += SDPUtil.build_mline(tmp) + '\r\n';
|
||||
} else {
|
||||
media += 'm=application 1 DTLS/SCTP ' + sctp.attr('number') + '\r\n';
|
||||
media += 'a=sctpmap:' + sctp.attr('number') +
|
||||
' ' + sctp.attr('protocol');
|
||||
|
||||
var streamCount = sctp.attr('streams');
|
||||
if (streamCount)
|
||||
media += ' ' + streamCount + '\r\n';
|
||||
else
|
||||
media += '\r\n';
|
||||
}
|
||||
|
||||
media += 'c=IN IP4 0.0.0.0\r\n';
|
||||
if (!sctp.length)
|
||||
media += 'a=rtcp:1 IN IP4 0.0.0.0\r\n';
|
||||
tmp = content.find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]');
|
||||
if (tmp.length) {
|
||||
if (tmp.attr('ufrag')) {
|
||||
media += SDPUtil.build_iceufrag(tmp.attr('ufrag')) + '\r\n';
|
||||
}
|
||||
if (tmp.attr('pwd')) {
|
||||
media += SDPUtil.build_icepwd(tmp.attr('pwd')) + '\r\n';
|
||||
}
|
||||
tmp.find('>fingerprint').each(function () {
|
||||
// FIXME: check namespace at some point
|
||||
media += 'a=fingerprint:' + this.getAttribute('hash');
|
||||
media += ' ' + $(this).text();
|
||||
media += '\r\n';
|
||||
if (this.getAttribute('setup')) {
|
||||
media += 'a=setup:' + this.getAttribute('setup') + '\r\n';
|
||||
}
|
||||
});
|
||||
}
|
||||
switch (content.attr('senders')) {
|
||||
case 'initiator':
|
||||
media += 'a=sendonly\r\n';
|
||||
break;
|
||||
case 'responder':
|
||||
media += 'a=recvonly\r\n';
|
||||
break;
|
||||
case 'none':
|
||||
media += 'a=inactive\r\n';
|
||||
break;
|
||||
case 'both':
|
||||
media += 'a=sendrecv\r\n';
|
||||
break;
|
||||
}
|
||||
media += 'a=mid:' + content.attr('name') + '\r\n';
|
||||
|
||||
// <description><rtcp-mux/></description>
|
||||
// see http://code.google.com/p/libjingle/issues/detail?id=309 -- no spec though
|
||||
// and http://mail.jabber.org/pipermail/jingle/2011-December/001761.html
|
||||
if (desc.find('rtcp-mux').length) {
|
||||
media += 'a=rtcp-mux\r\n';
|
||||
}
|
||||
|
||||
if (desc.find('encryption').length) {
|
||||
desc.find('encryption>crypto').each(function () {
|
||||
media += 'a=crypto:' + this.getAttribute('tag');
|
||||
media += ' ' + this.getAttribute('crypto-suite');
|
||||
media += ' ' + this.getAttribute('key-params');
|
||||
if (this.getAttribute('session-params')) {
|
||||
media += ' ' + this.getAttribute('session-params');
|
||||
}
|
||||
media += '\r\n';
|
||||
});
|
||||
}
|
||||
desc.find('payload-type').each(function () {
|
||||
media += SDPUtil.build_rtpmap(this) + '\r\n';
|
||||
if ($(this).find('>parameter').length) {
|
||||
media += 'a=fmtp:' + this.getAttribute('id') + ' ';
|
||||
media += $(this).find('parameter').map(function () {
|
||||
return (this.getAttribute('name')
|
||||
? (this.getAttribute('name') + '=') : '') +
|
||||
this.getAttribute('value');
|
||||
}).get().join('; ');
|
||||
media += '\r\n';
|
||||
}
|
||||
// xep-0293
|
||||
media += self.rtcpFbFromJingle($(this), this.getAttribute('id'));
|
||||
});
|
||||
|
||||
// xep-0293
|
||||
media += self.rtcpFbFromJingle(desc, '*');
|
||||
|
||||
// xep-0294
|
||||
tmp = desc.find('>rtp-hdrext[xmlns="urn:xmpp:jingle:apps:rtp:rtp-hdrext:0"]');
|
||||
tmp.each(function () {
|
||||
media += 'a=extmap:' + this.getAttribute('id') + ' ' + this.getAttribute('uri') + '\r\n';
|
||||
});
|
||||
|
||||
content.find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]>candidate').each(function () {
|
||||
var protocol = this.getAttribute('protocol');
|
||||
protocol = (typeof protocol === 'string') ? protocol.toLowerCase(): '';
|
||||
|
||||
if ((self.removeTcpCandidates && protocol === 'tcp') ||
|
||||
(self.removeUdpCandidates && protocol === 'udp')) {
|
||||
return;
|
||||
}
|
||||
|
||||
media += SDPUtil.candidateFromJingle(this);
|
||||
});
|
||||
|
||||
// XEP-0339 handle ssrc-group attributes
|
||||
content.find('description>ssrc-group[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]').each(function() {
|
||||
var semantics = this.getAttribute('semantics');
|
||||
var ssrcs = $(this).find('>source').map(function() {
|
||||
return this.getAttribute('ssrc');
|
||||
}).get();
|
||||
|
||||
if (ssrcs.length) {
|
||||
media += 'a=ssrc-group:' + semantics + ' ' + ssrcs.join(' ') + '\r\n';
|
||||
}
|
||||
});
|
||||
|
||||
tmp = content.find('description>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
|
||||
tmp.each(function () {
|
||||
var ssrc = this.getAttribute('ssrc');
|
||||
$(this).find('>parameter').each(function () {
|
||||
var name = this.getAttribute('name');
|
||||
var value = this.getAttribute('value');
|
||||
value = SDPUtil.filter_special_chars(value);
|
||||
media += 'a=ssrc:' + ssrc + ' ' + name;
|
||||
if (value && value.length)
|
||||
media += ':' + value;
|
||||
media += '\r\n';
|
||||
});
|
||||
});
|
||||
|
||||
return media;
|
||||
};
|
||||
|
||||
|
||||
module.exports = SDP;
|
||||
|
|
@ -1,168 +0,0 @@
|
|||
var SDPUtil = require("./SDPUtil");
|
||||
|
||||
function SDPDiffer(mySDP, otherSDP)
|
||||
{
|
||||
this.mySDP = mySDP;
|
||||
this.otherSDP = otherSDP;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns map of MediaChannel that contains media contained in
|
||||
* 'mySDP', but not contained in 'otherSdp'. Mapped by channel idx.
|
||||
*/
|
||||
SDPDiffer.prototype.getNewMedia = function() {
|
||||
|
||||
// this could be useful in Array.prototype.
|
||||
function arrayEquals(array) {
|
||||
// if the other array is a falsy value, return
|
||||
if (!array)
|
||||
return false;
|
||||
|
||||
// compare lengths - can save a lot of time
|
||||
if (this.length != array.length)
|
||||
return false;
|
||||
|
||||
for (var i = 0, l=this.length; i < l; i++) {
|
||||
// Check if we have nested arrays
|
||||
if (this[i] instanceof Array && array[i] instanceof Array) {
|
||||
// recurse into the nested arrays
|
||||
if (!this[i].equals(array[i]))
|
||||
return false;
|
||||
}
|
||||
else if (this[i] != array[i]) {
|
||||
// Warning - two different object instances will never be
|
||||
// equal: {x:20} != {x:20}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
var myMedias = this.mySDP.getMediaSsrcMap();
|
||||
var othersMedias = this.otherSDP.getMediaSsrcMap();
|
||||
var newMedia = {};
|
||||
Object.keys(othersMedias).forEach(function(othersMediaIdx) {
|
||||
var myMedia = myMedias[othersMediaIdx];
|
||||
var othersMedia = othersMedias[othersMediaIdx];
|
||||
if(!myMedia && othersMedia) {
|
||||
// Add whole channel
|
||||
newMedia[othersMediaIdx] = othersMedia;
|
||||
return;
|
||||
}
|
||||
// Look for new ssrcs across the channel
|
||||
Object.keys(othersMedia.ssrcs).forEach(function(ssrc) {
|
||||
if(Object.keys(myMedia.ssrcs).indexOf(ssrc) === -1) {
|
||||
// Allocate channel if we've found ssrc that doesn't exist in
|
||||
// our channel
|
||||
if(!newMedia[othersMediaIdx]){
|
||||
newMedia[othersMediaIdx] = {
|
||||
mediaindex: othersMedia.mediaindex,
|
||||
mid: othersMedia.mid,
|
||||
ssrcs: {},
|
||||
ssrcGroups: []
|
||||
};
|
||||
}
|
||||
newMedia[othersMediaIdx].ssrcs[ssrc] = othersMedia.ssrcs[ssrc];
|
||||
}
|
||||
});
|
||||
|
||||
// Look for new ssrc groups across the channels
|
||||
othersMedia.ssrcGroups.forEach(function(otherSsrcGroup){
|
||||
|
||||
// try to match the other ssrc-group with an ssrc-group of ours
|
||||
var matched = false;
|
||||
for (var i = 0; i < myMedia.ssrcGroups.length; i++) {
|
||||
var mySsrcGroup = myMedia.ssrcGroups[i];
|
||||
if (otherSsrcGroup.semantics == mySsrcGroup.semantics &&
|
||||
arrayEquals.apply(otherSsrcGroup.ssrcs,
|
||||
[mySsrcGroup.ssrcs])) {
|
||||
|
||||
matched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!matched) {
|
||||
// Allocate channel if we've found an ssrc-group that doesn't
|
||||
// exist in our channel
|
||||
|
||||
if(!newMedia[othersMediaIdx]){
|
||||
newMedia[othersMediaIdx] = {
|
||||
mediaindex: othersMedia.mediaindex,
|
||||
mid: othersMedia.mid,
|
||||
ssrcs: {},
|
||||
ssrcGroups: []
|
||||
};
|
||||
}
|
||||
newMedia[othersMediaIdx].ssrcGroups.push(otherSsrcGroup);
|
||||
}
|
||||
});
|
||||
});
|
||||
return newMedia;
|
||||
};
|
||||
|
||||
/**
|
||||
* TODO: document!
|
||||
*/
|
||||
SDPDiffer.prototype.toJingle = function(modify) {
|
||||
var sdpMediaSsrcs = this.getNewMedia();
|
||||
|
||||
var modified = false;
|
||||
Object.keys(sdpMediaSsrcs).forEach(function(mediaindex){
|
||||
modified = true;
|
||||
var media = sdpMediaSsrcs[mediaindex];
|
||||
modify.c('content', {name: media.mid});
|
||||
|
||||
modify.c('description',
|
||||
{xmlns:'urn:xmpp:jingle:apps:rtp:1', media: media.mid});
|
||||
// FIXME: not completely sure this operates on blocks and / or handles
|
||||
// different ssrcs correctly
|
||||
// generate sources from lines
|
||||
Object.keys(media.ssrcs).forEach(function(ssrcNum) {
|
||||
var mediaSsrc = media.ssrcs[ssrcNum];
|
||||
modify.c('source', { xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
|
||||
modify.attrs({ssrc: mediaSsrc.ssrc});
|
||||
// iterate over ssrc lines
|
||||
mediaSsrc.lines.forEach(function (line) {
|
||||
var idx = line.indexOf(' ');
|
||||
var kv = line.substr(idx + 1);
|
||||
modify.c('parameter');
|
||||
if (kv.indexOf(':') == -1) {
|
||||
modify.attrs({ name: kv });
|
||||
} else {
|
||||
var nv = kv.split(':', 2);
|
||||
var name = nv[0];
|
||||
var value = SDPUtil.filter_special_chars(nv[1]);
|
||||
modify.attrs({ name: name });
|
||||
modify.attrs({ value: value });
|
||||
}
|
||||
modify.up(); // end of parameter
|
||||
});
|
||||
modify.up(); // end of source
|
||||
});
|
||||
|
||||
// generate source groups from lines
|
||||
media.ssrcGroups.forEach(function(ssrcGroup) {
|
||||
if (ssrcGroup.ssrcs.length) {
|
||||
|
||||
modify.c('ssrc-group', {
|
||||
semantics: ssrcGroup.semantics,
|
||||
xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0'
|
||||
});
|
||||
|
||||
ssrcGroup.ssrcs.forEach(function (ssrc) {
|
||||
modify.c('source', { ssrc: ssrc })
|
||||
.up(); // end of source
|
||||
});
|
||||
modify.up(); // end of ssrc-group
|
||||
}
|
||||
});
|
||||
|
||||
modify.up(); // end of description
|
||||
modify.up(); // end of content
|
||||
});
|
||||
|
||||
return modified;
|
||||
};
|
||||
|
||||
module.exports = SDPDiffer;
|
|
@ -1,361 +0,0 @@
|
|||
/* jshint -W101 */
|
||||
var RTCBrowserType = require('../RTC/RTCBrowserType');
|
||||
|
||||
var SDPUtil = {
|
||||
filter_special_chars: function (text) {
|
||||
return text.replace(/[\\\/\{,\}\+]/g, "");
|
||||
},
|
||||
iceparams: function (mediadesc, sessiondesc) {
|
||||
var data = null;
|
||||
if (SDPUtil.find_line(mediadesc, 'a=ice-ufrag:', sessiondesc) &&
|
||||
SDPUtil.find_line(mediadesc, 'a=ice-pwd:', sessiondesc)) {
|
||||
data = {
|
||||
ufrag: SDPUtil.parse_iceufrag(SDPUtil.find_line(mediadesc, 'a=ice-ufrag:', sessiondesc)),
|
||||
pwd: SDPUtil.parse_icepwd(SDPUtil.find_line(mediadesc, 'a=ice-pwd:', sessiondesc))
|
||||
};
|
||||
}
|
||||
return data;
|
||||
},
|
||||
parse_iceufrag: function (line) {
|
||||
return line.substring(12);
|
||||
},
|
||||
build_iceufrag: function (frag) {
|
||||
return 'a=ice-ufrag:' + frag;
|
||||
},
|
||||
parse_icepwd: function (line) {
|
||||
return line.substring(10);
|
||||
},
|
||||
build_icepwd: function (pwd) {
|
||||
return 'a=ice-pwd:' + pwd;
|
||||
},
|
||||
parse_mid: function (line) {
|
||||
return line.substring(6);
|
||||
},
|
||||
parse_mline: function (line) {
|
||||
var parts = line.substring(2).split(' '),
|
||||
data = {};
|
||||
data.media = parts.shift();
|
||||
data.port = parts.shift();
|
||||
data.proto = parts.shift();
|
||||
if (parts[parts.length - 1] === '') { // trailing whitespace
|
||||
parts.pop();
|
||||
}
|
||||
data.fmt = parts;
|
||||
return data;
|
||||
},
|
||||
build_mline: function (mline) {
|
||||
return 'm=' + mline.media + ' ' + mline.port + ' ' + mline.proto + ' ' + mline.fmt.join(' ');
|
||||
},
|
||||
parse_rtpmap: function (line) {
|
||||
var parts = line.substring(9).split(' '),
|
||||
data = {};
|
||||
data.id = parts.shift();
|
||||
parts = parts[0].split('/');
|
||||
data.name = parts.shift();
|
||||
data.clockrate = parts.shift();
|
||||
data.channels = parts.length ? parts.shift() : '1';
|
||||
return data;
|
||||
},
|
||||
/**
|
||||
* Parses SDP line "a=sctpmap:..." and extracts SCTP port from it.
|
||||
* @param line eg. "a=sctpmap:5000 webrtc-datachannel"
|
||||
* @returns [SCTP port number, protocol, streams]
|
||||
*/
|
||||
parse_sctpmap: function (line)
|
||||
{
|
||||
var parts = line.substring(10).split(' ');
|
||||
var sctpPort = parts[0];
|
||||
var protocol = parts[1];
|
||||
// Stream count is optional
|
||||
var streamCount = parts.length > 2 ? parts[2] : null;
|
||||
return [sctpPort, protocol, streamCount];// SCTP port
|
||||
},
|
||||
build_rtpmap: function (el) {
|
||||
var line = 'a=rtpmap:' + el.getAttribute('id') + ' ' + el.getAttribute('name') + '/' + el.getAttribute('clockrate');
|
||||
if (el.getAttribute('channels') && el.getAttribute('channels') != '1') {
|
||||
line += '/' + el.getAttribute('channels');
|
||||
}
|
||||
return line;
|
||||
},
|
||||
parse_crypto: function (line) {
|
||||
var parts = line.substring(9).split(' '),
|
||||
data = {};
|
||||
data.tag = parts.shift();
|
||||
data['crypto-suite'] = parts.shift();
|
||||
data['key-params'] = parts.shift();
|
||||
if (parts.length) {
|
||||
data['session-params'] = parts.join(' ');
|
||||
}
|
||||
return data;
|
||||
},
|
||||
parse_fingerprint: function (line) { // RFC 4572
|
||||
var parts = line.substring(14).split(' '),
|
||||
data = {};
|
||||
data.hash = parts.shift();
|
||||
data.fingerprint = parts.shift();
|
||||
// TODO assert that fingerprint satisfies 2UHEX *(":" 2UHEX) ?
|
||||
return data;
|
||||
},
|
||||
parse_fmtp: function (line) {
|
||||
var parts = line.split(' '),
|
||||
i, key, value,
|
||||
data = [];
|
||||
parts.shift();
|
||||
parts = parts.join(' ').split(';');
|
||||
for (i = 0; i < parts.length; i++) {
|
||||
key = parts[i].split('=')[0];
|
||||
while (key.length && key[0] == ' ') {
|
||||
key = key.substring(1);
|
||||
}
|
||||
value = parts[i].split('=')[1];
|
||||
if (key && value) {
|
||||
data.push({name: key, value: value});
|
||||
} else if (key) {
|
||||
// rfc 4733 (DTMF) style stuff
|
||||
data.push({name: '', value: key});
|
||||
}
|
||||
}
|
||||
return data;
|
||||
},
|
||||
parse_icecandidate: function (line) {
|
||||
var candidate = {},
|
||||
elems = line.split(' ');
|
||||
candidate.foundation = elems[0].substring(12);
|
||||
candidate.component = elems[1];
|
||||
candidate.protocol = elems[2].toLowerCase();
|
||||
candidate.priority = elems[3];
|
||||
candidate.ip = elems[4];
|
||||
candidate.port = elems[5];
|
||||
// elems[6] => "typ"
|
||||
candidate.type = elems[7];
|
||||
candidate.generation = 0; // default value, may be overwritten below
|
||||
for (var i = 8; i < elems.length; i += 2) {
|
||||
switch (elems[i]) {
|
||||
case 'raddr':
|
||||
candidate['rel-addr'] = elems[i + 1];
|
||||
break;
|
||||
case 'rport':
|
||||
candidate['rel-port'] = elems[i + 1];
|
||||
break;
|
||||
case 'generation':
|
||||
candidate.generation = elems[i + 1];
|
||||
break;
|
||||
case 'tcptype':
|
||||
candidate.tcptype = elems[i + 1];
|
||||
break;
|
||||
default: // TODO
|
||||
console.log('parse_icecandidate not translating "' + elems[i] + '" = "' + elems[i + 1] + '"');
|
||||
}
|
||||
}
|
||||
candidate.network = '1';
|
||||
candidate.id = Math.random().toString(36).substr(2, 10); // not applicable to SDP -- FIXME: should be unique, not just random
|
||||
return candidate;
|
||||
},
|
||||
build_icecandidate: function (cand) {
|
||||
var line = ['a=candidate:' + cand.foundation, cand.component, cand.protocol, cand.priority, cand.ip, cand.port, 'typ', cand.type].join(' ');
|
||||
line += ' ';
|
||||
switch (cand.type) {
|
||||
case 'srflx':
|
||||
case 'prflx':
|
||||
case 'relay':
|
||||
if (cand.hasOwnAttribute('rel-addr') && cand.hasOwnAttribute('rel-port')) {
|
||||
line += 'raddr';
|
||||
line += ' ';
|
||||
line += cand['rel-addr'];
|
||||
line += ' ';
|
||||
line += 'rport';
|
||||
line += ' ';
|
||||
line += cand['rel-port'];
|
||||
line += ' ';
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (cand.hasOwnAttribute('tcptype')) {
|
||||
line += 'tcptype';
|
||||
line += ' ';
|
||||
line += cand.tcptype;
|
||||
line += ' ';
|
||||
}
|
||||
line += 'generation';
|
||||
line += ' ';
|
||||
line += cand.hasOwnAttribute('generation') ? cand.generation : '0';
|
||||
return line;
|
||||
},
|
||||
parse_ssrc: function (desc) {
|
||||
// proprietary mapping of a=ssrc lines
|
||||
// TODO: see "Jingle RTP Source Description" by Juberti and P. Thatcher on google docs
|
||||
// and parse according to that
|
||||
var lines = desc.split('\r\n'),
|
||||
data = {};
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
if (lines[i].substring(0, 7) == 'a=ssrc:') {
|
||||
var idx = lines[i].indexOf(' ');
|
||||
data[lines[i].substr(idx + 1).split(':', 2)[0]] = lines[i].substr(idx + 1).split(':', 2)[1];
|
||||
}
|
||||
}
|
||||
return data;
|
||||
},
|
||||
parse_rtcpfb: function (line) {
|
||||
var parts = line.substr(10).split(' ');
|
||||
var data = {};
|
||||
data.pt = parts.shift();
|
||||
data.type = parts.shift();
|
||||
data.params = parts;
|
||||
return data;
|
||||
},
|
||||
parse_extmap: function (line) {
|
||||
var parts = line.substr(9).split(' ');
|
||||
var data = {};
|
||||
data.value = parts.shift();
|
||||
if (data.value.indexOf('/') != -1) {
|
||||
data.direction = data.value.substr(data.value.indexOf('/') + 1);
|
||||
data.value = data.value.substr(0, data.value.indexOf('/'));
|
||||
} else {
|
||||
data.direction = 'both';
|
||||
}
|
||||
data.uri = parts.shift();
|
||||
data.params = parts;
|
||||
return data;
|
||||
},
|
||||
find_line: function (haystack, needle, sessionpart) {
|
||||
var lines = haystack.split('\r\n');
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
if (lines[i].substring(0, needle.length) == needle) {
|
||||
return lines[i];
|
||||
}
|
||||
}
|
||||
if (!sessionpart) {
|
||||
return false;
|
||||
}
|
||||
// search session part
|
||||
lines = sessionpart.split('\r\n');
|
||||
for (var j = 0; j < lines.length; j++) {
|
||||
if (lines[j].substring(0, needle.length) == needle) {
|
||||
return lines[j];
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
find_lines: function (haystack, needle, sessionpart) {
|
||||
var lines = haystack.split('\r\n'),
|
||||
needles = [];
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
if (lines[i].substring(0, needle.length) == needle)
|
||||
needles.push(lines[i]);
|
||||
}
|
||||
if (needles.length || !sessionpart) {
|
||||
return needles;
|
||||
}
|
||||
// search session part
|
||||
lines = sessionpart.split('\r\n');
|
||||
for (var j = 0; j < lines.length; j++) {
|
||||
if (lines[j].substring(0, needle.length) == needle) {
|
||||
needles.push(lines[j]);
|
||||
}
|
||||
}
|
||||
return needles;
|
||||
},
|
||||
candidateToJingle: function (line) {
|
||||
// a=candidate:2979166662 1 udp 2113937151 192.168.2.100 57698 typ host generation 0
|
||||
// <candidate component=... foundation=... generation=... id=... ip=... network=... port=... priority=... protocol=... type=.../>
|
||||
if (line.indexOf('candidate:') === 0) {
|
||||
line = 'a=' + line;
|
||||
} else if (line.substring(0, 12) != 'a=candidate:') {
|
||||
console.log('parseCandidate called with a line that is not a candidate line');
|
||||
console.log(line);
|
||||
return null;
|
||||
}
|
||||
if (line.substring(line.length - 2) == '\r\n') // chomp it
|
||||
line = line.substring(0, line.length - 2);
|
||||
var candidate = {},
|
||||
elems = line.split(' '),
|
||||
i;
|
||||
if (elems[6] != 'typ') {
|
||||
console.log('did not find typ in the right place');
|
||||
console.log(line);
|
||||
return null;
|
||||
}
|
||||
candidate.foundation = elems[0].substring(12);
|
||||
candidate.component = elems[1];
|
||||
candidate.protocol = elems[2].toLowerCase();
|
||||
candidate.priority = elems[3];
|
||||
candidate.ip = elems[4];
|
||||
candidate.port = elems[5];
|
||||
// elems[6] => "typ"
|
||||
candidate.type = elems[7];
|
||||
|
||||
candidate.generation = '0'; // default, may be overwritten below
|
||||
for (i = 8; i < elems.length; i += 2) {
|
||||
switch (elems[i]) {
|
||||
case 'raddr':
|
||||
candidate['rel-addr'] = elems[i + 1];
|
||||
break;
|
||||
case 'rport':
|
||||
candidate['rel-port'] = elems[i + 1];
|
||||
break;
|
||||
case 'generation':
|
||||
candidate.generation = elems[i + 1];
|
||||
break;
|
||||
case 'tcptype':
|
||||
candidate.tcptype = elems[i + 1];
|
||||
break;
|
||||
default: // TODO
|
||||
console.log('not translating "' + elems[i] + '" = "' + elems[i + 1] + '"');
|
||||
}
|
||||
}
|
||||
candidate.network = '1';
|
||||
candidate.id = Math.random().toString(36).substr(2, 10); // not applicable to SDP -- FIXME: should be unique, not just random
|
||||
return candidate;
|
||||
},
|
||||
candidateFromJingle: function (cand) {
|
||||
var line = 'a=candidate:';
|
||||
line += cand.getAttribute('foundation');
|
||||
line += ' ';
|
||||
line += cand.getAttribute('component');
|
||||
line += ' ';
|
||||
|
||||
var protocol = cand.getAttribute('protocol');
|
||||
// use tcp candidates for FF
|
||||
if (RTCBrowserType.isFirefox() && protocol.toLowerCase() == 'ssltcp') {
|
||||
protocol = 'tcp';
|
||||
}
|
||||
|
||||
line += ' ';
|
||||
line += cand.getAttribute('priority');
|
||||
line += ' ';
|
||||
line += cand.getAttribute('ip');
|
||||
line += ' ';
|
||||
line += cand.getAttribute('port');
|
||||
line += ' ';
|
||||
line += 'typ';
|
||||
line += ' ' + cand.getAttribute('type');
|
||||
line += ' ';
|
||||
switch (cand.getAttribute('type')) {
|
||||
case 'srflx':
|
||||
case 'prflx':
|
||||
case 'relay':
|
||||
if (cand.getAttribute('rel-addr') && cand.getAttribute('rel-port')) {
|
||||
line += 'raddr';
|
||||
line += ' ';
|
||||
line += cand.getAttribute('rel-addr');
|
||||
line += ' ';
|
||||
line += 'rport';
|
||||
line += ' ';
|
||||
line += cand.getAttribute('rel-port');
|
||||
line += ' ';
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (protocol.toLowerCase() == 'tcp') {
|
||||
line += 'tcptype';
|
||||
line += ' ';
|
||||
line += cand.getAttribute('tcptype');
|
||||
line += ' ';
|
||||
}
|
||||
line += 'generation';
|
||||
line += ' ';
|
||||
line += cand.getAttribute('generation') || '0';
|
||||
return line + '\r\n';
|
||||
}
|
||||
};
|
||||
module.exports = SDPUtil;
|
|
@ -1,449 +0,0 @@
|
|||
/* global $, config, mozRTCPeerConnection, RTCPeerConnection,
|
||||
webkitRTCPeerConnection, RTCSessionDescription */
|
||||
/* jshint -W101 */
|
||||
var RTC = require('../RTC/RTC');
|
||||
var RTCBrowserType = require("../RTC/RTCBrowserType");
|
||||
var RTCEvents = require("../../service/RTC/RTCEvents");
|
||||
var SSRCReplacement = require("./LocalSSRCReplacement");
|
||||
|
||||
function TraceablePeerConnection(ice_config, constraints, session) {
|
||||
var self = this;
|
||||
var RTCPeerConnectionType = null;
|
||||
if (RTCBrowserType.isFirefox()) {
|
||||
RTCPeerConnectionType = mozRTCPeerConnection;
|
||||
} else if (RTCBrowserType.isTemasysPluginUsed()) {
|
||||
RTCPeerConnectionType = RTCPeerConnection;
|
||||
} else {
|
||||
RTCPeerConnectionType = webkitRTCPeerConnection;
|
||||
}
|
||||
self.eventEmitter = session.eventEmitter;
|
||||
this.peerconnection = new RTCPeerConnectionType(ice_config, constraints);
|
||||
this.updateLog = [];
|
||||
this.stats = {};
|
||||
this.statsinterval = null;
|
||||
this.maxstats = 0; // limit to 300 values, i.e. 5 minutes; set to 0 to disable
|
||||
var Interop = require('sdp-interop').Interop;
|
||||
this.interop = new Interop();
|
||||
var Simulcast = require('sdp-simulcast');
|
||||
this.simulcast = new Simulcast({numOfLayers: 3, explodeRemoteSimulcast: false});
|
||||
|
||||
// override as desired
|
||||
this.trace = function (what, info) {
|
||||
/*console.warn('WTRACE', what, info);
|
||||
if (info && RTCBrowserType.isIExplorer()) {
|
||||
if (info.length > 1024) {
|
||||
console.warn('WTRACE', what, info.substr(1024));
|
||||
}
|
||||
if (info.length > 2048) {
|
||||
console.warn('WTRACE', what, info.substr(2048));
|
||||
}
|
||||
}*/
|
||||
self.updateLog.push({
|
||||
time: new Date(),
|
||||
type: what,
|
||||
value: info || ""
|
||||
});
|
||||
};
|
||||
this.onicecandidate = null;
|
||||
this.peerconnection.onicecandidate = function (event) {
|
||||
// FIXME: this causes stack overflow with Temasys Plugin
|
||||
if (!RTCBrowserType.isTemasysPluginUsed())
|
||||
self.trace('onicecandidate', JSON.stringify(event.candidate, null, ' '));
|
||||
if (self.onicecandidate !== null) {
|
||||
self.onicecandidate(event);
|
||||
}
|
||||
};
|
||||
this.onaddstream = null;
|
||||
this.peerconnection.onaddstream = function (event) {
|
||||
self.trace('onaddstream', event.stream.id);
|
||||
if (self.onaddstream !== null) {
|
||||
self.onaddstream(event);
|
||||
}
|
||||
};
|
||||
this.onremovestream = null;
|
||||
this.peerconnection.onremovestream = function (event) {
|
||||
self.trace('onremovestream', event.stream.id);
|
||||
if (self.onremovestream !== null) {
|
||||
self.onremovestream(event);
|
||||
}
|
||||
};
|
||||
this.onsignalingstatechange = null;
|
||||
this.peerconnection.onsignalingstatechange = function (event) {
|
||||
self.trace('onsignalingstatechange', self.signalingState);
|
||||
if (self.onsignalingstatechange !== null) {
|
||||
self.onsignalingstatechange(event);
|
||||
}
|
||||
};
|
||||
this.oniceconnectionstatechange = null;
|
||||
this.peerconnection.oniceconnectionstatechange = function (event) {
|
||||
self.trace('oniceconnectionstatechange', self.iceConnectionState);
|
||||
if (self.oniceconnectionstatechange !== null) {
|
||||
self.oniceconnectionstatechange(event);
|
||||
}
|
||||
};
|
||||
this.onnegotiationneeded = null;
|
||||
this.peerconnection.onnegotiationneeded = function (event) {
|
||||
self.trace('onnegotiationneeded');
|
||||
if (self.onnegotiationneeded !== null) {
|
||||
self.onnegotiationneeded(event);
|
||||
}
|
||||
};
|
||||
self.ondatachannel = null;
|
||||
this.peerconnection.ondatachannel = function (event) {
|
||||
self.trace('ondatachannel', event);
|
||||
if (self.ondatachannel !== null) {
|
||||
self.ondatachannel(event);
|
||||
}
|
||||
};
|
||||
// XXX: do all non-firefox browsers which we support also support this?
|
||||
if (!RTCBrowserType.isFirefox() && this.maxstats) {
|
||||
this.statsinterval = window.setInterval(function() {
|
||||
self.peerconnection.getStats(function(stats) {
|
||||
var results = stats.result();
|
||||
var now = new Date();
|
||||
for (var i = 0; i < results.length; ++i) {
|
||||
results[i].names().forEach(function (name) {
|
||||
var id = results[i].id + '-' + name;
|
||||
if (!self.stats[id]) {
|
||||
self.stats[id] = {
|
||||
startTime: now,
|
||||
endTime: now,
|
||||
values: [],
|
||||
times: []
|
||||
};
|
||||
}
|
||||
self.stats[id].values.push(results[i].stat(name));
|
||||
self.stats[id].times.push(now.getTime());
|
||||
if (self.stats[id].values.length > self.maxstats) {
|
||||
self.stats[id].values.shift();
|
||||
self.stats[id].times.shift();
|
||||
}
|
||||
self.stats[id].endTime = now;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of a SessionDescription object.
|
||||
*/
|
||||
var dumpSDP = function(description) {
|
||||
if (typeof description === 'undefined' || description === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return 'type: ' + description.type + '\r\n' + description.sdp;
|
||||
};
|
||||
|
||||
/**
|
||||
* Takes a SessionDescription object and returns a "normalized" version.
|
||||
* Currently it only takes care of ordering the a=ssrc lines.
|
||||
*/
|
||||
var normalizePlanB = function(desc) {
|
||||
if (typeof desc !== 'object' || desc === null ||
|
||||
typeof desc.sdp !== 'string') {
|
||||
console.warn('An empty description was passed as an argument.');
|
||||
return desc;
|
||||
}
|
||||
|
||||
var transform = require('sdp-transform');
|
||||
var session = transform.parse(desc.sdp);
|
||||
|
||||
if (typeof session !== 'undefined' && typeof session.media !== 'undefined' &&
|
||||
Array.isArray(session.media)) {
|
||||
session.media.forEach(function (mLine) {
|
||||
|
||||
// Chrome appears to be picky about the order in which a=ssrc lines
|
||||
// are listed in an m-line when rtx is enabled (and thus there are
|
||||
// a=ssrc-group lines with FID semantics). Specifically if we have
|
||||
// "a=ssrc-group:FID S1 S2" and the "a=ssrc:S2" lines appear before
|
||||
// the "a=ssrc:S1" lines, SRD fails.
|
||||
// So, put SSRC which appear as the first SSRC in an FID ssrc-group
|
||||
// first.
|
||||
var firstSsrcs = [];
|
||||
var newSsrcLines = [];
|
||||
|
||||
if (typeof mLine.ssrcGroups !== 'undefined' && Array.isArray(mLine.ssrcGroups)) {
|
||||
mLine.ssrcGroups.forEach(function (group) {
|
||||
if (typeof group.semantics !== 'undefined' &&
|
||||
group.semantics === 'FID') {
|
||||
if (typeof group.ssrcs !== 'undefined') {
|
||||
firstSsrcs.push(Number(group.ssrcs.split(' ')[0]));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof mLine.ssrcs !== 'undefined' && Array.isArray(mLine.ssrcs)) {
|
||||
var i;
|
||||
for (i = 0; i<mLine.ssrcs.length; i++){
|
||||
if (typeof mLine.ssrcs[i] === 'object'
|
||||
&& typeof mLine.ssrcs[i].id !== 'undefined'
|
||||
&& !$.inArray(mLine.ssrcs[i].id, firstSsrcs)) {
|
||||
newSsrcLines.push(mLine.ssrcs[i]);
|
||||
delete mLine.ssrcs[i];
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 0; i<mLine.ssrcs.length; i++){
|
||||
if (typeof mLine.ssrcs[i] !== 'undefined') {
|
||||
newSsrcLines.push(mLine.ssrcs[i]);
|
||||
}
|
||||
}
|
||||
|
||||
mLine.ssrcs = newSsrcLines;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var resStr = transform.write(session);
|
||||
return new RTCSessionDescription({
|
||||
type: desc.type,
|
||||
sdp: resStr
|
||||
});
|
||||
};
|
||||
|
||||
if (TraceablePeerConnection.prototype.__defineGetter__ !== undefined) {
|
||||
TraceablePeerConnection.prototype.__defineGetter__(
|
||||
'signalingState',
|
||||
function() { return this.peerconnection.signalingState; });
|
||||
TraceablePeerConnection.prototype.__defineGetter__(
|
||||
'iceConnectionState',
|
||||
function() { return this.peerconnection.iceConnectionState; });
|
||||
TraceablePeerConnection.prototype.__defineGetter__(
|
||||
'localDescription',
|
||||
function() {
|
||||
var desc = this.peerconnection.localDescription;
|
||||
|
||||
// FIXME this should probably be after the Unified Plan -> Plan B
|
||||
// transformation.
|
||||
desc = SSRCReplacement.mungeLocalVideoSSRC(desc);
|
||||
|
||||
this.trace('getLocalDescription::preTransform', dumpSDP(desc));
|
||||
|
||||
// if we're running on FF, transform to Plan B first.
|
||||
if (RTCBrowserType.usesUnifiedPlan()) {
|
||||
desc = this.interop.toPlanB(desc);
|
||||
this.trace('getLocalDescription::postTransform (Plan B)', dumpSDP(desc));
|
||||
}
|
||||
return desc;
|
||||
});
|
||||
TraceablePeerConnection.prototype.__defineGetter__(
|
||||
'remoteDescription',
|
||||
function() {
|
||||
var desc = this.peerconnection.remoteDescription;
|
||||
this.trace('getRemoteDescription::preTransform', dumpSDP(desc));
|
||||
|
||||
// if we're running on FF, transform to Plan B first.
|
||||
if (RTCBrowserType.usesUnifiedPlan()) {
|
||||
desc = this.interop.toPlanB(desc);
|
||||
this.trace('getRemoteDescription::postTransform (Plan B)', dumpSDP(desc));
|
||||
}
|
||||
return desc;
|
||||
});
|
||||
}
|
||||
|
||||
TraceablePeerConnection.prototype.addStream = function (stream) {
|
||||
this.trace('addStream', stream.id);
|
||||
try
|
||||
{
|
||||
this.peerconnection.addStream(stream);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
TraceablePeerConnection.prototype.removeStream = function (stream, stopStreams) {
|
||||
this.trace('removeStream', stream.id);
|
||||
if(stopStreams) {
|
||||
RTC.stopMediaStream(stream);
|
||||
}
|
||||
|
||||
try {
|
||||
// FF doesn't support this yet.
|
||||
if (this.peerconnection.removeStream)
|
||||
this.peerconnection.removeStream(stream);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
TraceablePeerConnection.prototype.createDataChannel = function (label, opts) {
|
||||
this.trace('createDataChannel', label, opts);
|
||||
return this.peerconnection.createDataChannel(label, opts);
|
||||
};
|
||||
|
||||
TraceablePeerConnection.prototype.setLocalDescription
|
||||
= function (description, successCallback, failureCallback) {
|
||||
this.trace('setLocalDescription::preTransform', dumpSDP(description));
|
||||
// if we're running on FF, transform to Plan A first.
|
||||
if (RTCBrowserType.usesUnifiedPlan()) {
|
||||
description = this.interop.toUnifiedPlan(description);
|
||||
this.trace('setLocalDescription::postTransform (Plan A)', dumpSDP(description));
|
||||
}
|
||||
|
||||
var self = this;
|
||||
this.peerconnection.setLocalDescription(description,
|
||||
function () {
|
||||
self.trace('setLocalDescriptionOnSuccess');
|
||||
successCallback();
|
||||
},
|
||||
function (err) {
|
||||
self.trace('setLocalDescriptionOnFailure', err);
|
||||
self.eventEmitter.emit(RTCEvents.SET_LOCAL_DESCRIPTION_FAILED, err, self.peerconnection);
|
||||
failureCallback(err);
|
||||
}
|
||||
);
|
||||
/*
|
||||
if (this.statsinterval === null && this.maxstats > 0) {
|
||||
// start gathering stats
|
||||
}
|
||||
*/
|
||||
};
|
||||
|
||||
TraceablePeerConnection.prototype.setRemoteDescription
|
||||
= function (description, successCallback, failureCallback) {
|
||||
this.trace('setRemoteDescription::preTransform', dumpSDP(description));
|
||||
// TODO the focus should squeze or explode the remote simulcast
|
||||
description = this.simulcast.mungeRemoteDescription(description);
|
||||
this.trace('setRemoteDescription::postTransform (simulcast)', dumpSDP(description));
|
||||
|
||||
// if we're running on FF, transform to Plan A first.
|
||||
if (RTCBrowserType.usesUnifiedPlan()) {
|
||||
description = this.interop.toUnifiedPlan(description);
|
||||
this.trace('setRemoteDescription::postTransform (Plan A)', dumpSDP(description));
|
||||
}
|
||||
|
||||
if (RTCBrowserType.usesPlanB()) {
|
||||
description = normalizePlanB(description);
|
||||
}
|
||||
|
||||
var self = this;
|
||||
this.peerconnection.setRemoteDescription(description,
|
||||
function () {
|
||||
self.trace('setRemoteDescriptionOnSuccess');
|
||||
successCallback();
|
||||
},
|
||||
function (err) {
|
||||
self.trace('setRemoteDescriptionOnFailure', err);
|
||||
self.eventEmitter.emit(RTCEvents.SET_REMOTE_DESCRIPTION_FAILED, err, self.peerconnection);
|
||||
failureCallback(err);
|
||||
}
|
||||
);
|
||||
/*
|
||||
if (this.statsinterval === null && this.maxstats > 0) {
|
||||
// start gathering stats
|
||||
}
|
||||
*/
|
||||
};
|
||||
|
||||
TraceablePeerConnection.prototype.close = function () {
|
||||
this.trace('stop');
|
||||
if (this.statsinterval !== null) {
|
||||
window.clearInterval(this.statsinterval);
|
||||
this.statsinterval = null;
|
||||
}
|
||||
this.peerconnection.close();
|
||||
};
|
||||
|
||||
TraceablePeerConnection.prototype.createOffer
|
||||
= function (successCallback, failureCallback, constraints) {
|
||||
var self = this;
|
||||
this.trace('createOffer', JSON.stringify(constraints, null, ' '));
|
||||
this.peerconnection.createOffer(
|
||||
function (offer) {
|
||||
self.trace('createOfferOnSuccess::preTransform', dumpSDP(offer));
|
||||
// NOTE this is not tested because in meet the focus generates the
|
||||
// offer.
|
||||
|
||||
// if we're running on FF, transform to Plan B first.
|
||||
if (RTCBrowserType.usesUnifiedPlan()) {
|
||||
offer = self.interop.toPlanB(offer);
|
||||
self.trace('createOfferOnSuccess::postTransform (Plan B)', dumpSDP(offer));
|
||||
}
|
||||
|
||||
offer = SSRCReplacement.mungeLocalVideoSSRC(offer);
|
||||
|
||||
if (config.enableSimulcast && self.simulcast.isSupported()) {
|
||||
offer = self.simulcast.mungeLocalDescription(offer);
|
||||
self.trace('createOfferOnSuccess::postTransform (simulcast)', dumpSDP(offer));
|
||||
}
|
||||
successCallback(offer);
|
||||
},
|
||||
function(err) {
|
||||
self.trace('createOfferOnFailure', err);
|
||||
self.eventEmitter.emit(RTCEvents.CREATE_OFFER_FAILED, err, self.peerconnection);
|
||||
failureCallback(err);
|
||||
},
|
||||
constraints
|
||||
);
|
||||
};
|
||||
|
||||
TraceablePeerConnection.prototype.createAnswer
|
||||
= function (successCallback, failureCallback, constraints) {
|
||||
var self = this;
|
||||
this.trace('createAnswer', JSON.stringify(constraints, null, ' '));
|
||||
this.peerconnection.createAnswer(
|
||||
function (answer) {
|
||||
self.trace('createAnswerOnSuccess::preTransform', dumpSDP(answer));
|
||||
// if we're running on FF, transform to Plan A first.
|
||||
if (RTCBrowserType.usesUnifiedPlan()) {
|
||||
answer = self.interop.toPlanB(answer);
|
||||
self.trace('createAnswerOnSuccess::postTransform (Plan B)', dumpSDP(answer));
|
||||
}
|
||||
|
||||
// munge local video SSRC
|
||||
answer = SSRCReplacement.mungeLocalVideoSSRC(answer);
|
||||
|
||||
if (config.enableSimulcast && self.simulcast.isSupported()) {
|
||||
answer = self.simulcast.mungeLocalDescription(answer);
|
||||
self.trace('createAnswerOnSuccess::postTransform (simulcast)', dumpSDP(answer));
|
||||
}
|
||||
successCallback(answer);
|
||||
},
|
||||
function(err) {
|
||||
self.trace('createAnswerOnFailure', err);
|
||||
self.eventEmitter.emit(RTCEvents.CREATE_ANSWER_FAILED, err, self.peerconnection);
|
||||
failureCallback(err);
|
||||
},
|
||||
constraints
|
||||
);
|
||||
};
|
||||
|
||||
TraceablePeerConnection.prototype.addIceCandidate
|
||||
= function (candidate, successCallback, failureCallback) {
|
||||
//var self = this;
|
||||
this.trace('addIceCandidate', JSON.stringify(candidate, null, ' '));
|
||||
this.peerconnection.addIceCandidate(candidate);
|
||||
/* maybe later
|
||||
this.peerconnection.addIceCandidate(candidate,
|
||||
function () {
|
||||
self.trace('addIceCandidateOnSuccess');
|
||||
successCallback();
|
||||
},
|
||||
function (err) {
|
||||
self.trace('addIceCandidateOnFailure', err);
|
||||
failureCallback(err);
|
||||
}
|
||||
);
|
||||
*/
|
||||
};
|
||||
|
||||
TraceablePeerConnection.prototype.getStats = function(callback, errback) {
|
||||
// TODO: Is this the correct way to handle Opera, Temasys?
|
||||
if (RTCBrowserType.isFirefox()) {
|
||||
// ignore for now...
|
||||
if(!errback)
|
||||
errback = function () {};
|
||||
this.peerconnection.getStats(null, callback, errback);
|
||||
} else {
|
||||
this.peerconnection.getStats(callback);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = TraceablePeerConnection;
|
|
@ -1,434 +0,0 @@
|
|||
/* global $, $iq, APP, config, messageHandler,
|
||||
roomName, sessionTerminated, Strophe, Util */
|
||||
var XMPPEvents = require("../../service/xmpp/XMPPEvents");
|
||||
var Settings = require("../settings/Settings");
|
||||
|
||||
var AuthenticationEvents
|
||||
= require("../../service/authentication/AuthenticationEvents");
|
||||
|
||||
/**
|
||||
* Contains logic responsible for enabling/disabling functionality available
|
||||
* only to moderator users.
|
||||
*/
|
||||
var connection = null;
|
||||
var focusUserJid;
|
||||
|
||||
function createExpBackoffTimer(step) {
|
||||
var count = 1;
|
||||
return function (reset) {
|
||||
// Reset call
|
||||
if (reset) {
|
||||
count = 1;
|
||||
return;
|
||||
}
|
||||
// Calculate next timeout
|
||||
var timeout = Math.pow(2, count - 1);
|
||||
count += 1;
|
||||
return timeout * step;
|
||||
};
|
||||
}
|
||||
|
||||
var getNextTimeout = createExpBackoffTimer(1000);
|
||||
var getNextErrorTimeout = createExpBackoffTimer(1000);
|
||||
// External authentication stuff
|
||||
var externalAuthEnabled = false;
|
||||
// Sip gateway can be enabled by configuring Jigasi host in config.js or
|
||||
// it will be enabled automatically if focus detects the component through
|
||||
// service discovery.
|
||||
var sipGatewayEnabled;
|
||||
|
||||
var eventEmitter = null;
|
||||
|
||||
var Moderator = {
|
||||
isModerator: function () {
|
||||
return connection && connection.emuc.isModerator();
|
||||
},
|
||||
|
||||
isPeerModerator: function (peerJid) {
|
||||
return connection &&
|
||||
connection.emuc.getMemberRole(peerJid) === 'moderator';
|
||||
},
|
||||
|
||||
isExternalAuthEnabled: function () {
|
||||
return externalAuthEnabled;
|
||||
},
|
||||
|
||||
isSipGatewayEnabled: function () {
|
||||
return sipGatewayEnabled;
|
||||
},
|
||||
|
||||
setConnection: function (con) {
|
||||
connection = con;
|
||||
},
|
||||
|
||||
init: function (xmpp, emitter) {
|
||||
this.xmppService = xmpp;
|
||||
eventEmitter = emitter;
|
||||
|
||||
sipGatewayEnabled =
|
||||
config.hosts && config.hosts.call_control !== undefined;
|
||||
|
||||
// Message listener that talks to POPUP window
|
||||
function listener(event) {
|
||||
if (event.data && event.data.sessionId) {
|
||||
if (event.origin !== window.location.origin) {
|
||||
console.warn("Ignoring sessionId from different origin: " +
|
||||
event.origin);
|
||||
return;
|
||||
}
|
||||
localStorage.setItem('sessionId', event.data.sessionId);
|
||||
// After popup is closed we will authenticate
|
||||
}
|
||||
}
|
||||
// Register
|
||||
if (window.addEventListener) {
|
||||
window.addEventListener("message", listener, false);
|
||||
} else {
|
||||
window.attachEvent("onmessage", listener);
|
||||
}
|
||||
},
|
||||
|
||||
onMucMemberLeft: function (jid) {
|
||||
console.info("Someone left is it focus ? " + jid);
|
||||
var resource = Strophe.getResourceFromJid(jid);
|
||||
if (resource === 'focus' && !this.xmppService.sessionTerminated) {
|
||||
console.info(
|
||||
"Focus has left the room - leaving conference");
|
||||
//hangUp();
|
||||
// We'd rather reload to have everything re-initialized
|
||||
// FIXME: show some message before reload
|
||||
location.reload();
|
||||
}
|
||||
},
|
||||
|
||||
setFocusUserJid: function (focusJid) {
|
||||
if (!focusUserJid) {
|
||||
focusUserJid = focusJid;
|
||||
console.info("Focus jid set to: " + focusUserJid);
|
||||
}
|
||||
},
|
||||
|
||||
getFocusUserJid: function () {
|
||||
return focusUserJid;
|
||||
},
|
||||
|
||||
getFocusComponent: function () {
|
||||
// Get focus component address
|
||||
var focusComponent = config.hosts.focus;
|
||||
// If not specified use default: 'focus.domain'
|
||||
if (!focusComponent) {
|
||||
focusComponent = 'focus.' + config.hosts.domain;
|
||||
}
|
||||
return focusComponent;
|
||||
},
|
||||
|
||||
createConferenceIq: function (roomName) {
|
||||
// Generate create conference IQ
|
||||
var elem = $iq({to: Moderator.getFocusComponent(), type: 'set'});
|
||||
|
||||
// Session Id used for authentication
|
||||
var sessionId = localStorage.getItem('sessionId');
|
||||
var machineUID = Settings.getSettings().uid;
|
||||
|
||||
console.info(
|
||||
"Session ID: " + sessionId + " machine UID: " + machineUID);
|
||||
|
||||
elem.c('conference', {
|
||||
xmlns: 'http://jitsi.org/protocol/focus',
|
||||
room: roomName,
|
||||
'machine-uid': machineUID
|
||||
});
|
||||
|
||||
if (sessionId) {
|
||||
elem.attrs({ 'session-id': sessionId});
|
||||
}
|
||||
|
||||
if (config.hosts.bridge !== undefined) {
|
||||
elem.c(
|
||||
'property',
|
||||
{ name: 'bridge', value: config.hosts.bridge})
|
||||
.up();
|
||||
}
|
||||
// Tell the focus we have Jigasi configured
|
||||
if (config.hosts.call_control !== undefined) {
|
||||
elem.c(
|
||||
'property',
|
||||
{ name: 'call_control', value: config.hosts.call_control})
|
||||
.up();
|
||||
}
|
||||
if (config.channelLastN !== undefined) {
|
||||
elem.c(
|
||||
'property',
|
||||
{ name: 'channelLastN', value: config.channelLastN})
|
||||
.up();
|
||||
}
|
||||
if (config.adaptiveLastN !== undefined) {
|
||||
elem.c(
|
||||
'property',
|
||||
{ name: 'adaptiveLastN', value: config.adaptiveLastN})
|
||||
.up();
|
||||
}
|
||||
if (config.adaptiveSimulcast !== undefined) {
|
||||
elem.c(
|
||||
'property',
|
||||
{ name: 'adaptiveSimulcast', value: config.adaptiveSimulcast})
|
||||
.up();
|
||||
}
|
||||
if (config.openSctp !== undefined) {
|
||||
elem.c(
|
||||
'property',
|
||||
{ name: 'openSctp', value: config.openSctp})
|
||||
.up();
|
||||
}
|
||||
if(config.startAudioMuted !== undefined)
|
||||
{
|
||||
elem.c(
|
||||
'property',
|
||||
{ name: 'startAudioMuted', value: config.startAudioMuted})
|
||||
.up();
|
||||
}
|
||||
if(config.startVideoMuted !== undefined)
|
||||
{
|
||||
elem.c(
|
||||
'property',
|
||||
{ name: 'startVideoMuted', value: config.startVideoMuted})
|
||||
.up();
|
||||
}
|
||||
elem.c(
|
||||
'property',
|
||||
{ name: 'simulcastMode', value: 'rewriting'})
|
||||
.up();
|
||||
elem.up();
|
||||
return elem;
|
||||
},
|
||||
|
||||
parseSessionId: function (resultIq) {
|
||||
var sessionId = $(resultIq).find('conference').attr('session-id');
|
||||
if (sessionId) {
|
||||
console.info('Received sessionId: ' + sessionId);
|
||||
localStorage.setItem('sessionId', sessionId);
|
||||
}
|
||||
},
|
||||
|
||||
parseConfigOptions: function (resultIq) {
|
||||
|
||||
Moderator.setFocusUserJid(
|
||||
$(resultIq).find('conference').attr('focusjid'));
|
||||
|
||||
var authenticationEnabled
|
||||
= $(resultIq).find(
|
||||
'>conference>property' +
|
||||
'[name=\'authentication\'][value=\'true\']').length > 0;
|
||||
|
||||
console.info("Authentication enabled: " + authenticationEnabled);
|
||||
|
||||
externalAuthEnabled = $(resultIq).find(
|
||||
'>conference>property' +
|
||||
'[name=\'externalAuth\'][value=\'true\']').length > 0;
|
||||
|
||||
console.info('External authentication enabled: ' + externalAuthEnabled);
|
||||
|
||||
if (!externalAuthEnabled) {
|
||||
// We expect to receive sessionId in 'internal' authentication mode
|
||||
Moderator.parseSessionId(resultIq);
|
||||
}
|
||||
|
||||
var authIdentity = $(resultIq).find('>conference').attr('identity');
|
||||
|
||||
eventEmitter.emit(AuthenticationEvents.IDENTITY_UPDATED,
|
||||
authenticationEnabled, authIdentity);
|
||||
|
||||
// Check if focus has auto-detected Jigasi component(this will be also
|
||||
// included if we have passed our host from the config)
|
||||
if ($(resultIq).find(
|
||||
'>conference>property' +
|
||||
'[name=\'sipGatewayEnabled\'][value=\'true\']').length) {
|
||||
sipGatewayEnabled = true;
|
||||
}
|
||||
|
||||
console.info("Sip gateway enabled: " + sipGatewayEnabled);
|
||||
},
|
||||
|
||||
// FIXME: we need to show the fact that we're waiting for the focus
|
||||
// to the user(or that focus is not available)
|
||||
allocateConferenceFocus: function (roomName, callback) {
|
||||
// Try to use focus user JID from the config
|
||||
Moderator.setFocusUserJid(config.focusUserJid);
|
||||
// Send create conference IQ
|
||||
var iq = Moderator.createConferenceIq(roomName);
|
||||
var self = this;
|
||||
connection.sendIQ(
|
||||
iq,
|
||||
function (result) {
|
||||
|
||||
// Setup config options
|
||||
Moderator.parseConfigOptions(result);
|
||||
|
||||
if ('true' === $(result).find('conference').attr('ready')) {
|
||||
// Reset both timers
|
||||
getNextTimeout(true);
|
||||
getNextErrorTimeout(true);
|
||||
// Exec callback
|
||||
callback();
|
||||
} else {
|
||||
var waitMs = getNextTimeout();
|
||||
console.info("Waiting for the focus... " + waitMs);
|
||||
// Reset error timeout
|
||||
getNextErrorTimeout(true);
|
||||
window.setTimeout(
|
||||
function () {
|
||||
Moderator.allocateConferenceFocus(
|
||||
roomName, callback);
|
||||
}, waitMs);
|
||||
}
|
||||
},
|
||||
function (error) {
|
||||
// Invalid session ? remove and try again
|
||||
// without session ID to get a new one
|
||||
var invalidSession
|
||||
= $(error).find('>error>session-invalid').length;
|
||||
if (invalidSession) {
|
||||
console.info("Session expired! - removing");
|
||||
localStorage.removeItem("sessionId");
|
||||
}
|
||||
if ($(error).find('>error>graceful-shutdown').length) {
|
||||
eventEmitter.emit(XMPPEvents.GRACEFUL_SHUTDOWN);
|
||||
return;
|
||||
}
|
||||
// Check for error returned by the reservation system
|
||||
var reservationErr = $(error).find('>error>reservation-error');
|
||||
if (reservationErr.length) {
|
||||
// Trigger error event
|
||||
var errorCode = reservationErr.attr('error-code');
|
||||
var errorMsg;
|
||||
if ($(error).find('>error>text')) {
|
||||
errorMsg = $(error).find('>error>text').text();
|
||||
}
|
||||
eventEmitter.emit(
|
||||
XMPPEvents.RESERVATION_ERROR, errorCode, errorMsg);
|
||||
return;
|
||||
}
|
||||
// Not authorized to create new room
|
||||
if ($(error).find('>error>not-authorized').length) {
|
||||
console.warn("Unauthorized to start the conference", error);
|
||||
var toDomain
|
||||
= Strophe.getDomainFromJid(error.getAttribute('to'));
|
||||
if (toDomain !== config.hosts.anonymousdomain) {
|
||||
// FIXME: "is external" should come either from
|
||||
// the focus or config.js
|
||||
externalAuthEnabled = true;
|
||||
}
|
||||
eventEmitter.emit(
|
||||
XMPPEvents.AUTHENTICATION_REQUIRED,
|
||||
function () {
|
||||
Moderator.allocateConferenceFocus(
|
||||
roomName, callback);
|
||||
});
|
||||
return;
|
||||
}
|
||||
var waitMs = getNextErrorTimeout();
|
||||
console.error("Focus error, retry after " + waitMs, error);
|
||||
// Show message
|
||||
var focusComponent = Moderator.getFocusComponent();
|
||||
var retrySec = waitMs / 1000;
|
||||
// FIXME: message is duplicated ?
|
||||
// Do not show in case of session invalid
|
||||
// which means just a retry
|
||||
if (!invalidSession) {
|
||||
eventEmitter.emit(XMPPEvents.FOCUS_DISCONNECTED,
|
||||
focusComponent, retrySec);
|
||||
}
|
||||
// Reset response timeout
|
||||
getNextTimeout(true);
|
||||
window.setTimeout(
|
||||
function () {
|
||||
Moderator.allocateConferenceFocus(roomName, callback);
|
||||
}, waitMs);
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
getLoginUrl: function (roomName, urlCallback) {
|
||||
var iq = $iq({to: Moderator.getFocusComponent(), type: 'get'});
|
||||
iq.c('login-url', {
|
||||
xmlns: 'http://jitsi.org/protocol/focus',
|
||||
room: roomName,
|
||||
'machine-uid': Settings.getSettings().uid
|
||||
});
|
||||
connection.sendIQ(
|
||||
iq,
|
||||
function (result) {
|
||||
var url = $(result).find('login-url').attr('url');
|
||||
url = url = decodeURIComponent(url);
|
||||
if (url) {
|
||||
console.info("Got auth url: " + url);
|
||||
urlCallback(url);
|
||||
} else {
|
||||
console.error(
|
||||
"Failed to get auth url from the focus", result);
|
||||
}
|
||||
},
|
||||
function (error) {
|
||||
console.error("Get auth url error", error);
|
||||
}
|
||||
);
|
||||
},
|
||||
getPopupLoginUrl: function (roomName, urlCallback) {
|
||||
var iq = $iq({to: Moderator.getFocusComponent(), type: 'get'});
|
||||
iq.c('login-url', {
|
||||
xmlns: 'http://jitsi.org/protocol/focus',
|
||||
room: roomName,
|
||||
'machine-uid': Settings.getSettings().uid,
|
||||
popup: true
|
||||
});
|
||||
connection.sendIQ(
|
||||
iq,
|
||||
function (result) {
|
||||
var url = $(result).find('login-url').attr('url');
|
||||
url = url = decodeURIComponent(url);
|
||||
if (url) {
|
||||
console.info("Got POPUP auth url: " + url);
|
||||
urlCallback(url);
|
||||
} else {
|
||||
console.error(
|
||||
"Failed to get POPUP auth url from the focus", result);
|
||||
}
|
||||
},
|
||||
function (error) {
|
||||
console.error('Get POPUP auth url error', error);
|
||||
}
|
||||
);
|
||||
},
|
||||
logout: function (callback) {
|
||||
var iq = $iq({to: Moderator.getFocusComponent(), type: 'set'});
|
||||
var sessionId = localStorage.getItem('sessionId');
|
||||
if (!sessionId) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
iq.c('logout', {
|
||||
xmlns: 'http://jitsi.org/protocol/focus',
|
||||
'session-id': sessionId
|
||||
});
|
||||
connection.sendIQ(
|
||||
iq,
|
||||
function (result) {
|
||||
var logoutUrl = $(result).find('logout').attr('logout-url');
|
||||
if (logoutUrl) {
|
||||
logoutUrl = decodeURIComponent(logoutUrl);
|
||||
}
|
||||
console.info("Log out OK, url: " + logoutUrl, result);
|
||||
localStorage.removeItem('sessionId');
|
||||
callback(logoutUrl);
|
||||
},
|
||||
function (error) {
|
||||
console.error("Logout error", error);
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = Moderator;
|
||||
|
||||
|
||||
|
|
@ -1,178 +0,0 @@
|
|||
/* global $, $iq, config, connection, focusMucJid, messageHandler,
|
||||
Toolbar, Util */
|
||||
var Moderator = require("./moderator");
|
||||
|
||||
|
||||
var recordingToken = null;
|
||||
var recordingEnabled;
|
||||
|
||||
/**
|
||||
* Whether to use a jirecon component for recording, or use the videobridge
|
||||
* through COLIBRI.
|
||||
*/
|
||||
var useJirecon;
|
||||
|
||||
/**
|
||||
* The ID of the jirecon recording session. Jirecon generates it when we
|
||||
* initially start recording, and it needs to be used in subsequent requests
|
||||
* to jirecon.
|
||||
*/
|
||||
var jireconRid = null;
|
||||
|
||||
/**
|
||||
* The callback to update the recording button. Currently used from colibri
|
||||
* after receiving a pending status.
|
||||
*/
|
||||
var recordingStateChangeCallback = null;
|
||||
|
||||
function setRecordingToken(token) {
|
||||
recordingToken = token;
|
||||
}
|
||||
|
||||
function setRecordingJirecon(state, token, callback, connection) {
|
||||
if (state == recordingEnabled){
|
||||
return;
|
||||
}
|
||||
|
||||
var iq = $iq({to: config.hosts.jirecon, type: 'set'})
|
||||
.c('recording', {xmlns: 'http://jitsi.org/protocol/jirecon',
|
||||
action: (state === 'on') ? 'start' : 'stop',
|
||||
mucjid: connection.emuc.roomjid});
|
||||
if (state === 'off'){
|
||||
iq.attrs({rid: jireconRid});
|
||||
}
|
||||
|
||||
console.log('Start recording');
|
||||
|
||||
connection.sendIQ(
|
||||
iq,
|
||||
function (result) {
|
||||
// TODO wait for an IQ with the real status, since this is
|
||||
// provisional?
|
||||
jireconRid = $(result).find('recording').attr('rid');
|
||||
console.log('Recording ' +
|
||||
((state === 'on') ? 'started' : 'stopped') +
|
||||
'(jirecon)' + result);
|
||||
recordingEnabled = state;
|
||||
if (state === 'off'){
|
||||
jireconRid = null;
|
||||
}
|
||||
|
||||
callback(state);
|
||||
},
|
||||
function (error) {
|
||||
console.log('Failed to start recording, error: ', error);
|
||||
callback(recordingEnabled);
|
||||
});
|
||||
}
|
||||
|
||||
// Sends a COLIBRI message which enables or disables (according to 'state')
|
||||
// the recording on the bridge. Waits for the result IQ and calls 'callback'
|
||||
// with the new recording state, according to the IQ.
|
||||
function setRecordingColibri(state, token, callback, connection) {
|
||||
var elem = $iq({to: connection.emuc.focusMucJid, type: 'set'});
|
||||
elem.c('conference', {
|
||||
xmlns: 'http://jitsi.org/protocol/colibri'
|
||||
});
|
||||
elem.c('recording', {state: state, token: token});
|
||||
|
||||
connection.sendIQ(elem,
|
||||
function (result) {
|
||||
console.log('Set recording "', state, '". Result:', result);
|
||||
var recordingElem = $(result).find('>conference>recording');
|
||||
var newState = recordingElem.attr('state');
|
||||
|
||||
recordingEnabled = newState;
|
||||
callback(newState);
|
||||
|
||||
if (newState === 'pending' && !recordingStateChangeCallback) {
|
||||
recordingStateChangeCallback = callback;
|
||||
connection.addHandler(function(iq){
|
||||
var state = $(iq).find('recording').attr('state');
|
||||
if (state)
|
||||
recordingStateChangeCallback(state);
|
||||
}, 'http://jitsi.org/protocol/colibri', 'iq', null, null, null);
|
||||
}
|
||||
},
|
||||
function (error) {
|
||||
console.warn(error);
|
||||
callback(recordingEnabled);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function setRecording(state, token, callback, connection) {
|
||||
if (useJirecon){
|
||||
setRecordingJirecon(state, token, callback, connection);
|
||||
} else {
|
||||
setRecordingColibri(state, token, callback, connection);
|
||||
}
|
||||
}
|
||||
|
||||
var Recording = {
|
||||
init: function () {
|
||||
useJirecon = config.hosts &&
|
||||
(typeof config.hosts.jirecon != "undefined");
|
||||
},
|
||||
toggleRecording: function (tokenEmptyCallback,
|
||||
recordingStateChangeCallback,
|
||||
connection) {
|
||||
if (!Moderator.isModerator()) {
|
||||
console.log(
|
||||
'non-focus, or conference not yet organized:' +
|
||||
' not enabling recording');
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
// Jirecon does not (currently) support a token.
|
||||
if (!recordingToken && !useJirecon) {
|
||||
tokenEmptyCallback(function (value) {
|
||||
setRecordingToken(value);
|
||||
self.toggleRecording(tokenEmptyCallback,
|
||||
recordingStateChangeCallback,
|
||||
connection);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var oldState = recordingEnabled;
|
||||
var newState = (oldState === 'off' || !oldState) ? 'on' : 'off';
|
||||
|
||||
setRecording(newState,
|
||||
recordingToken,
|
||||
function (state) {
|
||||
console.log("New recording state: ", state);
|
||||
if (state === oldState) {
|
||||
// FIXME: new focus:
|
||||
// this will not work when moderator changes
|
||||
// during active session. Then it will assume that
|
||||
// recording status has changed to true, but it might have
|
||||
// been already true(and we only received actual status from
|
||||
// the focus).
|
||||
//
|
||||
// SO we start with status null, so that it is initialized
|
||||
// here and will fail only after second click, so if invalid
|
||||
// token was used we have to press the button twice before
|
||||
// current status will be fetched and token will be reset.
|
||||
//
|
||||
// Reliable way would be to return authentication error.
|
||||
// Or status update when moderator connects.
|
||||
// Or we have to stop recording session when current
|
||||
// moderator leaves the room.
|
||||
|
||||
// Failed to change, reset the token because it might
|
||||
// have been wrong
|
||||
setRecordingToken(null);
|
||||
}
|
||||
recordingStateChangeCallback(state);
|
||||
|
||||
},
|
||||
connection
|
||||
);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
module.exports = Recording;
|
|
@ -1,706 +0,0 @@
|
|||
/* jshint -W117 */
|
||||
/* a simple MUC connection plugin
|
||||
* can only handle a single MUC room
|
||||
*/
|
||||
var XMPPEvents = require("../../service/xmpp/XMPPEvents");
|
||||
var Moderator = require("./moderator");
|
||||
|
||||
module.exports = function(XMPP, eventEmitter) {
|
||||
Strophe.addConnectionPlugin('emuc', {
|
||||
connection: null,
|
||||
roomjid: null,
|
||||
myroomjid: null,
|
||||
members: {},
|
||||
list_members: [], // so we can elect a new focus
|
||||
presMap: {},
|
||||
preziMap: {},
|
||||
lastPresenceMap: {},
|
||||
joined: false,
|
||||
isOwner: false,
|
||||
role: null,
|
||||
focusMucJid: null,
|
||||
bridgeIsDown: false,
|
||||
init: function (conn) {
|
||||
this.connection = conn;
|
||||
},
|
||||
initPresenceMap: function (myroomjid) {
|
||||
this.presMap['to'] = myroomjid;
|
||||
this.presMap['xns'] = 'http://jabber.org/protocol/muc';
|
||||
if (APP.RTC.localAudio && APP.RTC.localAudio.isMuted()) {
|
||||
this.addAudioInfoToPresence(true);
|
||||
}
|
||||
if (APP.RTC.localVideo && APP.RTC.localVideo.isMuted()) {
|
||||
this.addVideoInfoToPresence(true);
|
||||
}
|
||||
},
|
||||
doJoin: function (jid, password) {
|
||||
this.myroomjid = jid;
|
||||
|
||||
console.info("Joined MUC as " + this.myroomjid);
|
||||
|
||||
this.initPresenceMap(this.myroomjid);
|
||||
|
||||
if (!this.roomjid) {
|
||||
this.roomjid = Strophe.getBareJidFromJid(jid);
|
||||
// add handlers (just once)
|
||||
this.connection.addHandler(this.onPresence.bind(this), null, 'presence', null, null, this.roomjid, {matchBare: true});
|
||||
this.connection.addHandler(this.onPresenceUnavailable.bind(this), null, 'presence', 'unavailable', null, this.roomjid, {matchBare: true});
|
||||
this.connection.addHandler(this.onPresenceError.bind(this), null, 'presence', 'error', null, this.roomjid, {matchBare: true});
|
||||
this.connection.addHandler(this.onMessage.bind(this), null, 'message', null, null, this.roomjid, {matchBare: true});
|
||||
}
|
||||
if (password !== undefined) {
|
||||
this.presMap['password'] = password;
|
||||
}
|
||||
this.sendPresence();
|
||||
},
|
||||
doLeave: function () {
|
||||
console.log("do leave", this.myroomjid);
|
||||
var pres = $pres({to: this.myroomjid, type: 'unavailable' });
|
||||
this.presMap.length = 0;
|
||||
this.connection.send(pres);
|
||||
},
|
||||
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");
|
||||
});
|
||||
},
|
||||
onPresence: function (pres) {
|
||||
var from = pres.getAttribute('from');
|
||||
|
||||
// What is this for? A workaround for something?
|
||||
if (pres.getAttribute('type')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Parse etherpad tag.
|
||||
var etherpad = $(pres).find('>etherpad');
|
||||
if (etherpad.length) {
|
||||
if (config.etherpad_base) {
|
||||
eventEmitter.emit(XMPPEvents.ETHERPAD, etherpad.text());
|
||||
}
|
||||
}
|
||||
|
||||
var url;
|
||||
// Parse prezi tag.
|
||||
var presentation = $(pres).find('>prezi');
|
||||
if (presentation.length) {
|
||||
url = presentation.attr('url');
|
||||
var current = presentation.find('>current').text();
|
||||
|
||||
console.log('presentation info received from', from, url);
|
||||
|
||||
if (this.preziMap[from] == null) {
|
||||
this.preziMap[from] = url;
|
||||
|
||||
$(document).trigger('presentationadded.muc', [from, url, current]);
|
||||
}
|
||||
else {
|
||||
$(document).trigger('gotoslide.muc', [from, url, current]);
|
||||
}
|
||||
}
|
||||
else if (this.preziMap[from] != null) {
|
||||
url = this.preziMap[from];
|
||||
delete this.preziMap[from];
|
||||
$(document).trigger('presentationremoved.muc', [from, url]);
|
||||
}
|
||||
|
||||
// store the last presence for participant
|
||||
this.lastPresenceMap[from] = {};
|
||||
|
||||
// Parse audio info tag.
|
||||
var audioMuted = $(pres).find('>audiomuted');
|
||||
if (audioMuted.length) {
|
||||
eventEmitter.emit(XMPPEvents.PARTICIPANT_AUDIO_MUTED,
|
||||
from, (audioMuted.text() === "true"));
|
||||
}
|
||||
|
||||
// Parse video info tag.
|
||||
var videoMuted = $(pres).find('>videomuted');
|
||||
if (videoMuted.length) {
|
||||
var value = (videoMuted.text() === "true");
|
||||
this.lastPresenceMap[from].videoMuted = value;
|
||||
eventEmitter.emit(XMPPEvents.PARTICIPANT_VIDEO_MUTED, from, value);
|
||||
}
|
||||
|
||||
var startMuted = $(pres).find('>startmuted');
|
||||
if (startMuted.length && Moderator.isPeerModerator(from)) {
|
||||
eventEmitter.emit(XMPPEvents.START_MUTED_SETTING_CHANGED,
|
||||
startMuted.attr("audio") === "true",
|
||||
startMuted.attr("video") === "true");
|
||||
}
|
||||
|
||||
var devices = $(pres).find('>devices');
|
||||
if(devices.length)
|
||||
{
|
||||
var audio = devices.find('>audio');
|
||||
var video = devices.find('>video');
|
||||
var devicesValues = {audio: false, video: false};
|
||||
if(audio.length && audio.text() === "true")
|
||||
{
|
||||
devicesValues.audio = true;
|
||||
}
|
||||
|
||||
if(video.length && video.text() === "true")
|
||||
{
|
||||
devicesValues.video = true;
|
||||
}
|
||||
eventEmitter.emit(XMPPEvents.DEVICE_AVAILABLE,
|
||||
Strophe.getResourceFromJid(from), devicesValues);
|
||||
}
|
||||
|
||||
var videoType = $(pres).find('>videoType');
|
||||
if (videoType.length)
|
||||
{
|
||||
if (videoType.text().length)
|
||||
{
|
||||
eventEmitter.emit(XMPPEvents.PARTICIPANT_VIDEO_TYPE_CHANGED,
|
||||
Strophe.getResourceFromJid(from), videoType.text());
|
||||
}
|
||||
}
|
||||
|
||||
var stats = $(pres).find('>stats');
|
||||
if (stats.length) {
|
||||
var statsObj = {};
|
||||
Strophe.forEachChild(stats[0], "stat", function (el) {
|
||||
statsObj[el.getAttribute("name")] = el.getAttribute("value");
|
||||
});
|
||||
eventEmitter.emit(XMPPEvents.REMOTE_STATS, from, statsObj);
|
||||
}
|
||||
|
||||
// Parse status.
|
||||
if ($(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>status[code="201"]').length) {
|
||||
this.isOwner = true;
|
||||
this.createNonAnonymousRoom();
|
||||
}
|
||||
|
||||
// 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(Moderator.getFocusUserJid() + "/") == 0) {
|
||||
member.isFocus = true;
|
||||
}
|
||||
|
||||
var nicktag = $(pres).find('>nick[xmlns="http://jabber.org/protocol/nick"]');
|
||||
member.displayName = (nicktag.length > 0 ? nicktag.text() : null);
|
||||
|
||||
if (from == this.myroomjid) {
|
||||
if (member.affiliation == 'owner') this.isOwner = true;
|
||||
if (this.role !== member.role) {
|
||||
this.role = member.role;
|
||||
|
||||
eventEmitter.emit(XMPPEvents.LOCAL_ROLE_CHANGED,
|
||||
from, member, pres, Moderator.isModerator());
|
||||
}
|
||||
if (!this.joined) {
|
||||
this.joined = true;
|
||||
console.log("(TIME) MUC joined:\t",
|
||||
window.performance.now());
|
||||
eventEmitter.emit(XMPPEvents.MUC_JOINED, from, member);
|
||||
this.list_members.push(from);
|
||||
}
|
||||
} else if (this.members[from] === undefined) {
|
||||
// new participant
|
||||
this.members[from] = member;
|
||||
this.list_members.push(from);
|
||||
console.log('entered', from, member);
|
||||
if (member.isFocus) {
|
||||
this.focusMucJid = from;
|
||||
console.info("Ignore focus: " + from + ", real JID: " + member.jid);
|
||||
}
|
||||
else {
|
||||
var id = $(pres).find('>userId').text();
|
||||
var email = $(pres).find('>email');
|
||||
if (email.length > 0) {
|
||||
id = email.text();
|
||||
}
|
||||
eventEmitter.emit(XMPPEvents.MUC_MEMBER_JOINED, from, id, member.displayName);
|
||||
}
|
||||
} else {
|
||||
// Presence update for existing participant
|
||||
// Watch role change:
|
||||
if (this.members[from].role != member.role) {
|
||||
this.members[from].role = member.role;
|
||||
eventEmitter.emit(XMPPEvents.MUC_ROLE_CHANGED,
|
||||
member.role, member.displayName);
|
||||
}
|
||||
|
||||
// store the new
|
||||
if(member.displayName)
|
||||
this.members[from].displayName = member.displayName;
|
||||
}
|
||||
|
||||
// Always trigger presence to update bindings
|
||||
this.parsePresence(from, member, pres);
|
||||
|
||||
// Trigger status message update
|
||||
if (member.status) {
|
||||
eventEmitter.emit(XMPPEvents.PRESENCE_STATUS, from, member);
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
onPresenceUnavailable: function (pres) {
|
||||
var from = pres.getAttribute('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();
|
||||
}
|
||||
XMPP.disposeConference(false);
|
||||
eventEmitter.emit(XMPPEvents.MUC_DESTROYED, reason);
|
||||
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.list_members.splice(this.list_members.indexOf(from), 1);
|
||||
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 (this.list_members.length > 1) {
|
||||
for (var i = 0; i < this.list_members.length; i++) {
|
||||
var member = this.list_members[i];
|
||||
delete this.members[i];
|
||||
this.list_members.splice(i, 1);
|
||||
this.onParticipantLeft(member);
|
||||
}
|
||||
}
|
||||
if ($(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>status[code="307"]').length) {
|
||||
$(document).trigger('kicked.muc', [from]);
|
||||
if (this.myroomjid === from) {
|
||||
XMPP.disposeConference(false);
|
||||
eventEmitter.emit(XMPPEvents.KICKED);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.lastPresenceMap[from] != null) {
|
||||
delete this.lastPresenceMap[from];
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
onPresenceError: function (pres) {
|
||||
var from = pres.getAttribute('from');
|
||||
if ($(pres).find('>error[type="auth"]>not-authorized[xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]').length) {
|
||||
console.log('on password required', from);
|
||||
var self = this;
|
||||
eventEmitter.emit(XMPPEvents.PASSWORD_REQUIRED, function (value) {
|
||||
self.doJoin(from, value);
|
||||
});
|
||||
} 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 === config.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.
|
||||
// XMPP.promptLogin();
|
||||
eventEmitter.emit(XMPPEvents.ROOM_JOIN_ERROR, pres);
|
||||
|
||||
} else {
|
||||
console.warn('onPresError ', pres);
|
||||
eventEmitter.emit(XMPPEvents.ROOM_CONNECT_ERROR, pres);
|
||||
}
|
||||
} else {
|
||||
console.warn('onPresError ', pres);
|
||||
eventEmitter.emit(XMPPEvents.ROOM_CONNECT_ERROR, pres);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
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);
|
||||
eventEmitter.emit(XMPPEvents.SENDING_CHAT_MESSAGE, body);
|
||||
},
|
||||
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);
|
||||
},
|
||||
onMessage: function (msg) {
|
||||
// FIXME: this is a hack. but jingle on muc makes nickchanges hard
|
||||
var from = msg.getAttribute('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") {
|
||||
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 == "") {
|
||||
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);
|
||||
eventEmitter.emit(XMPPEvents.MESSAGE_RECEIVED,
|
||||
from, nick, txt, this.myroomjid, stamp);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
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);
|
||||
},
|
||||
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);
|
||||
});
|
||||
},
|
||||
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.presMap['password']) {
|
||||
pres.c('password').t(this.presMap['password']).up();
|
||||
}
|
||||
|
||||
pres.up();
|
||||
|
||||
// Send XEP-0115 'c' stanza that contains our capabilities info
|
||||
if (this.connection.caps) {
|
||||
this.connection.caps.node = config.clientNode;
|
||||
pres.c('c', this.connection.caps.generateCapsAttrs()).up();
|
||||
}
|
||||
|
||||
pres.c('user-agent', {xmlns: 'http://jitsi.org/jitmeet/user-agent'})
|
||||
.t(navigator.userAgent).up();
|
||||
|
||||
if (this.presMap['bridgeIsDown']) {
|
||||
pres.c('bridgeIsDown').up();
|
||||
}
|
||||
|
||||
if (this.presMap['email']) {
|
||||
pres.c('email').t(this.presMap['email']).up();
|
||||
}
|
||||
|
||||
if (this.presMap['userId']) {
|
||||
pres.c('userId').t(this.presMap['userId']).up();
|
||||
}
|
||||
|
||||
if (this.presMap['displayName']) {
|
||||
// XEP-0172
|
||||
pres.c('nick', {xmlns: 'http://jabber.org/protocol/nick'})
|
||||
.t(this.presMap['displayName']).up();
|
||||
}
|
||||
|
||||
if(this.presMap["devices"])
|
||||
{
|
||||
pres.c('devices').c('audio').t(this.presMap['devices'].audio).up()
|
||||
.c('video').t(this.presMap['devices'].video).up().up();
|
||||
}
|
||||
if (this.presMap['audions']) {
|
||||
pres.c('audiomuted', {xmlns: this.presMap['audions']})
|
||||
.t(this.presMap['audiomuted']).up();
|
||||
}
|
||||
|
||||
if (this.presMap['videons']) {
|
||||
pres.c('videomuted', {xmlns: this.presMap['videons']})
|
||||
.t(this.presMap['videomuted']).up();
|
||||
}
|
||||
|
||||
if (this.presMap['videoTypeNs']) {
|
||||
pres.c('videoType', { xmlns: this.presMap['videoTypeNs'] })
|
||||
.t(this.presMap['videoType']).up();
|
||||
}
|
||||
|
||||
if (this.presMap['statsns']) {
|
||||
var stats = pres.c('stats', {xmlns: this.presMap['statsns']});
|
||||
for (var stat in this.presMap["stats"])
|
||||
if (this.presMap["stats"][stat] != null)
|
||||
stats.c("stat", {name: stat, value: this.presMap["stats"][stat]}).up();
|
||||
pres.up();
|
||||
}
|
||||
|
||||
if (this.presMap['prezins']) {
|
||||
pres.c('prezi',
|
||||
{xmlns: this.presMap['prezins'],
|
||||
'url': this.presMap['preziurl']})
|
||||
.c('current').t(this.presMap['prezicurrent']).up().up();
|
||||
}
|
||||
|
||||
// This is only for backward compatibility with clients which
|
||||
// don't support getting sources from Jingle (i.e. jirecon).
|
||||
if (this.presMap['medians']) {
|
||||
pres.c('media', {xmlns: this.presMap['medians']});
|
||||
var sourceNumber = 0;
|
||||
Object.keys(this.presMap).forEach(function (key) {
|
||||
if (key.indexOf('source') >= 0) {
|
||||
sourceNumber++;
|
||||
}
|
||||
});
|
||||
if (sourceNumber > 0) {
|
||||
for (var i = 1; i <= sourceNumber / 3; i++) {
|
||||
pres.c('source',
|
||||
{
|
||||
type: this.presMap['source' + i + '_type'],
|
||||
ssrc: this.presMap['source' + i + '_ssrc'],
|
||||
direction: this.presMap['source' + i + '_direction']
|
||||
|| 'sendrecv'
|
||||
}
|
||||
).up();
|
||||
}
|
||||
}
|
||||
pres.up();
|
||||
}
|
||||
|
||||
if(this.presMap["startMuted"] !== undefined)
|
||||
{
|
||||
pres.c("startmuted", {audio: this.presMap["startMuted"].audio,
|
||||
video: this.presMap["startMuted"].video,
|
||||
xmlns: "http://jitsi.org/jitmeet/start-muted"});
|
||||
delete this.presMap["startMuted"];
|
||||
}
|
||||
|
||||
if (config.token) {
|
||||
pres.c('token', { xmlns: 'http://jitsi.org/jitmeet/auth-token'}).t(config.token).up();
|
||||
}
|
||||
|
||||
pres.up();
|
||||
this.connection.send(pres);
|
||||
},
|
||||
addDisplayNameToPresence: function (displayName) {
|
||||
this.presMap['displayName'] = displayName;
|
||||
},
|
||||
// This is only for backward compatibility with clients which
|
||||
// don't support getting sources from Jingle (i.e. jirecon).
|
||||
addMediaToPresence: function (sourceNumber, mtype, ssrcs, direction) {
|
||||
if (!this.presMap['medians'])
|
||||
this.presMap['medians'] = 'http://estos.de/ns/mjs';
|
||||
|
||||
this.presMap['source' + sourceNumber + '_type'] = mtype;
|
||||
this.presMap['source' + sourceNumber + '_ssrc'] = ssrcs;
|
||||
this.presMap['source' + sourceNumber + '_direction'] = direction;
|
||||
},
|
||||
// This is only for backward compatibility with clients which
|
||||
// don't support getting sources from Jingle (i.e. jirecon).
|
||||
clearPresenceMedia: function () {
|
||||
var self = this;
|
||||
Object.keys(this.presMap).forEach(function (key) {
|
||||
if (key.indexOf('source') != -1) {
|
||||
delete self.presMap[key];
|
||||
}
|
||||
});
|
||||
},
|
||||
addDevicesToPresence: function (devices) {
|
||||
this.presMap['devices'] = devices;
|
||||
},
|
||||
/**
|
||||
* Adds the info about the type of our video stream.
|
||||
* @param videoType 'camera' or 'screen'
|
||||
*/
|
||||
addVideoTypeToPresence: function (videoType) {
|
||||
this.presMap['videoTypeNs'] = 'http://jitsi.org/jitmeet/video';
|
||||
this.presMap['videoType'] = videoType;
|
||||
},
|
||||
addPreziToPresence: function (url, currentSlide) {
|
||||
this.presMap['prezins'] = 'http://jitsi.org/jitmeet/prezi';
|
||||
this.presMap['preziurl'] = url;
|
||||
this.presMap['prezicurrent'] = currentSlide;
|
||||
},
|
||||
removePreziFromPresence: function () {
|
||||
delete this.presMap['prezins'];
|
||||
delete this.presMap['preziurl'];
|
||||
delete this.presMap['prezicurrent'];
|
||||
},
|
||||
addCurrentSlideToPresence: function (currentSlide) {
|
||||
this.presMap['prezicurrent'] = currentSlide;
|
||||
},
|
||||
getPrezi: function (roomjid) {
|
||||
return this.preziMap[roomjid];
|
||||
},
|
||||
addAudioInfoToPresence: function (isMuted) {
|
||||
this.presMap['audions'] = 'http://jitsi.org/jitmeet/audio';
|
||||
this.presMap['audiomuted'] = isMuted.toString();
|
||||
},
|
||||
addVideoInfoToPresence: function (isMuted) {
|
||||
this.presMap['videons'] = 'http://jitsi.org/jitmeet/video';
|
||||
this.presMap['videomuted'] = isMuted.toString();
|
||||
},
|
||||
addConnectionInfoToPresence: function (stats) {
|
||||
this.presMap['statsns'] = 'http://jitsi.org/jitmeet/stats';
|
||||
this.presMap['stats'] = stats;
|
||||
},
|
||||
findJidFromResource: function (resourceJid) {
|
||||
if (resourceJid &&
|
||||
resourceJid === Strophe.getResourceFromJid(this.myroomjid)) {
|
||||
return this.myroomjid;
|
||||
}
|
||||
var peerJid = null;
|
||||
Object.keys(this.members).some(function (jid) {
|
||||
peerJid = jid;
|
||||
return Strophe.getResourceFromJid(jid) === resourceJid;
|
||||
});
|
||||
return peerJid;
|
||||
},
|
||||
addBridgeIsDownToPresence: function () {
|
||||
this.presMap['bridgeIsDown'] = true;
|
||||
},
|
||||
addEmailToPresence: function (email) {
|
||||
this.presMap['email'] = email;
|
||||
},
|
||||
addUserIdToPresence: function (userId) {
|
||||
this.presMap['userId'] = userId;
|
||||
},
|
||||
addStartMutedToPresence: function (audio, video) {
|
||||
this.presMap["startMuted"] = {audio: audio, video: video};
|
||||
},
|
||||
isModerator: function () {
|
||||
return this.role === 'moderator';
|
||||
},
|
||||
getMemberRole: function (peerJid) {
|
||||
if (this.members[peerJid]) {
|
||||
return this.members[peerJid].role;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onParticipantLeft: function (jid) {
|
||||
|
||||
eventEmitter.emit(XMPPEvents.MUC_MEMBER_LEFT, jid);
|
||||
|
||||
this.connection.jingle.terminateByJid(jid);
|
||||
|
||||
if (this.getPrezi(jid)) {
|
||||
$(document).trigger('presentationremoved.muc',
|
||||
[jid, this.getPrezi(jid)]);
|
||||
}
|
||||
|
||||
Moderator.onMucMemberLeft(jid);
|
||||
},
|
||||
parsePresence: function (from, member, pres) {
|
||||
if($(pres).find(">bridgeIsDown").length > 0 && !this.bridgeIsDown) {
|
||||
this.bridgeIsDown = true;
|
||||
eventEmitter.emit(XMPPEvents.BRIDGE_DOWN);
|
||||
}
|
||||
|
||||
if(member.isFocus)
|
||||
return;
|
||||
|
||||
var displayName = !config.displayJids
|
||||
? member.displayName : Strophe.getResourceFromJid(from);
|
||||
|
||||
if (displayName && displayName.length > 0) {
|
||||
eventEmitter.emit(XMPPEvents.DISPLAY_NAME_CHANGED, from, displayName);
|
||||
}
|
||||
|
||||
var id = $(pres).find('>userID').text();
|
||||
var email = $(pres).find('>email');
|
||||
if (email.length > 0) {
|
||||
id = email.text();
|
||||
}
|
||||
|
||||
eventEmitter.emit(XMPPEvents.USER_ID_CHANGED, from, id);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
@ -1,341 +0,0 @@
|
|||
/* jshint -W117 */
|
||||
/* jshint -W101 */
|
||||
|
||||
var JingleSession = require("./JingleSessionPC");
|
||||
var XMPPEvents = require("../../service/xmpp/XMPPEvents");
|
||||
var RTCBrowserType = require("../RTC/RTCBrowserType");
|
||||
|
||||
|
||||
module.exports = function(XMPP, eventEmitter) {
|
||||
Strophe.addConnectionPlugin('jingle', {
|
||||
connection: null,
|
||||
sessions: {},
|
||||
jid2session: {},
|
||||
ice_config: {iceServers: []},
|
||||
pc_constraints: {},
|
||||
activecall: null,
|
||||
media_constraints: {
|
||||
mandatory: {
|
||||
'OfferToReceiveAudio': true,
|
||||
'OfferToReceiveVideo': true
|
||||
}
|
||||
// MozDontOfferDataChannel: true when this is firefox
|
||||
},
|
||||
init: function (conn) {
|
||||
this.connection = conn;
|
||||
if (this.connection.disco) {
|
||||
// http://xmpp.org/extensions/xep-0167.html#support
|
||||
// http://xmpp.org/extensions/xep-0176.html#support
|
||||
this.connection.disco.addFeature('urn:xmpp:jingle:1');
|
||||
this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:1');
|
||||
this.connection.disco.addFeature('urn:xmpp:jingle:transports:ice-udp:1');
|
||||
this.connection.disco.addFeature('urn:xmpp:jingle:apps:dtls:0');
|
||||
this.connection.disco.addFeature('urn:xmpp:jingle:transports:dtls-sctp:1');
|
||||
this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:audio');
|
||||
this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:video');
|
||||
|
||||
if (RTCBrowserType.isChrome() || RTCBrowserType.isOpera() ||
|
||||
RTCBrowserType.isTemasysPluginUsed()) {
|
||||
this.connection.disco.addFeature('urn:ietf:rfc:4588');
|
||||
}
|
||||
|
||||
// this is dealt with by SDP O/A so we don't need to announce this
|
||||
//this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:rtcp-fb:0'); // XEP-0293
|
||||
//this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:rtp-hdrext:0'); // XEP-0294
|
||||
|
||||
this.connection.disco.addFeature('urn:ietf:rfc:5761'); // rtcp-mux
|
||||
this.connection.disco.addFeature('urn:ietf:rfc:5888'); // a=group, e.g. bundle
|
||||
|
||||
//this.connection.disco.addFeature('urn:ietf:rfc:5576'); // a=ssrc
|
||||
}
|
||||
this.connection.addHandler(this.onJingle.bind(this), 'urn:xmpp:jingle:1', 'iq', 'set', null, null);
|
||||
},
|
||||
onJingle: function (iq) {
|
||||
var sid = $(iq).find('jingle').attr('sid');
|
||||
var action = $(iq).find('jingle').attr('action');
|
||||
var fromJid = iq.getAttribute('from');
|
||||
// send ack first
|
||||
var ack = $iq({type: 'result',
|
||||
to: fromJid,
|
||||
id: iq.getAttribute('id')
|
||||
});
|
||||
console.log('on jingle ' + action + ' from ' + fromJid, iq);
|
||||
var sess = this.sessions[sid];
|
||||
if ('session-initiate' != action) {
|
||||
if (sess === null) {
|
||||
ack.type = 'error';
|
||||
ack.c('error', {type: 'cancel'})
|
||||
.c('item-not-found', {xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'}).up()
|
||||
.c('unknown-session', {xmlns: 'urn:xmpp:jingle:errors:1'});
|
||||
this.connection.send(ack);
|
||||
return true;
|
||||
}
|
||||
// local jid is not checked
|
||||
if (fromJid != sess.peerjid) {
|
||||
console.warn('jid mismatch for session id', sid, fromJid, sess.peerjid);
|
||||
ack.type = 'error';
|
||||
ack.c('error', {type: 'cancel'})
|
||||
.c('item-not-found', {xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'}).up()
|
||||
.c('unknown-session', {xmlns: 'urn:xmpp:jingle:errors:1'});
|
||||
this.connection.send(ack);
|
||||
return true;
|
||||
}
|
||||
} else if (sess !== undefined) {
|
||||
// existing session with same session id
|
||||
// this might be out-of-order if the sess.peerjid is the same as from
|
||||
ack.type = 'error';
|
||||
ack.c('error', {type: 'cancel'})
|
||||
.c('service-unavailable', {xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'}).up();
|
||||
console.warn('duplicate session id', sid);
|
||||
this.connection.send(ack);
|
||||
return true;
|
||||
}
|
||||
// FIXME: check for a defined action
|
||||
this.connection.send(ack);
|
||||
// see http://xmpp.org/extensions/xep-0166.html#concepts-session
|
||||
switch (action) {
|
||||
case 'session-initiate':
|
||||
console.log("(TIME) received session-initiate:\t",
|
||||
window.performance.now(), iq);
|
||||
var startMuted = $(iq).find('jingle>startmuted');
|
||||
if (startMuted && startMuted.length > 0) {
|
||||
var audioMuted = startMuted.attr("audio");
|
||||
var videoMuted = startMuted.attr("video");
|
||||
eventEmitter.emit(XMPPEvents.START_MUTED_FROM_FOCUS,
|
||||
audioMuted === "true", videoMuted === "true");
|
||||
}
|
||||
sess = new JingleSession(
|
||||
$(iq).attr('to'), $(iq).find('jingle').attr('sid'),
|
||||
this.connection, XMPP, eventEmitter);
|
||||
// configure session
|
||||
|
||||
sess.media_constraints = this.media_constraints;
|
||||
sess.pc_constraints = this.pc_constraints;
|
||||
sess.ice_config = this.ice_config;
|
||||
|
||||
sess.initialize(fromJid, false);
|
||||
// FIXME: setRemoteDescription should only be done when this call is to be accepted
|
||||
sess.setOffer($(iq).find('>jingle'));
|
||||
|
||||
this.sessions[sess.sid] = sess;
|
||||
this.jid2session[sess.peerjid] = sess;
|
||||
|
||||
// the callback should either
|
||||
// .sendAnswer and .accept
|
||||
// or .sendTerminate -- not necessarily synchronous
|
||||
|
||||
// TODO: do we check activecall == null?
|
||||
this.connection.jingle.activecall = sess;
|
||||
|
||||
eventEmitter.emit(XMPPEvents.CALL_INCOMING, sess);
|
||||
|
||||
// TODO: check affiliation and/or role
|
||||
console.log('emuc data for', sess.peerjid,
|
||||
this.connection.emuc.members[sess.peerjid]);
|
||||
sess.sendAnswer();
|
||||
sess.accept();
|
||||
break;
|
||||
case 'session-accept':
|
||||
sess.setAnswer($(iq).find('>jingle'));
|
||||
sess.accept();
|
||||
$(document).trigger('callaccepted.jingle', [sess.sid]);
|
||||
break;
|
||||
case 'session-terminate':
|
||||
if (!sess) {
|
||||
break;
|
||||
}
|
||||
console.log('terminating...', sess.sid);
|
||||
sess.terminate();
|
||||
this.terminate(sess.sid);
|
||||
if ($(iq).find('>jingle>reason').length) {
|
||||
$(document).trigger('callterminated.jingle', [
|
||||
sess.sid,
|
||||
sess.peerjid,
|
||||
$(iq).find('>jingle>reason>:first')[0].tagName,
|
||||
$(iq).find('>jingle>reason>text').text()
|
||||
]);
|
||||
} else {
|
||||
$(document).trigger('callterminated.jingle',
|
||||
[sess.sid, sess.peerjid]);
|
||||
}
|
||||
break;
|
||||
case 'transport-info':
|
||||
sess.addIceCandidate($(iq).find('>jingle>content'));
|
||||
break;
|
||||
case 'session-info':
|
||||
var affected;
|
||||
if ($(iq).find('>jingle>ringing[xmlns="urn:xmpp:jingle:apps:rtp:info:1"]').length) {
|
||||
$(document).trigger('ringing.jingle', [sess.sid]);
|
||||
} else if ($(iq).find('>jingle>mute[xmlns="urn:xmpp:jingle:apps:rtp:info:1"]').length) {
|
||||
affected = $(iq).find('>jingle>mute[xmlns="urn:xmpp:jingle:apps:rtp:info:1"]').attr('name');
|
||||
$(document).trigger('mute.jingle', [sess.sid, affected]);
|
||||
} else if ($(iq).find('>jingle>unmute[xmlns="urn:xmpp:jingle:apps:rtp:info:1"]').length) {
|
||||
affected = $(iq).find('>jingle>unmute[xmlns="urn:xmpp:jingle:apps:rtp:info:1"]').attr('name');
|
||||
$(document).trigger('unmute.jingle', [sess.sid, affected]);
|
||||
}
|
||||
break;
|
||||
case 'addsource': // FIXME: proprietary, un-jingleish
|
||||
case 'source-add': // FIXME: proprietary
|
||||
console.info("source-add", iq);
|
||||
sess.addSource($(iq).find('>jingle>content'));
|
||||
break;
|
||||
case 'removesource': // FIXME: proprietary, un-jingleish
|
||||
case 'source-remove': // FIXME: proprietary
|
||||
console.info("source-remove", iq);
|
||||
sess.removeSource($(iq).find('>jingle>content'));
|
||||
break;
|
||||
default:
|
||||
console.warn('jingle action not implemented', action);
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
initiate: function (peerjid, myjid) { // initiate a new jinglesession to peerjid
|
||||
var sess = new JingleSession(myjid || this.connection.jid,
|
||||
Math.random().toString(36).substr(2, 12), // random string
|
||||
this.connection, XMPP, eventEmitter);
|
||||
// configure session
|
||||
|
||||
sess.media_constraints = this.media_constraints;
|
||||
sess.pc_constraints = this.pc_constraints;
|
||||
sess.ice_config = this.ice_config;
|
||||
|
||||
sess.initialize(peerjid, true);
|
||||
this.sessions[sess.sid] = sess;
|
||||
this.jid2session[sess.peerjid] = sess;
|
||||
sess.sendOffer();
|
||||
return sess;
|
||||
},
|
||||
terminate: function (sid, reason, text) { // terminate by sessionid (or all sessions)
|
||||
if (sid === null || sid === undefined) {
|
||||
for (sid in this.sessions) {
|
||||
if (this.sessions[sid].state != 'ended') {
|
||||
this.sessions[sid].sendTerminate(reason || (!this.sessions[sid].active()) ? 'cancel' : null, text);
|
||||
this.sessions[sid].terminate();
|
||||
}
|
||||
delete this.jid2session[this.sessions[sid].peerjid];
|
||||
delete this.sessions[sid];
|
||||
}
|
||||
} else if (this.sessions.hasOwnProperty(sid)) {
|
||||
if (this.sessions[sid].state != 'ended') {
|
||||
this.sessions[sid].sendTerminate(reason || (!this.sessions[sid].active()) ? 'cancel' : null, text);
|
||||
this.sessions[sid].terminate();
|
||||
}
|
||||
delete this.jid2session[this.sessions[sid].peerjid];
|
||||
delete this.sessions[sid];
|
||||
}
|
||||
},
|
||||
// Used to terminate a session when an unavailable presence is received.
|
||||
terminateByJid: function (jid) {
|
||||
if (this.jid2session.hasOwnProperty(jid)) {
|
||||
var sess = this.jid2session[jid];
|
||||
if (sess) {
|
||||
sess.terminate();
|
||||
console.log('peer went away silently', jid);
|
||||
delete this.sessions[sess.sid];
|
||||
delete this.jid2session[jid];
|
||||
$(document).trigger('callterminated.jingle',
|
||||
[sess.sid, jid], 'gone');
|
||||
}
|
||||
}
|
||||
},
|
||||
terminateRemoteByJid: function (jid, reason) {
|
||||
if (this.jid2session.hasOwnProperty(jid)) {
|
||||
var sess = this.jid2session[jid];
|
||||
if (sess) {
|
||||
sess.sendTerminate(reason || (!sess.active()) ? 'kick' : null);
|
||||
sess.terminate();
|
||||
console.log('terminate peer with jid', sess.sid, jid);
|
||||
delete this.sessions[sess.sid];
|
||||
delete this.jid2session[jid];
|
||||
$(document).trigger('callterminated.jingle',
|
||||
[sess.sid, jid, 'kicked']);
|
||||
}
|
||||
}
|
||||
},
|
||||
getStunAndTurnCredentials: function () {
|
||||
// get stun and turn configuration from server via xep-0215
|
||||
// uses time-limited credentials as described in
|
||||
// http://tools.ietf.org/html/draft-uberti-behave-turn-rest-00
|
||||
//
|
||||
// see https://code.google.com/p/prosody-modules/source/browse/mod_turncredentials/mod_turncredentials.lua
|
||||
// for a prosody module which implements this
|
||||
//
|
||||
// currently, this doesn't work with updateIce and therefore credentials with a long
|
||||
// validity have to be fetched before creating the peerconnection
|
||||
// TODO: implement refresh via updateIce as described in
|
||||
// https://code.google.com/p/webrtc/issues/detail?id=1650
|
||||
var self = this;
|
||||
this.connection.sendIQ(
|
||||
$iq({type: 'get', to: this.connection.domain})
|
||||
.c('services', {xmlns: 'urn:xmpp:extdisco:1'}).c('service', {host: 'turn.' + this.connection.domain}),
|
||||
function (res) {
|
||||
var iceservers = [];
|
||||
$(res).find('>services>service').each(function (idx, el) {
|
||||
el = $(el);
|
||||
var dict = {};
|
||||
var type = el.attr('type');
|
||||
switch (type) {
|
||||
case 'stun':
|
||||
dict.url = 'stun:' + el.attr('host');
|
||||
if (el.attr('port')) {
|
||||
dict.url += ':' + el.attr('port');
|
||||
}
|
||||
iceservers.push(dict);
|
||||
break;
|
||||
case 'turn':
|
||||
case 'turns':
|
||||
dict.url = type + ':';
|
||||
if (el.attr('username')) { // https://code.google.com/p/webrtc/issues/detail?id=1508
|
||||
if (navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./) && parseInt(navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)[2], 10) < 28) {
|
||||
dict.url += el.attr('username') + '@';
|
||||
} else {
|
||||
dict.username = el.attr('username'); // only works in M28
|
||||
}
|
||||
}
|
||||
dict.url += el.attr('host');
|
||||
if (el.attr('port') && el.attr('port') != '3478') {
|
||||
dict.url += ':' + el.attr('port');
|
||||
}
|
||||
if (el.attr('transport') && el.attr('transport') != 'udp') {
|
||||
dict.url += '?transport=' + el.attr('transport');
|
||||
}
|
||||
if (el.attr('password')) {
|
||||
dict.credential = el.attr('password');
|
||||
}
|
||||
iceservers.push(dict);
|
||||
break;
|
||||
}
|
||||
});
|
||||
self.ice_config.iceServers = iceservers;
|
||||
},
|
||||
function (err) {
|
||||
console.warn('getting turn credentials failed', err);
|
||||
console.warn('is mod_turncredentials or similar installed?');
|
||||
}
|
||||
);
|
||||
// implement push?
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the data saved in 'updateLog' in a format to be logged.
|
||||
*/
|
||||
getLog: function () {
|
||||
var data = {};
|
||||
var self = this;
|
||||
Object.keys(this.sessions).forEach(function (sid) {
|
||||
var session = self.sessions[sid];
|
||||
if (session.peerconnection && session.peerconnection.updateLog) {
|
||||
// FIXME: should probably be a .dump call
|
||||
data["jingle_" + session.sid] = {
|
||||
updateLog: session.peerconnection.updateLog,
|
||||
stats: session.peerconnection.stats,
|
||||
url: window.location.href
|
||||
};
|
||||
}
|
||||
});
|
||||
return data;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
/* global Strophe */
|
||||
module.exports = function () {
|
||||
|
||||
Strophe.addConnectionPlugin('logger', {
|
||||
// logs raw stanzas and makes them available for download as JSON
|
||||
connection: null,
|
||||
log: [],
|
||||
init: function (conn) {
|
||||
this.connection = conn;
|
||||
this.connection.rawInput = this.log_incoming.bind(this);
|
||||
this.connection.rawOutput = this.log_outgoing.bind(this);
|
||||
},
|
||||
log_incoming: function (stanza) {
|
||||
this.log.push([new Date().getTime(), 'incoming', stanza]);
|
||||
},
|
||||
log_outgoing: function (stanza) {
|
||||
this.log.push([new Date().getTime(), 'outgoing', stanza]);
|
||||
}
|
||||
});
|
||||
};
|
|
@ -1,60 +0,0 @@
|
|||
/* global $, $iq, config, connection, focusMucJid, forceMuted, Strophe */
|
||||
/**
|
||||
* Moderate connection plugin.
|
||||
*/
|
||||
var XMPPEvents = require("../../service/xmpp/XMPPEvents");
|
||||
|
||||
module.exports = function (XMPP, eventEmitter) {
|
||||
Strophe.addConnectionPlugin('moderate', {
|
||||
connection: null,
|
||||
init: function (conn) {
|
||||
this.connection = conn;
|
||||
|
||||
this.connection.addHandler(this.onMute.bind(this),
|
||||
'http://jitsi.org/jitmeet/audio',
|
||||
'iq',
|
||||
'set',
|
||||
null,
|
||||
null);
|
||||
},
|
||||
setMute: function (jid, mute) {
|
||||
console.info("set mute", mute);
|
||||
var iqToFocus =
|
||||
$iq({to: this.connection.emuc.focusMucJid, type: 'set'})
|
||||
.c('mute', {
|
||||
xmlns: 'http://jitsi.org/jitmeet/audio',
|
||||
jid: jid
|
||||
})
|
||||
.t(mute.toString())
|
||||
.up();
|
||||
|
||||
this.connection.sendIQ(
|
||||
iqToFocus,
|
||||
function (result) {
|
||||
console.log('set mute', result);
|
||||
},
|
||||
function (error) {
|
||||
console.log('set mute error', error);
|
||||
});
|
||||
},
|
||||
onMute: function (iq) {
|
||||
var from = iq.getAttribute('from');
|
||||
if (from !== this.connection.emuc.focusMucJid) {
|
||||
console.warn("Ignored mute from non focus peer");
|
||||
return false;
|
||||
}
|
||||
var mute = $(iq).find('mute');
|
||||
if (mute.length) {
|
||||
var doMuteAudio = mute.text() === "true";
|
||||
eventEmitter.emit(XMPPEvents.AUDIO_MUTED_BY_FOCUS, doMuteAudio);
|
||||
XMPP.forceMuted = doMuteAudio;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
eject: function (jid) {
|
||||
// We're not the focus, so can't terminate
|
||||
//connection.jingle.terminateRemoteByJid(jid, 'kick');
|
||||
this.connection.emuc.kick(jid);
|
||||
}
|
||||
});
|
||||
};
|
|
@ -1,121 +0,0 @@
|
|||
/* global $, $iq, Strophe */
|
||||
|
||||
var XMPPEvents = require("../../service/xmpp/XMPPEvents");
|
||||
|
||||
/**
|
||||
* Ping every 20 sec
|
||||
*/
|
||||
var PING_INTERVAL = 20000;
|
||||
|
||||
/**
|
||||
* Ping timeout error after 15 sec of waiting.
|
||||
*/
|
||||
var PING_TIMEOUT = 15000;
|
||||
|
||||
/**
|
||||
* Will close the connection after 3 consecutive ping errors.
|
||||
*/
|
||||
var PING_THRESHOLD = 3;
|
||||
|
||||
/**
|
||||
* XEP-0199 ping plugin.
|
||||
*
|
||||
* Registers "urn:xmpp:ping" namespace under Strophe.NS.PING.
|
||||
*/
|
||||
module.exports = function (XMPP, eventEmitter) {
|
||||
Strophe.addConnectionPlugin('ping', {
|
||||
|
||||
connection: null,
|
||||
|
||||
failedPings: 0,
|
||||
|
||||
/**
|
||||
* Initializes the plugin. Method called by Strophe.
|
||||
* @param connection Strophe connection instance.
|
||||
*/
|
||||
init: function (connection) {
|
||||
this.connection = connection;
|
||||
Strophe.addNamespace('PING', "urn:xmpp:ping");
|
||||
},
|
||||
|
||||
/**
|
||||
* Sends "ping" to given <tt>jid</tt>
|
||||
* @param jid the JID to which ping request will be sent.
|
||||
* @param success callback called on success.
|
||||
* @param error callback called on error.
|
||||
* @param timeout ms how long are we going to wait for the response. On
|
||||
* timeout <tt>error<//t> callback is called with undefined error
|
||||
* argument.
|
||||
*/
|
||||
ping: function (jid, success, error, timeout) {
|
||||
var iq = $iq({type: 'get', to: jid});
|
||||
iq.c('ping', {xmlns: Strophe.NS.PING});
|
||||
this.connection.sendIQ(iq, success, error, timeout);
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if given <tt>jid</tt> has XEP-0199 ping support.
|
||||
* @param jid the JID to be checked for ping support.
|
||||
* @param callback function with boolean argument which will be
|
||||
* <tt>true</tt> if XEP-0199 ping is supported by given <tt>jid</tt>
|
||||
*/
|
||||
hasPingSupport: function (jid, callback) {
|
||||
this.connection.disco.info(
|
||||
jid, null,
|
||||
function (result) {
|
||||
var ping = $(result).find('>>feature[var="urn:xmpp:ping"]');
|
||||
callback(ping.length > 0);
|
||||
},
|
||||
function (error) {
|
||||
console.error("Ping feature discovery error", error);
|
||||
callback(false);
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Starts to send ping in given interval to specified remote JID.
|
||||
* This plugin supports only one such task and <tt>stopInterval</tt>
|
||||
* must be called before starting a new one.
|
||||
* @param remoteJid remote JID to which ping requests will be sent to.
|
||||
* @param interval task interval in ms.
|
||||
*/
|
||||
startInterval: function (remoteJid, interval) {
|
||||
if (this.intervalId) {
|
||||
console.error("Ping task scheduled already");
|
||||
return;
|
||||
}
|
||||
if (!interval)
|
||||
interval = PING_INTERVAL;
|
||||
var self = this;
|
||||
this.intervalId = window.setInterval(function () {
|
||||
self.ping(remoteJid,
|
||||
function (result) {
|
||||
// Ping OK
|
||||
self.failedPings = 0;
|
||||
},
|
||||
function (error) {
|
||||
self.failedPings += 1;
|
||||
console.error(
|
||||
"Ping " + (error ? "error" : "timeout"), error);
|
||||
if (self.failedPings >= PING_THRESHOLD) {
|
||||
self.connection.disconnect();
|
||||
}
|
||||
}, PING_TIMEOUT);
|
||||
}, interval);
|
||||
console.info("XMPP pings will be sent every " + interval + " ms");
|
||||
},
|
||||
|
||||
/**
|
||||
* Stops current "ping" interval task.
|
||||
*/
|
||||
stopInterval: function () {
|
||||
if (this.intervalId) {
|
||||
window.clearInterval(this.intervalId);
|
||||
this.intervalId = null;
|
||||
this.failedPings = 0;
|
||||
console.info("Ping interval cleared");
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
|
@ -1,96 +0,0 @@
|
|||
/* jshint -W117 */
|
||||
module.exports = function() {
|
||||
Strophe.addConnectionPlugin('rayo',
|
||||
{
|
||||
RAYO_XMLNS: 'urn:xmpp:rayo:1',
|
||||
connection: null,
|
||||
init: function (conn) {
|
||||
this.connection = conn;
|
||||
if (this.connection.disco) {
|
||||
this.connection.disco.addFeature('urn:xmpp:rayo:client:1');
|
||||
}
|
||||
|
||||
this.connection.addHandler(
|
||||
this.onRayo.bind(this), this.RAYO_XMLNS, 'iq', 'set',
|
||||
null, null);
|
||||
},
|
||||
onRayo: function (iq) {
|
||||
console.info("Rayo IQ", iq);
|
||||
},
|
||||
dial: function (to, from, roomName, roomPass) {
|
||||
var self = this;
|
||||
var req = $iq(
|
||||
{
|
||||
type: 'set',
|
||||
to: this.connection.emuc.focusMucJid
|
||||
}
|
||||
);
|
||||
req.c('dial',
|
||||
{
|
||||
xmlns: this.RAYO_XMLNS,
|
||||
to: to,
|
||||
from: from
|
||||
});
|
||||
req.c('header',
|
||||
{
|
||||
name: 'JvbRoomName',
|
||||
value: roomName
|
||||
}).up();
|
||||
|
||||
if (roomPass !== null && roomPass.length) {
|
||||
|
||||
req.c('header',
|
||||
{
|
||||
name: 'JvbRoomPassword',
|
||||
value: roomPass
|
||||
}).up();
|
||||
}
|
||||
|
||||
this.connection.sendIQ(
|
||||
req,
|
||||
function (result) {
|
||||
console.info('Dial result ', result);
|
||||
|
||||
var resource = $(result).find('ref').attr('uri');
|
||||
self.call_resource = resource.substr('xmpp:'.length);
|
||||
console.info(
|
||||
"Received call resource: " + self.call_resource);
|
||||
},
|
||||
function (error) {
|
||||
console.info('Dial error ', error);
|
||||
}
|
||||
);
|
||||
},
|
||||
hang_up: function () {
|
||||
if (!this.call_resource) {
|
||||
console.warn("No call in progress");
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
var req = $iq(
|
||||
{
|
||||
type: 'set',
|
||||
to: this.call_resource
|
||||
}
|
||||
);
|
||||
req.c('hangup',
|
||||
{
|
||||
xmlns: this.RAYO_XMLNS
|
||||
});
|
||||
|
||||
this.connection.sendIQ(
|
||||
req,
|
||||
function (result) {
|
||||
console.info('Hangup result ', result);
|
||||
self.call_resource = null;
|
||||
},
|
||||
function (error) {
|
||||
console.info('Hangup error ', error);
|
||||
self.call_resource = null;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
|
@ -1,43 +0,0 @@
|
|||
/* global Strophe */
|
||||
/**
|
||||
* Strophe logger implementation. Logs from level WARN and above.
|
||||
*/
|
||||
module.exports = function () {
|
||||
|
||||
Strophe.log = function (level, msg) {
|
||||
switch (level) {
|
||||
case Strophe.LogLevel.WARN:
|
||||
console.warn("Strophe: " + msg);
|
||||
break;
|
||||
case Strophe.LogLevel.ERROR:
|
||||
case Strophe.LogLevel.FATAL:
|
||||
console.error("Strophe: " + msg);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
Strophe.getStatusString = function (status) {
|
||||
switch (status) {
|
||||
case Strophe.Status.ERROR:
|
||||
return "ERROR";
|
||||
case Strophe.Status.CONNECTING:
|
||||
return "CONNECTING";
|
||||
case Strophe.Status.CONNFAIL:
|
||||
return "CONNFAIL";
|
||||
case Strophe.Status.AUTHENTICATING:
|
||||
return "AUTHENTICATING";
|
||||
case Strophe.Status.AUTHFAIL:
|
||||
return "AUTHFAIL";
|
||||
case Strophe.Status.CONNECTED:
|
||||
return "CONNECTED";
|
||||
case Strophe.Status.DISCONNECTED:
|
||||
return "DISCONNECTED";
|
||||
case Strophe.Status.DISCONNECTING:
|
||||
return "DISCONNECTING";
|
||||
case Strophe.Status.ATTACHED:
|
||||
return "ATTACHED";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
};
|
||||
};
|
|
@ -1,616 +0,0 @@
|
|||
/* global $, APP, config, Strophe, Base64, $msg */
|
||||
/* jshint -W101 */
|
||||
var Moderator = require("./moderator");
|
||||
var EventEmitter = require("events");
|
||||
var Recording = require("./recording");
|
||||
var SDP = require("./SDP");
|
||||
var SDPUtil = require("./SDPUtil");
|
||||
var Settings = require("../settings/Settings");
|
||||
var Pako = require("pako");
|
||||
var StreamEventTypes = require("../../service/RTC/StreamEventTypes");
|
||||
var RTCEvents = require("../../service/RTC/RTCEvents");
|
||||
var XMPPEvents = require("../../service/xmpp/XMPPEvents");
|
||||
var retry = require('retry');
|
||||
var RandomUtil = require("../util/RandomUtil");
|
||||
|
||||
var eventEmitter = new EventEmitter();
|
||||
var connection = null;
|
||||
var authenticatedUser = false;
|
||||
|
||||
/**
|
||||
* Utility method that generates user name based on random hex values.
|
||||
* Eg. 12345678-1234-1234-12345678
|
||||
* @returns {string}
|
||||
*/
|
||||
function generateUserName() {
|
||||
return RandomUtil.randomHexString(8) + "-" + RandomUtil.randomHexString(4)
|
||||
+ "-" + RandomUtil.randomHexString(4) + "-"
|
||||
+ RandomUtil.randomHexString(8);
|
||||
}
|
||||
|
||||
function connect(jid, password) {
|
||||
|
||||
var faultTolerantConnect = retry.operation({
|
||||
retries: 3
|
||||
});
|
||||
|
||||
// fault tolerant connect
|
||||
faultTolerantConnect.attempt(function () {
|
||||
|
||||
connection = XMPP.createConnection();
|
||||
Moderator.setConnection(connection);
|
||||
|
||||
connection.jingle.pc_constraints = APP.RTC.getPCConstraints();
|
||||
if (config.useIPv6) {
|
||||
// https://code.google.com/p/webrtc/issues/detail?id=2828
|
||||
if (!connection.jingle.pc_constraints.optional)
|
||||
connection.jingle.pc_constraints.optional = [];
|
||||
connection.jingle.pc_constraints.optional.push({googIPv6: true});
|
||||
}
|
||||
|
||||
// Include user info in MUC presence
|
||||
var settings = Settings.getSettings();
|
||||
if (settings.email) {
|
||||
connection.emuc.addEmailToPresence(settings.email);
|
||||
}
|
||||
if (settings.uid) {
|
||||
connection.emuc.addUserIdToPresence(settings.uid);
|
||||
}
|
||||
if (settings.displayName) {
|
||||
connection.emuc.addDisplayNameToPresence(settings.displayName);
|
||||
}
|
||||
|
||||
|
||||
// connection.connect() starts the connection process.
|
||||
//
|
||||
// As the connection process proceeds, the user supplied callback will
|
||||
// be triggered multiple times with status updates. The callback should
|
||||
// take two arguments - the status code and the error condition.
|
||||
//
|
||||
// The status code will be one of the values in the Strophe.Status
|
||||
// constants. The error condition will be one of the conditions defined
|
||||
// in RFC 3920 or the condition ‘strophe-parsererror’.
|
||||
//
|
||||
// The Parameters wait, hold and route are optional and only relevant
|
||||
// for BOSH connections. Please see XEP 124 for a more detailed
|
||||
// explanation of the optional parameters.
|
||||
//
|
||||
// Connection status constants for use by the connection handler
|
||||
// callback.
|
||||
//
|
||||
// Status.ERROR - An error has occurred (websockets specific)
|
||||
// Status.CONNECTING - The connection is currently being made
|
||||
// Status.CONNFAIL - The connection attempt failed
|
||||
// Status.AUTHENTICATING - The connection is authenticating
|
||||
// Status.AUTHFAIL - The authentication attempt failed
|
||||
// Status.CONNECTED - The connection has succeeded
|
||||
// Status.DISCONNECTED - The connection has been terminated
|
||||
// Status.DISCONNECTING - The connection is currently being terminated
|
||||
// Status.ATTACHED - The connection has been attached
|
||||
|
||||
var anonymousConnectionFailed = false;
|
||||
var connectionFailed = false;
|
||||
var lastErrorMsg;
|
||||
connection.connect(jid, password, function (status, msg) {
|
||||
console.log("(TIME) Strophe " + Strophe.getStatusString(status) +
|
||||
(msg ? "[" + msg + "]" : "") +
|
||||
"\t:" + window.performance.now());
|
||||
if (status === Strophe.Status.CONNECTED) {
|
||||
if (config.useStunTurn) {
|
||||
connection.jingle.getStunAndTurnCredentials();
|
||||
}
|
||||
|
||||
console.info("My Jabber ID: " + connection.jid);
|
||||
|
||||
// Schedule ping ?
|
||||
var pingJid = connection.domain;
|
||||
connection.ping.hasPingSupport(
|
||||
pingJid,
|
||||
function (hasPing) {
|
||||
if (hasPing)
|
||||
connection.ping.startInterval(pingJid);
|
||||
else
|
||||
console.warn("Ping NOT supported by " + pingJid);
|
||||
}
|
||||
);
|
||||
|
||||
if (password)
|
||||
authenticatedUser = true;
|
||||
maybeDoJoin();
|
||||
} else if (status === Strophe.Status.CONNFAIL) {
|
||||
if (msg === 'x-strophe-bad-non-anon-jid') {
|
||||
anonymousConnectionFailed = true;
|
||||
} else {
|
||||
connectionFailed = true;
|
||||
}
|
||||
lastErrorMsg = msg;
|
||||
} else if (status === Strophe.Status.DISCONNECTED) {
|
||||
// Stop ping interval
|
||||
connection.ping.stopInterval();
|
||||
if (anonymousConnectionFailed) {
|
||||
// prompt user for username and password
|
||||
XMPP.promptLogin();
|
||||
} else {
|
||||
|
||||
// Strophe already has built-in HTTP/BOSH error handling and
|
||||
// request retry logic. Requests are resent automatically
|
||||
// until their error count reaches 5. Strophe.js disconnects
|
||||
// if the error count is > 5. We are not replicating this
|
||||
// here.
|
||||
//
|
||||
// The "problem" is that failed HTTP/BOSH requests don't
|
||||
// trigger a callback with a status update, so when a
|
||||
// callback with status Strophe.Status.DISCONNECTED arrives,
|
||||
// we can't be sure if it's a graceful disconnect or if it's
|
||||
// triggered by some HTTP/BOSH error.
|
||||
//
|
||||
// But that's a minor issue in Jitsi Meet as we never
|
||||
// disconnect anyway, not even when the user closes the
|
||||
// browser window (which is kind of wrong, but the point is
|
||||
// that we should never ever get disconnected).
|
||||
//
|
||||
// On the other hand, failed connections due to XMPP layer
|
||||
// errors, trigger a callback with status Strophe.Status.CONNFAIL.
|
||||
//
|
||||
// Here we implement retry logic for failed connections due
|
||||
// to XMPP layer errors and we display an error to the user
|
||||
// if we get disconnected from the XMPP server permanently.
|
||||
|
||||
// If the connection failed, retry.
|
||||
if (connectionFailed &&
|
||||
faultTolerantConnect.retry("connection-failed")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If we failed to connect to the XMPP server, fire an event
|
||||
// to let all the interested module now about it.
|
||||
eventEmitter.emit(XMPPEvents.CONNECTION_FAILED,
|
||||
msg ? msg : lastErrorMsg);
|
||||
}
|
||||
} else if (status === Strophe.Status.AUTHFAIL) {
|
||||
// wrong password or username, prompt user
|
||||
XMPP.promptLogin();
|
||||
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function maybeDoJoin() {
|
||||
if (connection && connection.connected &&
|
||||
Strophe.getResourceFromJid(connection.jid) &&
|
||||
(APP.RTC.localAudio || APP.RTC.localVideo)) {
|
||||
// .connected is true while connecting?
|
||||
doJoin();
|
||||
}
|
||||
}
|
||||
|
||||
function doJoin() {
|
||||
eventEmitter.emit(XMPPEvents.READY_TO_JOIN);
|
||||
}
|
||||
|
||||
function initStrophePlugins()
|
||||
{
|
||||
require("./strophe.emuc")(XMPP, eventEmitter);
|
||||
require("./strophe.jingle")(XMPP, eventEmitter);
|
||||
require("./strophe.moderate")(XMPP, eventEmitter);
|
||||
require("./strophe.util")();
|
||||
require("./strophe.ping")(XMPP, eventEmitter);
|
||||
require("./strophe.rayo")();
|
||||
require("./strophe.logger")();
|
||||
}
|
||||
|
||||
/**
|
||||
* If given <tt>localStream</tt> is video one this method will advertise it's
|
||||
* video type in MUC presence.
|
||||
* @param localStream new or modified <tt>LocalStream</tt>.
|
||||
*/
|
||||
function broadcastLocalVideoType(localStream) {
|
||||
if (localStream.videoType)
|
||||
XMPP.addToPresence('videoType', localStream.videoType);
|
||||
}
|
||||
|
||||
function registerListeners() {
|
||||
APP.RTC.addStreamListener(
|
||||
function (localStream) {
|
||||
maybeDoJoin();
|
||||
broadcastLocalVideoType(localStream);
|
||||
},
|
||||
StreamEventTypes.EVENT_TYPE_LOCAL_CREATED
|
||||
);
|
||||
APP.RTC.addStreamListener(
|
||||
broadcastLocalVideoType,
|
||||
StreamEventTypes.EVENT_TYPE_LOCAL_CHANGED
|
||||
);
|
||||
APP.RTC.addListener(RTCEvents.AVAILABLE_DEVICES_CHANGED, function (devices) {
|
||||
XMPP.addToPresence("devices", devices);
|
||||
});
|
||||
}
|
||||
|
||||
var unload = (function () {
|
||||
var unloaded = false;
|
||||
|
||||
return function () {
|
||||
if (unloaded) { return; }
|
||||
unloaded = true;
|
||||
|
||||
if (connection && connection.connected) {
|
||||
// ensure signout
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: config.bosh,
|
||||
async: false,
|
||||
cache: false,
|
||||
contentType: 'application/xml',
|
||||
data: "<body rid='" +
|
||||
(connection.rid || connection._proto.rid) +
|
||||
"' xmlns='http://jabber.org/protocol/httpbind' sid='" +
|
||||
(connection.sid || connection._proto.sid) +
|
||||
"' type='terminate'>" +
|
||||
"<presence xmlns='jabber:client' type='unavailable'/>" +
|
||||
"</body>",
|
||||
success: function (data) {
|
||||
console.log('signed out');
|
||||
console.log(data);
|
||||
},
|
||||
error: function (XMLHttpRequest, textStatus, errorThrown) {
|
||||
console.log('signout error',
|
||||
textStatus + ' (' + errorThrown + ')');
|
||||
}
|
||||
});
|
||||
}
|
||||
XMPP.disposeConference(true);
|
||||
};
|
||||
})();
|
||||
|
||||
function setupEvents() {
|
||||
// In recent versions of FF the 'beforeunload' event is not fired when the
|
||||
// window or the tab is closed. It is only fired when we leave the page
|
||||
// (change URL). If this participant doesn't unload properly, then it
|
||||
// becomes a ghost for the rest of the participants that stay in the
|
||||
// conference. Thankfully handling the 'unload' event in addition to the
|
||||
// 'beforeunload' event seems to guarantee the execution of the 'unload'
|
||||
// method at least once.
|
||||
//
|
||||
// The 'unload' method can safely be run multiple times, it will actually do
|
||||
// something only the first time that it's run, so we're don't have to worry
|
||||
// about browsers that fire both events.
|
||||
|
||||
$(window).bind('beforeunload', unload);
|
||||
$(window).bind('unload', unload);
|
||||
}
|
||||
|
||||
var XMPP = {
|
||||
getConnection: function(){ return connection; },
|
||||
sessionTerminated: false,
|
||||
|
||||
/**
|
||||
* XMPP connection status
|
||||
*/
|
||||
Status: Strophe.Status,
|
||||
|
||||
/**
|
||||
* Remembers if we were muted by the focus.
|
||||
* @type {boolean}
|
||||
*/
|
||||
forceMuted: false,
|
||||
start: function () {
|
||||
setupEvents();
|
||||
initStrophePlugins();
|
||||
registerListeners();
|
||||
Moderator.init(this, eventEmitter);
|
||||
Recording.init();
|
||||
var configDomain = config.hosts.anonymousdomain || config.hosts.domain;
|
||||
// Force authenticated domain if room is appended with '?login=true'
|
||||
if (config.hosts.anonymousdomain &&
|
||||
window.location.search.indexOf("login=true") !== -1) {
|
||||
configDomain = config.hosts.domain;
|
||||
}
|
||||
var jid = configDomain || window.location.hostname;
|
||||
var password = null;
|
||||
if (config.token) {
|
||||
password = config.token;
|
||||
if (config.id) {
|
||||
jid = config.id + "@" + jid;
|
||||
} else {
|
||||
jid = generateUserName() + "@" + jid;
|
||||
}
|
||||
}
|
||||
connect(jid, password);
|
||||
},
|
||||
createConnection: function () {
|
||||
var bosh = config.bosh || '/http-bind';
|
||||
// adds the room name used to the bosh connection
|
||||
return new Strophe.Connection(bosh + '?room=' + APP.UI.getRoomNode());
|
||||
},
|
||||
getStatusString: function (status) {
|
||||
return Strophe.getStatusString(status);
|
||||
},
|
||||
promptLogin: function () {
|
||||
eventEmitter.emit(XMPPEvents.PROMPT_FOR_LOGIN, connect);
|
||||
},
|
||||
joinRoom: function(roomName, useNicks, nick) {
|
||||
var roomjid = roomName;
|
||||
|
||||
if (useNicks) {
|
||||
if (nick) {
|
||||
roomjid += '/' + nick;
|
||||
} else {
|
||||
roomjid += '/' + Strophe.getNodeFromJid(connection.jid);
|
||||
}
|
||||
} else {
|
||||
var tmpJid = Strophe.getNodeFromJid(connection.jid);
|
||||
|
||||
if(!authenticatedUser)
|
||||
tmpJid = tmpJid.substr(0, 8);
|
||||
|
||||
roomjid += '/' + tmpJid;
|
||||
}
|
||||
connection.emuc.doJoin(roomjid);
|
||||
},
|
||||
myJid: function () {
|
||||
if(!connection)
|
||||
return null;
|
||||
return connection.emuc.myroomjid;
|
||||
},
|
||||
myResource: function () {
|
||||
if(!connection || ! connection.emuc.myroomjid)
|
||||
return null;
|
||||
return Strophe.getResourceFromJid(connection.emuc.myroomjid);
|
||||
},
|
||||
getLastPresence: function (from) {
|
||||
if(!connection)
|
||||
return null;
|
||||
return connection.emuc.lastPresenceMap[from];
|
||||
},
|
||||
disposeConference: function (onUnload) {
|
||||
var handler = connection.jingle.activecall;
|
||||
if (handler && handler.peerconnection) {
|
||||
// FIXME: probably removing streams is not required and close() should
|
||||
// be enough
|
||||
if (APP.RTC.localAudio) {
|
||||
handler.peerconnection.removeStream(
|
||||
APP.RTC.localAudio.getOriginalStream(), onUnload);
|
||||
}
|
||||
if (APP.RTC.localVideo) {
|
||||
handler.peerconnection.removeStream(
|
||||
APP.RTC.localVideo.getOriginalStream(), onUnload);
|
||||
}
|
||||
handler.peerconnection.close();
|
||||
}
|
||||
eventEmitter.emit(XMPPEvents.DISPOSE_CONFERENCE, onUnload);
|
||||
connection.jingle.activecall = null;
|
||||
if (!onUnload) {
|
||||
this.sessionTerminated = true;
|
||||
connection.emuc.doLeave();
|
||||
}
|
||||
},
|
||||
addListener: function(type, listener) {
|
||||
eventEmitter.on(type, listener);
|
||||
},
|
||||
removeListener: function (type, listener) {
|
||||
eventEmitter.removeListener(type, listener);
|
||||
},
|
||||
allocateConferenceFocus: function(roomName, callback) {
|
||||
Moderator.allocateConferenceFocus(roomName, callback);
|
||||
},
|
||||
getLoginUrl: function (roomName, callback) {
|
||||
Moderator.getLoginUrl(roomName, callback);
|
||||
},
|
||||
getPopupLoginUrl: function (roomName, callback) {
|
||||
Moderator.getPopupLoginUrl(roomName, callback);
|
||||
},
|
||||
isModerator: function () {
|
||||
return Moderator.isModerator();
|
||||
},
|
||||
isSipGatewayEnabled: function () {
|
||||
return Moderator.isSipGatewayEnabled();
|
||||
},
|
||||
isExternalAuthEnabled: function () {
|
||||
return Moderator.isExternalAuthEnabled();
|
||||
},
|
||||
isConferenceInProgress: function () {
|
||||
return connection && connection.jingle.activecall &&
|
||||
connection.jingle.activecall.peerconnection;
|
||||
},
|
||||
switchStreams: function (stream, oldStream, callback, isAudio) {
|
||||
if (this.isConferenceInProgress()) {
|
||||
// FIXME: will block switchInProgress on true value in case of exception
|
||||
connection.jingle.activecall.switchStreams(stream, oldStream, callback, isAudio);
|
||||
} else {
|
||||
// We are done immediately
|
||||
console.warn("No conference handler or conference not started yet");
|
||||
callback();
|
||||
}
|
||||
},
|
||||
sendVideoInfoPresence: function (mute) {
|
||||
if(!connection)
|
||||
return;
|
||||
connection.emuc.addVideoInfoToPresence(mute);
|
||||
connection.emuc.sendPresence();
|
||||
},
|
||||
setVideoMute: function (mute, callback, options) {
|
||||
if(!connection)
|
||||
return;
|
||||
var self = this;
|
||||
var localCallback = function (mute) {
|
||||
self.sendVideoInfoPresence(mute);
|
||||
return callback(mute);
|
||||
};
|
||||
|
||||
if(connection.jingle.activecall)
|
||||
{
|
||||
connection.jingle.activecall.setVideoMute(
|
||||
mute, localCallback, options);
|
||||
}
|
||||
else {
|
||||
localCallback(mute);
|
||||
}
|
||||
|
||||
},
|
||||
setAudioMute: function (mute, callback) {
|
||||
if (!(connection && APP.RTC.localAudio)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.forceMuted && !mute) {
|
||||
console.info("Asking focus for unmute");
|
||||
connection.moderate.setMute(connection.emuc.myroomjid, mute);
|
||||
// FIXME: wait for result before resetting muted status
|
||||
this.forceMuted = false;
|
||||
}
|
||||
|
||||
if (mute == APP.RTC.localAudio.isMuted()) {
|
||||
// Nothing to do
|
||||
return true;
|
||||
}
|
||||
|
||||
APP.RTC.localAudio.setMute(mute);
|
||||
this.sendAudioInfoPresence(mute, callback);
|
||||
return true;
|
||||
},
|
||||
sendAudioInfoPresence: function(mute, callback) {
|
||||
if(connection) {
|
||||
connection.emuc.addAudioInfoToPresence(mute);
|
||||
connection.emuc.sendPresence();
|
||||
}
|
||||
callback();
|
||||
return true;
|
||||
},
|
||||
toggleRecording: function (tokenEmptyCallback,
|
||||
recordingStateChangeCallback) {
|
||||
Recording.toggleRecording(tokenEmptyCallback,
|
||||
recordingStateChangeCallback, connection);
|
||||
},
|
||||
addToPresence: function (name, value, dontSend) {
|
||||
switch (name) {
|
||||
case "displayName":
|
||||
connection.emuc.addDisplayNameToPresence(value);
|
||||
break;
|
||||
case "prezi":
|
||||
connection.emuc.addPreziToPresence(value, 0);
|
||||
break;
|
||||
case "preziSlide":
|
||||
connection.emuc.addCurrentSlideToPresence(value);
|
||||
break;
|
||||
case "connectionQuality":
|
||||
connection.emuc.addConnectionInfoToPresence(value);
|
||||
break;
|
||||
case "email":
|
||||
connection.emuc.addEmailToPresence(value);
|
||||
break;
|
||||
case "devices":
|
||||
connection.emuc.addDevicesToPresence(value);
|
||||
break;
|
||||
case "videoType":
|
||||
connection.emuc.addVideoTypeToPresence(value);
|
||||
break;
|
||||
case "startMuted":
|
||||
if(!Moderator.isModerator())
|
||||
return;
|
||||
connection.emuc.addStartMutedToPresence(value[0],
|
||||
value[1]);
|
||||
break;
|
||||
default :
|
||||
console.log("Unknown tag for presence: " + name);
|
||||
return;
|
||||
}
|
||||
if (!dontSend)
|
||||
connection.emuc.sendPresence();
|
||||
},
|
||||
/**
|
||||
* Sends 'data' as a log message to the focus. Returns true iff a message
|
||||
* was sent.
|
||||
* @param data
|
||||
* @returns {boolean} true iff a message was sent.
|
||||
*/
|
||||
sendLogs: function (data) {
|
||||
if(!connection.emuc.focusMucJid)
|
||||
return false;
|
||||
|
||||
var deflate = true;
|
||||
|
||||
var content = JSON.stringify(data);
|
||||
if (deflate) {
|
||||
content = String.fromCharCode.apply(null, Pako.deflateRaw(content));
|
||||
}
|
||||
content = Base64.encode(content);
|
||||
// XEP-0337-ish
|
||||
var message = $msg({to: connection.emuc.focusMucJid, type: 'normal'});
|
||||
message.c('log', { xmlns: 'urn:xmpp:eventlog',
|
||||
id: 'PeerConnectionStats'});
|
||||
message.c('message').t(content).up();
|
||||
if (deflate) {
|
||||
message.c('tag', {name: "deflated", value: "true"}).up();
|
||||
}
|
||||
message.up();
|
||||
|
||||
connection.send(message);
|
||||
return true;
|
||||
},
|
||||
// Gets the logs from strophe.jingle.
|
||||
getJingleLog: function () {
|
||||
return connection.jingle ? connection.jingle.getLog() : {};
|
||||
},
|
||||
// Gets the logs from strophe.
|
||||
getXmppLog: function () {
|
||||
return connection.logger ? connection.logger.log : null;
|
||||
},
|
||||
getPrezi: function () {
|
||||
return connection.emuc.getPrezi(this.myJid());
|
||||
},
|
||||
removePreziFromPresence: function () {
|
||||
connection.emuc.removePreziFromPresence();
|
||||
connection.emuc.sendPresence();
|
||||
},
|
||||
sendChatMessage: function (message, nickname) {
|
||||
connection.emuc.sendMessage(message, nickname);
|
||||
},
|
||||
setSubject: function (topic) {
|
||||
connection.emuc.setSubject(topic);
|
||||
},
|
||||
lockRoom: function (key, onSuccess, onError, onNotSupported) {
|
||||
connection.emuc.lockRoom(key, onSuccess, onError, onNotSupported);
|
||||
},
|
||||
dial: function (to, from, roomName,roomPass) {
|
||||
connection.rayo.dial(to, from, roomName,roomPass);
|
||||
},
|
||||
setMute: function (jid, mute) {
|
||||
connection.moderate.setMute(jid, mute);
|
||||
},
|
||||
eject: function (jid) {
|
||||
connection.moderate.eject(jid);
|
||||
},
|
||||
logout: function (callback) {
|
||||
Moderator.logout(callback);
|
||||
},
|
||||
findJidFromResource: function (resource) {
|
||||
return connection.emuc.findJidFromResource(resource);
|
||||
},
|
||||
getMembers: function () {
|
||||
return connection.emuc.members;
|
||||
},
|
||||
getJidFromSSRC: function (ssrc) {
|
||||
if (!this.isConferenceInProgress())
|
||||
return null;
|
||||
return connection.jingle.activecall.getSsrcOwner(ssrc);
|
||||
},
|
||||
// Returns true iff we have joined the MUC.
|
||||
isMUCJoined: function () {
|
||||
return connection === null ? false : connection.emuc.joined;
|
||||
},
|
||||
getSessions: function () {
|
||||
return connection.jingle.sessions;
|
||||
},
|
||||
removeStream: function (stream) {
|
||||
if (!this.isConferenceInProgress())
|
||||
return;
|
||||
connection.jingle.activecall.peerconnection.removeStream(stream);
|
||||
},
|
||||
filter_special_chars: function (text) {
|
||||
return SDPUtil.filter_special_chars(text);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = XMPP;
|
|
@ -2,7 +2,6 @@ export default {
|
|||
NICKNAME_CHANGED: "UI.nickname_changed",
|
||||
SELECTED_ENDPOINT: "UI.selected_endpoint",
|
||||
PINNED_ENDPOINT: "UI.pinned_endpoint",
|
||||
LARGEVIDEO_INIT: "UI.largevideo_init",
|
||||
/**
|
||||
* Notifies that local user created text message.
|
||||
*/
|
||||
|
@ -22,6 +21,9 @@ export default {
|
|||
AUDIO_MUTED: "UI.audio_muted",
|
||||
VIDEO_MUTED: "UI.video_muted",
|
||||
PREZI_CLICKED: "UI.prezi_clicked",
|
||||
SHARE_PREZI: "UI.share_prezi",
|
||||
PREZI_SLIDE_CHANGED: "UI.prezi_slide_changed",
|
||||
STOP_SHARING_PREZI: "UI.stop_sharing_prezi",
|
||||
ETHERPAD_CLICKED: "UI.etherpad_clicked",
|
||||
ROOM_LOCK_CLICKED: "UI.room_lock_clicked",
|
||||
USER_INVITED: "UI.user_invited",
|
||||
|
@ -32,6 +34,7 @@ export default {
|
|||
TOGGLE_SETTINGS: "UI.toggle_settings",
|
||||
TOGGLE_CONTACT_LIST: "UI.toggle_contact_list",
|
||||
TOGGLE_FILM_STRIP: "UI.toggle_film_strip",
|
||||
TOGGLE_SCREENSHARING: "UI.toggle_screensharing",
|
||||
CONTACT_CLICKED: "UI.contact_clicked",
|
||||
HANGUP: "UI.hangup",
|
||||
LOGOUT: "UI.logout",
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
var DesktopSharingEventTypes = {
|
||||
INIT: "ds.init",
|
||||
|
||||
export default {
|
||||
SWITCHING_DONE: "ds.switching_done",
|
||||
|
||||
NEW_STREAM_CREATED: "ds.new_stream_created",
|
||||
|
@ -11,5 +9,3 @@ var DesktopSharingEventTypes = {
|
|||
*/
|
||||
FIREFOX_EXTENSION_NEEDED: "ds.firefox_extension_needed"
|
||||
};
|
||||
|
||||
module.exports = DesktopSharingEventTypes;
|
Loading…
Reference in New Issue