Merge branch 'master' into jingle-protocol-changes

pull/82/head
Philipp Hancke 10 years ago
commit 8c3b533bb7
  1. 23
      INSTALL.md
  2. 509
      app.js
  3. 217
      audio_levels.js
  4. 32
      bottom_toolbar.js
  5. 109
      canvas_util.js
  6. 55
      chat.js
  7. 22
      config.js
  8. 235
      contact_list.js
  9. 35
      css/contact_list.css
  10. 22
      css/font.css
  11. 254
      css/main.css
  12. 2
      css/modaldialog.css
  13. 60
      css/videolayout_default.css
  14. 82
      data_channels.js
  15. 6
      debian/changelog
  16. 1
      debian/compat
  17. 33
      debian/control
  18. 7
      debian/jitsi-meet-prosody.README.Debian
  19. 31
      debian/jitsi-meet-prosody.copyright
  20. 1
      debian/jitsi-meet-prosody.docs
  21. 1
      debian/jitsi-meet-prosody.install
  22. 59
      debian/jitsi-meet-prosody.postinst
  23. 48
      debian/jitsi-meet-prosody.postrm
  24. 35
      debian/jitsi-meet-prosody.preinst
  25. 36
      debian/jitsi-meet-prosody.prerm
  26. 1
      debian/jitsi-meet-prosody.substvars
  27. 8
      debian/jitsi-meet.README.Debian
  28. 6
      debian/jitsi-meet.README.source
  29. 31
      debian/jitsi-meet.copyright
  30. 3
      debian/jitsi-meet.docs
  31. 2
      debian/jitsi-meet.install
  32. 64
      debian/jitsi-meet.postinst
  33. 48
      debian/jitsi-meet.postrm
  34. 35
      debian/jitsi-meet.preinst
  35. 36
      debian/jitsi-meet.prerm
  36. 16
      debian/rules
  37. 1
      debian/source/format
  38. 19
      debian/source/include-binaries
  39. 1
      debian/usr/share/doc/jitsi-meet-prosody/README
  40. BIN
      debian/usr/share/doc/jitsi-meet-prosody/changelog.Debian.gz
  41. 31
      debian/usr/share/doc/jitsi-meet-prosody/copyright
  42. BIN
      debian/usr/share/doc/jitsi-meet-prosody/prosody.cfg.lua-jvb.example.gz
  43. 13
      debian/usr/share/doc/jitsi-meet/README
  44. BIN
      debian/usr/share/doc/jitsi-meet/changelog.Debian.gz
  45. 31
      debian/usr/share/doc/jitsi-meet/copyright
  46. 36
      debian/usr/share/doc/jitsi-meet/jitsi-meet.example
  47. 103
      doc/quick-install.md
  48. BIN
      favicon.ico
  49. BIN
      fonts/jitsi.eot
  50. 12
      fonts/jitsi.svg
  51. BIN
      fonts/jitsi.ttf
  52. BIN
      fonts/jitsi.woff
  53. BIN
      images/avatar2.png
  54. BIN
      images/welcome_page/bubble.png
  55. BIN
      images/welcome_page/jitsi-logo.png
  56. BIN
      images/welcome_page/pattern-body.png
  57. BIN
      images/welcome_page/pattern-header.png
  58. 291
      index.html
  59. 455
      libs/colibri/colibri.focus.js
  60. 15008
      libs/jquery-ui.js
  61. 6
      libs/jquery.min.js
  62. 94
      libs/rayo.js
  63. 51
      libs/strophe/strophe.jingle.adapter.js
  64. 16
      libs/strophe/strophe.jingle.sdp.util.js
  65. 5
      libs/strophe/strophe.jingle.session.js
  66. 131
      local_stats.js
  67. 30
      media_stream.js
  68. 25
      muc.js
  69. 4
      rtp_stats.js
  70. 135
      toolbar.js
  71. 4
      util.js
  72. 610
      videolayout.js

@ -230,3 +230,26 @@ org.jitsi.videobridge.NAT_HARVESTER_PUBLIC_ADDRESS=<Public.IP.Address>
# Hold your first conference
You are now all set and ready to have your first meet by going to http://jitsi.example.com
## Enabling recording
Currently recording is only supported for linux-64 and macos. To enable it, add
the following properties to sip-communicator.properties:
```
org.jitsi.videobridge.ENABLE_MEDIA_RECORDING=true
org.jitsi.videobridge.MEDIA_RECORDING_PATH=/path/to/recordings/dir
org.jitsi.videobridge.MEDIA_RECORDING_TOKEN=secret
```
where /path/to/recordings/dir is the path to a pre-existing directory where recordings
will be stored (needs to be writeable by the user running jitsi-videobridge),
and "secret" is a string which will be used for authentication.
Then, edit the Jitsi-Meet config.js file and set:
```
enableRecording: true
```
Restart jitsi-videobridge and start a new conference (making sure that the page
is reloaded with the new config.js) -- the organizer of the conference should
now have a "recoriding" button in the floating menu, near the "mute" button.

509
app.js

@ -1,19 +1,30 @@
/* jshint -W117 */
/* application specific logic */
var connection = null;
var authenticatedUser = false;
var focus = null;
var activecall = null;
var RTC = null;
var nickname = null;
var sharedKey = '';
var recordingToken ='';
var roomUrl = null;
var roomName = null;
var ssrc2jid = {};
var mediaStreams = [];
/**
* The stats collector that process stats data and triggers updates to app.js.
* @type {StatsCollector}
*/
var statsCollector = null;
/**
* The stats collector for the local stream.
* @type {LocalStatsCollector}
*/
var localStatsCollector = null;
/**
* Indicates whether ssrc is camera video or desktop stream.
* FIXME: remove those maps
@ -58,6 +69,11 @@ function init() {
return;
}
var jid = document.getElementById('jid').value || config.hosts.anonymousdomain || config.hosts.domain || window.location.hostname;
connect(jid);
}
function connect(jid, password) {
connection = new Strophe.Connection(document.getElementById('boshURL').value || config.bosh || '/http-bind');
if (nickname) {
@ -74,9 +90,11 @@ function init() {
connection.jingle.pc_constraints.optional.push({googIPv6: true});
}
var jid = document.getElementById('jid').value || config.hosts.domain || window.location.hostname;
if(!password)
password = document.getElementById('password').value;
connection.connect(jid, document.getElementById('password').value, function (status) {
var anonymousConnectionFailed = false;
connection.connect(jid, password, function (status, msg) {
if (status === Strophe.Status.CONNECTED) {
console.log('connected');
if (config.useStunTurn) {
@ -90,6 +108,23 @@ function init() {
});
document.getElementById('connect').disabled = true;
if(password)
authenticatedUser = true;
} else if (status === Strophe.Status.CONNFAIL) {
if(msg === 'x-strophe-bad-non-anon-jid') {
anonymousConnectionFailed = true;
}
console.log('status', status);
} else if (status === Strophe.Status.DISCONNECTED) {
if(anonymousConnectionFailed) {
// prompt user for username and password
$(document).trigger('passwordrequired.main');
}
} else if (status === Strophe.Status.AUTHFAIL) {
// wrong password or username, prompt user
$(document).trigger('passwordrequired.main');
} else {
console.log('status', status);
}
@ -123,6 +158,8 @@ function audioStreamReady(stream) {
VideoLayout.changeLocalAudio(stream);
startLocalRtpStatsCollector(stream);
if (RTC.browser !== 'firefox') {
getUserMediaWithConstraints(['video'],
videoStreamReady,
@ -173,7 +210,9 @@ function doJoin() {
}
}
roomjid = roomnode + '@' + config.hosts.muc;
roomName = roomnode + '@' + config.hosts.muc;
roomjid = roomName;
if (config.useNicks) {
var nick = window.prompt('Your nickname (optional)');
@ -183,45 +222,52 @@ function doJoin() {
roomjid += '/' + Strophe.getNodeFromJid(connection.jid);
}
} else {
roomjid += '/' + Strophe.getNodeFromJid(connection.jid).substr(0, 8);
var tmpJid = Strophe.getNodeFromJid(connection.jid);
if(!authenticatedUser)
tmpJid = tmpJid.substr(0, 8);
roomjid += '/' + tmpJid;
}
connection.emuc.doJoin(roomjid);
}
$(document).bind('remotestreamadded.jingle', function (event, data, sid) {
function waitForRemoteVideo(selector, sid, ssrc) {
if (selector.removed) {
console.warn("media removed before had started", selector);
return;
}
var sess = connection.jingle.sessions[sid];
if (data.stream.id === 'mixedmslabel') return;
var videoTracks = data.stream.getVideoTracks();
// console.log("waiting..", videoTracks, selector[0]);
if (videoTracks.length === 0 || selector[0].currentTime > 0) {
RTC.attachMediaStream(selector, data.stream); // FIXME: why do i have to do this for FF?
// FIXME: add a class that will associate peer Jid, video.src, it's ssrc and video type
// in order to get rid of too many maps
if (ssrc) {
videoSrcToSsrc[sel.attr('src')] = ssrc;
} else {
console.warn("No ssrc given for video", sel);
}
function waitForRemoteVideo(selector, ssrc, stream) {
if (selector.removed || !selector.parent().is(":visible")) {
console.warn("Media removed before had started", selector);
return;
}
$(document).trigger('callactive.jingle', [selector, sid]);
console.log('waitForremotevideo', sess.peerconnection.iceConnectionState, sess.peerconnection.signalingState);
if (stream.id === 'mixedmslabel') return;
if (selector[0].currentTime > 0) {
RTC.attachMediaStream(selector, stream); // FIXME: why do i have to do this for FF?
// FIXME: add a class that will associate peer Jid, video.src, it's ssrc and video type
// in order to get rid of too many maps
if (ssrc && selector.attr('src')) {
videoSrcToSsrc[selector.attr('src')] = ssrc;
} else {
setTimeout(function () { waitForRemoteVideo(selector, sid, ssrc); }, 250);
console.warn("No ssrc given for video", selector);
}
$(document).trigger('videoactive.jingle', [selector]);
} else {
setTimeout(function () {
waitForRemoteVideo(selector, ssrc, stream);
}, 250);
}
}
$(document).bind('remotestreamadded.jingle', function (event, data, sid) {
var sess = connection.jingle.sessions[sid];
var thessrc;
// look up an associated JID for a stream id
if (data.stream.id.indexOf('mixedmslabel') === -1) {
var ssrclines = SDPUtil.find_lines(sess.peerconnection.remoteDescription.sdp, 'a=ssrc');
var ssrclines
= SDPUtil.find_lines(sess.peerconnection.remoteDescription.sdp, 'a=ssrc');
ssrclines = ssrclines.filter(function (line) {
return line.indexOf('mslabel:' + data.stream.label) !== -1;
});
@ -235,11 +281,14 @@ $(document).bind('remotestreamadded.jingle', function (event, data, sid) {
}
}
mediaStreams.push(new MediaStream(data, sid, thessrc));
var container;
var remotes = document.getElementById('remoteVideos');
if (data.peerjid) {
VideoLayout.ensurePeerContainerExists(data.peerjid);
container = document.getElementById(
'participant_' + Strophe.getResourceFromJid(data.peerjid));
} else {
@ -252,78 +301,22 @@ $(document).bind('remotestreamadded.jingle', function (event, data, sid) {
}
// FIXME: for the mixed ms we dont need a video -- currently
container = document.createElement('span');
container.id = 'mixedstream';
container.className = 'videocontainer';
remotes.appendChild(container);
Util.playSoundNotification('userJoined');
}
var isVideo = data.stream.getVideoTracks().length > 0;
var vid = isVideo ? document.createElement('video') : document.createElement('audio');
var id = (isVideo ? 'remoteVideo_' : 'remoteAudio_') + sid + '_' + data.stream.id;
vid.id = id;
vid.autoplay = true;
vid.oncontextmenu = function () { return false; };
container.appendChild(vid);
// TODO: make mixedstream display:none via css?
if (id.indexOf('mixedmslabel') !== -1) {
container.id = 'mixedstream';
$(container).hide();
}
var sel = $('#' + id);
sel.hide();
RTC.attachMediaStream(sel, data.stream);
if (isVideo) {
waitForRemoteVideo(sel, sid, thessrc);
if (container) {
VideoLayout.addRemoteStreamElement( container,
sid,
data.stream,
data.peerjid,
thessrc);
}
data.stream.onended = function () {
console.log('stream ended', this.id);
// Mark video as removed to cancel waiting loop(if video is removed before has started)
sel.removed = true;
sel.remove();
var audioCount = $('#' + container.id + '>audio').length;
var videoCount = $('#' + container.id + '>video').length;
if (!audioCount && !videoCount) {
console.log("Remove whole user", container.id);
// Remove whole container
container.remove();
Util.playSoundNotification('userLeft');
VideoLayout.resizeThumbnails();
}
VideoLayout.checkChangeLargeVideo(vid.src);
};
// Add click handler
sel.click(function () {
VideoLayout.handleVideoThumbClicked(vid.src);
});
// Add hover handler
$(container).hover(
function() {
VideoLayout.showDisplayName(container.id, true);
},
function() {
var videoSrc = null;
if ($('#' + container.id + '>video')
&& $('#' + container.id + '>video').length > 0) {
videoSrc = $('#' + container.id + '>video').get(0).src;
}
// If the video has been "pinned" by the user we want to keep the
// display name on place.
if (focusedVideoSrc !== videoSrc)
VideoLayout.showDisplayName(container.id, false);
}
);
// an attempt to work around https://github.com/jitsi/jitmeet/issues/32
if (isVideo &&
data.peerjid && sess.peerjid === data.peerjid &&
@ -423,21 +416,25 @@ function muteVideo(pc, unmute) {
}
/**
* Callback called by {@link StatsCollector} in intervals supplied to it's
* constructor.
* @param statsCollector {@link StatsCollector} source of the event.
* Callback for audio levels changed.
* @param jid JID of the user
* @param audioLevel the audio level value
*/
function statsUpdated(statsCollector)
function audioLevelUpdated(jid, audioLevel)
{
Object.keys(statsCollector.jid2stats).forEach(function (jid)
var resourceJid;
if(jid === LocalStatsCollector.LOCAL_JID)
{
var peerStats = statsCollector.jid2stats[jid];
Object.keys(peerStats.ssrc2AudioLevel).forEach(function (ssrc)
{
console.info(jid + " audio level: " +
peerStats.ssrc2AudioLevel[ssrc] + " of ssrc: " + ssrc);
});
});
resourceJid = AudioLevels.LOCAL_LEVEL;
if(isAudioMuted())
return;
}
else
{
resourceJid = Strophe.getResourceFromJid(jid);
}
AudioLevels.updateAudioLevel(resourceJid, audioLevel);
}
/**
@ -445,15 +442,52 @@ function statsUpdated(statsCollector)
*/
function startRtpStatsCollector()
{
stopRTPStatsCollector();
if (config.enableRtpStats)
{
statsCollector = new StatsCollector(
getConferenceHandler().peerconnection, 200, statsUpdated);
getConferenceHandler().peerconnection, 200, audioLevelUpdated);
statsCollector.start();
}
}
/**
* Stops the {@link StatsCollector}.
*/
function stopRTPStatsCollector()
{
if (statsCollector)
{
statsCollector.stop();
statsCollector = null;
}
}
/**
* Starts the {@link LocalStatsCollector} if the feature is enabled in config.js
* @param stream the stream that will be used for collecting statistics.
*/
function startLocalRtpStatsCollector(stream)
{
if(config.enableRtpStats)
{
localStatsCollector = new LocalStatsCollector(stream, 100, audioLevelUpdated);
localStatsCollector.start();
}
}
/**
* Stops the {@link LocalStatsCollector}.
*/
function stopLocalRtpStatsCollector()
{
if(localStatsCollector)
{
localStatsCollector.stop();
localStatsCollector = null;
}
}
$(document).bind('callincoming.jingle', function (event, sid) {
var sess = connection.jingle.sessions[sid];
@ -490,19 +524,6 @@ $(document).bind('conferenceCreated.jingle', function (event, focus)
}
});
$(document).bind('callactive.jingle', function (event, videoelem, sid) {
if (videoelem.attr('id').indexOf('mixedmslabel') === -1) {
// ignore mixedmslabela0 and v0
videoelem.show();
VideoLayout.resizeThumbnails();
if (!focusedVideoSrc)
VideoLayout.updateLargeVideo(videoelem.attr('src'), 1);
VideoLayout.showFocusIndicator();
}
});
$(document).bind('callterminated.jingle', function (event, sid, jid, reason) {
// Leave the room if my call has been remotely terminated.
if (connection.emuc.joined && focus == null && reason === 'kick') {
@ -530,8 +551,8 @@ $(document).bind('setLocalDescription.jingle', function (event, sid) {
directions[type] = (
SDPUtil.find_line(media, 'a=sendrecv') ||
SDPUtil.find_line(media, 'a=recvonly') ||
SDPUtil.find_line('a=sendonly') ||
SDPUtil.find_line('a=inactive') ||
SDPUtil.find_line(media, 'a=sendonly') ||
SDPUtil.find_line(media, 'a=inactive') ||
'a=sendrecv').substr(2);
}
});
@ -562,6 +583,17 @@ $(document).bind('joined.muc', function (event, jid, info) {
if (Object.keys(connection.emuc.members).length < 1) {
focus = new ColibriFocus(connection, config.hosts.bridge);
if (nickname !== null) {
focus.setEndpointDisplayName(connection.emuc.myroomjid,
nickname);
}
Toolbar.showSipCallButton(true);
Toolbar.showRecordingButton(false);
}
if (!focus)
{
Toolbar.showSipCallButton(false);
}
if (focus && config.etherpad_base) {
@ -570,18 +602,25 @@ $(document).bind('joined.muc', function (event, jid, info) {
VideoLayout.showFocusIndicator();
// Add myself to the contact list.
ContactList.addContact(jid);
// Once we've joined the muc show the toolbar
Toolbar.showToolbar();
var displayName = '';
if (info.displayName)
displayName = info.displayName + ' (me)';
else
displayName = "Me";
VideoLayout.setDisplayName('localVideoContainer', displayName);
$(document).trigger('displaynamechanged',
['localVideoContainer', displayName]);
});
$(document).bind('entered.muc', function (event, jid, info, pres) {
console.log('entered', jid, info);
console.log('is focus?' + focus ? 'true' : 'false');
// Add Peer's container
@ -592,6 +631,7 @@ $(document).bind('entered.muc', function (event, jid, info, pres) {
if (focus.confid === null) {
console.log('make new conference with', jid);
focus.makeConference(Object.keys(connection.emuc.members));
Toolbar.showRecordingButton(true);
} else {
console.log('invite', jid, 'into conference');
focus.addNewParticipant(jid);
@ -637,17 +677,31 @@ $(document).bind('left.muc', function (event, jid) {
&& !sessionTerminated) {
console.log('welcome to our new focus... myself');
focus = new ColibriFocus(connection, config.hosts.bridge);
if (nickname !== null) {
focus.setEndpointDisplayName(connection.emuc.myroomjid,
nickname);
}
Toolbar.showSipCallButton(true);
if (Object.keys(connection.emuc.members).length > 0) {
focus.makeConference(Object.keys(connection.emuc.members));
Toolbar.showRecordingButton(true);
}
$(document).trigger('focusechanged.muc', [focus]);
}
else if (focus && Object.keys(connection.emuc.members).length === 0) {
console.log('everyone left');
// FIXME: closing the connection is a hack to avoid some
// problemswith reinit
// problems with reinit
disposeConference();
focus = new ColibriFocus(connection, config.hosts.bridge);
if (nickname !== null) {
focus.setEndpointDisplayName(connection.emuc.myroomjid,
nickname);
}
Toolbar.showSipCallButton(true);
Toolbar.showRecordingButton(false);
}
if (connection.emuc.getPrezi(jid)) {
$(document).trigger('presentationremoved.muc',
@ -662,7 +716,7 @@ $(document).bind('presence.muc', function (event, jid, info, pres) {
if (ssrc2jid[ssrc] == jid) {
delete ssrc2jid[ssrc];
}
if (ssrc2videoType == jid) {
if (ssrc2videoType[ssrc] == jid) {
delete ssrc2videoType[ssrc];
}
});
@ -685,25 +739,28 @@ $(document).bind('presence.muc', function (event, jid, info, pres) {
case 'recvonly':
el.hide();
// FIXME: Check if we have to change large video
//VideoLayout.checkChangeLargeVideo(el);
//VideoLayout.updateLargeVideo(el);
break;
}
}
});
if (info.displayName) {
if (jid === connection.emuc.myroomjid) {
VideoLayout.setDisplayName('localVideoContainer',
info.displayName + ' (me)');
} else {
VideoLayout.ensurePeerContainerExists(jid);
VideoLayout.setDisplayName(
'participant_' + Strophe.getResourceFromJid(jid),
info.displayName);
}
if (info.displayName && info.displayName.length > 0)
$(document).trigger('displaynamechanged',
[jid, info.displayName]);
if (focus !== null && info.displayName !== null) {
focus.setEndpointDisplayName(jid, info.displayName);
}
});
$(document).bind('presence.status.muc', function (event, jid, info, pres) {
VideoLayout.setPresenceStatus(
'participant_' + Strophe.getResourceFromJid(jid), info.status);
});
$(document).bind('passwordrequired.muc', function (event, jid) {
console.log('on password required', jid);
@ -728,6 +785,31 @@ $(document).bind('passwordrequired.muc', function (event, jid) {
});
});
$(document).bind('passwordrequired.main', function (event) {
console.log('password is required');
$.prompt('<h2>Password required</h2>' +
'<input id="passwordrequired.username" type="text" placeholder="user@domain.net" autofocus>' +
'<input id="passwordrequired.password" type="password" placeholder="user password">', {
persistent: true,
buttons: { "Ok": true, "Cancel": false},
defaultButton: 1,
loaded: function (event) {
document.getElementById('passwordrequired.username').focus();
},
submit: function (e, v, m, f) {
if (v) {
var username = document.getElementById('passwordrequired.username');
var password = document.getElementById('passwordrequired.password');
if (username.value !== null && password.value != null) {
connect(username.value, password.value);
}
}
}
});
});
/**
* Checks if video identified by given src is desktop stream.
* @param videoSrc eg.
@ -818,6 +900,71 @@ function toggleAudio() {
buttonClick("#mute", "icon-microphone icon-mic-disabled");
}
/**
* Checks whether the audio is muted or not.
* @returns {boolean} true if audio is muted and false if not.
*/
function isAudioMuted()
{
var localAudio = connection.jingle.localAudio;
for (var idx = 0; idx < localAudio.getAudioTracks().length; idx++) {
if(localAudio.getAudioTracks()[idx].enabled === true)
return false;
}
return true;
}
// Starts or stops the recording for the conference.
function toggleRecording() {
if (focus === null || focus.confid === null) {
console.log('non-focus, or conference not yet organized: not enabling recording');
return;
}
if (!recordingToken)
{
$.prompt('<h2>Enter recording token</h2>' +
'<input id="recordingToken" type="text" placeholder="token" autofocus>',
{
persistent: false,
buttons: { "Save": true, "Cancel": false},
defaultButton: 1,
loaded: function (event) {
document.getElementById('recordingToken').focus();
},
submit: function (e, v, m, f) {
if (v) {
var token = document.getElementById('recordingToken');
if (token.value) {
setRecordingToken(Util.escapeHtml(token.value));
toggleRecording();
}
}
}
}
);
return;
}
var oldState = focus.recordingEnabled;
Toolbar.toggleRecordingButtonState();
focus.setRecording(!oldState,
recordingToken,
function (state) {
console.log("New recording state: ", state);
if (state == oldState) //failed to change, reset the token because it might have been wrong
{
Toolbar.toggleRecordingButtonState();
setRecordingToken(null);
}
}
);
}
/**
* Returns an array of the video horizontal and vertical indents,
* so that if fits its parent.
@ -897,6 +1044,33 @@ function getCameraVideoSize(videoWidth,
}
$(document).ready(function () {
if(config.enableWelcomePage && window.location.pathname == "/")
{
$("#videoconference_page").hide();
$("#enter_room_button").click(function()
{
var val = Util.escapeHtml($("#enter_room_field").val());
window.location.pathname = "/" + val;
});
$("#enter_room_field").keydown(function (event) {
if (event.keyCode === 13) {
var val = Util.escapeHtml(this.value);
window.location.pathname = "/" + val;
}
});
if(!config.isBrand)
{
$("#brand_logo").hide();
$("#brand_header").hide();
$("#header_text").hide();
}
return;
}
$("#welcome_page").hide();
Chat.init();
$('body').popover({ selector: '[data-toggle=popover]',
@ -960,10 +1134,10 @@ $(window).bind('beforeunload', function () {
}
});
}
disposeConference();
disposeConference(true);
});
function disposeConference() {
function disposeConference(onUnload) {
var handler = getConferenceHandler();
if (handler && handler.peerconnection) {
// FIXME: probably removing streams is not required and close() should be enough
@ -975,10 +1149,9 @@ function disposeConference() {
}
handler.peerconnection.close();
}
if (statsCollector)
{
statsCollector.stop();
statsCollector = null;
stopRTPStatsCollector();
if(onUnload) {
stopLocalRtpStatsCollector();
}
focus = null;
activecall = null;
@ -1055,6 +1228,10 @@ function setSharedKey(sKey) {
sharedKey = sKey;
}
function setRecordingToken(token) {
recordingToken = token;
}
/**
* Updates the room invite url.
*/
@ -1115,6 +1292,28 @@ function setView(viewName) {
// }
}
function hangUp() {
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 + ')');
}
});
}
disposeConference(true);
}
$(document).bind('fatalError.jingle',
function (event, session, error)
{
@ -1124,3 +1323,31 @@ $(document).bind('fatalError.jingle',
"Your browser version is too old. Please update and try again...");
}
);
function callSipButtonClicked()
{
$.prompt('<h2>Enter SIP number</h2>' +
'<input id="sipNumber" type="text" value="" autofocus>',
{
persistent: false,
buttons: { "Dial": true, "Cancel": false},
defaultButton: 2,
loaded: function (event)
{
document.getElementById('sipNumber').focus();
},
submit: function (e, v, m, f)
{
if (v)
{
var numberInput = document.getElementById('sipNumber');
if (numberInput.value && numberInput.value.length)
{
connection.rayo.dial(
numberInput.value, 'fromnumber', roomName);
}
}
}
}
);
}

@ -0,0 +1,217 @@
/**
* The audio Levels plugin.
*/
var AudioLevels = (function(my) {
var CANVAS_EXTRA = 104;
var CANVAS_RADIUS = 7;
var SHADOW_COLOR = '#00ccff';
var audioLevelCanvasCache = {};
my.LOCAL_LEVEL = 'local';
/**
* Updates the audio level canvas for the given peerJid. If the canvas
* didn't exist we create it.
*/
my.updateAudioLevelCanvas = function (peerJid) {
var resourceJid = null;
var videoSpanId = null;
if (!peerJid)
videoSpanId = 'localVideoContainer';
else {
resourceJid = Strophe.getResourceFromJid(peerJid);
videoSpanId = 'participant_' + resourceJid;
}
videoSpan = document.getElementById(videoSpanId);
if (!videoSpan) {
if (resourceJid)
console.error("No video element for jid", resourceJid);
else
console.error("No video element for local video.");
return;
}
var audioLevelCanvas = $('#' + videoSpanId + '>canvas');
var videoSpaceWidth = $('#remoteVideos').width();
var thumbnailSize
= VideoLayout.calculateThumbnailSize(videoSpaceWidth);
var thumbnailWidth = thumbnailSize[0];
var thumbnailHeight = thumbnailSize[1];
if (!audioLevelCanvas || audioLevelCanvas.length === 0) {
audioLevelCanvas = document.createElement('canvas');
audioLevelCanvas.className = "audiolevel";
audioLevelCanvas.style.bottom = "-" + CANVAS_EXTRA/2 + "px";
audioLevelCanvas.style.left = "-" + CANVAS_EXTRA/2 + "px";
resizeAudioLevelCanvas( audioLevelCanvas,
thumbnailWidth,
thumbnailHeight);
videoSpan.appendChild(audioLevelCanvas);
} else {
audioLevelCanvas = audioLevelCanvas.get(0);
resizeAudioLevelCanvas( audioLevelCanvas,
thumbnailWidth,
thumbnailHeight);
}
};
/**
* Updates the audio level UI for the given resourceJid.
*
* @param resourceJid the resource jid indicating the video element for
* which we draw the audio level
* @param audioLevel the newAudio level to render
*/
my.updateAudioLevel = function (resourceJid, audioLevel) {
drawAudioLevelCanvas(resourceJid, audioLevel);
var videoSpanId = getVideoSpanId(resourceJid);
var audioLevelCanvas = $('#' + videoSpanId + '>canvas').get(0);
if (!audioLevelCanvas)
return;
var drawContext = audioLevelCanvas.getContext('2d');
var canvasCache = audioLevelCanvasCache[resourceJid];
drawContext.clearRect (0, 0,
audioLevelCanvas.width, audioLevelCanvas.height);
drawContext.drawImage(canvasCache, 0, 0);
};
/**
* Resizes the given audio level canvas to match the given thumbnail size.
*/
function resizeAudioLevelCanvas(audioLevelCanvas,
thumbnailWidth,
thumbnailHeight) {
audioLevelCanvas.width = thumbnailWidth + CANVAS_EXTRA;
audioLevelCanvas.height = thumbnailHeight + CANVAS_EXTRA;
};
/**
* Draws the audio level canvas into the cached canvas object.
*
* @param resourceJid the resource jid indicating the video element for
* which we draw the audio level
* @param audioLevel the newAudio level to render
*/
function drawAudioLevelCanvas(resourceJid, audioLevel) {
if (!audioLevelCanvasCache[resourceJid]) {
var videoSpanId = getVideoSpanId(resourceJid);
var audioLevelCanvasOrig = $('#' + videoSpanId + '>canvas').get(0);
/*
* FIXME Testing has shown that audioLevelCanvasOrig may not exist.
* In such a case, the method CanvasUtil.cloneCanvas may throw an
* error. Since audio levels are frequently updated, the errors have
* been observed to pile into the console, strain the CPU.
*/
if (audioLevelCanvasOrig)
{
audioLevelCanvasCache[resourceJid]
= CanvasUtil.cloneCanvas(audioLevelCanvasOrig);
}
}
var canvas = audioLevelCanvasCache[resourceJid];
if (!canvas)
return;
var drawContext = canvas.getContext('2d');
drawContext.clearRect(0, 0, canvas.width, canvas.height);
var shadowLevel = getShadowLevel(audioLevel);
if (shadowLevel > 0)
// drawContext, x, y, w, h, r, shadowColor, shadowLevel
CanvasUtil.drawRoundRectGlow( drawContext,
CANVAS_EXTRA/2, CANVAS_EXTRA/2,
canvas.width - CANVAS_EXTRA,
canvas.height - CANVAS_EXTRA,
CANVAS_RADIUS,
SHADOW_COLOR,
shadowLevel);
};
/**
* Returns the shadow/glow level for the given audio level.
*
* @param audioLevel the audio level from which we determine the shadow
* level
*/
function getShadowLevel (audioLevel) {
var shadowLevel = 0;
if (audioLevel <= 0.3) {
shadowLevel = Math.round(CANVAS_EXTRA/2*(audioLevel/0.3));
}
else if (audioLevel <= 0.6) {
shadowLevel = Math.round(CANVAS_EXTRA/2*((audioLevel - 0.3) / 0.3));
}
else {
shadowLevel = Math.round(CANVAS_EXTRA/2*((audioLevel - 0.6) / 0.4));
}
return shadowLevel;
};
/**
* Returns the video span id corresponding to the given resourceJid or local
* user.
*/
function getVideoSpanId(resourceJid) {
var videoSpanId = null;
if (resourceJid === AudioLevels.LOCAL_LEVEL
|| (connection.emuc.myroomjid && resourceJid
=== Strophe.getResourceFromJid(connection.emuc.myroomjid)))
videoSpanId = 'localVideoContainer';
else
videoSpanId = 'participant_' + resourceJid;
return videoSpanId;
};
/**
* Indicates that the remote video has been resized.
*/
$(document).bind('remotevideo.resized', function (event, width, height) {
var resized = false;
$('#remoteVideos>span>canvas').each(function() {
var canvas = $(this).get(0);
if (canvas.width !== width + CANVAS_EXTRA) {
canvas.width = width + CANVAS_EXTRA;
resized = true;
}
if (canvas.heigh !== height + CANVAS_EXTRA) {
canvas.height = height + CANVAS_EXTRA;
resized = true;
}
});
if (resized)
Object.keys(audioLevelCanvasCache).forEach(function (resourceJid) {
audioLevelCanvasCache[resourceJid].width
= width + CANVAS_EXTRA;
audioLevelCanvasCache[resourceJid].height
= height + CANVAS_EXTRA;
});
});
return my;
})(AudioLevels || {});

@ -0,0 +1,32 @@
var BottomToolbar = (function (my) {
my.toggleChat = function() {
if (ContactList.isVisible()) {
buttonClick("#contactListButton", "active");
ContactList.toggleContactList();
}
buttonClick("#chatBottomButton", "active");
Chat.toggleChat();
};
my.toggleContactList = function() {
if (Chat.isVisible()) {
buttonClick("#chatBottomButton", "active");
Chat.toggleChat();
}
buttonClick("#contactListButton", "active");
ContactList.toggleContactList();
};
$(document).bind("remotevideo.resized", function (event, width, height) {
var bottom = (height - $('#bottomToolbar').outerHeight())/2 + 18;
$('#bottomToolbar').css({bottom: bottom + 'px'});
});
return my;
}(BottomToolbar || {}));

@ -0,0 +1,109 @@
/**
* Utility class for drawing canvas shapes.
*/
var CanvasUtil = (function(my) {
/**
* Draws a round rectangle with a glow. The glowWidth indicates the depth
* of the glow.
*
* @param drawContext the context of the canvas to draw to
* @param x the x coordinate of the round rectangle
* @param y the y coordinate of the round rectangle
* @param w the width of the round rectangle
* @param h the height of the round rectangle
* @param glowColor the color of the glow
* @param glowWidth the width of the glow
*/
my.drawRoundRectGlow
= function(drawContext, x, y, w, h, r, glowColor, glowWidth) {
// Save the previous state of the context.
drawContext.save();
if (w < 2 * r) r = w / 2;
if (h < 2 * r) r = h / 2;
// Draw a round rectangle.
drawContext.beginPath();
drawContext.moveTo(x+r, y);
drawContext.arcTo(x+w, y, x+w, y+h, r);
drawContext.arcTo(x+w, y+h, x, y+h, r);
drawContext.arcTo(x, y+h, x, y, r);
drawContext.arcTo(x, y, x+w, y, r);
drawContext.closePath();
// Add a shadow around the rectangle
drawContext.shadowColor = glowColor;
drawContext.shadowBlur = glowWidth;
drawContext.shadowOffsetX = 0;
drawContext.shadowOffsetY = 0;
// Fill the shape.
drawContext.fill();
drawContext.save();
drawContext.restore();
// 1) Uncomment this line to use Composite Operation, which is doing the
// same as the clip function below and is also antialiasing the round
// border, but is said to be less fast performance wise.
// drawContext.globalCompositeOperation='destination-out';
drawContext.beginPath();
drawContext.moveTo(x+r, y);
drawContext.arcTo(x+w, y, x+w, y+h, r);
drawContext.arcTo(x+w, y+h, x, y+h, r);
drawContext.arcTo(x, y+h, x, y, r);
drawContext.arcTo(x, y, x+w, y, r);
drawContext.closePath();
// 2) Uncomment this line to use Composite Operation, which is doing the
// same as the clip function below and is also antialiasing the round
// border, but is said to be less fast performance wise.
// drawContext.fill();
// Comment these two lines if choosing to do the same with composite
// operation above 1 and 2.
drawContext.clip();
drawContext.clearRect(0, 0, 277, 200);
// Restore the previous context state.
drawContext.restore();
};
/**
* Clones the given canvas.
*
* @return the new cloned canvas.
*/
my.cloneCanvas = function (oldCanvas) {
/*
* FIXME Testing has shown that oldCanvas may not exist. In such a case,
* the method CanvasUtil.cloneCanvas may throw an error. Since audio
* levels are frequently updated, the errors have been observed to pile
* into the console, strain the CPU.
*/
if (!oldCanvas)
return oldCanvas;
//create a new canvas
var newCanvas = document.createElement('canvas');
var context = newCanvas.getContext('2d');
//set dimensions
newCanvas.width = oldCanvas.width;
newCanvas.height = oldCanvas.height;
//apply the old canvas to the new one
context.drawImage(oldCanvas, 0, 0);
//return the new canvas
return newCanvas;
};
return my;
})(CanvasUtil || {});

@ -80,7 +80,7 @@ var Chat = (function (my) {
else {
divClassName = "remoteuser";
if (!$('#chatspace').is(":visible")) {
if (!Chat.isVisible()) {
unreadMessages++;
Util.playSoundNotification('chatNotification');
setVisualNotification(true);
@ -115,8 +115,7 @@ var Chat = (function (my) {
+ '</div>');
$('#chatconversation').animate(
{ scrollTop: $('#chatconversation')[0].scrollHeight}, 1000);
}
};
/**
* Sets the subject to the UI
@ -135,8 +134,7 @@ var Chat = (function (my) {
{
$("#subject").css({display: "block"});
}
}
};
/**
* Opens / closes the chat area.
@ -159,6 +157,10 @@ var Chat = (function (my) {
var horizontalIndent = videoPosition[0];
var verticalIndent = videoPosition[1];
var thumbnailSize = VideoLayout.calculateThumbnailSize(videospaceWidth);
var thumbnailsWidth = thumbnailSize[0];
var thumbnailsHeight = thumbnailSize[1];
if (chatspace.is(":visible")) {
videospace.animate({right: chatSize[0],
width: videospaceWidth,
@ -166,6 +168,21 @@ var Chat = (function (my) {
{queue: false,
duration: 500});
$('#remoteVideos').animate({height: thumbnailsHeight},
{queue: false,
duration: 500});
$('#remoteVideos>span').animate({height: thumbnailsHeight,
width: thumbnailsWidth},
{queue: false,
duration: 500,
complete: function() {
$(document).trigger(
"remotevideo.resized",
[thumbnailsWidth,
thumbnailsHeight]);
}});
$('#largeVideoContainer').animate({ width: videospaceWidth,
height: videospaceHeight},
{queue: false,
@ -187,6 +204,11 @@ var Chat = (function (my) {
duration: 500});
}
else {
// Undock the toolbar when the chat is shown and if we're in a
// video mode.
if (VideoLayout.isLargeVideoVisible())
Toolbar.dockToolbar(false);
videospace.animate({right: chatSize[0],
width: videospaceWidth,
height: videospaceHeight},
@ -198,6 +220,20 @@ var Chat = (function (my) {
}
});
$('#remoteVideos').animate({height: thumbnailsHeight},
{queue: false,
duration: 500});
$('#remoteVideos>span').animate({height: thumbnailsHeight,
width: thumbnailsWidth},
{queue: false,
duration: 500,
complete: function() {
$(document).trigger(
"remotevideo.resized",
[thumbnailsWidth, thumbnailsHeight]);
}});
$('#largeVideoContainer').animate({ width: videospaceWidth,
height: videospaceHeight},
{queue: false,
@ -265,6 +301,13 @@ var Chat = (function (my) {
return [chatWidth, availableHeight];
};
/**
* Indicates if the chat is currently visible.
*/
my.isVisible = function () {
return $('#chatspace').is(":visible");
};
/**
* Resizes the chat conversation.
*/
@ -290,7 +333,7 @@ var Chat = (function (my) {
if (unreadMessages) {
unreadMsgElement.innerHTML = unreadMessages.toString();
Toolbar.showToolbar();
Toolbar.dockToolbar(true);
var chatButtonElement
= document.getElementById('chatButton').parentNode;

@ -1,17 +1,25 @@
var config = {
hosts: {
domain: 'guest.jit.si',
muc: 'meet.jit.si', // FIXME: use XEP-0030
bridge: 'jitsi-videobridge.lambada.jitsi.net' // FIXME: use XEP-0030
domain: 'jitsi-meet.example.com',
//anonymousdomain: 'guest.example.com',
muc: 'conference.jitsi-meet.example.com', // FIXME: use XEP-0030
bridge: 'jitsi-videobridge.jitsi-meet.example.com', // FIXME: use XEP-0030
//call_control: 'callcontrol.jitsi-meet.example.com'
},
// getroomnode: function (path) { return 'someprefixpossiblybasedonpath'; },
// useStunTurn: true, // use XEP-0215 to fetch STUN and TURN server
// useIPv6: true, // ipv6 support. use at your own risk
useNicks: false,
bosh: '//lambada.jitsi.net/http-bind', // FIXME: use xep-0156 for that
bosh: '//jitsi-meet.example.com/http-bind', // FIXME: use xep-0156 for that
desktopSharing: 'ext', // Desktop sharing method. Can be set to 'ext', 'webrtc' or false to disable.
chromeExtensionId: 'diibjkoicjeejcmhdnailmkgecihlobk', // Id of desktop streamer Chrome extension
minChromeExtVersion: '0.1', // Required version of Chrome extension
enableRtpStats: false, // Enables RTP stats processing
openSctp: true //Toggle to enable/disable SCTP channels
};
enableRtpStats: true, // Enables RTP stats processing
openSctp: true, // Toggle to enable/disable SCTP channels
channelLastN: -1, // The default value of the channel attribute last-n.
// useRtcpMux: true,
// useBundle: true,
enableRecording: false,
enableWelcomePage: false,
isBrand: false
};

@ -0,0 +1,235 @@
/**
* Contact list.
*/
var ContactList = (function (my) {
/**
* Indicates if the chat is currently visible.
*
* @return <tt>true</tt> if the chat is currently visible, <tt>false</tt> -
* otherwise
*/
my.isVisible = function () {
return $('#contactlist').is(":visible");
};
/**
* Adds a contact for the given peerJid if such doesn't yet exist.
*
* @param peerJid the peerJid corresponding to the contact
*/
my.ensureAddContact = function(peerJid) {
var resourceJid = Strophe.getResourceFromJid(peerJid);
var contact = $('#contactlist>ul>li[id="' + resourceJid + '"]');
if (!contact || contact.length <= 0)
ContactList.addContact(peerJid);
};
/**
* Adds a contact for the given peer jid.
*
* @param peerJid the jid of the contact to add
*/
my.addContact = function(peerJid) {
var resourceJid = Strophe.getResourceFromJid(peerJid);
var contactlist = $('#contactlist>ul');
var newContact = document.createElement('li');
newContact.id = resourceJid;
newContact.appendChild(createAvatar());
newContact.appendChild(createDisplayNameParagraph("Participant"));
var clElement = contactlist.get(0);
if (resourceJid === Strophe.getResourceFromJid(connection.emuc.myroomjid)
&& $('#contactlist>ul .title')[0].nextSibling.nextSibling)
{
clElement.insertBefore(newContact,
$('#contactlist>ul .title')[0].nextSibling.nextSibling);
}
else {
clElement.appendChild(newContact);
}
};
/**
* Removes a contact for the given peer jid.
*
* @param peerJid the peerJid corresponding to the contact to remove
*/
my.removeContact = function(peerJid) {
var resourceJid = Strophe.getResourceFromJid(peerJid);
var contact = $('#contactlist>ul>li[id="' + resourceJid + '"]');
if (contact && contact.length > 0) {
var contactlist = $('#contactlist>ul');
contactlist.get(0).removeChild(contact.get(0));
}
};
/**
* Opens / closes the contact list area.
*/
my.toggleContactList = function () {
var contactlist = $('#contactlist');
var videospace = $('#videospace');
var chatSize = (ContactList.isVisible()) ? [0, 0] : Chat.getChatSize();
var videospaceWidth = window.innerWidth - chatSize[0];
var videospaceHeight = window.innerHeight;
var videoSize
= getVideoSize(null, null, videospaceWidth, videospaceHeight);
var videoWidth = videoSize[0];
var videoHeight = videoSize[1];
var videoPosition = getVideoPosition(videoWidth,
videoHeight,
videospaceWidth,
videospaceHeight);
var horizontalIndent = videoPosition[0];
var verticalIndent = videoPosition[1];
var thumbnailSize = VideoLayout.calculateThumbnailSize(videospaceWidth);
var thumbnailsWidth = thumbnailSize[0];
var thumbnailsHeight = thumbnailSize[1];
if (ContactList.isVisible()) {
videospace.animate({right: chatSize[0],
width: videospaceWidth,
height: videospaceHeight},
{queue: false,
duration: 500});
$('#remoteVideos').animate({height: thumbnailsHeight},
{queue: false,
duration: 500});
$('#remoteVideos>span').animate({height: thumbnailsHeight,
width: thumbnailsWidth},
{queue: false,
duration: 500,
complete: function() {
$(document).trigger(
"remotevideo.resized",
[thumbnailsWidth,
thumbnailsHeight]);
}});
$('#largeVideoContainer').animate({ width: videospaceWidth,
height: videospaceHeight},
{queue: false,
duration: 500
});
$('#largeVideo').animate({ width: videoWidth,
height: videoHeight,
top: verticalIndent,
bottom: verticalIndent,
left: horizontalIndent,
right: horizontalIndent},
{ queue: false,
duration: 500
});
$('#contactlist').hide("slide", { direction: "right",
queue: false,
duration: 500});
}
else {
// Undock the toolbar when the chat is shown and if we're in a
// video mode.
if (VideoLayout.isLargeVideoVisible())
Toolbar.dockToolbar(false);
videospace.animate({right: chatSize[0],
width: videospaceWidth,
height: videospaceHeight},
{queue: false,
duration: 500,
complete: function () {
contactlist.trigger('shown');
}
});
$('#remoteVideos').animate({height: thumbnailsHeight},
{queue: false,
duration: 500});
$('#remoteVideos>span').animate({height: thumbnailsHeight,
width: thumbnailsWidth},
{queue: false,
duration: 500,
complete: function() {
$(document).trigger(
"remotevideo.resized",
[thumbnailsWidth, thumbnailsHeight]);
}});
$('#largeVideoContainer').animate({ width: videospaceWidth,
height: videospaceHeight},
{queue: false,
duration: 500
});
$('#largeVideo').animate({ width: videoWidth,
height: videoHeight,
top: verticalIndent,
bottom: verticalIndent,
left: horizontalIndent,
right: horizontalIndent},
{queue: false,
duration: 500
});
$('#contactlist').show("slide", { direction: "right",
queue: false,
duration: 500});
}
};
/**
* Creates the avatar element.
*
* @return the newly created avatar element
*/
function createAvatar() {
var avatar = document.createElement('i');
avatar.className = "icon-avatar avatar";
return avatar;
};
/**
* Creates the display name paragraph.
*
* @param displayName the display name to set
*/
function createDisplayNameParagraph(displayName) {
var p = document.createElement('p');
p.innerHTML = displayName;
return p;
};
/**
* Indicates that the display name has changed.
*/
$(document).bind( 'displaynamechanged',
function (event, peerJid, displayName) {
if (peerJid === 'localVideoContainer')
peerJid = connection.emuc.myroomjid;
var resourceJid = Strophe.getResourceFromJid(peerJid);
var contactName = $('#contactlist #' + resourceJid + '>p');
if (contactName && displayName && displayName.length > 0)
contactName.html(displayName);
});
return my;
}(ContactList || {}));

@ -0,0 +1,35 @@
#contactlist {
background-color:rgba(0,0,0,.65);
}
#contactlist>ul {
margin: 0px;
padding: 0px;
}
#contactlist>ul>li {
list-style-type: none;
text-align: left;
color: #FFF;
font-size: 10pt;
padding: 8px 10px;
}
#contactlist>ul>li>p {
display: inline-block;
vertical-align: middle;
margin: 0px;
}
#contactlist>ul>li.title {
color: #00ccff;
font-size: 11pt;
border-bottom: 1px solid #676767;
}
.avatar {
padding: 0px;
margin-right: 10px;
vertical-align: middle;
font-size: 22pt;
}

@ -23,7 +23,24 @@
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-contactList:before {
content: "\e615";
}
.icon-avatar:before {
content: "\e616";
}
.icon-callRetro:before {
content: "\e611";
}
.icon-callModern:before {
content: "\e612";
}
.icon-recDisable:before {
content: "\e613";
}
.icon-recEnable:before {
content: "\e614";
}
.icon-kick1:before {
content: "\e60f";
}
@ -60,6 +77,9 @@
.icon-share-doc:before {
content: "\e605";
}
.icon-telephone:before {
content: "\e611";
}
.icon-security-locked:before {
content: "\e607";
}

@ -8,7 +8,8 @@ html, body{
overflow-x: hidden;
}
#chatspace {
#chatspace,
#contactlist {
display:none;
position:absolute;
float: right;
@ -18,12 +19,14 @@ html, body{
width: 20%;
max-width: 200px;
overflow: hidden;
/* background-color:#dfebf1;*/
background-color:#FFFFFF;
border-left:1px solid #424242;
z-index: 5;
}
#chatspace {
background-color:#FFF;
border-left:1px solid #424242;
}
#chatconversation {
visibility: hidden;
position: relative;
@ -131,18 +134,49 @@ html, body{
}
#chatButton {
-webkit-transition: all .5s ease-in-out;;
-moz-transition: all .5s ease-in-out;;
transition: all .5s ease-in-out;;
-webkit-transition: all .5s ease-in-out;
-moz-transition: all .5s ease-in-out;
transition: all .5s ease-in-out;
}
/*#ffde00*/
#chatButton.active {
-webkit-text-shadow: 0 0 10px #ffffff;
-moz-text-shadow: 0 0 10px #ffffff;
text-shadow: 0 0 10px #ffffff;
-webkit-text-shadow: -1px 0 10px #00ccff,
0 1px 10px #00ccff,
1px 0 10px #00ccff,
0 -1px 10px #00ccff;
-moz-text-shadow: 1px 0 10px #00ccff,
0 1px 10px #00ccff,
1px 0 10px #00ccff,
0 -1px 10px #00ccff;
text-shadow: -1px 0 10px #00ccff,
0 1px 10px #00ccff,
1px 0 10px #00ccff,
0 -1px 10px #00ccff;
}
a.button:hover {
#recordButton {
-webkit-transition: all .5s ease-in-out;
-moz-transition: all .5s ease-in-out;
transition: all .5s ease-in-out;
}
/*#ffde00*/
#recordButton.active {
-webkit-text-shadow: -1px 0 10px #00ccff,
0 1px 10px #00ccff,
1px 0 10px #00ccff,
0 -1px 10px #00ccff;
-moz-text-shadow: 1px 0 10px #00ccff,
0 1px 10px #00ccff,
1px 0 10px #00ccff,
0 -1px 10px #00ccff;
text-shadow: -1px 0 10px #00ccff,
0 1px 10px #00ccff,
1px 0 10px #00ccff,
0 -1px 10px #00ccff;
}
a.button:hover,
a.bottomToolbarButton:hover {
top: 0;
cursor: pointer;
background: rgba(0, 0, 0, 0.3);
@ -165,7 +199,7 @@ a.button:hover {
background: #676767;
}
input[type='text'], textarea {
input[type='text'], input[type='password'], textarea {
display: inline-block;
font-size: 14px;
padding: 5px;
@ -181,7 +215,7 @@ input[type='text'], textarea {
resize: none; /* prevents the user-resizing, adjust to taste */
}
input[type='text'], textarea:focus {
input[type='text'], input[type='password'], textarea:focus {
box-shadow: inset 0 0 3px 2px #ACD8F0; /* provides a more style-able
replacement to the outline */
}
@ -229,3 +263,195 @@ form {
overflow: visible;
z-index: 100;
}
#enter_room_field {
border-radius: 10px;
font-size: 16px;
padding: 15px 55px 10px 30px;
border: none;
-moz-border-radius: 10px;
-webkit-border-radius: 10px;
-webkit-appearance: none;
width: 318px;
height: 55px;
position:absolute;
font-weight: 500;
font-family: Helvetica;
box-shadow: none;
z-index: 2;
}
#enter_room_button {
width: 73px;
height: 45px;
background-color: #16a8fe;
moz-border-radius: 15px;
-webkit-border-radius: 15px;
color: #ffffff;
font-weight: 600;
border: none;
position:absolute;
margin-left: 240px;
margin-top: 5px;
font-size: 19px;
font-family: Helvetica;
padding-top: 6px;
z-index: 2;
outline: none;
}
#enter_room {
margin: 70px auto 0px auto;
width:318px;
}
#welcome_page_header
{
background-image: url(../images/welcome_page/pattern-header.png);
height: 290px;
width: 100%;
position: absolute;
}
#welcome_page_main
{
background-image:url(../images/welcome_page/pattern-body.png);
width: 100%;
position: absolute;
margin-top: 290px;
}
#jitsi_logo
{
background-image:url(../images/welcome_page/jitsi-logo.png);
width: 186px;
height: 74px;
position: absolute;
top: 15px;
left: 30px;
}
#brand_logo
{
background-image:url(../images/welcome_page/brand-logo.png);
width: 215px;
height: 55px;
position: absolute;
top: 15px;
right: 30px;
}
#brand_header
{
background-image:url(../images/welcome_page/header-big.png);
position:absolute;
width: 583px;
height: 274px;
left: 340px;
top:15px;
}
#header_text
{
position: absolute;
left: 200px;
top: 150px;
width: 885px;
height: 100px;
color: #ffffff;
font-family: Helvetica;
font-size: 24px;
text-align: center;
}
#features
{
margin-top: 30px;
}
.feature_row
{
width: 100%;
left: 115px;
position: relative;
float: left;
margin-bottom: 30px;
}
.feature_holder
{
float:left;
width: 169px;
padding-left: 75px;
}
.feature_icon
{
background-image:url(../images/welcome_page/bubble.png);
background-repeat: no-repeat;
width: 169px;
height: 169px;
font-family: Helvetica;
color: #ffffff;
font-size: 22px;
/*font-weight: bold;*/
text-align: center;
display: table-cell;
padding: 50px 29px 0px 17px;
}
.feature_description
{
width: 169px;
font-family: Helvetica;
color: #ffffff;
font-size: 16px;
padding-top: 30px;
line-height: 22px;
font-weight: 200;
}
#bottomToolbar {
display:block;
position: absolute;
right: -1;
bottom: 40px;
width: 29px;
border-top-left-radius: 10px;
border-bottom-left-radius: 10px;
color: #FFF;
border: 1px solid #000;
background: rgba(50,50,50,.65);
padding-top: 5px;
padding-bottom: 5px;
z-index: 6; /*+1 from #remoteVideos*/
}
.bottomToolbarButton {
display: inline-block;
position: relative;
color: #FFFFFF;
top: 0;
padding-top: 3px;
width: 29px;
height: 20px;
cursor: pointer;
font-size: 10pt;
text-align: center;
text-shadow: 0px 1px 0px rgba(255,255,255,.3), 0px -1px 0px rgba(0,0,0,.7);
z-index: 1;
}
.active {
color: #00ccff;
}
.bottomToolbar_span>span {
display: inline-block;
position: absolute;
font-size: 7pt;
color: #ffffff;
text-align:center;
cursor: pointer;
}

@ -15,7 +15,7 @@
margin: 10px 0;
}
.jqistates input[type="text"] {
.jqistates input[type='text'], input[type='password'] {
width: 100%;
}

@ -4,6 +4,7 @@
top: 0px;
left: 0px;
right: 0px;
overflow: hidden;
}
#remoteVideos {
@ -14,7 +15,7 @@
padding: 18px;
bottom: 0;
left: 0;
right: 0;
right: 20px;
width:auto;
border:1px solid transparent;
z-index: 5;
@ -32,6 +33,7 @@
background-size: contain;
border-radius:8px;
border: 2px solid #212425;
margin-right: 3px;
}
#remoteVideos .videocontainer:hover,
@ -48,9 +50,21 @@
-webkit-animation-name: greyPulse;
-webkit-animation-duration: 2s;
-webkit-animation-iteration-count: 1;
-webkit-box-shadow: 0 0 18px #388396;
border: 2px solid #388396;
z-index: 3;
}
#remoteVideos .videocontainer:hover {
-webkit-box-shadow: inset 0 0 10px #FFFFFF, 0 0 10px #FFFFFF;
border: 2px solid #FFFFFF;
}
#remoteVideos .videocontainer.videoContainerFocused {
-webkit-box-shadow: inset 0 0 28px #006d91;
border: 2px solid #006d91;
}
#remoteVideos .videocontainer.videoContainerFocused:hover {
-webkit-box-shadow: inset 0 0 5px #FFFFFF, 0 0 10px #FFFFFF, inset 0 0 60px #006d91;
border: 2px solid #FFFFFF;
}
#localVideoWrapper {
@ -94,9 +108,8 @@
height: 100%;
}
.activespeaker {
-webkit-filter: grayscale(1);
filter: grayscale(1);
.dominantspeaker {
background: #000 !important;
}
#etherpad,
@ -158,10 +171,31 @@
border-radius:20px;
}
.videocontainer>span.status {
display: inline-block;
position: absolute;
color: #FFFFFF;
background: rgba(0,0,0,.7);
text-align: center;
text-overflow: ellipsis;
width: 70%;
height: 15%;
left: 15%;
bottom: 2%;
padding: 5px;
font-size: 10pt;
overflow: hidden;
white-space: nowrap;
z-index: 2;
border-radius:20px;
}
#localVideoContainer>span.status:hover,
#localVideoContainer>span.displayname:hover {
cursor: text;
}
.videocontainer>span.status,
.videocontainer>span.displayname {
pointer-events: none;
}
@ -174,6 +208,7 @@
pointer-events: auto !important;
}
.videocontainer>a.status,
.videocontainer>a.displayname {
display: inline-block;
position: absolute;
@ -292,3 +327,14 @@
background-image:url(../images/rightwatermark.png);
background-position: center right;
}
.audiolevel {
display: inline-block;
position: absolute;
z-index: 0;
border-radius:10px;
}
#mixedstream {
display:none !important;
}

@ -1,16 +1,22 @@
/* global connection, Strophe, updateLargeVideo, focusedVideoSrc*/
// cache datachannels to avoid garbage collection
// https://code.google.com/p/chromium/issues/detail?id=405545
var _dataChannels = [];
/**
* Callback triggered by PeerConnection when new data channel is opened
* on the bridge.
* @param event the event info object.
*/
function onDataChannel(event)
{
var dataChannel = event.channel;
dataChannel.onopen = function ()
{
console.info("Data channel opened by the bridge !!!", dataChannel);
console.info("Data channel opened by the Videobridge!", dataChannel);
// Code sample for sending string and/or binary data
// Sends String message to the bridge
@ -26,30 +32,61 @@ function onDataChannel(event)
dataChannel.onmessage = function (event)
{
var msgData = event.data;
console.info("Got Data Channel Message:", msgData, dataChannel);
var data = event.data;
// JSON
var obj;
// Active speaker event
if (msgData.indexOf('activeSpeaker') === 0 && !focusedVideoSrc)
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))
{
// Endpoint ID from the bridge
var endpointId = msgData.split(":")[1];
console.info("New active speaker: " + endpointId);
var colibriClass = obj.colibriClass;
var container = document.getElementById(
'participant_' + endpointId);
if ("DominantSpeakerEndpointChangeEvent" === colibriClass)
{
// Endpoint ID from the Videobridge.
var dominantSpeakerEndpoint = obj.dominantSpeakerEndpoint;
console.info(
"Data channel new dominant speaker event: ",
dominantSpeakerEndpoint);
$(document).trigger(
'dominantspeakerchanged',
[dominantSpeakerEndpoint]);
}
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;
// Local video will not have container found, but that's ok
// since we don't want to switch to local video
var stream = obj.stream;
if (container)
console.log(
"Data channel new last-n event: ",
lastNEndpoints, endpointsEnteringLastN, obj);
$(document).trigger(
'lastnchanged',
[lastNEndpoints, endpointsEnteringLastN, stream]);
}
else
{
var video = container.getElementsByTagName("video");
if (video.length)
{
VideoLayout.updateLargeVideo(video[0].src);
VideoLayout.enableActiveSpeaker(endpointId, true);
}
console.debug("Data channel JSON-formatted message: ", obj);
}
}
};
@ -57,7 +94,11 @@ function onDataChannel(event)
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);
}
/**
@ -90,4 +131,5 @@ function bindDataChannelListener(peerConnection)
var msgData = event.data;
console.info("Got My Data Channel Message:", msgData, dataChannel);
};*/
}
}

6
debian/changelog vendored

@ -0,0 +1,6 @@
jitsi-meet (1.0.1-1) unstable; urgency=low
* Initial release
* Jitsi Meet github snapshot from 2014-07-01
-- Yasen Pramatarov <yasen@bluejimp.com> Tue, 01 Jul 2014 16:31:41 +0300

1
debian/compat vendored

@ -0,0 +1 @@
8

33
debian/control vendored

@ -0,0 +1,33 @@
Source: jitsi-meet
Section: net
Priority: extra
Maintainer: Jitsi Team <dev@jitsi.org>
Uploaders: Emil Ivov <emcho@jitsi.org>, Damian Minkov <damencho@jitsi.org>
Build-Depends: debhelper (>= 8.0.0)
Standards-Version: 3.9.3
Homepage: https://jitsi.org/meet
Package: jitsi-meet
Architecture: all
Pre-Depends: adduser, openssl, jitsi-videobridge
Depends: ${misc:Depends}, nginx, jitsi-meet-prosody
Description: WebRTC JavaScript video conferences
Jitsi Meet is a WebRTC JavaScript application that uses Jitsi
Videobridge to provide high quality, scalable video conferences.
.
It is a web interface to Jitsi Videobridge for audio and video
forwarding and relaying, configured to work with nginx
Package: jitsi-meet-prosody
Architecture: all
Pre-Depends: adduser, openssl, prosody-trunk, jitsi-videobridge
Depends: ${misc:Depends}, nginx, prosody-modules-otalk, lua-sec
Description: Prosody configuration for Jitsi Meet
Jitsi Meet is a WebRTC JavaScript application that uses Jitsi
Videobridge to provide high quality, scalable video conferences.
.
It is a web interface to Jitsi Videobridge for audio and video
forwarding and relaying, configured to work with nginx
.
This package contains configuration for Prosody to be used with
Jitsi Meet.

@ -0,0 +1,7 @@
Prosody configuration for Jitsi Meet for Debian
----------------------------
Jitsi Meet is a WebRTC video conferencing application. This package contains
configuration of prosody which are needed for Jitsi Meet to work.
-- Yasen Pramatarov <yasen@bluejimp.com> Mon, 30 Jun 2014 23:05:18 +0100

@ -0,0 +1,31 @@
Format: http://dep.debian.net/deps/dep5
Upstream-Name: Jitsi Meet
Upstream-Contact: Emil Ivov <emcho@jitsi.org>
Source: https://github.com/jitsi/jitsi-meet
Files: *
Copyright: 2013-2014 Jitsi
License: MIT
License: MIT
The MIT License (MIT)
.
Copyright (c) 2013 ESTOS GmbH
Copyright (c) 2013 BlueJimp SARL
.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

@ -0,0 +1 @@
debian/usr/share/doc/jitsi-meet-prosody/prosody.cfg.lua-jvb.example.gz

@ -0,0 +1 @@
debian/usr/share/* usr/share/

@ -0,0 +1,59 @@
#!/bin/sh
# postinst script for jitsi-meet-prosody
#
# see: dh_installdeb(1)
set -e
# summary of how this script can be called:
# * <postinst> `configure' <most-recently-configured-version>
# * <old-postinst> `abort-upgrade' <new version>
# * <conflictor's-postinst> `abort-remove' `in-favour' <package>
# <new-version>
# * <postinst> `abort-remove'
# * <deconfigured's-postinst> `abort-deconfigure' `in-favour'
# <failed-install-package> <version> `removing'
# <conflicting-package> <version>
# for details, see http://www.debian.org/doc/debian-policy/ or
# the debian-policy package
case "$1" in
configure)
. /etc/default/jitsi-videobridge
if [ -x /etc/prosody/prosody.cfg.lua ]; then
mv /etc/prosody/prosody.cfg.lua /etc/prosody/prosody.cfg.lua.orig
fi
gunzip -c /usr/share/doc/jitsi-meet-prosody/prosody.cfg.lua-jvb.example.gz > /etc/prosody/prosody.cfg.lua
sed -i "s/jitmeet.example.com/$JVB_HOSTNAME/g" /etc/prosody/prosody.cfg.lua
sed -i "s/jitmeetSecret/$JVB_SECRET/g" /etc/prosody/prosody.cfg.lua
if [ ! -f /var/lib/prosody/$JVB_HOSTNAME.crt ]; then
HOST="$( (hostname -s; echo localhost) | head -n 1)"
DOMAIN="$( (hostname -d; echo localdomain) | head -n 1)"
openssl req -new -newkey rsa:4096 -days 365 -nodes -x509 -subj \
"/O=$DOMAIN/OU=$HOST/CN=$JVB_HOSTNAME/emailAddress=webmaster@$HOST.$DOMAIN" \
-keyout /var/lib/prosody/$JVB_HOSTNAME.key \
-out /var/lib/prosody/$JVB_HOSTNAME.crt
fi
ln -sf /var/lib/prosody/$JVB_HOSTNAME.key /etc/prosody/certs/$JVB_HOSTNAME.key
ln -sf /var/lib/prosody/$JVB_HOSTNAME.crt /etc/prosody/certs/$JVB_HOSTNAME.crt
invoke-rc.d prosody restart
;;
abort-upgrade|abort-remove|abort-deconfigure)
;;
*)
echo "postinst called with unknown argument \`$1'" >&2
exit 1
;;
esac
# dh_installdeb will replace this with shell code automatically
# generated by other debhelper scripts.
#DEBHELPER#
exit 0

@ -0,0 +1,48 @@
#!/bin/sh
# postrm script for jitsi-meet-prosody
#
# see: dh_installdeb(1)
set -e
# summary of how this script can be called:
# * <postrm> `remove'
# * <postrm> `purge'
# * <old-postrm> `upgrade' <new-version>
# * <new-postrm> `failed-upgrade' <old-version>
# * <new-postrm> `abort-install'
# * <new-postrm> `abort-install' <old-version>
# * <new-postrm> `abort-upgrade' <old-version>
# * <disappearer's-postrm> `disappear' <overwriter>
# <overwriter-version>
# for details, see http://www.debian.org/doc/debian-policy/ or
# the debian-policy package
# Load debconf
. /usr/share/debconf/confmodule
case "$1" in
purge|remove)
if [ -x "/etc/init.d/prosody" ]; then
invoke-rc.d nginx reload
fi
;;
upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)
;;
*)
echo "postrm called with unknown argument \`$1'" >&2
exit 1
;;
esac
# dh_installdeb will replace this with shell code automatically
# generated by other debhelper scripts.
#DEBHELPER#
db_stop
exit 0

@ -0,0 +1,35 @@
#!/bin/sh
# preinst script for jitsi-meet-prosody
#
# see: dh_installdeb(1)
set -e
# summary of how this script can be called:
# * <new-preinst> `install'
# * <new-preinst> `install' <old-version>
# * <new-preinst> `upgrade' <old-version>
# * <old-preinst> `abort-upgrade' <new-version>
# for details, see http://www.debian.org/doc/debian-policy/ or
# the debian-policy package
case "$1" in
install|upgrade)
;;
abort-upgrade)
;;
*)
echo "preinst called with unknown argument \`$1'" >&2
exit 1
;;
esac
# dh_installdeb will replace this with shell code automatically
# generated by other debhelper scripts.
#DEBHELPER#
exit 0

@ -0,0 +1,36 @@
#!/bin/sh
# prerm script for jitsi-meet-prosody
#
# see: dh_installdeb(1)
set -e
# summary of how this script can be called:
# * <prerm> `remove'
# * <old-prerm> `upgrade' <new-version>
# * <new-prerm> `failed-upgrade' <old-version>
# * <conflictor's-prerm> `remove' `in-favour' <package> <new-version>
# * <deconfigured's-prerm> `deconfigure' `in-favour'
# <package-being-installed> <version> `removing'
# <conflicting-package> <version>
# for details, see http://www.debian.org/doc/debian-policy/ or
# the debian-policy package
case "$1" in
remove|purge)
;;
upgrade|deconfigure)
;;
failed-upgrade)
;;
*)
echo "prerm called with unknown argument \`$1'" >&2
exit 1
;;
esac
exit 0

@ -0,0 +1 @@
misc:Depends=

@ -0,0 +1,8 @@
Jitsi Meet for Debian
----------------------------
This is a WebRTC frontend of the video conferencing tool Jitsi Meet. It depends on the
jitsi-videobridge package, which is a SFU (Selective Forwarding Unit) and both packages
are designed to work together.
-- Yasen Pramatarov <yasen@bluejimp.com> Mon, 30 Jun 2014 23:05:18 +0100

@ -0,0 +1,6 @@
jitsi-meet for Debian
---------------------
The jitsi-meet package is built from the sources of Jitsi Meet.
Jitsi Meet is downloaded from https://github.com/jitsi/jitsi-meet and the git files are removed. you can recreate the source with 'git clone https://github.com/jitsi/jitsi-meet.git'.

@ -0,0 +1,31 @@
Format: http://dep.debian.net/deps/dep5
Upstream-Name: Jitsi Meet
Upstream-Contact: Emil Ivov <emcho@jitsi.org>
Source: https://github.com/jitsi/jitsi-meet
Files: *
Copyright: 2013-2014 Jitsi
License: MIT
License: MIT
The MIT License (MIT)
.
Copyright (c) 2013 ESTOS GmbH
Copyright (c) 2013 BlueJimp SARL
.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

@ -0,0 +1,3 @@
README.md
debian/usr/share/doc/jitsi-meet/README
debian/usr/share/doc/jitsi-meet/jitsi-meet.example

@ -0,0 +1,2 @@
* usr/share/jitsi-meet/
debian/usr/share/* usr/share/

@ -0,0 +1,64 @@
#!/bin/sh
# postinst script for jitsi-meet
#
# see: dh_installdeb(1)
set -e
# summary of how this script can be called:
# * <postinst> `configure' <most-recently-configured-version>
# * <old-postinst> `abort-upgrade' <new version>
# * <conflictor's-postinst> `abort-remove' `in-favour' <package>
# <new-version>
# * <postinst> `abort-remove'
# * <deconfigured's-postinst> `abort-deconfigure' `in-favour'
# <failed-install-package> <version> `removing'
# <conflicting-package> <version>
# for details, see http://www.debian.org/doc/debian-policy/ or
# the debian-policy package
case "$1" in
configure)
# nginx conf
. /etc/default/jitsi-videobridge
cp /usr/share/doc/jitsi-meet/jitsi-meet.example /etc/nginx/sites-available/$JVB_HOSTNAME.conf
if [ ! -f /etc/nginx/sites-enabled/$JVB_HOSTNAME.conf ]; then
ln -s /etc/nginx/sites-available/$JVB_HOSTNAME.conf /etc/nginx/sites-enabled/$JVB_HOSTNAME.conf
fi
sed -i "s/jitsi-meet.example.com/$JVB_HOSTNAME/g" /etc/nginx/sites-available/$JVB_HOSTNAME.conf
# FIXME do we need the default?
if [ ! -f /etc/nginx/sites-enabled/default ]; then
ln -s /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default
fi
if grep "# server_names_hash_bucket_size 64" /etc/nginx/nginx.conf > /dev/null; then
sed -i "s/#\ server_names_hash_bucket_size\ 64/\ server_names_hash_bucket_size\ 64/" /etc/nginx/nginx.conf
fi
# jitsi meet
chown -R www-data:www-data /usr/share/jitsi-meet/
sed -i "s/jitsi-meet.example.com/$JVB_HOSTNAME/g" /usr/share/jitsi-meet/config.js
# enable turn
if grep "// useStunTurn: true," /usr/share/jitsi-meet/config.js > /dev/null; then
sed -i "s/\/\/\ \ useStunTurn:\ true,/\ \ \ \ useStunTurn:\ true,/" /usr/share/jitsi-meet/config.js
fi
invoke-rc.d nginx restart
;;
abort-upgrade|abort-remove|abort-deconfigure)
;;
*)
echo "postinst called with unknown argument \`$1'" >&2
exit 1
;;
esac
# dh_installdeb will replace this with shell code automatically
# generated by other debhelper scripts.
#DEBHELPER#
exit 0

@ -0,0 +1,48 @@
#!/bin/sh
# postrm script for jitsi-meet
#
# see: dh_installdeb(1)
set -e
# summary of how this script can be called:
# * <postrm> `remove'
# * <postrm> `purge'
# * <old-postrm> `upgrade' <new-version>
# * <new-postrm> `failed-upgrade' <old-version>
# * <new-postrm> `abort-install'
# * <new-postrm> `abort-install' <old-version>
# * <new-postrm> `abort-upgrade' <old-version>
# * <disappearer's-postrm> `disappear' <overwriter>
# <overwriter-version>
# for details, see http://www.debian.org/doc/debian-policy/ or
# the debian-policy package
# Load debconf
. /usr/share/debconf/confmodule
case "$1" in
purge|remove)
if [ -x "/etc/init.d/nginx" ]; then
invoke-rc.d nginx reload
fi
;;
upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)
;;
*)
echo "postrm called with unknown argument \`$1'" >&2
exit 1
;;
esac
# dh_installdeb will replace this with shell code automatically
# generated by other debhelper scripts.
#DEBHELPER#
db_stop
exit 0

@ -0,0 +1,35 @@
#!/bin/sh
# preinst script for jitsi-meet
#
# see: dh_installdeb(1)
set -e
# summary of how this script can be called:
# * <new-preinst> `install'
# * <new-preinst> `install' <old-version>
# * <new-preinst> `upgrade' <old-version>
# * <old-preinst> `abort-upgrade' <new-version>
# for details, see http://www.debian.org/doc/debian-policy/ or
# the debian-policy package
case "$1" in
install|upgrade)
;;
abort-upgrade)
;;
*)
echo "preinst called with unknown argument \`$1'" >&2
exit 1
;;
esac
# dh_installdeb will replace this with shell code automatically
# generated by other debhelper scripts.
#DEBHELPER#
exit 0

@ -0,0 +1,36 @@
#!/bin/sh
# prerm script for jitsi-meet
#
# see: dh_installdeb(1)
set -e
# summary of how this script can be called:
# * <prerm> `remove'
# * <old-prerm> `upgrade' <new-version>
# * <new-prerm> `failed-upgrade' <old-version>
# * <conflictor's-prerm> `remove' `in-favour' <package> <new-version>
# * <deconfigured's-prerm> `deconfigure' `in-favour'
# <package-being-installed> <version> `removing'
# <conflicting-package> <version>
# for details, see http://www.debian.org/doc/debian-policy/ or
# the debian-policy package
case "$1" in
remove|purge)
;;
upgrade|deconfigure)
;;
failed-upgrade)
;;
*)
echo "prerm called with unknown argument \`$1'" >&2
exit 1
;;
esac
exit 0

16
debian/rules vendored

@ -0,0 +1,16 @@
#!/usr/bin/make -f
# -*- makefile -*-
# Sample debian/rules that uses debhelper.
# This file was originally written by Joey Hess and Craig Small.
# As a special exception, when this file is copied by dh-make into a
# dh-make output file, you may use that output file without restriction.
# This special exception was added by Craig Small in version 0.37 of dh-make.
# Uncomment this to turn on verbose mode.
#export DH_VERBOSE=1
%:
dh $@
override_dh_install-indep:
dh_install -Xdebian -Xdoc -XINSTALL.md -XLICENSE -XREADME.md usr/share/jitsi-meet/

@ -0,0 +1 @@
3.0 (quilt)

@ -0,0 +1,19 @@
debian/usr/share/jitsi-meet/favicon.ico
debian/usr/share/jitsi-meet/fonts/jitsi.eot
debian/usr/share/jitsi-meet/fonts/jitsi.woff
debian/usr/share/jitsi-meet/fonts/jitsi.ttf
debian/usr/share/jitsi-meet/sounds/left.wav
debian/usr/share/jitsi-meet/sounds/incomingMessage.wav
debian/usr/share/jitsi-meet/sounds/joined.wav
debian/usr/share/jitsi-meet/images/estoslogo.png
debian/usr/share/jitsi-meet/images/chromelogo.png
debian/usr/share/jitsi-meet/images/jitsilogo.png
debian/usr/share/jitsi-meet/images/watermark.png
debian/usr/share/jitsi-meet/images/avatarprezi.png
debian/usr/share/jitsi-meet/images/chromepointer.png
debian/usr/share/jitsi-meet/images/avatar1.png
debian/usr/share/jitsi-meet/images/popupPointer.png
debian/usr/share/jitsi-meet/images/favicon.ico
debian/usr/share/doc/jitsi-meet/changelog.Debian.gz
debian/usr/share/doc/jitsi-meet-prosody/changelog.Debian.gz
debian/usr/share/doc/jitsi-meet-prosody/prosody.cfg.lua-jvb.example.gz

@ -0,0 +1 @@
Prosody configuration for Jitsi Meet

@ -0,0 +1,31 @@
Format: http://dep.debian.net/deps/dep5
Upstream-Name: Jitsi Meet
Upstream-Contact: Emil Ivov <emcho@jitsi.org>
Source: https://github.com/jitsi/jitsi-meet
Files: *
Copyright: 2013-2014 Jitsi
License: MIT
License: MIT
The MIT License (MIT)
.
Copyright (c) 2013 ESTOS GmbH
Copyright (c) 2013 BlueJimp SARL
.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

@ -0,0 +1,13 @@
Jitsi Meet
====
A WebRTC-powered multi-user videochat. For a live demo, check out either
https://meet.estos.de/ or https://meet.jit.si/.
Built using colibri.js[0] and strophe.jingle[1], powered by the jitsi-videobridge[2]
[0] https://github.com/ESTOS/colibri.js
[1] https://github.com/ESTOS/strophe.jingle
[3] https://github.com/jitsi/jitsi-videobridge

@ -0,0 +1,31 @@
Format: http://dep.debian.net/deps/dep5
Upstream-Name: Jitsi Meet
Upstream-Contact: Emil Ivov <emcho@jitsi.org>
Source: https://github.com/jitsi/jitsi-meet
Files: *
Copyright: 2013-2014 Jitsi
License: MIT
License: MIT
The MIT License (MIT)
.
Copyright (c) 2013 ESTOS GmbH
Copyright (c) 2013 BlueJimp SARL
.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

@ -0,0 +1,36 @@
server {
listen 80;
server_name jitsi-meet.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name jitsi-meet.example.com;
ssl_certificate /var/lib/prosody/jitsi-meet.example.com.crt;
ssl_certificate_key /var/lib/prosody/jitsi-meet.example.com.key;
root /usr/share/jitsi-meet;
index index.html index.htm;
location ~ ^/([a-zA-Z0-9]+)$ {
rewrite ^/(.*)$ / break;
}
# BOSH
location /http-bind {
proxy_pass http://localhost:5280/http-bind;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Host $http_host;
}
# xmpp websockets
location /xmpp-websocket {
proxy_pass http://localhost:5280;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
tcp_nodelay on;
}
}

@ -0,0 +1,103 @@
# Jitsi Meet quick install
This documents decribes the needed steps for quick Jitsi Meet installation on a Debian based GNU/Linux system.
N.B.: All commands are supposed to be run by root. If you are logged in as a regular user with sudo rights, please prepend ___sudo___ to each of the commands.
## Basic Jitsi Meet install
### Add the repository
```sh
add-apt-repository 'deb http://download.jitsi.org/nightly/deb unstable/'
wget -qO - https://download.jitsi.org/nightly/deb/unstable/archive.key | apt-key add -
```
add-apt-repository is in the default Ubuntu install and is available for both Ubuntu and Debian, but if it's not present, either install it with
```sh
apt-get -y install software-properties-common
add-apt-repository 'deb http://download.jitsi.org/nightly/deb unstable/'
wget -qO - https://download.jitsi.org/nightly/deb/unstable/archive.key | apt-key add -
```
or add the repository by hand with
```sh
echo 'deb http://download.jitsi.org/nightly/deb unstable/' >> /etc/apt/sources.list
wget -qO - https://download.jitsi.org/nightly/deb/unstable/archive.key | apt-key add -
```
### Update the package lists
```sh
apt-get update
```
### Install Jitsi Meet
```sh
apt-get -y install jitsi-meet
```
During the installation you'll be asked to enter the hostname of the Jitsi Meet instance. If you have a FQDN hostname for the instance already set ip in DNS, enter it there. If you don't have a resolvable hostname, you can enter the IP address of the machine (if it is static or doesn't change).
This hostname (or IP address) will be used for virtualhost configuration inside the Jitsi Meet and also you and your correspondents will be using it to access the web conferences.
### Open a conference
Launch a web broswer (Chrome, Chromium or latest Opera) and enter in the URL bar the hostname (or IP address) you used in the previous step.
Confirm that you trust the self-signed certificate of the newly installed Jitsi Meet.
Enjoy!
## Adding sip-gateway to Jitsi Meet
### Install Jigasi
```sh
apt-get -o Dpkg::Options::="--force-overwrite" -y install jigasi
```
or
```sh
wget https://download.jitsi.org/jigasi_1.0-1_amd64.deb
dpkg -i --force-overwrite jigasi_1.0-1_amd64.deb
```
You need to pass "--force-overwrite" option to dpkg, because the jigasi package patches some of the files in the jitsi-meet package in order to enable the SIP support in Jitsi Meet.
During the installation you'll be asked to enter your SIP account and password. This account will be used to invite the other SIP participants.
### Reload Jitsi Meet
Launch again a browser with the Jitsi Meet URL and you'll see a telephone icon on the right end of the toolbar. Use it to invite SIP accounts to join the current conference.
Enjoy!
## Troubleshoot
If the SIP gateway doesn't work on first try, restart it.
```sh
/etc/init.d/jigasi restart
```
## Deinstall
```sh
apt-get purge jigasi jitsi-meet jitsi-videobridge
```
Somethimes the following packages will fail to uninstall properly:
- jigasi
- jitsi-videobridge
When this happens, just run the deinstall command a second time and it should be ok.
The reason for failure is that not allways the daemons are stopped right away, there is a timeout before the actual stop. And if the unistall script goes on before the services' stop, there is an error.
The second run of the deinstall command fixes this, as by then the jigasi or jvb daemons are already stopped.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

@ -15,13 +15,19 @@
<glyph unicode="&#xe605;" d="M0.759 320.807h138.767v159.899c0 0-39.017-4.051-88.090-55.817-49.069-51.764-50.676-104.082-50.676-104.082zM341.64 480.706h-169.842v-192.298l-171.040 0.125-0.757 1.734v-255.251c0-36.923 30.7-66.99 68.424-66.99h273.217c37.757 0 68.456 30.068 68.456 66.99v378.702c-0.002 36.921-30.699 66.988-68.457 66.988zM345.927 72.582h-286.424v46.394h286.423v-46.394zM345.927 169.401h-286.424v46.392h286.423v-46.392z" horiz-adv-x="410" />
<glyph unicode="&#xe606;" d="M476.95 481.193h-409.887c-36.483 0-66.209-30.356-66.209-67.672v-270.084c0-37.284 29.727-67.639 66.209-67.639h17.912v-106.445l172.483 106.445h219.493c36.482 0 66.208 30.355 66.208 67.639v270.084c0.001 37.316-29.725 67.672-66.207 67.672zM247.214 146.677l-97.885-62v62h-79.092v263.626h403.539l0.062-263.626h-226.625z" horiz-adv-x="545" />
<glyph unicode="&#xe607;" d="M354.757 310.047v22.227c0 81.545-66.331 147.875-147.875 147.875-81.546 0-147.876-66.329-147.876-147.875v-22.227c-33.113-3.697-59.007-32.458-59.007-67.304v-205.811c0-37.315 29.741-67.683 66.236-67.683h281.291c36.529 0 66.267 30.368 66.267 67.683v205.811c0 34.848-25.896 63.609-59.037 67.304zM206.882 415.769c46.022 0 83.493-37.472 83.493-83.494v-21.816h-166.989v21.816c0 46.022 37.441 83.494 83.495 83.494z" horiz-adv-x="414" />
<glyph unicode="&#xe608;" d="M613.039 358.427l-90.297-88.124v109.103c0 37.441-29.829 67.911-66.474 67.911h-318.101c-36.644 0-66.469-30.47-66.469-67.911v-305.329c0-37.425 29.826-67.894 66.469-67.894h318.101c36.645 0 66.474 30.469 66.474 67.894v86.562l96.954-77.070c24.451-17.791 48.463 2.608 48.463 20.292v242.327c0.001 17.682-28.015 35.754-55.122 12.24zM301.315 99.154c-70.723 0-128.065 57.342-128.065 128.066s57.341 128.065 128.065 128.065c70.724 0 128.067-57.341 128.067-128.065 0-70.725-57.344-128.066-128.067-128.066zM296.894 299.788c-40.335 0-73.037-32.704-73.037-73.036 0-40.335 32.703-73.040 73.037-73.040 40.331 0 73.036 32.707 73.036 73.040 0 40.332-32.705 73.036-73.036 73.036zM296.894 260.049c-14.916 0-27.014-12.116-27.014-27.013 0-4.423-3.594-8.004-8.005-8.004-4.44 0-8.002 3.58-8.002 8.004 0 23.716 19.291 43.024 43.022 43.024 4.409 0 8.002-3.58 8.002-8.005-0.002-4.426-3.596-8.005-8.002-8.005z" horiz-adv-x="667" />
<glyph unicode="&#xe608;" d="M613.039 358.427l-90.297-88.124v109.103c0 37.441-29.829 67.911-66.474 67.911h-318.101c-36.644 0-66.469-30.47-66.469-67.911v-305.329c0-37.425 29.826-67.894 66.469-67.894h318.101c36.645 0 66.474 30.469 66.474 67.894v86.562l96.954-77.070c24.451-17.791 48.463 2.608 48.463 20.292v242.327c0.001 17.682-28.015 35.754-55.122 12.24zM301.315 99.154c-70.723 0-128.065 57.342-128.065 128.066s57.341 128.065 128.065 128.065c70.724 0 128.067-57.341 128.067-128.065s-57.344-128.066-128.067-128.066zM296.894 299.788c-40.335 0-73.037-32.704-73.037-73.036 0-40.335 32.703-73.040 73.037-73.040 40.331 0 73.036 32.707 73.036 73.040 0 40.332-32.705 73.036-73.036 73.036zM296.894 260.049c-14.916 0-27.014-12.116-27.014-27.013 0-4.423-3.594-8.004-8.005-8.004-4.44 0-8.002 3.58-8.002 8.004 0 23.716 19.291 43.024 43.022 43.024 4.409 0 8.002-3.58 8.002-8.005-0.002-4.426-3.596-8.005-8.002-8.005z" horiz-adv-x="667" />
<glyph unicode="&#xe609;" d="M611.967 358.573l-90.149-87.978v108.924c0 3.831-0.333 7.574-0.951 11.216l36.847 32.673c13.174 11.705 14.42 31.9 2.684 45.12-11.737 13.203-31.902 14.436-45.136 2.7l-504.51-447.356c-13.204-11.705-14.421-31.903-2.699-45.104 6.303-7.118 15.091-10.769 23.925-10.769 7.538 0 15.107 2.652 21.195 8.050l47.92 42.49c10.498-7.313 23.13-11.616 36.796-11.616h317.596c36.55 0 66.33 30.404 66.33 67.769v86.434l96.83-76.978c24.408-17.73 48.383 2.624 48.383 20.292v241.914c0.001 17.653-27.966 35.693-55.062 12.22zM300.757 99.724c-29.405 0-56.283 10.108-77.763 26.899l41.465 36.767c10.431-5.832 22.425-9.193 35.235-9.193 40.267 0 72.916 32.649 72.916 72.918 0 9.71-1.948 18.928-5.428 27.357l43.423 38.501c11.424-19.13 18.006-41.484 18.006-65.391 0-70.607-57.246-127.855-127.853-127.855zM172.899 227.58c0 70.608 57.248 127.857 127.858 127.857 10.75 0 21.038-1.717 30.993-4.214l108.379 96.096h-302.237c-36.569 0-66.349-30.419-66.349-67.799v-259.037l102.947 91.272c-0.654 5.243-1.592 10.426-1.592 15.826z" horiz-adv-x="667" />
<glyph unicode="&#xe60a;" d="M560.562 469.433c-11.74 13.207-31.942 14.425-45.148 2.686l-504.653-447.452c-13.207-11.709-14.426-31.908-2.717-45.116 6.306-7.122 15.112-10.774 23.947-10.774 7.525 0 15.112 2.654 21.201 8.054l128.536 113.967c16.613-11.432 34.994-19.839 54.305-24.856-35.096-17.827-59.386-53.858-59.386-95.947h215.936c0 42.868-25.164 79.558-61.382 97.039 27.475 7.245 52.921 19.983 73.748 38.748 25.944 23.356 56.856 65.757 56.856 135.165v65.162c0 9.406-3.962 17.883-10.293 23.899l106.332 94.279c13.21 11.738 14.424 31.908 2.717 45.147zM395.767 240.946c0-78.993-58.825-114.961-113.495-114.961-17.607 0-34.329 3.608-49.142 10.393l27.868 24.708c7.366-2.295 15.178-3.566 23.305-3.566 44.678 0 80.992 36.344 80.992 80.99v15.050l30.474 27.021v-39.635zM365.295 396.933c0 44.649-36.313 80.992-80.992 80.992-44.649 0-80.992-36.344-80.992-80.992v-158.425c0-0.125 0-0.249 0-0.374l161.984 143.625v15.174zM175.398 213.345c-1.623 8.741-2.559 17.891-2.559 27.601v65.161c0 18.203-14.8 33.002-33.003 33.002-18.233 0-33.002-14.798-33.002-33.002v-65.161c0-28.599 5.558-53.513 14.549-75.466l54.015 47.865z" horiz-adv-x="569" />
<glyph unicode="&#xe60b;" d="M429.207 339.972c-18.298 0-33.123-14.826-33.123-33.091v-65.362c0-79.211-58.991-115.298-113.817-115.298-29.337 0-56.309 9.935-75.93 27.98-22.115 20.409-33.848 50.601-33.848 87.32v65.363c0 18.265-14.827 33.091-33.091 33.091-18.265 0-33.091-14.826-33.091-33.091v-65.363c0-97.917 59.747-157.382 129.589-175.52-35.204-17.855-59.588-54.007-59.588-96.216h216.559c0 42.996-25.204 79.81-61.514 97.286 27.539 7.32 53.060 20.063 73.943 38.895 26.025 23.438 57.004 65.963 57.004 135.553v65.363c0 18.265-14.795 33.091-33.092 33.091zM284.286 157.86c-44.794 0-81.23 36.466-81.23 81.26v158.832c0 44.795 36.435 81.23 81.23 81.23 44.796 0 81.262-36.435 81.262-81.23v-158.832c0.002-44.796-36.464-81.26-81.262-81.26z" horiz-adv-x="569" />
<glyph unicode="&#xe60c;" d="M256.178 480c-141.228 0-256.178-114.919-256.178-256.239 0-141.195 114.95-256.113 256.178-256.113 141.257 0 256.207 114.919 256.207 256.113 0 141.32-114.95 256.239-256.207 256.239zM256.178 7.428c-119.272 0-216.335 97.063-216.335 216.333 0 119.398 97.063 216.429 216.335 216.429 119.3 0 216.428-97.031 216.428-216.429 0-119.27-97.127-216.333-216.428-216.333zM256.272 427.481c-112.377 0-203.754-91.375-203.754-203.657 0-112.281 91.375-203.657 203.754-203.657 112.219 0 203.594 91.377 203.594 203.658-0.002 112.283-91.375 203.658-203.594 203.658zM256.272 63.661c-88.358 0-160.226 71.902-160.226 160.162 0 88.262 71.868 160.162 160.226 160.162 88.262 0 160.098-71.901 160.098-160.162 0-88.26-71.837-160.162-160.098-160.162zM141.925 281.394l-0.477-0.699v-117.207l0.477-0.699c7.879-11.53 18.237-22.271 30.85-31.899l4.481-3.401v189.171l-4.481-3.368c-12.55-9.595-22.907-20.272-30.85-31.899zM207.819 332.865l-1.81-0.667v-220.18l1.81-0.699c9.341-3.527 19.444-5.97 30.883-7.466l3.112-0.381v237.207l-3.082-0.381c-11.119-1.398-21.508-3.876-30.913-7.435zM273.683 340.299l-3.082 0.381v-237.208l3.082 0.381c11.151 1.397 21.538 3.906 30.882 7.432l1.842 0.7v220.244l-1.842 0.667c-9.406 3.526-19.762 6.005-30.882 7.403zM370.49 281.394c-7.846 11.501-18.236 22.24-30.849 31.899l-4.447 3.43v-189.234l4.447 3.401c12.675 9.69 23.066 20.431 30.849 31.93l0.445 0.699v117.176l-0.445 0.7z" horiz-adv-x="513" />
<glyph unicode="&#xe60b;" d="M429.207 339.972c-18.298 0-33.123-14.826-33.123-33.091v-65.362c0-79.211-58.991-115.298-113.817-115.298-29.337 0-56.309 9.935-75.93 27.98-22.115 20.409-33.848 50.601-33.848 87.32v65.363c0 18.265-14.827 33.091-33.091 33.091s-33.091-14.826-33.091-33.091v-65.363c0-97.917 59.747-157.382 129.589-175.52-35.204-17.855-59.588-54.007-59.588-96.216h216.559c0 42.996-25.204 79.81-61.514 97.286 27.539 7.32 53.060 20.063 73.943 38.895 26.025 23.438 57.004 65.963 57.004 135.553v65.363c0 18.265-14.795 33.091-33.092 33.091zM284.286 157.86c-44.794 0-81.23 36.466-81.23 81.26v158.832c0 44.795 36.435 81.23 81.23 81.23 44.796 0 81.262-36.435 81.262-81.23v-158.832c0.002-44.796-36.464-81.26-81.262-81.26z" horiz-adv-x="569" />
<glyph unicode="&#xe60c;" d="M256.178 480c-141.228 0-256.178-114.919-256.178-256.239 0-141.195 114.95-256.113 256.178-256.113 141.257 0 256.207 114.919 256.207 256.113 0 141.32-114.95 256.239-256.207 256.239zM256.178 7.428c-119.272 0-216.335 97.063-216.335 216.333 0 119.398 97.063 216.429 216.335 216.429 119.3 0 216.428-97.031 216.428-216.429 0-119.27-97.127-216.333-216.428-216.333zM256.272 427.481c-112.377 0-203.754-91.375-203.754-203.657s91.375-203.657 203.754-203.657c112.219 0 203.594 91.377 203.594 203.658-0.002 112.283-91.375 203.658-203.594 203.658zM256.272 63.661c-88.358 0-160.226 71.902-160.226 160.162 0 88.262 71.868 160.162 160.226 160.162 88.262 0 160.098-71.901 160.098-160.162 0-88.26-71.837-160.162-160.098-160.162zM141.925 281.394l-0.477-0.699v-117.207l0.477-0.699c7.879-11.53 18.237-22.271 30.85-31.899l4.481-3.401v189.171l-4.481-3.368c-12.55-9.595-22.907-20.272-30.85-31.899zM207.819 332.865l-1.81-0.667v-220.18l1.81-0.699c9.341-3.527 19.444-5.97 30.883-7.466l3.112-0.381v237.207l-3.082-0.381c-11.119-1.398-21.508-3.876-30.913-7.435zM273.683 340.299l-3.082 0.381v-237.208l3.082 0.381c11.151 1.397 21.538 3.906 30.882 7.432l1.842 0.7v220.244l-1.842 0.667c-9.406 3.526-19.762 6.005-30.882 7.403zM370.49 281.394c-7.846 11.501-18.236 22.24-30.849 31.899l-4.447 3.43v-189.234l4.447 3.401c12.675 9.69 23.066 20.431 30.849 31.93l0.445 0.699v117.176l-0.445 0.7z" horiz-adv-x="513" />
<glyph unicode="&#xe60d;" d="M476.183 480.067h-410.238c-36.514 0-66.266-30.38-66.266-67.728v-376.179c0-37.33 29.752-67.712 66.266-67.712h410.24c36.545 0 66.298 30.383 66.298 67.712v376.179c-0.001 37.347-29.754 67.728-66.299 67.728zM473.067 39.401h-403.947v369.731h403.917l0.029-369.731zM284.871 255.938l45.886 48.433-38.652 38.654 158.197 42.52-42.49-158.195-37.678 37.647-45.917-48.433zM257.382 192.281l-45.883-48.433 38.65-38.652-158.194-42.522 42.489 158.194 37.678-37.645 45.917 48.435z" horiz-adv-x="545" />
<glyph unicode="&#xe60e;" d="M476.613 479.59h-410.332c-36.523 0-66.281-30.388-66.281-67.744v-376.262c0-37.324 29.759-67.71 66.281-67.71h410.33c36.553 0 66.312 30.388 66.312 67.711v376.262c0.001 37.356-29.758 67.744-66.311 67.744zM473.497 38.824h-404.039v369.798h404.009l0.031-369.798zM457.769 353.35l-45.897-48.445 38.663-38.661-158.232-42.515 42.5 158.232 37.687-37.67 45.926 48.445zM85.313 94.111l45.897 48.442-38.661 38.663 158.232 42.514-42.499-158.23-37.686 37.671-45.928-48.445z" horiz-adv-x="545" />
<glyph unicode="&#xe60f;" d="M256.518 480c141.785-0.094 256.207-114.737 256.018-256.332-0.188-141.878-114.483-256.114-256.271-256.144-141.595-0.034-256.456 114.737-256.267 256.050 0.187 142.319 114.483 256.551 256.518 256.426zM256.142 405.365c-100.395-0.063-181.478-81.146-181.416-181.507 0-100.553 80.894-181.541 181.416-181.667 100.582-0.156 181.791 81.147 181.728 181.886-0.125 100.426-81.209 181.351-181.727 181.287zM298.696 223.794h-0.252l-0.063 0.063h0.315l57.808 57.933c0 0-39.123 39.252-41.077 41.077l-58.877-58.091-59.507 59.098-41.012-41.139 44.407-44.409 13.337-14.531h0.251l0.126-0.127h-0.378l-57.744-57.903c0 0 39.125-39.282 41.012-41.106l58.88 58.123 59.504-59.13 41.077 41.203-44.408 44.41-13.4 14.531z" horiz-adv-x="513" />
<glyph unicode="&#xe610;" d="M33.245-31.998h513.97zM599.298 243.993c-67.851 67.947-135.707 135.83-203.683 203.62-3.122 3.045-6.934 5.857-10.933 7.327-15.618 5.7-31.74-6.404-31.864-23.837-0.127-33.832-0.063-67.666-0.063-101.483 0-2.14 0-4.31 0-6.982-3.062 0-5.435 0-7.81 0-53.123 0-106.167 0.031-159.243 0-17.589-0.016-27.43-9.855-27.43-27.461-0.030-46.389-0.030-92.75 0-139.173 0-17.9 9.841-27.74 27.805-27.74 52.887-0.031 105.712 0 158.555 0 2.438 0 4.811 0 8.123 0 0-2.688 0-4.687 0-6.654 0-33.397 0.125-66.851 0-100.248 0-11.529 4.623-20.12 15.121-24.774 10.558-4.685 19.68-1.405 27.615 6.469 67.978 68.039 136.016 136.017 203.994 204.087 11.745 11.716 11.62 25.056-0.187 36.847zM266.298 22.365c-1.313 9.81-11.028 16.058-13.964 17.713-4.405 2.593-9.185 2.906-12.841 2.906l-3.185-0.063-113.963 0.094c-28.021 0.062-49.234 21.087-49.296 48.858-0.094 88.563-0.094 177.161 0.032 265.755 0.029 26.991 21.335 48.265 48.484 48.406l117.961 0.031c16.713 0.015 25.647 8.060 27.24 24.618 0.5 6.482 0.405 13.026 0.219 19.586-0.564 19.869-9.685 28.74-29.553 28.756l-108.933-0.060c-7.747 0-15.463-0.281-23.18-1.172-57.76-6.577-103.778-56.825-104.841-114.399-0.656-31.944-0.498-63.902-0.342-95.859l0.093-30.101h-0.189c0 0-0.060-113.977 0.033-144.906 0.124-67.851 50.764-120.398 117.772-122.24 16.588-0.438 33.209-0.594 49.827-0.594h0.032l74.006 0.377c14.684 0 23.741 8.685 24.865 23.772 0.877 11.029 0.814 20.087-0.279 28.522z" horiz-adv-x="608" />
<glyph unicode="&#xe611;" d="M419.667 193.243c0-39.599-32.128-71.731-71.743-71.731-39.587 0-71.716 32.131-71.716 71.731 0 39.613 32.129 71.716 71.716 71.716 39.615-0.002 71.743-32.102 71.743-71.716zM686.385 353.243c-3.298 19.729-14.748 32.084-35.303 34.638-11.894 1.459-19.128 7.819-22.363 19.52-4.853 17.76-16.839 29.496-33.905 35.38-12.404 4.298-25.15 8.231-38.093 10.245-34.827 5.455-70.255 7.962-104.763 14.971-34.765 7.089-69.526 11.671-104.447 12.036-34.923-0.365-69.685-4.947-104.447-12.036-34.511-7.008-69.938-9.514-104.763-14.97-12.942-2.014-25.692-5.948-38.094-10.245-17.065-5.884-29.052-17.62-33.907-35.38-3.234-11.702-10.467-18.061-22.361-19.52-20.552-2.554-32.003-14.91-35.3-34.639-3.394-20.205-5.869-40.601-8.405-60.942-1.364-11.055 3.203-16.288 15.193-16.224 60.419 0.349 120.846 0.349 181.298 0.048 12.023-0.064 16.558 4.694 16.716 16.669 0.381 28.685-2.315 55.947-23.568 78.309-3.52 3.695-5.424 12.64-3.363 16.923 2.031 4.25 20.428 8.358 22.996 8.358 21.664-0.095 21.569-0.111 24.709-21.712 0.602-4.14 1.269-9.23 3.901-11.926 15.16-15.431 10.626-33.353 3.617-48.817-15.195-33.57-30.768-67.413-50.434-98.435-36.729-57.916-52.205-80.372-99.34-130.422-29.337-31.147-34.286-50.844-34.286-68.733 0-35.495 20.519-48.372 68.574-48.372 90.807 0 130.454 0.158 221.264 0.158 90.807 0 130.452-0.158 221.264-0.158 48.051 0 68.571 12.876 68.571 48.372 0 17.889-4.949 37.587-34.287 68.733-47.132 50.050-62.611 72.506-99.339 130.422-19.666 31.021-35.238 64.865-50.43 98.435-7.010 15.464-11.546 33.385 3.617 48.817 2.632 2.697 3.298 7.787 3.901 11.926 3.14 21.601 3.045 21.617 24.706 21.712 2.57 0 20.966-4.108 22.996-8.358 2.063-4.283 0.159-13.227-3.361-16.923-21.251-22.362-23.949-49.625-23.568-78.309 0.158-11.974 4.694-16.733 16.715-16.669 60.454 0.302 120.877 0.302 181.3-0.048 11.987-0.063 16.554 5.171 15.192 16.224-2.538 20.343-5.009 40.737-8.403 60.942zM479.995 191.752c0-73.157-59.312-132.468-132.485-132.468s-132.487 59.31-132.487 132.468c0 73.159 59.312 132.468 132.487 132.468 73.171 0 132.485-59.31 132.485-132.468z" horiz-adv-x="695" />
<glyph unicode="&#xe612;" d="M155.131 15.215c0-26.065-21.103-47.215-47.2-47.215h-60.703c-26.098 0-47.229 21.15-47.229 47.215v417.835c0 26.079 21.133 47.229 47.229 47.229h60.701c26.097 0 47.2-21.15 47.2-47.229v-417.835zM538.559 480.28h-280.993c-36.459 0-66.165-30.337-66.165-67.626v-377.058c0-37.259 29.706-67.596 66.165-67.596h280.993c36.49 0 66.197 30.337 66.197 67.596v377.058c0 37.29-29.707 67.626-66.197 67.626zM264.915 413.453h266.327l0.031-71.649h-266.358v71.649zM321.627 25.814h-56.776v56.776h56.776v-56.776zM321.627 128.374h-56.776v56.777h56.776v-56.777zM321.691 231.878h-56.776v56.777h56.776v-56.777zM426.45 25.814h-56.776v56.776h56.776v-56.776zM426.45 128.374h-56.776v56.777h56.776v-56.777zM426.514 231.878h-56.778v56.777h56.778v-56.777zM531.274 25.814h-56.778v56.776h56.778v-56.776zM531.274 128.374h-56.778v56.777h56.778v-56.777zM531.335 231.878h-56.777v56.777h56.777v-56.777z" horiz-adv-x="605" />
<glyph unicode="&#xe613;" d="M561.722 469.507c-11.797 13.24-32.066 14.495-45.37 2.697l-504.135-446.718c-13.305-11.734-14.495-32.065-2.73-45.338 6.337-7.153 15.154-10.825 24.063-10.825 7.562 0 15.186 2.7 21.272 8.098l65.023 57.61c45.371-40.922 105.284-66.082 171.237-66.050 141.408 0.031 255.457 113.985 255.644 255.486 0.063 54.967-17.341 105.683-46.75 147.36l59.044 52.313c13.241 11.763 14.499 32.065 2.702 45.368zM472.211 224.909c0.064-100.461-80.948-181.601-181.255-181.476-43.78 0.062-83.786 15.575-115.031 41.284l165.638 146.755v-36.588c0.536-30.497 16.348-46.090 47.472-46.846 30.998 0.756 46.843 16.382 47.565 46.878v20.548h-36.113v-23.75c0.125-2.321-0.282-5.303-1.255-8.974-0.625-1.63-1.724-3.010-3.262-4.046-1.599-1.286-3.923-1.914-6.934-1.914-5.272 0.155-8.566 2.134-9.913 5.961-0.534 1.756-0.974 3.452-1.254 5.082-0.127 1.443-0.188 2.766-0.188 3.893v71.755l21.238 18.817c0.108-0.216 0.226-0.425 0.315-0.651 0.974-3.233 1.381-6.399 1.255-9.538v-18.386h36.113v15.060c-0.123 15.623-4.543 27.35-13.181 35.224l20.356 18.034c17.894-28.028 28.401-61.293 28.433-97.122zM119.897 165.735c-6.306 18.512-9.913 38.279-9.913 58.957-0.095 100.118 80.792 181.005 180.973 181.067 28.426 0 55.156-6.651 79.067-18.199l58.923 52.211c-39.722 25.476-86.879 40.409-137.646 40.474-141.689 0.091-255.677-113.865-255.894-255.838-0.063-39.783 9.318-77.34 25.569-110.941l58.924 52.269zM194.288 313.010h-48.757v-124.529l36.115 32.035v0.344h0.407l58.86 52.209c0 0.282 0.061 0.47 0.061 0.755 0.376 26.949-15.184 40.034-46.687 39.185zM202.98 280.759c0.971-1.384 1.537-3.235 1.661-5.556 0.156-2.226 0.219-4.8 0.219-7.623 0.125-5.458-0.345-9.915-1.475-13.493-1.476-3.797-5.492-5.678-12.079-5.678h-9.662v37.022h7.655c3.921 0 6.933-0.341 9.036-1.095 2.198-0.787 3.734-1.976 4.645-3.577z" horiz-adv-x="570" />
<glyph unicode="&#xe614;" d="M290.639 480.854c142.428-0.095 257.404-115.258 257.213-257.498-0.189-142.524-115.036-257.277-257.435-257.308-142.27-0.031-257.656 115.259-257.466 257.211 0.219 142.968 115.005 257.719 257.688 257.595zM290.289 405.878c-100.882-0.061-182.333-81.516-182.239-182.332 0-101.009 81.262-182.368 182.239-182.492 101.009-0.158 182.587 81.515 182.524 182.712-0.126 100.884-81.578 182.175-182.524 182.112zM143.849 312.453h49.098c31.721 0.884 47.392-12.259 47.013-39.431 0.127-9.541-1.106-17.441-3.728-23.761-3.002-6.254-9.353-10.994-19.083-14.090v-0.41c14.186-3.13 21.516-11.787 21.99-25.973v-28.844c0-5.623 0.127-11.406 0.379-17.378 0.41-6.002 1.517-10.49 3.348-13.522h-35.923c-1.863 3.032-3.064 7.519-3.57 13.522-0.506 5.971-0.727 11.755-0.569 17.378v26.161c0 4.801-1.107 8.276-3.286 10.49-2.338 2.053-6.351 3.095-12.006 3.095h-7.299v-70.645h-36.365v163.41zM180.214 247.43h9.732c6.636 0 10.679 1.897 12.166 5.688 1.138 3.602 1.611 8.152 1.484 13.585 0 2.908-0.063 5.434-0.221 7.709-0.126 2.337-0.696 4.202-1.676 5.623-0.916 1.579-2.463 2.781-4.676 3.57-2.117 0.727-5.149 1.106-9.1 1.106h-7.709v-37.282zM249.186 312.453h81.041v-31.343h-44.675v-32.794h39.051v-31.343h-39.051v-36.555h46.411v-31.375h-82.779v163.409zM341.253 268c0.158 15.891 4.708 27.771 13.712 35.703 8.72 7.645 20.093 11.468 34.091 11.468 14.123 0 25.56-3.823 34.312-11.5 8.91-7.899 13.46-19.811 13.586-35.704v-15.166h-36.365v18.516c0.127 3.096-0.285 6.319-1.264 9.604-0.632 1.579-1.738 2.969-3.286 4.107-1.611 0.757-3.949 1.202-6.982 1.202-5.308-0.158-8.625-1.928-9.983-5.309-1.106-3.286-1.611-6.508-1.454-9.604v-80.978c0-1.137 0.063-2.433 0.19-3.886 0.284-1.705 0.727-3.412 1.264-5.116 1.358-3.887 4.676-5.878 9.983-6.003 3.034 0 5.372 0.63 6.982 1.896 1.549 1.075 2.654 2.433 3.286 4.108 0.98 3.665 1.391 6.665 1.264 9.003v23.918h36.365v-20.664c-0.726-30.774-16.682-46.507-47.897-47.235-31.342 0.727-47.265 16.461-47.803 47.171v74.469z" horiz-adv-x="571" />
<glyph unicode="&#xe615;" d="M508.412 2.883c-1.026 7.687-2.666 15.269-3.93 22.923-4.167 25.229-16.503 43.252-41.031 53.961-39.187 17.099-77.551 36.060-116.055 54.697-27.843 13.512-26.204 44.26-17.048 57.207 5.945 8.44 11.172 17.286 11.788 28.426 0.222 4.113 4.151 9.495 7.909 11.647 13.035 7.518 19.081 19.782 25.010 32.491 1.555 3.348 3.69 6.594 6.133 9.361 4.236 4.834 6.132 9.618 3.039 15.921-0.717 1.485 0.666 4.167 1.4 6.183 2.152 6.013 5.142 11.838 6.56 18.022 1.778 7.669 2.699 15.612 3.126 23.487 0.187 3.262-3.022 6.764-2.681 9.975 1.741 15.956-7.279 28.101-12.37 41.988-6.233 17.099-18.464 27.81-29.26 40.553-2.033 2.392-2.613 6.526-2.786 9.943-0.36 7.294-3.366 10.898-11.002 9.906-3.055-0.394-6.386-1.248-9.205-0.496-2.478 0.667-6.203 3.144-6.338 5.056-0.769 9.668-4.132 11.258-14.008 9.618-6.182-1.025-14.228 4.577-20.292 8.78-5.072 3.521-9.445 5.023-15.341 3.588-2.457-0.598-5.772-0.495-7.858 0.717-2.221 1.332-4.387 2.119-6.559 2.562v0.374c-0.478-0.016-0.991-0.102-1.469-0.154-0.477 0.051-0.956 0.137-1.434 0.154v-0.375c-2.185-0.444-4.375-1.231-6.578-2.562-2.066-1.213-5.381-1.316-7.84-0.718-5.911 1.434-10.285-0.068-15.342-3.588-6.079-4.202-14.108-9.805-20.292-8.781-9.873 1.641-13.255 0.052-14.024-9.618-0.154-1.912-3.843-4.389-6.338-5.056-2.834-0.752-6.149 0.102-9.223 0.495-7.618 0.992-10.625-2.613-10.985-9.906-0.169-3.416-0.751-7.551-2.784-9.943-10.794-12.743-23.025-23.454-29.278-40.553-5.058-13.886-14.094-26.031-12.335-41.987 0.343-3.211-2.872-6.714-2.7-9.975 0.445-7.875 1.368-15.818 3.127-23.487 1.418-6.184 4.407-12.010 6.576-18.022 0.719-2.016 2.121-4.698 1.384-6.183-3.091-6.303-1.179-11.087 3.058-15.921 2.427-2.767 4.56-6.013 6.115-9.361 5.929-12.709 11.974-24.974 25.007-32.491 3.76-2.152 7.689-7.534 7.929-11.647 0.596-11.14 5.825-19.986 11.785-28.426 9.141-12.947 10.573-43.369-17.081-57.207-38.228-19.132-76.871-37.6-116.021-54.697-24.564-10.709-36.863-28.731-41.032-53.961-1.263-7.656-2.939-15.238-3.929-22.923-1.505-11.464-3.912-34.883-3.912-34.883h512.306c-0.001 0-2.39 23.419-3.894 34.883z" horiz-adv-x="513" />
<glyph unicode="&#xe616;" d="M513.087 224.534c0-141.673-114.855-256.526-256.554-256.526-141.674 0-256.534 114.851-256.534 256.526 0 141.692 114.861 256.553 256.534 256.553 141.7 0 256.554-114.861 256.554-256.553zM256.534-31.993c67.863 0 129.556 26.356 175.437 69.37-4.858 5.825-11.276 10.557-19.557 14.171-29.467 12.873-58.313 27.128-87.267 41.128-20.935 10.161-19.702 33.293-12.82 43.029 4.471 6.346 8.402 12.999 8.864 21.373 0.166 3.084 3.12 7.142 5.945 8.761 9.802 5.652 14.349 14.873 18.802 24.43 1.17 2.515 2.777 4.945 4.615 7.038 3.185 3.622 4.612 7.218 2.286 11.971-0.543 1.104 0.502 3.12 1.053 4.637 1.619 4.534 3.866 8.901 4.93 13.558 1.335 5.774 2.029 11.74 2.351 17.661 0.14 2.451-2.272 5.092-2.017 7.493 1.31 12.011-5.471 21.136-9.299 31.579-4.688 12.857-13.885 20.91-22.002 30.485-1.529 1.812-1.964 4.919-2.094 7.476-0.269 5.49-2.53 8.207-8.272 7.462-2.299-0.3-4.805-0.943-6.921-0.378-1.864 0.494-4.663 2.362-4.767 3.802-0.577 7.269-3.106 8.465-10.533 7.238-4.648-0.772-10.697 3.429-15.257 6.601-3.816 2.646-7.104 3.777-11.534 2.69-1.849-0.45-4.341-0.373-5.908 0.547-1.671 0.988-3.303 1.592-4.933 1.919v0.276c-0.36-0.007-0.745-0.065-1.104-0.108-0.361 0.044-0.72 0.103-1.078 0.108v-0.276c-1.645-0.327-3.287-0.931-4.945-1.919-1.556-0.918-4.046-0.996-5.899-0.547-4.443 1.087-7.724-0.044-11.532-2.69-4.578-3.173-10.611-7.373-15.259-6.601-7.431 1.226-9.97 0.031-10.547-7.238-0.109-1.439-2.897-3.308-4.758-3.802-2.139-0.565-4.624 0.077-6.944 0.378-5.728 0.745-7.994-1.971-8.258-7.462-0.131-2.555-0.565-5.665-2.095-7.476-8.111-9.575-17.308-17.629-22.009-30.485-3.814-10.443-10.602-19.568-9.285-31.579 0.256-2.401-2.152-5.042-2.023-7.493 0.327-5.923 1.020-11.888 2.351-17.661 1.065-4.656 3.313-9.024 4.945-13.558 0.547-1.516 1.587-3.531 1.041-4.637-2.325-4.754-0.894-8.351 2.291-11.971 1.837-2.094 3.437-4.523 4.612-7.038 4.45-9.555 8.996-18.779 18.798-24.43 2.827-1.619 5.78-5.676 5.952-8.761 0.457-8.374 4.387-15.027 8.869-21.373 6.873-9.735 7.951-32.623-12.837-43.029-28.76-14.386-57.8-28.255-87.251-41.128-8.285-3.615-14.704-8.347-19.561-14.169 45.88-43.015 107.569-69.372 175.422-69.372z" horiz-adv-x="513" />
</font></defs></svg>

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

@ -1,4 +1,4 @@
<html itemscope itemtype="http://schema.org/Product" prefix="og: http://ogp.me/ns#">
<html itemscope itemtype="http://schema.org/Product" prefix="og: http://ogp.me/ns#" xmlns="http://www.w3.org/1999/html">
<head>
<title>Jitsi Videobridge meets WebRTC</title>
<link rel="icon" type="image/png" href="/images/favicon.ico"/>
@ -9,7 +9,7 @@
<meta itemprop="name" content="Jitsi Meet"/>
<meta itemprop="description" content="Join a WebRTC video conference powered by the Jitsi Videobridge"/>
<meta itemprop="image" content="/images/jitsilogo.png"/>
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
<script src="libs/jquery.min.js"></script>
<script src="libs/strophe/strophe.jingle.adapter.js?v=1"></script><!-- strophe.jingle bundles -->
<script src="libs/strophe/strophe.jingle.bundle.js?v=8"></script>
<script src="libs/strophe/strophe.jingle.js?v=1"></script>
@ -19,18 +19,20 @@
<script src="libs/strophe/strophe.jingle.session.js?v=1"></script>
<script src="libs/colibri/colibri.focus.js?v=8"></script><!-- colibri focus implementation -->
<script src="libs/colibri/colibri.session.js?v=1"></script>
<script src="//code.jquery.com/ui/1.10.4/jquery-ui.js"></script>
<script src="libs/jquery-ui.js"></script>
<script src="libs/rayo.js?v=1"></script>
<script src="libs/tooltip.js?v=1"></script><!-- bootstrap tooltip lib -->
<script src="libs/popover.js?v=1"></script><!-- bootstrap tooltip lib -->
<script src="config.js?v=2"></script><!-- adapt to your needs, i.e. set hosts and bosh path -->
<script src="muc.js?v=12"></script><!-- simple MUC library -->
<script src="config.js?v=4"></script><!-- adapt to your needs, i.e. set hosts and bosh path -->
<script src="muc.js?v=13"></script><!-- simple MUC library -->
<script src="estos_log.js?v=2"></script><!-- simple stanza logger -->
<script src="desktopsharing.js?v=2"></script><!-- desktop sharing -->
<script src="data_channels.js?v=1"></script><!-- data channels -->
<script src="app.js?v=28"></script><!-- application logic -->
<script src="data_channels.js?v=3"></script><!-- data channels -->
<script src="app.js?v=5"></script><!-- application logic -->
<script src="commands.js?v=1"></script><!-- application logic -->
<script src="chat.js?v=6"></script><!-- chat logic -->
<script src="util.js?v=5"></script><!-- utility functions -->
<script src="chat.js?v=9"></script><!-- chat logic -->
<script src="contact_list.js?v=1"></script><!-- contact list logic -->
<script src="util.js?v=6"></script><!-- utility functions -->
<script src="etherpad.js?v=8"></script><!-- etherpad plugin -->
<script src="prezi.js?v=4"></script><!-- prezi plugin -->
<script src="smileys.js?v=2"></script><!-- smiley images -->
@ -38,16 +40,22 @@
<script src="moderatemuc.js?v=3"></script><!-- moderator plugin -->
<script src="analytics.js?v=1"></script><!-- google analytics plugin -->
<script src="rtp_stats.js?v=1"></script><!-- RTP stats processing -->
<script src="videolayout.js?v=3"></script><!-- video ui -->
<script src="toolbar.js?v=2"></script><!-- toolbar ui -->
<script src="local_stats.js?v=1"></script><!-- Local stats processing -->
<script src="videolayout.js?v=8"></script><!-- video ui -->
<script src="toolbar.js?v=4"></script><!-- toolbar ui -->
<script src="canvas_util.js?v=1"></script><!-- canvas drawing utils -->
<script src="audio_levels.js?v=1"></script><!-- audio levels plugin -->
<script src="media_stream.js?v=1"></script><!-- media stream -->
<script src="bottom_toolbar.js?v=1"></script><!-- media stream -->
<link href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" rel="stylesheet">
<link rel="stylesheet" href="css/font.css"/>
<link rel="stylesheet" type="text/css" media="screen" href="css/main.css?v=21"/>
<link rel="stylesheet" type="text/css" media="screen" href="css/videolayout_default.css?v=7" id="videolayout_default"/>
<link rel="stylesheet" href="css/font.css?v=3"/>
<link rel="stylesheet" type="text/css" media="screen" href="css/main.css?v=23"/>
<link rel="stylesheet" type="text/css" media="screen" href="css/videolayout_default.css?v=9" id="videolayout_default"/>
<link rel="stylesheet" href="css/jquery-impromptu.css?v=4">
<link rel="stylesheet" href="css/modaldialog.css?v=3">
<link rel="stylesheet" href="css/popup_menu.css?v=2">
<link rel="stylesheet" href="css/popover.css?v=1">
<link rel="stylesheet" href="css/contact_list.css?v=1">
<!--
Link used for inline installation of chrome desktop streaming extension,
is updated automatically from the code with the value defined in config.js -->
@ -57,84 +65,201 @@
<script src="libs/prezi_player.js?v=2"></script>
</head>
<body>
<div style="position: relative;" id="header_container">
<div id="header">
<span id="toolbar">
<a class="button" data-toggle="popover" data-placement="bottom" data-content="Mute / Unmute" onclick='toggleAudio();'>
<i id="mute" class="icon-microphone"></i></a>
<div class="header_button_separator"></div>
<a class="button" data-toggle="popover" data-placement="bottom" data-content="Start / stop camera" onclick='buttonClick("#video", "icon-camera icon-camera-disabled");toggleVideo();'>
<i id="video" class="icon-camera"></i></a>
<div class="header_button_separator"></div>
<a class="button" data-toggle="popover" data-placement="bottom" data-content="Lock / unlock room" onclick="Toolbar.openLockDialog();">
<i id="lockIcon" class="icon-security"></i></a>
<div class="header_button_separator"></div>
<a class="button" data-toggle="popover" data-placement="bottom" data-content="Invite others" onclick="Toolbar.openLinkDialog();"><i class="icon-link"></i></a>
<div class="header_button_separator"></div>
<span class="toolbar_span">
<a class="button" data-toggle="popover" data-placement="bottom" data-content="Open / close chat" onclick='Chat.toggleChat();'><i id="chatButton" class="icon-chat"></i></a>
<span id="unreadMessages"></span>
</span>
<div class="header_button_separator"></div>
<a class="button" data-toggle="popover" data-placement="bottom" data-content="Share Prezi" onclick='Prezi.openPreziDialog();'><i class="icon-prezi"></i></a>
<span id="etherpadButton">
<div id="welcome_page">
<div id="welcome_page_header">
<a href="http://jitsi.org" target="_new">
<div id="jitsi_logo"></div>
</a>
<div id="enter_room">
<input id="enter_room_field" type="text" placeholder="Enter room name" />
<input id="enter_room_button" type="button" value="GO" />
</div>
<div id="brand_header"></div>
<div id="header_text"></div>
</div>
<div id="welcome_page_main">
<div id="features">
<div class="feature_row">
<div class="feature_holder">
<div class="feature_icon">Simple to use</div>
<div class="feature_description">
No downloads required. uTalk works directly within your browser. Simply share your conference URL with others to get started.
</div>
</div>
<div class="feature_holder">
<div class="feature_icon">Low bandwidth</div>
<div class="feature_description">
Multi-party video conferences work with as little as 128Kbps. Screen-sharing and audio-only conferences are possible with far less.
</div>
</div>
<div class="feature_holder">
<div class="feature_icon">Open source</div>
<div class="feature_description">
uTalk is licensed under the &lt;GPL/LGPL/WHATEVER&gt;. You can download, use, modify, and share the software without any restrictions.
</div>
</div>
<div class="feature_holder">
<div class="feature_icon">Unlimited users</div>
<div class="feature_description">
There are no artificial restrictions on the number of users or conference participants. Server power and bandwidth are the only limiting factors.
</div>
</div>
</div>
<div class="feature_row">
<div class="feature_holder">
<div class="feature_icon">Screen sharing</div>
<div class="feature_description">
It's easy to share your screen with others. uTalk is ideal for on-line presentations, lectures, and tech support sessions.
</div>
</div>
<div class="feature_holder">
<div class="feature_icon">Secure rooms</div>
<div class="feature_description">
Need some privacy? uTalk conference rooms can be secured with a password in order to exclude unwanted guests and prevent interruptions.
</div>
</div>
<div class="feature_holder">
<div class="feature_icon">Shared notes</div>
<div class="feature_description">
uTalk features Etherpad, a real-time collaborative text editor that's great for meeting minutes, writing articles, and more.
</div>
</div>
<div class="feature_holder">
<div class="feature_icon">Usage statistics</div>
<div class="feature_description">
Learn about your users through easy integration with Piwik, Google Analytics, and other usage monitoring and statistics systems.
</div>
</div>
</div>
</div>
</div>
</div>
<div id="videoconference_page">
<div style="position: relative;" id="header_container">
<div id="header">
<span id="toolbar">
<a class="button" data-toggle="popover" data-placement="bottom" data-content="Mute / Unmute" onclick='toggleAudio();'>
<i id="mute" class="icon-microphone"></i>
</a>
<div class="header_button_separator"></div>
<a class="button" data-toggle="popover" data-placement="bottom" data-content="Shared document" onclick='Etherpad.toggleEtherpad(0);'><i class="icon-share-doc"></i></a>
</span>
<div class="header_button_separator"></div>
<span id="desktopsharing" style="display: none">
<a class="button" data-toggle="popover" data-placement="bottom" data-content="Share screen" onclick="toggleScreenSharing();"><i class="icon-share-desktop"></i></a>
<a class="button" data-toggle="popover" data-placement="bottom" data-content="Start / stop camera" onclick='buttonClick("#video", "icon-camera icon-camera-disabled");toggleVideo();'>
<i id="video" class="icon-camera"></i>
</a>
<span id="recording" style="display: none">
<div class="header_button_separator"></div>
<a class="button" data-toggle="popover" data-placement="bottom" data-content="Record" onclick='toggleRecording();'>
<i id="recordButton" class="icon-recEnable"></i>
</a>
</span>
<div class="header_button_separator"></div>
<a class="button" data-toggle="popover" data-placement="bottom" data-content="Lock / unlock room" onclick="Toolbar.openLockDialog();">
<i id="lockIcon" class="icon-security"></i>
</a>
<div class="header_button_separator"></div>
<a class="button" data-toggle="popover" data-placement="bottom" data-content="Invite others" onclick="Toolbar.openLinkDialog();">
<i class="icon-link"></i>
</a>
<div class="header_button_separator"></div>
<span class="toolbar_span">
<a class="button" data-toggle="popover" data-placement="bottom" data-content="Open / close chat" onclick='BottomToolbar.toggleChat();'>
<i id="chatButton" class="icon-chat"></i>
</a>
<span id="unreadMessages"></span>
</span>
<div class="header_button_separator"></div>
<a class="button" data-toggle="popover" data-placement="bottom" data-content="Share Prezi" onclick='Prezi.openPreziDialog();'>
<i class="icon-prezi"></i>
</a>
<span id="etherpadButton">
<div class="header_button_separator"></div>
<a class="button" data-toggle="popover" data-placement="bottom" data-content="Shared document" onclick='Etherpad.toggleEtherpad(0);'>
<i class="icon-share-doc"></i>
</a>
</span>
<div class="header_button_separator"></div>
<span id="desktopsharing" style="display: none">
<a class="button" data-toggle="popover" data-placement="bottom" data-content="Share screen" onclick="toggleScreenSharing();">
<i class="icon-share-desktop"></i>
</a>
</span>
<div class="header_button_separator"></div>
<a class="button" data-toggle="popover" data-placement="bottom" data-content="Enter / Exit Full Screen" onclick='buttonClick("#fullScreen", "icon-full-screen icon-exit-full-screen");Toolbar.toggleFullScreen();'>
<i id="fullScreen" class="icon-full-screen"></i>
</a>
<span id="sipCallButton">
<div class="header_button_separator"></div>
<a class="button" data-toggle="popover" data-placement="bottom" data-content="Call SIP number" onclick='callSipButtonClicked();'>
<i class="icon-telephone"></i></a>
</span>
</span>
<a class="button" data-toggle="popover" data-placement="bottom" data-content="Enter / Exit Full Screen" onclick='buttonClick("#fullScreen", "icon-full-screen icon-exit-full-screen");Toolbar.toggleFullScreen();'>
<i id="fullScreen" class="icon-full-screen"></i></a>
</span>
</div>
<div id="subject"></div>
</div>
<div id="subject"></div>
</div>
<div id="settings">
<h1>Connection Settings</h1>
<form id="loginInfo">
<label>JID: <input id="jid" type="text" name="jid" placeholder="me@example.com"/></label>
<label>Password: <input id="password" type="password" name="password" placeholder="secret"/></label>
<label>BOSH URL: <input id="boshURL" type="text" name="boshURL" placeholder="/http-bind"/></label>
<input id="connect" type="submit" value="Connect" />
</form>
</div>
<div id="reloadPresentation"><a onclick='Prezi.reloadPresentation();'><i title="Reload Prezi" class="fa fa-repeat fa-lg"></i></a></div>
<div id="videospace" onmousemove="Toolbar.showToolbar();">
<div id="largeVideoContainer" class="videocontainer">
<div id="presentation"></div>
<div id="etherpad"></div>
<a href="http://jitsi.org" target="_new"><div class="watermark" id="leftwatermark"></div></a>
<!-- a href="http://jitsi.org" target="_new"><div class="watermark" id="rightwatermark"></div></a -->
<video id="largeVideo" autoplay oncontextmenu="return false;"></video>
<div id="settings">
<h1>Connection Settings</h1>
<form id="loginInfo">
<label>JID: <input id="jid" type="text" name="jid" placeholder="me@example.com"/></label>
<label>Password: <input id="password" type="password" name="password" placeholder="secret"/></label>
<label>BOSH URL: <input id="boshURL" type="text" name="boshURL" placeholder="/http-bind"/></label>
<input id="connect" type="submit" value="Connect" />
</form>
</div>
<div id="remoteVideos">
<span id="localVideoContainer" class="videocontainer">
<span id="localNick" class="nick"></span>
<span id="localVideoWrapper">
<!--<video id="localVideo" autoplay oncontextmenu="return false;" muted></video> - is now per stream generated -->
<div id="reloadPresentation"><a onclick='Prezi.reloadPresentation();'><i title="Reload Prezi" class="fa fa-repeat fa-lg"></i></a></div>
<div id="videospace" onmousemove="Toolbar.showToolbar();">
<div id="largeVideoContainer" class="videocontainer">
<div id="presentation"></div>
<div id="etherpad"></div>
<a href="http://jitsi.org" target="_new"><div class="watermark" id="leftwatermark"></div></a>
<!-- a href="http://jitsi.org" target="_new"><div class="watermark" id="rightwatermark"></div></a -->
<video id="largeVideo" autoplay oncontextmenu="return false;"></video>
</div>
<div id="remoteVideos">
<span id="localVideoContainer" class="videocontainer">
<span id="localNick" class="nick"></span>
<span id="localVideoWrapper">
<!--<video id="localVideo" autoplay oncontextmenu="return false;" muted></video> - is now per stream generated -->
</span>
<audio id="localAudio" autoplay oncontextmenu="return false;" muted></audio>
<span class="focusindicator"></span>
</span>
<audio id="userJoined" src="sounds/joined.wav" preload="auto"></audio>
<audio id="userLeft" src="sounds/left.wav" preload="auto"></audio>
</div>
<span id="bottomToolbar">
<span class="bottomToolbar_span">
<a class="bottomToolbarButton" data-toggle="popover" data-placement="top" data-content="Open / close chat" onclick='BottomToolbar.toggleChat();'>
<i id="chatBottomButton" class="icon-chat-simple"></i>
</a>
<span id="unreadMessages"></span>
</span>
<span class="bottomToolbar_span">
<a class="bottomToolbarButton" data-toggle="popover" data-placement="top" data-content="Open / close contact list" onclick='BottomToolbar.toggleContactList();'>
<i id="contactListButton" class="icon-contactList"></i>
</a>
<span id="unreadMessages"></span>
</span>
<audio id="localAudio" autoplay oncontextmenu="return false;" muted></audio>
<span class="focusindicator" data-content="The owner of&#10;this conference" data-toggle="popover" data-placement="top"></span>
</span>
<audio id="userJoined" src="sounds/joined.wav" preload="auto"></audio>
<audio id="userLeft" src="sounds/left.wav" preload="auto"></audio>
</div>
</div>
<div id="chatspace">
<div id="nickname">
Enter a nickname in the box below
<form>
<input type='text' id="nickinput" placeholder='Choose a nickname' autofocus>
</form>
</div>
<div id="chatspace">
<div id="nickname">
Enter a nickname in the box below
<form>
<input type='text' id="nickinput" placeholder='Choose a nickname' autofocus>
</form>
</div>
<!--div><i class="fa fa-comments">&nbsp;</i><span class='nick'></span>:&nbsp;<span class='chattext'></span></div-->
<div id="chatconversation"></div>
<audio id="chatNotification" src="sounds/incomingMessage.wav" preload="auto"></audio>
<textarea id="usermsg" placeholder='Enter text...' autofocus></textarea>
<!--div><i class="fa fa-comments">&nbsp;</i><span class='nick'></span>:&nbsp;<span class='chattext'></span></div-->
<div id="chatconversation"></div>
<audio id="chatNotification" src="sounds/incomingMessage.wav" preload="auto"></audio>
<textarea id="usermsg" placeholder='Enter text...' autofocus></textarea>
</div>
<a id="downloadlog" onclick='dump(event.target);' data-toggle="popover" data-placement="right" data-content="Download logs" ><i class="fa fa-cloud-download"></i></a>
</div>
<div id="contactlist">
<ul>
<li class="title"><i class="icon-contact-list"></i> CONTACT LIST</li>
</ul>
</div>
<a id="downloadlog" onclick='dump(event.target);' data-toggle="popover" data-placement="right" data-content="Download logs" ><i class="fa fa-cloud-download"></i></a>
</body>

@ -54,19 +54,25 @@ function ColibriFocus(connection, bridgejid) {
* Default channel expire value in seconds.
* @type {number}
*/
this.channelExpire = 60;
this.channelExpire
= ('number' === typeof(config.channelExpire))
? config.channelExpire
: 15;
/**
* Default channel last-n value.
* @type {number}
*/
this.channelLastN
= ('number' === typeof(config.channelLastN)) ? config.channelLastN : -1;
// media types of the conference
if (config.openSctp)
{
this.media = ['audio', 'video', 'data'];
}
else
{
this.media = ['audio', 'video'];
}
this.connection.jingle.sessions[this.sid] = this;
this.bundledTransports = {};
this.mychannel = [];
this.channels = [];
this.remotessrc = {};
@ -77,6 +83,12 @@ function ColibriFocus(connection, bridgejid) {
// silly wait flag
this.wait = true;
this.recordingEnabled = false;
// stores information about the endpoints (i.e. display names) to
// be sent to the videobridge.
this.endpointsInfo = null;
}
// creates a conferences with an initial set of peers
@ -164,44 +176,162 @@ ColibriFocus.prototype.makeConference = function (peers) {
*/
};
// 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.
ColibriFocus.prototype.setRecording = function(state, token, callback) {
var self = this;
var elem = $iq({to: this.bridgejid, type: 'set'});
elem.c('conference', {
xmlns: 'http://jitsi.org/protocol/colibri',
id: this.confid
});
elem.c('recording', {state: state, token: token});
elem.up();
this.connection.sendIQ(elem,
function (result) {
console.log('Set recording "', state, '". Result:', result);
var recordingElem = $(result).find('>conference>recording');
var newState = ('true' === recordingElem.attr('state'));
self.recordingEnabled = newState;
callback(newState);
},
function (error) {
console.warn(error);
}
);
};
/*
* Updates the display name for an endpoint with a specific jid.
* jid: the jid associated with the endpoint.
* displayName: the new display name for the endpoint.
*/
ColibriFocus.prototype.setEndpointDisplayName = function(jid, displayName) {
var endpointId = jid.substr(1 + jid.lastIndexOf('/'));
var update = false;
if (this.endpointsInfo === null) {
this.endpointsInfo = {};
}
var endpointInfo = this.endpointsInfo[endpointId];
if ('undefined' === typeof endpointInfo) {
endpointInfo = this.endpointsInfo[endpointId] = {};
}
if (endpointInfo['displayname'] !== displayName) {
endpointInfo['displayname'] = displayName;
update = true;
}
if (update) {
this.updateEndpoints();
}
};
/*
* Sends a colibri message to the bridge that contains the
* current endpoints and their display names.
*/
ColibriFocus.prototype.updateEndpoints = function() {
if (this.confid === null
|| this.endpointsInfo === null) {
return;
}
if (this.confid === 0) {
// the colibri conference is currently initiating
var self = this;
window.setTimeout(function() { self.updateEndpoints()}, 1000);
return;
}
var elem = $iq({to: this.bridgejid, type: 'set'});
elem.c('conference', {
xmlns: 'http://jitsi.org/protocol/colibri',
id: this.confid
});
for (var id in this.endpointsInfo) {
elem.c('endpoint');
elem.attrs({ id: id,
displayname: this.endpointsInfo[id]['displayname']
});
elem.up();
}
//elem.up(); //conference
this.connection.sendIQ(
elem,
function (result) {},
function (error) { console.warn(error); }
);
};
ColibriFocus.prototype._makeConference = function () {
var self = this;
var elem = $iq({to: this.bridgejid, type: 'get'});
elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri'});
var elem = $iq({ to: this.bridgejid, type: 'get' });
elem.c('conference', { xmlns: 'http://jitsi.org/protocol/colibri' });
this.media.forEach(function (name) {
var isData = name === 'data';
var channel = isData ? 'sctpconnection' : 'channel';
var elemName;
var elemAttrs = { initiator: 'true', expire: self.channelExpire };
elem.c('content', {name: name});
if ('data' === name)
{
elemName = 'sctpconnection';
elemAttrs['port'] = 5000;
}
else
{
elemName = 'channel';
if (('video' === name) && (self.channelLastN >= 0))
elemAttrs['last-n'] = self.channelLastN;
}
elem.c(channel, {
initiator: 'true',
expire: '15',
endpoint: self.myMucResource
});
if (isData)
elem.attrs({port: 5000});
elem.up();// end of channel
elem.c('content', { name: name });
elem.c(elemName, elemAttrs);
elem.attrs({ endpoint: self.myMucResource });
if (config.useBundle) {
elem.attrs({ 'channel-bundle-id': self.myMucResource });
}
elem.up();// end of channel/sctpconnection
for (var j = 0; j < self.peers.length; j++) {
elem.c(channel, {
initiator: 'true',
expire: '15',
endpoint: self.peers[j].substr(1 + self.peers[j].lastIndexOf('/'))
});
if (isData)
elem.attrs({port: 5000});
elem.up(); // end of channel
var peer = self.peers[j];
var peerEndpoint = peer.substr(1 + peer.lastIndexOf('/'));
elem.c(elemName, elemAttrs);
elem.attrs({ endpoint: peerEndpoint });
if (config.useBundle) {
elem.attrs({ 'channel-bundle-id': peerEndpoint });
}
elem.up(); // end of channel/sctpconnection
}
elem.up(); // end of content
});
if (this.endpointsInfo !== null) {
for (var id in this.endpointsInfo) {
elem.c('endpoint');
elem.attrs({ id: id,
displayname: this.endpointsInfo[id]['displayname']
});
elem.up();
}
}
/*
var localSDP = new SDP(this.peerconnection.localDescription.sdp);
localSDP.media.forEach(function (media, channel) {
var name = SDPUtil.parse_mline(media.split('\r\n')[0]).media;
elem.c('content', {name: name});
elem.c('channel', {initiator: 'false', expire: '15'});
elem.c('channel', {initiator: 'false', expire: self.channelExpire});
// FIXME: should reuse code from .toJingle
var mline = SDPUtil.parse_mline(media.split('\r\n')[0]);
@ -215,7 +345,7 @@ ColibriFocus.prototype._makeConference = function () {
elem.up(); // end of channel
for (j = 0; j < self.peers.length; j++) {
elem.c('channel', {initiator: 'true', expire:'15' }).up();
elem.c('channel', {initiator: 'true', expire: self.channelExpire }).up();
}
elem.up(); // end of content
});
@ -231,7 +361,7 @@ ColibriFocus.prototype._makeConference = function () {
);
};
// callback when a conference was created
// callback when a colibri conference was created
ColibriFocus.prototype.createdConference = function (result) {
console.log('created a conference on the bridge');
var self = this;
@ -257,6 +387,14 @@ ColibriFocus.prototype.createdConference = function (result) {
}
}
// save the 'transport' elements from 'channel-bundle'-s
var channelBundles = $(result).find('>conference>channel-bundle');
for (var i = 0; i < channelBundles.length; i++)
{
var endpointId = $(channelBundles[i]).attr('id');
this.bundledTransports[endpointId] = $(channelBundles[i]).find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]');
}
console.log('remote channels', this.channels);
// Notify that the focus has created the conference on the bridge
@ -268,6 +406,11 @@ ColibriFocus.prototype.createdConference = function (result) {
's=-\r\n' +
't=0 0\r\n' +
/* Audio */
(config.useBundle
? ('a=group:BUNDLE audio video' +
(config.openSctp ? ' data' : '') +
'\r\n')
: '') +
'm=audio 1 RTP/SAVPF 111 103 104 0 8 106 105 13 126\r\n' +
'c=IN IP4 0.0.0.0\r\n' +
'a=rtcp:1 IN IP4 0.0.0.0\r\n' +
@ -285,6 +428,7 @@ ColibriFocus.prototype.createdConference = function (result) {
'a=rtpmap:13 CN/8000\r\n' +
'a=rtpmap:126 telephone-event/8000\r\n' +
'a=maxptime:60\r\n' +
(config.useRtcpMux ? 'a=rtcp-mux\r\n' : '') +
/* Video */
'm=video 1 RTP/SAVPF 100 116 117\r\n' +
'c=IN IP4 0.0.0.0\r\n' +
@ -299,6 +443,7 @@ ColibriFocus.prototype.createdConference = function (result) {
'a=rtcp-fb:100 goog-remb\r\n' +
'a=rtpmap:116 red/90000\r\n' +
'a=rtpmap:117 ulpfec/90000\r\n' +
(config.useRtcpMux ? 'a=rtcp-mux\r\n' : '') +
/* Data SCTP */
(config.openSctp ?
'm=application 1 DTLS/SCTP 5000\r\n' +
@ -347,26 +492,25 @@ ColibriFocus.prototype.createdConference = function (result) {
tmp = $(this.mychannel[channel]).find('>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
// FIXME: check rtp-level-relay-type
var isData = bridgeSDP.media[channel].indexOf('application') !== -1;
if (!isData && tmp.length)
{
bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'cname:mixed' + '\r\n';
bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'label:mixedlabela0' + '\r\n';
bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'msid:mixedmslabel mixedlabela0' + '\r\n';
bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'mslabel:mixedmslabel' + '\r\n';
}
else if (!isData)
{
var name = bridgeSDP.media[channel].split(" ")[0].substr(2); // 'm=audio ...'
if (name === 'audio' || name === 'video') {
// make chrome happy... '3735928559' == 0xDEADBEEF
// FIXME: this currently appears as two streams, should be one
bridgeSDP.media[channel] += 'a=ssrc:' + '3735928559' + ' ' + 'cname:mixed' + '\r\n';
bridgeSDP.media[channel] += 'a=ssrc:' + '3735928559' + ' ' + 'label:mixedlabelv0' + '\r\n';
bridgeSDP.media[channel] += 'a=ssrc:' + '3735928559' + ' ' + 'msid:mixedmslabel mixedlabelv0' + '\r\n';
bridgeSDP.media[channel] += 'a=ssrc:' + '3735928559' + ' ' + 'mslabel:mixedmslabel' + '\r\n';
var ssrc = tmp.length ? tmp.attr('ssrc') : '3735928559';
bridgeSDP.media[channel] += 'a=ssrc:' + ssrc + ' cname:mixed\r\n';
bridgeSDP.media[channel] += 'a=ssrc:' + ssrc + ' label:mixedlabel' + name + '0\r\n';
bridgeSDP.media[channel] += 'a=ssrc:' + ssrc + ' msid:mixedmslabel mixedlabel' + name + '0\r\n';
bridgeSDP.media[channel] += 'a=ssrc:' + ssrc + ' mslabel:mixedmslabel\r\n';
}
// FIXME: should take code from .fromJingle
tmp = $(this.mychannel[channel]).find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]');
var channelBundleId = $(this.mychannel[channel]).attr('channel-bundle-id');
if (typeof channelBundleId != 'undefined') {
tmp = this.bundledTransports[channelBundleId];
} else {
tmp = $(this.mychannel[channel]).find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]');
}
if (tmp.length) {
bridgeSDP.media[channel] += 'a=ice-ufrag:' + tmp.attr('ufrag') + '\r\n';
bridgeSDP.media[channel] += 'a=ice-pwd:' + tmp.attr('pwd') + '\r\n';
@ -390,7 +534,7 @@ ColibriFocus.prototype.createdConference = function (result) {
function (answer) {
self.peerconnection.setLocalDescription(answer,
function () {
console.log('setLocalDescription succeded.');
console.log('setLocalDescription succeeded.');
// make sure our presence is updated
$(document).trigger('setLocalDescription.jingle', [self.sid]);
var elem = $iq({to: self.bridgejid, type: 'get'});
@ -428,6 +572,7 @@ ColibriFocus.prototype.createdConference = function (result) {
{
initiator: 'true',
expire: self.channelExpire,
id: self.mychannel[channel].attr('id'),
endpoint: self.myMucResource,
port: sctpPort
}
@ -446,7 +591,7 @@ ColibriFocus.prototype.createdConference = function (result) {
},
function (error) {
console.error(
"ERROR setLocalDescription succeded",
"ERROR sending colibri message",
error, elem);
}
);
@ -492,10 +637,14 @@ ColibriFocus.prototype.initiate = function (peer, isInitiator) {
var localSDP = new SDP(this.peerconnection.localDescription.sdp);
// throw away stuff we don't want
// not needed with static offer
sdp.removeSessionLines('a=group:');
if (!config.useBundle) {
sdp.removeSessionLines('a=group:');
}
sdp.removeSessionLines('a=msid-semantic:'); // FIXME: not mapped over jingle anyway...
for (var i = 0; i < sdp.media.length; i++) {
sdp.removeMediaLines(i, 'a=rtcp-mux');
if (!config.useRtcpMux){
sdp.removeMediaLines(i, 'a=rtcp-mux');
}
sdp.removeMediaLines(i, 'a=ssrc:');
sdp.removeMediaLines(i, 'a=crypto:');
sdp.removeMediaLines(i, 'a=candidate:');
@ -508,7 +657,8 @@ ColibriFocus.prototype.initiate = function (peer, isInitiator) {
if (1) { //i > 0) { // not for audio FIXME: does not work as intended
// re-add all remote a=ssrcs
for (var jid in this.remotessrc) {
if (jid == peer) continue;
if (jid == peer || !this.remotessrc[jid][i])
continue;
sdp.media[i] += this.remotessrc[jid][i];
}
// and local a=ssrc lines
@ -527,23 +677,26 @@ ColibriFocus.prototype.initiate = function (peer, isInitiator) {
console.log('channel id', chan.attr('id'));
tmp = chan.find('>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
if (tmp.length) {
sdp.media[j] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'cname:mixed' + '\r\n';
sdp.media[j] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'label:mixedlabela0' + '\r\n';
sdp.media[j] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'msid:mixedmslabel mixedlabela0' + '\r\n';
sdp.media[j] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'mslabel:mixedmslabel' + '\r\n';
}
// No SSRCs for 'data', comes when j == 2
else if (j < 2)
{
var name = sdp.media[j].split(" ")[0].substr(2); // 'm=audio ...'
if (name === 'audio' || name === 'video') {
// make chrome happy... '3735928559' == 0xDEADBEEF
sdp.media[j] += 'a=ssrc:' + '3735928559' + ' ' + 'cname:mixed' + '\r\n';
sdp.media[j] += 'a=ssrc:' + '3735928559' + ' ' + 'label:mixedlabelv0' + '\r\n';
sdp.media[j] += 'a=ssrc:' + '3735928559' + ' ' + 'msid:mixedmslabel mixedlabelv0' + '\r\n';
sdp.media[j] += 'a=ssrc:' + '3735928559' + ' ' + 'mslabel:mixedmslabel' + '\r\n';
var ssrc = tmp.length ? tmp.attr('ssrc') : '3735928559';
sdp.media[j] += 'a=ssrc:' + ssrc + ' cname:mixed\r\n';
sdp.media[j] += 'a=ssrc:' + ssrc + ' label:mixedlabel' + name + '0\r\n';
sdp.media[j] += 'a=ssrc:' + ssrc + ' msid:mixedmslabel mixedlabel' + name + '0\r\n';
sdp.media[j] += 'a=ssrc:' + ssrc + ' mslabel:mixedmslabel\r\n';
}
// In the case of bundle, we add each candidate to all m= lines/jingle contents,
// just as chrome does
if (config.useBundle){
tmp = this.bundledTransports[chan.attr('channel-bundle-id')];
} else {
tmp = chan.find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]');
}
tmp = chan.find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]');
if (tmp.length) {
if (tmp.attr('ufrag'))
sdp.media[j] += 'a=ice-ufrag:' + tmp.attr('ufrag') + '\r\n';
@ -615,9 +768,7 @@ ColibriFocus.prototype.addNewParticipant = function (peer) {
{
console.error('local description not ready yet, postponing', peer);
}
window.setTimeout(function () {
self.addNewParticipant(peer);
}, 250);
window.setTimeout(function () { self.addNewParticipant(peer); }, 250);
return;
}
var index = this.channels.length;
@ -625,28 +776,39 @@ ColibriFocus.prototype.addNewParticipant = function (peer) {
this.peers.push(peer);
var elem = $iq({to: this.bridgejid, type: 'get'});
elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
elem.c(
'conference',
{ xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid });
var localSDP = new SDP(this.peerconnection.localDescription.sdp);
localSDP.media.forEach(function (media, channel) {
var name = SDPUtil.parse_mid(SDPUtil.find_line(media, 'a=mid:'));
elem.c('content', {name: name});
if (name !== 'data')
{
elem.c('channel', {
var elemName;
var endpointId = peer.substr(1 + peer.lastIndexOf('/'));
var elemAttrs
= {
initiator: 'true',
expire: self.channelExpire,
endpoint: peer.substr(1 + peer.lastIndexOf('/'))
});
endpoint: endpointId
};
if (config.useBundle) {
elemAttrs['channel-bundle-id'] = endpointId;
}
if ('data' == name)
{
elemName = 'sctpconnection';
elemAttrs['port'] = 5000;
}
else
{
elem.c('sctpconnection', {
endpoint: peer.substr(1 + peer.lastIndexOf('/')),
initiator: 'true',
expire: self.channelExpire,
port: 5000
});
elemName = 'channel';
if (('video' === name) && (self.channelLastN >= 0))
elemAttrs['last-n'] = self.channelLastN;
}
elem.c('content', { name: name });
elem.c(elemName, elemAttrs);
elem.up(); // end of channel/sctpconnection
elem.up(); // end of content
});
@ -654,7 +816,8 @@ ColibriFocus.prototype.addNewParticipant = function (peer) {
this.connection.sendIQ(elem,
function (result) {
var contents = $(result).find('>conference>content').get();
for (var i = 0; i < contents.length; i++) {
var i;
for (i = 0; i < contents.length; i++) {
var channelXml = $(contents[i]).find('>channel');
if (channelXml.length)
{
@ -666,6 +829,12 @@ ColibriFocus.prototype.addNewParticipant = function (peer) {
}
self.channels[index][i] = tmp[0];
}
var channelBundles = $(result).find('>conference>channel-bundle');
for (i = 0; i < channelBundles.length; i++)
{
var endpointId = $(channelBundles[i]).attr('id');
self.bundledTransports[endpointId] = $(channelBundles[i]).find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]');
}
self.initiate(peer, true);
},
function (error) {
@ -682,6 +851,9 @@ ColibriFocus.prototype.updateChannel = function (remoteSDP, participant) {
change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
for (channel = 0; channel < this.channels[participant].length; channel++)
{
if (!remoteSDP.media[channel])
continue;
var name = SDPUtil.parse_mid(SDPUtil.find_line(remoteSDP.media[channel], 'a=mid:'));
change.c('content', {name: name});
if (name !== 'data')
@ -714,6 +886,7 @@ ColibriFocus.prototype.updateChannel = function (remoteSDP, participant) {
{
var sctpmap = SDPUtil.find_line(remoteSDP.media[channel], 'a=sctpmap:');
change.c('sctpconnection', {
id: $(this.channels[participant][channel]).attr('id'),
endpoint: $(this.channels[participant][channel]).attr('endpoint'),
expire: self.channelExpire,
port: SDPUtil.parse_sctpmap(sctpmap)[0]
@ -770,12 +943,7 @@ ColibriFocus.prototype.addSource = function (elem, fromJid) {
if (!this.peerconnection.localDescription)
{
console.warn("addSource - localDescription not ready yet")
setTimeout(function()
{
self.addSource(elem, fromJid);
},
200
);
setTimeout(function() { self.addSource(elem, fromJid); }, 200);
return;
}
@ -816,12 +984,7 @@ ColibriFocus.prototype.removeSource = function (elem, fromJid) {
if (!self.peerconnection.localDescription)
{
console.warn("removeSource - localDescription not ready yet");
setTimeout(function()
{
self.removeSource(elem, fromJid);
},
200
);
setTimeout(function() { self.removeSource(elem, fromJid); }, 200);
return;
}
@ -862,6 +1025,9 @@ ColibriFocus.prototype.setRemoteDescription = function (session, elem, desctype)
this.remotessrc[session.peerjid] = [];
for (channel = 0; channel < this.channels[participant].length; channel++) {
//if (channel == 0) continue; FIXME: does not work as intended
if (!remoteSDP.media[channel])
continue;
if (SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:').length)
{
this.remotessrc[session.peerjid][channel] =
@ -873,6 +1039,9 @@ ColibriFocus.prototype.setRemoteDescription = function (session, elem, desctype)
// ACT 4: add new a=ssrc lines to local remotedescription
for (channel = 0; channel < this.channels[participant].length; channel++) {
//if (channel == 0) continue; FIXME: does not work as intended
if (!remoteSDP.media[channel])
continue;
if (SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:').length) {
this.peerconnection.enqueueAddSsrc(
channel,
@ -893,6 +1062,12 @@ ColibriFocus.prototype.addIceCandidate = function (session, elem) {
$(elem).each(function () {
var name = $(this).attr('name');
// If we are using bundle, audio/video/data channel will have the same candidates, so only send them for
// the audio channel.
if (config.useBundle && name !== 'audio') {
return;
}
var channel = name == 'audio' ? 0 : 1; // FIXME: search mlineindex in localdesc
if (name != 'audio' && name != 'video')
channel = 2; // name == 'data'
@ -909,6 +1084,7 @@ ColibriFocus.prototype.addIceCandidate = function (session, elem) {
else
{
change.c('sctpconnection', {
id: $(self.channels[participant][channel]).attr('id'),
endpoint: $(self.channels[participant][channel]).attr('endpoint'),
expire: self.channelExpire
});
@ -919,6 +1095,10 @@ ColibriFocus.prototype.addIceCandidate = function (session, elem) {
pwd: $(this).attr('pwd'),
xmlns: $(this).attr('xmlns')
});
if (config.useRtcpMux
&& 'channel' === change.node.parentNode.nodeName) {
change.c('rtcp-mux').up();
}
$(this).find('>candidate').each(function () {
/* not yet
@ -956,11 +1136,13 @@ ColibriFocus.prototype.sendIceCandidate = function (candidate) {
}
if (this.drip_container.length === 0) {
// start 20ms callout
window.setTimeout(function () {
if (self.drip_container.length === 0) return;
self.sendIceCandidates(self.drip_container);
self.drip_container = [];
}, 20);
window.setTimeout(
function () {
if (self.drip_container.length === 0) return;
self.sendIceCandidates(self.drip_container);
self.drip_container = [];
},
20);
}
this.drip_container.push(candidate);
};
@ -990,12 +1172,16 @@ ColibriFocus.prototype.sendIceCandidates = function (candidates) {
else
{
mycands.c('sctpconnection', {
id: $(this.mychannel[cands[0].sdpMLineIndex]).attr('id'),
endpoint: $(this.mychannel[cands[0].sdpMLineIndex]).attr('endpoint'),
port: $(this.mychannel[cands[0].sdpMLineIndex]).attr('port'),
expire: self.channelExpire
});
}
mycands.c('transport', {xmlns: 'urn:xmpp:jingle:transports:ice-udp:1'});
if (config.useRtcpMux && name !== 'data') {
mycands.c('rtcp-mux').up();
}
for (var i = 0; i < cands.length; i++) {
mycands.c('candidate', SDPUtil.candidateToJingle(cands[i].candidate)).up();
}
@ -1046,6 +1232,7 @@ ColibriFocus.prototype.terminate = function (session, reason) {
else
{
change.c('sctpconnection', {
id: $(this.channels[participant][channel]).attr('id'),
endpoint: $(this.channels[participant][channel]).attr('endpoint'),
expire: '0'
});
@ -1120,4 +1307,74 @@ ColibriFocus.prototype.sendTerminate = function (session, reason, text) {
window.clearInterval(this.statsinterval);
this.statsinterval = null;
}
};
};
ColibriFocus.prototype.setRTCPTerminationStrategy = function (strategyFQN) {
var self = this;
var strategyIQ = $iq({to: this.bridgejid, type: 'set'});
strategyIQ.c('conference', {
xmlns: 'http://jitsi.org/protocol/colibri',
id: this.confid,
});
strategyIQ.c('rtcp-termination-strategy', {name: strategyFQN });
strategyIQ.c('content', {name: "video"});
strategyIQ.up(); // end of content
console.log('setting RTCP termination strategy', strategyFQN);
this.connection.sendIQ(strategyIQ,
function (res) {
console.log('got result');
},
function (err) {
console.error('got error', err);
}
);
};
/**
* Sets the default value of the channel last-n attribute in this conference and
* updates/patches the existing channels.
*/
ColibriFocus.prototype.setChannelLastN = function (channelLastN) {
if (('number' === typeof(channelLastN))
&& (this.channelLastN !== channelLastN))
{
this.channelLastN = channelLastN;
// Update/patch the existing channels.
var patch = $iq({ to: this.bridgejid, type: 'set' });
patch.c(
'conference',
{ xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid });
patch.c('content', { name: 'video' });
patch.c(
'channel',
{
id: $(this.mychannel[1 /* video */]).attr('id'),
'last-n': this.channelLastN
});
patch.up(); // end of channel
for (var p = 0; p < this.channels.length; p++)
{
patch.c(
'channel',
{
id: $(this.channels[p][1 /* video */]).attr('id'),
'last-n': this.channelLastN
});
patch.up(); // end of channel
}
this.connection.sendIQ(
patch,
function (res) {
console.info('Set channel last-n succeeded:', res);
},
function (err) {
console.error('Set channel last-n failed:', err);
});
}
};

15008
libs/jquery-ui.js vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

@ -0,0 +1,94 @@
/* jshint -W117 */
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)
{
var self = this;
var req = $iq(
{
type: 'set',
to: config.hosts.call_control
}
);
req.c('dial',
{
xmlns: this.RAYO_XMLNS,
to: to,
from: from
});
req.c('header',
{
name: 'JvbRoomName',
value: roomName
});
this.connection.sendIQ(
req,
function (result)
{
console.info('Dial result ', result);
var resource = $(result).find('ref').attr('uri');
this.call_resource = resource.substr('xmpp:'.length);
console.info(
"Received call resource: " + this.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;
}
);
}
}
);

@ -510,19 +510,21 @@ function getUserMediaWithConstraints(um, success_callback, failure_callback, res
var constraints = {audio: false, video: false};
if (um.indexOf('video') >= 0) {
constraints.video = {mandatory: {}};// same behaviour as true
constraints.video = { mandatory: {}, optional: [] };// same behaviour as true
}
if (um.indexOf('audio') >= 0) {
constraints.audio = {};// same behaviour as true
constraints.audio = { mandatory: {}, optional: []};// same behaviour as true
}
if (um.indexOf('screen') >= 0) {
constraints.video = {
mandatory: {
chromeMediaSource: "screen",
googLeakyBucket: true,
maxWidth: window.screen.width,
maxHeight: window.screen.height,
maxFrameRate: 3
}
},
optional: []
};
}
if (um.indexOf('desktop') >= 0) {
@ -530,10 +532,35 @@ function getUserMediaWithConstraints(um, success_callback, failure_callback, res
mandatory: {
chromeMediaSource: "desktop",
chromeMediaSourceId: desktopStream,
googLeakyBucket: true,
maxWidth: window.screen.width,
maxHeight: window.screen.height,
maxFrameRate: 3
}
},
optional: []
}
}
if (constraints.audio) {
// 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}
);
}
if (constraints.video) {
constraints.video.optional.push(
{googNoiseReduction: true}
);
if (um.indexOf('video') >= 0) {
constraints.video.optional.push(
{googLeakyBucket: true}
);
}
}
@ -541,7 +568,7 @@ function getUserMediaWithConstraints(um, success_callback, failure_callback, res
var isAndroid = navigator.userAgent.indexOf('Android') != -1;
if (resolution && !constraints.video || isAndroid) {
constraints.video = {mandatory: {}};// same behaviour as true
constraints.video = { mandatory: {}, optional: [] };// same behaviour as true
}
// see https://code.google.com/p/chromium/issues/detail?id=143631#c9 for list of supported resolutions
switch (resolution) {
@ -550,23 +577,23 @@ function getUserMediaWithConstraints(um, success_callback, failure_callback, res
case 'fullhd':
constraints.video.mandatory.minWidth = 1920;
constraints.video.mandatory.minHeight = 1080;
constraints.video.mandatory.minAspectRatio = 1.77;
constraints.video.optional.push({ minAspectRatio: 1.77 });
break;
case '720':
case 'hd':
constraints.video.mandatory.minWidth = 1280;
constraints.video.mandatory.minHeight = 720;
constraints.video.mandatory.minAspectRatio = 1.77;
constraints.video.optional.push({ minAspectRatio: 1.77 });
break;
case '360':
constraints.video.mandatory.minWidth = 640;
constraints.video.mandatory.minHeight = 360;
constraints.video.mandatory.minAspectRatio = 1.77;
constraints.video.optional.push({ minAspectRatio: 1.77 });
break;
case '180':
constraints.video.mandatory.minWidth = 320;
constraints.video.mandatory.minHeight = 180;
constraints.video.mandatory.minAspectRatio = 1.77;
constraints.video.optional.push({ minAspectRatio: 1.77 });
break;
// 4:3
case '960':
@ -592,12 +619,12 @@ function getUserMediaWithConstraints(um, success_callback, failure_callback, res
}
if (bandwidth) { // doesn't work currently, see webrtc issue 1846
if (!constraints.video) constraints.video = {mandatory: {}};//same behaviour as true
constraints.video.optional = [{bandwidth: bandwidth}];
if (!constraints.video) constraints.video = {mandatory: {}, optional: []};//same behaviour as true
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) constraints.video = {mandatory: {}};// same behaviour as tru;
if (!constraints.video) constraints.video = {mandatory: {}, optional: []};// same behaviour as true;
constraints.video.mandatory.minFrameRate = fps;
}

@ -174,6 +174,9 @@ SDPUtil = {
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] + '"');
}
@ -201,6 +204,12 @@ SDPUtil = {
}
break;
}
if (cand.hasOwnAttribute('tcptype')) {
line += 'tcptype';
line += ' ';
line += cand.tcptype;
line += ' ';
}
line += 'generation';
line += ' ';
line += cand.hasOwnAttribute('generation') ? cand.generation : '0';
@ -283,7 +292,9 @@ SDPUtil = {
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.substring(0, 12) != 'a=candidate:') {
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;
@ -319,6 +330,9 @@ SDPUtil = {
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] + '"');
}

@ -287,7 +287,10 @@ JingleSession.prototype.sendIceCandidates = function (candidates) {
if (SDPUtil.find_line(this.localSDP.media[mid], 'a=fingerprint:', this.localSDP.session)) {
var tmp = SDPUtil.parse_fingerprint(SDPUtil.find_line(this.localSDP.media[mid], 'a=fingerprint:', this.localSDP.session));
tmp.required = true;
cand.c('fingerprint').t(tmp.fingerprint);
cand.c(
'fingerprint',
{xmlns: 'urn:xmpp:jingle:apps:dtls:0'})
.t(tmp.fingerprint);
delete tmp.fingerprint;
cand.attrs(tmp);
cand.up();

@ -0,0 +1,131 @@
/**
* Provides statistics for the local stream.
*/
var LocalStatsCollector = (function() {
/**
* Size of the webaudio analizer buffer.
* @type {number}
*/
var WEBAUDIO_ANALIZER_FFT_SIZE = 2048;
/**
* Value of the webaudio analizer smoothing time parameter.
* @type {number}
*/
var WEBAUDIO_ANALIZER_SMOOTING_TIME = 0.8;
/**
* <tt>LocalStatsCollector</tt> calculates statistics for the local stream.
*
* @param stream the local stream
* @param interval stats refresh interval given in ms.
* @param {function(LocalStatsCollector)} updateCallback the callback called on stats
* update.
* @constructor
*/
function LocalStatsCollectorProto(stream, interval, updateCallback) {
window.AudioContext = window.AudioContext || window.webkitAudioContext;
this.stream = stream;
this.intervalId = null;
this.intervalMilis = interval;
this.updateCallback = updateCallback;
this.audioLevel = 0;
}
/**
* Starts the collecting the statistics.
*/
LocalStatsCollectorProto.prototype.start = function () {
if (!window.AudioContext)
return;
var context = new AudioContext();
var analyser = context.createAnalyser();
analyser.smoothingTimeConstant = WEBAUDIO_ANALIZER_SMOOTING_TIME;
analyser.fftSize = WEBAUDIO_ANALIZER_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.updateCallback(LocalStatsCollectorProto.LOCAL_JID, self.audioLevel);
}
},
this.intervalMilis
);
};
/**
* Stops collecting the statistics.
*/
LocalStatsCollectorProto.prototype.stop = function () {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
}
};
/**
* Converts time domain data array to audio level.
* @param array the time domain data array.
* @returns {number} the audio level
*/
var TimeDomainDataToAudioLevel = function (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));
}
/**
* Indicates that this audio level is for local jid.
* @type {string}
*/
LocalStatsCollectorProto.LOCAL_JID = 'local';
return LocalStatsCollectorProto;
})();

@ -0,0 +1,30 @@
/**
* Provides a wrapper class for the MediaStream.
*
* TODO : Add here the src from the video element and other related properties
* and get rid of some of the mappings that we use throughout the UI.
*/
var MediaStream = (function() {
/**
* Creates a MediaStream object for the given data, session id and ssrc.
*
* @param data the data object from which we obtain the stream,
* the peerjid, etc.
* @param sid the session id
* @param ssrc the ssrc corresponding to this MediaStream
*
* @constructor
*/
function MediaStreamProto(data, sid, ssrc) {
this.VIDEO_TYPE = "Video";
this.AUDIO_TYPE = "Audio";
this.stream = data.stream;
this.peerjid = data.peerjid;
this.ssrc = ssrc;
this.session = connection.jingle.sessions[sid];
this.type = (this.stream.getVideoTracks().length > 0)
? this.VIDEO_TYPE : this.AUDIO_TYPE;
}
return MediaStreamProto;
})();

@ -131,6 +131,12 @@ Strophe.addConnectionPlugin('emuc', {
// Always trigger presence to update bindings
console.log('presence change from', from);
$(document).trigger('presence.muc', [from, member, pres]);
// Trigger status message update
if (member.status) {
$(document).trigger('presence.status.muc', [from, member, pres]);
}
return true;
},
onPresenceUnavailable: function (pres) {
@ -157,6 +163,17 @@ Strophe.addConnectionPlugin('emuc', {
var from = pres.getAttribute('from');
if ($(pres).find('>error[type="auth"]>not-authorized[xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]').length) {
$(document).trigger('passwordrequired.muc', [from]);
} 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) {
// we are connected with anonymous domain and only non anonymous users can create rooms
// we must authorize the user
$(document).trigger('passwordrequired.main');
}
else
console.warn('onPresError ', pres);
} else {
console.warn('onPresError ', pres);
}
@ -356,5 +373,13 @@ Strophe.addConnectionPlugin('emuc', {
addVideoInfoToPresence: function(isMuted) {
this.presMap['videons'] = 'http://jitsi.org/jitmeet/video';
this.presMap['videomuted'] = isMuted.toString();
},
findJidFromResource: function(resourceJid) {
var peerJid = null;
Object.keys(this.members).some(function (jid) {
peerJid = jid;
return Strophe.getResourceFromJid(jid) === resourceJid;
});
return peerJid;
}
});

@ -213,6 +213,8 @@ StatsCollector.prototype.processReport = function ()
// but it seems to vary between 0 and around 32k.
audioLevel = audioLevel / 32767;
jidStats.setSsrcAudioLevel(ssrc, audioLevel);
if(jid != connection.emuc.myroomjid)
this.updateCallback(jid, audioLevel);
}
var key = 'packetsReceived';
@ -281,7 +283,5 @@ StatsCollector.prototype.processReport = function ()
// bar indicator
//console.info("Loss SMA3: " + outputAvg + " Q: " + quality);
}
self.updateCallback(self);
};

@ -5,23 +5,23 @@ var Toolbar = (function (my) {
/**
* Opens the lock room dialog.
*/
my.openLockDialog = function() {
my.openLockDialog = function () {
// Only the focus is able to set a shared key.
if (focus === null) {
if (sharedKey)
$.prompt("This conversation is currently protected by"
+ " a shared secret key.",
{
title: "Secrect key",
title: "Secret key",
persistent: false
}
);
else
$.prompt("This conversation isn't currently protected by"
+ " a secret key. Only the owner of the conference" +
+ " a secret key. Only the owner of the conference"
+ " could set a shared key.",
{
title: "Secrect key",
title: "Secret key",
persistent: false
}
);
@ -29,7 +29,7 @@ var Toolbar = (function (my) {
if (sharedKey) {
$.prompt("Are you sure you would like to remove your secret key?",
{
title: "Remove secrect key",
title: "Remove secret key",
persistent: false,
buttons: { "Remove": true, "Cancel": false},
defaultButton: 1,
@ -42,7 +42,7 @@ var Toolbar = (function (my) {
}
);
} else {
$.prompt('<h2>Set a secrect key to lock your room</h2>' +
$.prompt('<h2>Set a secret key to lock your room</h2>' +
'<input id="lockKey" type="text" placeholder="your shared key" autofocus>',
{
persistent: false,
@ -54,7 +54,7 @@ var Toolbar = (function (my) {
submit: function (e, v, m, f) {
if (v) {
var lockKey = document.getElementById('lockKey');
if (lockKey.value) {
setSharedKey(Util.escapeHtml(lockKey.value));
lockRoom(true);
@ -70,28 +70,80 @@ var Toolbar = (function (my) {
/**
* Opens the invite link dialog.
*/
my.openLinkDialog = function() {
my.openLinkDialog = function () {
var inviteLink;
if (roomUrl == null)
inviteLink = "Your conference is currently being created...";
else
inviteLink = encodeURI(roomUrl);
$.prompt('<input id="inviteLinkRef" type="text" value="' +
encodeURI(roomUrl) + '" onclick="this.select();" readonly>',
{
title: "Share this link with everyone you want to invite",
persistent: false,
buttons: { "Cancel": false},
loaded: function (event) {
document.getElementById('inviteLinkRef').select();
inviteLink + '" onclick="this.select();" readonly>',
{
title: "Share this link with everyone you want to invite",
persistent: false,
buttons: { "Invite": true, "Cancel": false},
defaultButton: 1,
loaded: function (event) {
if (roomUrl)
document.getElementById('inviteLinkRef').select();
else
document.getElementById('jqi_state0_buttonInvite')
.disabled = true;
},
submit: function (e, v, m, f) {
if (v) {
if (roomUrl) {
inviteParticipants();
}
}
}
}
}
);
);
};
/**
* Invite participants to conference.
*/
function inviteParticipants() {
if (roomUrl == null)
return;
var sharedKeyText = "";
if (sharedKey && sharedKey.length > 0)
sharedKeyText
= "This conference is password protected. Please use the "
+ "following pin when joining:%0D%0A%0D%0A"
+ sharedKey + "%0D%0A%0D%0A";
var conferenceName = roomUrl.substring(roomUrl.lastIndexOf('/') + 1);
var subject = "Invitation to a Jitsi Meet (" + conferenceName + ")";
var body = "Hey there, I%27d like to invite you to a Jitsi Meet" +
" conference I%27ve just set up.%0D%0A%0D%0A" +
"Please click on the following link in order" +
" to join the conference.%0D%0A%0D%0A" +
roomUrl +
"%0D%0A%0D%0A" +
sharedKeyText +
"Note that Jitsi Meet is currently only supported by Chromium," +
" Google Chrome and Opera, so you need" +
" to be using one of these browsers.%0D%0A%0D%0A" +
"Talk to you in a sec!";
if (window.localStorage.displayname)
body += "%0D%0A%0D%0A" + window.localStorage.displayname;
window.open("mailto:?subject=" + subject + "&body=" + body, '_blank');
}
/**
* Opens the settings dialog.
*/
my.openSettingsDialog = function() {
my.openSettingsDialog = function () {
$.prompt('<h2>Configure your conference</h2>' +
'<input type="checkbox" id="initMuted"> Participants join muted<br/>' +
'<input type="checkbox" id="requireNicknames"> Require nicknames<br/><br/>' +
'Set a secrect key to lock your room: <input id="lockKey" type="text" placeholder="your shared key" autofocus>',
'Set a secret key to lock your room: <input id="lockKey" type="text" placeholder="your shared key" autofocus>',
{
persistent: false,
buttons: { "Save": true, "Cancel": false},
@ -104,13 +156,13 @@ var Toolbar = (function (my) {
if ($('#initMuted').is(":checked")) {
// it is checked
}
if ($('#requireNicknames').is(":checked")) {
// it is checked
}
/*
var lockKey = document.getElementById('lockKey');
if (lockKey.value)
{
setSharedKey(lockKey.value);
@ -185,9 +237,12 @@ var Toolbar = (function (my) {
if (!$('#header').is(':visible')) {
Toolbar.showToolbar();
}
// Then clear the time out, to dock the toolbar.
clearTimeout(toolbarTimeout);
toolbarTimeout = null;
if (toolbarTimeout) {
clearTimeout(toolbarTimeout);
toolbarTimeout = null;
}
}
else {
if (!$('#header').is(':visible')) {
@ -230,5 +285,37 @@ var Toolbar = (function (my) {
}
};
// Shows or hides the 'recording' button.
my.showRecordingButton = function (show) {
if (!config.enableRecording) {
return;
}
if (show) {
$('#recording').css({display: "inline"});
}
else {
$('#recording').css({display: "none"});
}
};
// Toggle the state of the recording button
my.toggleRecordingButtonState = function() {
$('#recordButton').toggleClass('active');
};
// Shows or hides SIP calls button
my.showSipCallButton = function (show)
{
if (config.hosts.call_control && show)
{
$('#sipCallButton').css({display: "inline"});
}
else
{
$('#sipCallButton').css({display: "none"});
}
};
return my;
}(Toolbar || {}));
}(Toolbar || {}));

@ -52,7 +52,9 @@ var Util = (function (my) {
*/
my.getAvailableVideoWidth = function () {
var chatspaceWidth
= $('#chatspace').is(":visible") ? $('#chatspace').width() : 0;
= (Chat.isVisible() || ContactList.isVisible())
? $('#chatspace').width()
: 0;
return window.innerWidth - chatspaceWidth;
};

@ -1,6 +1,8 @@
var VideoLayout = (function (my) {
var preMuted = false;
var currentActiveSpeaker = null;
var currentDominantSpeaker = null;
var lastNCount = config.channelLastN;
var lastNEndpointsCache = [];
my.changeLocalAudio = function(stream) {
connection.jingle.localAudio = stream;
@ -26,25 +28,33 @@ var VideoLayout = (function (my) {
var localVideoContainer = document.getElementById('localVideoWrapper');
localVideoContainer.appendChild(localVideo);
AudioLevels.updateAudioLevelCanvas();
var localVideoSelector = $('#' + localVideo.id);
// Add click handler
// Add click handler to both video and video wrapper elements in case
// there's no video.
localVideoSelector.click(function () {
VideoLayout.handleVideoThumbClicked(localVideo.src);
});
$('#localVideoContainer').click(function () {
VideoLayout.handleVideoThumbClicked(localVideo.src);
});
// Add hover handler
$('#localVideoContainer').hover(
function() {
VideoLayout.showDisplayName('localVideoContainer', true);
},
function() {
if (focusedVideoSrc !== localVideo.src)
if (!VideoLayout.isLargeVideoVisible()
|| localVideo.src !== $('#largeVideo').attr('src'))
VideoLayout.showDisplayName('localVideoContainer', false);
}
);
// Add stream ended handler
stream.onended = function () {
localVideoContainer.removeChild(localVideo);
VideoLayout.checkChangeLargeVideo(localVideo.src);
VideoLayout.updateRemovedVideo(localVideo.src);
};
// Flip video x axis if needed
flipXLocalVideo = flipX;
@ -55,6 +65,7 @@ var VideoLayout = (function (my) {
RTC.attachMediaStream(localVideoSelector, stream);
localVideoSrc = localVideo.src;
VideoLayout.updateLargeVideo(localVideoSrc, 0);
};
@ -63,7 +74,7 @@ var VideoLayout = (function (my) {
* another one instead.
* @param removedVideoSrc src stream identifier of the video.
*/
my.checkChangeLargeVideo = function(removedVideoSrc) {
my.updateRemovedVideo = function(removedVideoSrc) {
if (removedVideoSrc === $('#largeVideo').attr('src')) {
// this is currently displayed as large
// pick the last visible video in the row
@ -75,7 +86,8 @@ var VideoLayout = (function (my) {
if (!pick) {
console.info("Last visible video no longer exists");
pick = $('#remoteVideos>span[id!="mixedstream"]>video').get(0);
if (!pick) {
if (!pick || !pick.src) {
// Try local video
console.info("Fallback to local video...");
pick = $('#remoteVideos>span>span>video').get(0);
@ -103,6 +115,8 @@ var VideoLayout = (function (my) {
var isVisible = $('#largeVideo').is(':visible');
$('#largeVideo').fadeOut(300, function () {
var oldSrc = $(this).attr('src');
$(this).attr('src', newSrc);
// Screen stream is already rotated
@ -129,8 +143,25 @@ var VideoLayout = (function (my) {
? getDesktopVideoPosition
: getCameraVideoPosition;
if (isVisible)
if (isVisible) {
// Only if the large video is currently visible.
// Disable previous dominant speaker video.
var oldJid = getJidFromVideoSrc(oldSrc);
if (oldJid) {
var oldResourceJid = Strophe.getResourceFromJid(oldJid);
VideoLayout.enableDominantSpeaker(oldResourceJid, false);
}
// Enable new dominant speaker in the remote videos section.
var userJid = getJidFromVideoSrc(newSrc);
if (userJid)
{
var resourceJid = Strophe.getResourceFromJid(userJid);
VideoLayout.enableDominantSpeaker(resourceJid, true);
}
$(this).fadeIn(300);
}
});
}
};
@ -138,35 +169,44 @@ var VideoLayout = (function (my) {
my.handleVideoThumbClicked = function(videoSrc) {
// Restore style for previously focused video
var focusJid = getJidFromVideoSrc(focusedVideoSrc);
var oldContainer =
getParticipantContainer(focusJid);
var oldContainer = getParticipantContainer(focusJid);
if (oldContainer) {
oldContainer.removeClass("videoContainerFocused");
VideoLayout.enableActiveSpeaker(
Strophe.getResourceFromJid(focusJid), false);
}
// Unlock
// Unlock current focused.
if (focusedVideoSrc === videoSrc)
{
focusedVideoSrc = null;
var dominantSpeakerVideo = null;
// Enable the currently set dominant speaker.
if (currentDominantSpeaker) {
dominantSpeakerVideo
= $('#participant_' + currentDominantSpeaker + '>video')
.get(0);
if (dominantSpeakerVideo) {
VideoLayout.updateLargeVideo(dominantSpeakerVideo.src, 1);
}
}
return;
}
// Lock new video
focusedVideoSrc = videoSrc;
// Update focused/pinned interface.
var userJid = getJidFromVideoSrc(videoSrc);
if (userJid)
{
var container = getParticipantContainer(userJid);
container.addClass("videoContainerFocused");
var resourceJid = Strophe.getResourceFromJid(userJid);
VideoLayout.enableActiveSpeaker(resourceJid, true);
}
// Triggers a "video.selected" event. The "false" parameter indicates
// this isn't a prezi.
$(document).trigger("video.selected", [false]);
VideoLayout.updateLargeVideo(videoSrc, 1);
@ -215,44 +255,71 @@ var VideoLayout = (function (my) {
* Shows/hides the large video.
*/
my.setLargeVideoVisible = function(isVisible) {
var largeVideoJid = getJidFromVideoSrc($('#largeVideo').attr('src'));
var resourceJid = Strophe.getResourceFromJid(largeVideoJid);
if (isVisible) {
$('#largeVideo').css({visibility: 'visible'});
$('.watermark').css({visibility: 'visible'});
VideoLayout.enableDominantSpeaker(resourceJid, true);
}
else {
$('#largeVideo').css({visibility: 'hidden'});
$('.watermark').css({visibility: 'hidden'});
VideoLayout.enableDominantSpeaker(resourceJid, false);
}
};
/**
* Indicates if the large video is currently visible.
*
* @return <tt>true</tt> if visible, <tt>false</tt> - otherwise
*/
my.isLargeVideoVisible = function() {
return $('#largeVideo').is(':visible');
};
/**
* Checks if container for participant identified by given peerJid exists
* in the document and creates it eventually.
*
* @param peerJid peer Jid to check.
*
* @return Returns <tt>true</tt> if the peer container exists,
* <tt>false</tt> - otherwise
*/
my.ensurePeerContainerExists = function(peerJid) {
var peerResource = Strophe.getResourceFromJid(peerJid);
var videoSpanId = 'participant_' + peerResource;
ContactList.ensureAddContact(peerJid);
var resourceJid = Strophe.getResourceFromJid(peerJid);
var videoSpanId = 'participant_' + resourceJid;
if ($('#' + videoSpanId).length > 0) {
// If there's been a focus change, make sure we add focus related
// interface!!
if (focus && $('#remote_popupmenu_' + peerResource).length <= 0)
if (focus && $('#remote_popupmenu_' + resourceJid).length <= 0)
addRemoteVideoMenu( peerJid,
document.getElementById(videoSpanId));
return;
}
var container
= VideoLayout.addRemoteVideoContainer(peerJid, videoSpanId);
var nickfield = document.createElement('span');
nickfield.className = "nick";
nickfield.appendChild(document.createTextNode(peerResource));
container.appendChild(nickfield);
VideoLayout.resizeThumbnails();
else {
var container
= VideoLayout.addRemoteVideoContainer(peerJid, videoSpanId);
var nickfield = document.createElement('span');
nickfield.className = "nick";
nickfield.appendChild(document.createTextNode(resourceJid));
container.appendChild(nickfield);
// In case this is not currently in the last n we don't show it.
if (lastNCount
&& lastNCount > 0
&& $('#remoteVideos>span').length >= lastNCount + 2) {
showPeerContainer(resourceJid, false);
}
else
VideoLayout.resizeThumbnails();
}
};
my.addRemoteVideoContainer = function(peerJid, spanId) {
@ -267,14 +334,167 @@ var VideoLayout = (function (my) {
addRemoteVideoMenu(peerJid, container);
remotes.appendChild(container);
AudioLevels.updateAudioLevelCanvas(peerJid);
return container;
};
/**
* Shows the display name for the given video.
* Creates an audio or video stream element.
*/
my.createStreamElement = function (sid, stream) {
var isVideo = stream.getVideoTracks().length > 0;
var element = isVideo
? document.createElement('video')
: document.createElement('audio');
var id = (isVideo ? 'remoteVideo_' : 'remoteAudio_')
+ sid + '_' + stream.id;
element.id = id;
element.autoplay = true;
element.oncontextmenu = function () { return false; };
return element;
};
my.addRemoteStreamElement
= function (container, sid, stream, peerJid, thessrc) {
var newElementId = null;
var isVideo = stream.getVideoTracks().length > 0;
if (container) {
var streamElement = VideoLayout.createStreamElement(sid, stream);
newElementId = streamElement.id;
container.appendChild(streamElement);
var sel = $('#' + newElementId);
sel.hide();
// If the container is currently visible we attach the stream.
if (!isVideo
|| (container.offsetParent !== null && isVideo)) {
RTC.attachMediaStream(sel, stream);
if (isVideo)
waitForRemoteVideo(sel, thessrc, stream);
}
stream.onended = function () {
console.log('stream ended', this);
VideoLayout.removeRemoteStreamElement(stream, container);
if (peerJid)
ContactList.removeContact(peerJid);
};
// Add click handler.
container.onclick = function (event) {
/*
* FIXME It turns out that videoThumb may not exist (if there is
* no actual video).
*/
var videoThumb = $('#' + container.id + '>video').get(0);
if (videoThumb)
VideoLayout.handleVideoThumbClicked(videoThumb.src);
event.preventDefault();
return false;
};
// Add hover handler
$(container).hover(
function() {
VideoLayout.showDisplayName(container.id, true);
},
function() {
var videoSrc = null;
if ($('#' + container.id + '>video')
&& $('#' + container.id + '>video').length > 0) {
videoSrc = $('#' + container.id + '>video').get(0).src;
}
// If the video has been "pinned" by the user we want to
// keep the display name on place.
if (!VideoLayout.isLargeVideoVisible()
|| videoSrc !== $('#largeVideo').attr('src'))
VideoLayout.showDisplayName(container.id, false);
}
);
}
return newElementId;
};
/**
* Removes the remote stream element corresponding to the given stream and
* parent container.
*
* @param stream the stream
* @param container
*/
my.setDisplayName = function(videoSpanId, displayName) {
my.removeRemoteStreamElement = function (stream, container) {
if (!container)
return;
var select = null;
var removedVideoSrc = null;
if (stream.getVideoTracks().length > 0) {
select = $('#' + container.id + '>video');
removedVideoSrc = select.get(0).src;
}
else
select = $('#' + container.id + '>audio');
// Remove video source from the mapping.
delete videoSrcToSsrc[removedVideoSrc];
// Mark video as removed to cancel waiting loop(if video is removed
// before has started)
select.removed = true;
select.remove();
var audioCount = $('#' + container.id + '>audio').length;
var videoCount = $('#' + container.id + '>video').length;
if (!audioCount && !videoCount) {
console.log("Remove whole user", container.id);
// Remove whole container
container.remove();
Util.playSoundNotification('userLeft');
VideoLayout.resizeThumbnails();
}
if (removedVideoSrc)
VideoLayout.updateRemovedVideo(removedVideoSrc);
};
/**
* Show/hide peer container for the given resourceJid.
*/
function showPeerContainer(resourceJid, isShow) {
var peerContainer = $('#participant_' + resourceJid);
if (!peerContainer)
return;
if (!peerContainer.is(':visible') && isShow)
peerContainer.show();
else if (peerContainer.is(':visible') && !isShow)
peerContainer.hide();
};
/**
* Sets the display name for the given video span id.
*/
function setDisplayName(videoSpanId, displayName) {
var nameSpan = $('#' + videoSpanId + '>span.displayname');
var defaultLocalDisplayName = "Me";
var defaultRemoteDisplayName = "Speaker";
// If we already have a display name for this video.
if (nameSpan.length > 0) {
@ -282,21 +502,33 @@ var VideoLayout = (function (my) {
if (nameSpanElement.id === 'localDisplayName' &&
$('#localDisplayName').text() !== displayName) {
$('#localDisplayName').text(displayName);
if (displayName && displayName.length > 0)
$('#localDisplayName').text(displayName + ' (me)');
else
$('#localDisplayName').text(defaultLocalDisplayName);
} else {
$('#' + videoSpanId + '_name').text(displayName);
if (displayName && displayName.length > 0)
$('#' + videoSpanId + '_name').text(displayName);
else
$('#' + videoSpanId + '_name').text(defaultRemoteDisplayName);
}
} else {
var editButton = null;
nameSpan = document.createElement('span');
nameSpan.className = 'displayname';
$('#' + videoSpanId)[0].appendChild(nameSpan);
if (videoSpanId === 'localVideoContainer') {
editButton = createEditDisplayNameButton();
nameSpan.innerText = defaultLocalDisplayName;
}
if (displayName.length) {
nameSpan = document.createElement('span');
nameSpan.className = 'displayname';
else {
nameSpan.innerText = defaultRemoteDisplayName;
}
if (displayName && displayName.length > 0) {
nameSpan.innerText = displayName;
$('#' + videoSpanId)[0].appendChild(nameSpan);
}
if (!editButton) {
@ -318,7 +550,9 @@ var VideoLayout = (function (my) {
editableText.setAttribute('placeholder', 'ex. Jane Pink');
$('#' + videoSpanId)[0].appendChild(editableText);
$('#localVideoContainer .displayname').bind("click", function (e) {
$('#localVideoContainer .displayname')
.bind("click", function (e) {
e.preventDefault();
$('#localDisplayName').hide();
$('#editDisplayName').show();
@ -336,16 +570,15 @@ var VideoLayout = (function (my) {
}
if (!$('#localDisplayName').is(":visible")) {
if (nickname) {
if (nickname)
$('#localDisplayName').text(nickname + " (me)");
$('#localDisplayName').show();
}
else {
$('#localDisplayName').text(nickname);
}
$('#editDisplayName').hide();
else
$('#localDisplayName')
.text(defaultLocalDisplayName);
$('#localDisplayName').show();
}
$('#editDisplayName').hide();
};
$('#editDisplayName').one("focusout", function (e) {
@ -370,7 +603,6 @@ var VideoLayout = (function (my) {
*/
my.showDisplayName = function(videoSpanId, isShow) {
var nameSpan = $('#' + videoSpanId + '>span.displayname').get(0);
if (isShow) {
if (nameSpan && nameSpan.innerHTML && nameSpan.innerHTML.length)
nameSpan.setAttribute("style", "display:inline-block;");
@ -381,6 +613,38 @@ var VideoLayout = (function (my) {
}
};
/**
* Shows the presence status message for the given video.
*/
my.setPresenceStatus = function (videoSpanId, statusMsg) {
if (!$('#' + videoSpanId).length) {
// No container
return;
}
var statusSpan = $('#' + videoSpanId + '>span.status');
if (!statusSpan.length) {
//Add status span
statusSpan = document.createElement('span');
statusSpan.className = 'status';
statusSpan.id = videoSpanId + '_status';
$('#' + videoSpanId)[0].appendChild(statusSpan);
statusSpan = $('#' + videoSpanId + '>span.status');
}
// Display status
if (statusMsg && statusMsg.length) {
$('#' + videoSpanId + '_status').text(statusMsg);
statusSpan.get(0).setAttribute("style", "display:inline-block;");
}
else {
// Hide
statusSpan.get(0).setAttribute("style", "display:none;");
}
};
/**
* Shows a visual indicator for the focus of the conference.
* Currently if we're not the owner of the conference we obtain the focus
@ -413,9 +677,7 @@ var VideoLayout = (function (my) {
if (!indicatorSpan || indicatorSpan.length === 0) {
indicatorSpan = document.createElement('span');
indicatorSpan.className = 'focusindicator';
Util.setTooltip(indicatorSpan,
"The owner of<br/>this conference",
"top");
focusContainer.appendChild(indicatorSpan);
createFocusIndicatorElement(indicatorSpan);
@ -506,7 +768,9 @@ var VideoLayout = (function (my) {
* Resizes thumbnails.
*/
my.resizeThumbnails = function() {
var thumbnailSize = calculateThumbnailSize();
var videoSpaceWidth = $('#remoteVideos').width();
var thumbnailSize = VideoLayout.calculateThumbnailSize(videoSpaceWidth);
var width = thumbnailSize[0];
var height = thumbnailSize[1];
@ -515,63 +779,66 @@ var VideoLayout = (function (my) {
$('#remoteVideos').height(height);
$('#remoteVideos>span').width(width);
$('#remoteVideos>span').height(height);
$(document).trigger("remotevideo.resized", [width, height]);
};
/**
* Enables the active speaker UI.
* Enables the dominant speaker UI.
*
* @param resourceJid the jid indicating the video element to
* activate/deactivate
* @param isEnable indicates if the active speaker should be enabled or
* @param isEnable indicates if the dominant speaker should be enabled or
* disabled
*/
my.enableActiveSpeaker = function(resourceJid, isEnable) {
console.log("Enable active speaker", resourceJid, isEnable);
my.enableDominantSpeaker = function(resourceJid, isEnable) {
var displayName = resourceJid;
var nameSpan = $('#participant_' + resourceJid + '>span.displayname');
if (nameSpan.length > 0)
displayName = nameSpan.text();
console.log("UI enable dominant speaker",
displayName,
resourceJid,
isEnable);
var videoSpanId = null;
var videoContainerId = null;
if (resourceJid
=== Strophe.getResourceFromJid(connection.emuc.myroomjid))
=== Strophe.getResourceFromJid(connection.emuc.myroomjid)) {
videoSpanId = 'localVideoWrapper';
else
videoContainerId = 'localVideoContainer';
}
else {
videoSpanId = 'participant_' + resourceJid;
videoContainerId = videoSpanId;
}
videoSpan = document.getElementById(videoSpanId);
videoSpan = document.getElementById(videoContainerId);
if (!videoSpan) {
console.error("No video element for jid", resourceJid);
return;
}
// If there's an active speaker (automatically) selected we have to
// disable this state and update the current active speaker.
if (isEnable) {
if (currentActiveSpeaker) {
var oldSpeaker = currentActiveSpeaker;
setTimeout(function () {
VideoLayout.enableActiveSpeaker(oldSpeaker, false);
}, 200);
}
currentActiveSpeaker = resourceJid;
}
else if (resourceJid === currentActiveSpeaker)
currentActiveSpeaker = null;
var video = $('#' + videoSpanId + '>video');
if (video && video.length > 0) {
var videoElement = video.get(0);
if (isEnable) {
if (!videoElement.classList.contains("activespeaker"))
videoElement.classList.add("activespeaker");
VideoLayout.showDisplayName(videoContainerId, true);
if (!videoSpan.classList.contains("dominantspeaker"))
videoSpan.classList.add("dominantspeaker");
VideoLayout.showDisplayName(videoSpanId, true);
video.css({visibility: 'hidden'});
}
else {
VideoLayout.showDisplayName(videoSpanId, false);
VideoLayout.showDisplayName(videoContainerId, false);
if (videoSpan.classList.contains("dominantspeaker"))
videoSpan.classList.remove("dominantspeaker");
if (videoElement.classList.contains("activespeaker"))
videoElement.classList.remove("activespeaker");
video.css({visibility: 'visible'});
}
}
};
@ -616,18 +883,26 @@ var VideoLayout = (function (my) {
/**
* Calculates the thumbnail size.
*
* @param videoSpaceWidth the width of the video space
*/
var calculateThumbnailSize = function () {
my.calculateThumbnailSize = function (videoSpaceWidth) {
// 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;
var numvids = $('#remoteVideos>span:visible').length;
var numvids = 0;
if (lastNCount && lastNCount > 0)
numvids = lastNCount + 1;
else
numvids = $('#remoteVideos>span:visible').length;
// Remove the 3px borders arround videos and border around the remote
// videos area
var availableWinWidth = videoSpaceWidth - 2 * 3 * numvids - 70;
// Remove the 1px borders arround videos and the chat width.
var availableWinWidth = $('#remoteVideos').width() - 2 * numvids - 50;
var availableWidth = availableWinWidth / numvids;
var aspectRatio = 16.0 / 9.0;
var maxHeight = Math.min(160, availableHeight);
@ -703,6 +978,10 @@ var VideoLayout = (function (my) {
var focusIndicator = document.createElement('i');
focusIndicator.className = 'fa fa-star';
parentElement.appendChild(focusIndicator);
Util.setTooltip(parentElement,
"The owner of<br/>this conference",
"top");
}
/**
@ -733,6 +1012,27 @@ var VideoLayout = (function (my) {
}
};
/**
* Returns the current dominant speaker resource jid.
*/
my.getDominantSpeakerResourceJid = function () {
return currentDominantSpeaker;
};
/**
* Returns the corresponding resource jid to the given peer container
* DOM element.
*
* @return the corresponding resource jid to the given peer container
* DOM element
*/
my.getPeerContainerResourceJid = function (containerElement) {
var i = containerElement.id.indexOf('participant_');
if (i >= 0)
return containerElement.id.substring(i + 12);
};
/**
* Adds the remote video menu element for the given <tt>jid</tt> in the
* given <tt>parentElement</tt>.
@ -810,6 +1110,9 @@ var VideoLayout = (function (my) {
popupmenuElement.appendChild(ejectMenuItem);
}
/**
* On audio muted event.
*/
$(document).bind('audiomuted.muc', function (event, jid, isMuted) {
var videoSpanId = null;
if (jid === connection.emuc.myroomjid) {
@ -828,6 +1131,9 @@ var VideoLayout = (function (my) {
VideoLayout.showAudioIndicator(videoSpanId, isMuted);
});
/**
* On video muted event.
*/
$(document).bind('videomuted.muc', function (event, jid, isMuted) {
var videoSpanId = null;
if (jid === connection.emuc.myroomjid) {
@ -841,5 +1147,141 @@ var VideoLayout = (function (my) {
VideoLayout.showVideoIndicator(videoSpanId, isMuted);
});
/**
* Display name changed.
*/
$(document).bind('displaynamechanged',
function (event, jid, displayName, status) {
if (jid === 'localVideoContainer'
|| jid === connection.emuc.myroomjid) {
setDisplayName('localVideoContainer',
displayName);
} else {
VideoLayout.ensurePeerContainerExists(jid);
setDisplayName(
'participant_' + Strophe.getResourceFromJid(jid),
displayName,
status);
}
});
/**
* On dominant speaker changed event.
*/
$(document).bind('dominantspeakerchanged', function (event, resourceJid) {
// We ignore local user events.
if (resourceJid
=== Strophe.getResourceFromJid(connection.emuc.myroomjid))
return;
// Update the current dominant speaker.
if (resourceJid !== currentDominantSpeaker)
currentDominantSpeaker = resourceJid;
else
return;
// Obtain container for new dominant speaker.
var container = document.getElementById(
'participant_' + resourceJid);
// Local video will not have container found, but that's ok
// since we don't want to switch to local video.
if (container && !focusedVideoSrc)
{
var video = container.getElementsByTagName("video");
// Update the large video if the video source is already available,
// otherwise wait for the "videoactive.jingle" event.
if (video.length && video[0].currentTime > 0)
VideoLayout.updateLargeVideo(video[0].src);
}
});
/**
* On last N change event.
*
* @param event the event that notified us
* @param lastNEndpoints the list of last N endpoints
* @param endpointsEnteringLastN the list currently entering last N
* endpoints
*/
$(document).bind('lastnchanged', function ( event,
lastNEndpoints,
endpointsEnteringLastN,
stream) {
if (lastNCount !== lastNEndpoints.length)
lastNCount = lastNEndpoints.length;
lastNEndpointsCache = lastNEndpoints;
$('#remoteVideos>span').each(function( index, element ) {
var resourceJid = VideoLayout.getPeerContainerResourceJid(element);
if (resourceJid
&& lastNEndpoints.length > 0
&& lastNEndpoints.indexOf(resourceJid) < 0) {
console.log("Remove from last N", resourceJid);
showPeerContainer(resourceJid, false);
}
});
if (!endpointsEnteringLastN || endpointsEnteringLastN.length < 0)
endpointsEnteringLastN = lastNEndpoints;
if (endpointsEnteringLastN && endpointsEnteringLastN.length > 0) {
endpointsEnteringLastN.forEach(function (resourceJid) {
if (!$('#participant_' + resourceJid).is(':visible')) {
console.log("Add to last N", resourceJid);
showPeerContainer(resourceJid, true);
mediaStreams.some(function (mediaStream) {
if (mediaStream.peerjid
&& Strophe.getResourceFromJid(mediaStream.peerjid)
=== resourceJid
&& mediaStream.type === mediaStream.VIDEO_TYPE) {
var sel = $('#participant_' + resourceJid + '>video');
RTC.attachMediaStream(sel, mediaStream.stream);
waitForRemoteVideo(
sel,
mediaStream.ssrc,
mediaStream.stream);
return true;
}
});
}
});
}
});
$(document).bind('videoactive.jingle', function (event, videoelem) {
if (videoelem.attr('id').indexOf('mixedmslabel') === -1) {
// ignore mixedmslabela0 and v0
videoelem.show();
VideoLayout.resizeThumbnails();
var videoParent = videoelem.parent();
var parentResourceJid = null;
if (videoParent)
parentResourceJid
= VideoLayout.getPeerContainerResourceJid(videoParent[0]);
// Update the large video to the last added video only if there's no
// current dominant or focused speaker or update it to the current
// dominant speaker.
if ((!focusedVideoSrc && !VideoLayout.getDominantSpeakerResourceJid())
|| (parentResourceJid
&& VideoLayout.getDominantSpeakerResourceJid()
=== parentResourceJid)) {
VideoLayout.updateLargeVideo(videoelem.attr('src'), 1);
}
VideoLayout.showFocusIndicator();
}
});
return my;
}(VideoLayout || {}));
}(VideoLayout || {}));

Loading…
Cancel
Save