Merge remote-tracking branch 'upstream/master'

pull/74/head
turint 11 years ago
commit 77cb10d6a1
  1. 3
      README.md
  2. 1171
      app.js
  3. 52
      chat.js
  4. 98
      commands.js
  5. 4
      config.js
  6. 4
      css/main.css
  7. 124
      css/popover.css
  8. 4
      css/popup_menu.css
  9. 56
      css/videolayout_default.css
  10. 80
      data_channels.js
  11. 6
      desktopsharing.js
  12. 8
      etherpad.js
  13. 70
      index.html
  14. 304
      libs/colibri/colibri.focus.js
  15. 110
      libs/popover.js
  16. 10
      libs/strophe/strophe.jingle.adapter.js
  17. 56
      libs/strophe/strophe.jingle.sdp.js
  18. 18
      libs/strophe/strophe.jingle.sdp.util.js
  19. 1
      libs/strophe/strophe.jingle.session.js
  20. 28
      libs/strophe/strophe.jingle.sessionbase.js
  21. 399
      libs/tooltip.js
  22. 97
      local_stats.js
  23. 31
      muc.js
  24. 12
      prezi.js
  25. 287
      rtp_stats.js
  26. 234
      toolbar.js
  27. 27
      util.js
  28. 901
      videolayout.js

@ -14,6 +14,9 @@ You can find information on how to deploy Jitsi Meet in the [installation instru
You may also find it helpful to have a look at our sample [config files](https://github.com/jitsi/jitsi-meet/tree/master/doc/example-config-files/)
## Discuss
Please use the [Jitsi dev mailing list](http://lists.jitsi.org/pipermail/dev/) to discuss feature requests before opening an issue on github.
## Acknowledgements
Jitsi Meet started out as a sample conferencing application using Jitsi Videobridge. It was originally developed by Philipp Hancke who then contributed it to the community where development continues with joint forces!

1171
app.js

File diff suppressed because it is too large Load Diff

@ -39,11 +39,20 @@ var Chat = (function (my) {
$('#usermsg').keydown(function (event) {
if (event.keyCode === 13) {
event.preventDefault();
var message = Util.escapeHtml(this.value);
var value = this.value;
$('#usermsg').val('').trigger('autosize.resize');
this.focus();
var command = new CommandsProcessor(value);
if(command.isCommand())
{
command.processCommand();
}
else
{
var message = Util.escapeHtml(value);
connection.emuc.sendMessage(message, nickname);
}
}
});
var onTextAreaResize = function () {
@ -90,6 +99,45 @@ var Chat = (function (my) {
{ scrollTop: $('#chatconversation')[0].scrollHeight}, 1000);
};
/**
* Appends error message to the conversation
* @param errorMessage the received error message.
* @param originalText the original message.
*/
my.chatAddError = function(errorMessage, originalText)
{
errorMessage = Util.escapeHtml(errorMessage);
originalText = Util.escapeHtml(originalText);
$('#chatconversation').append('<div class="errorMessage"><b>Error: </b>'
+ 'Your message' + (originalText? (' \"'+ originalText + '\"') : "")
+ ' was not sent.' + (errorMessage? (' Reason: ' + errorMessage) : '')
+ '</div>');
$('#chatconversation').animate(
{ scrollTop: $('#chatconversation')[0].scrollHeight}, 1000);
}
/**
* Sets the subject to the UI
* @param subject the subject
*/
my.chatSetSubject = function(subject)
{
if(subject)
subject = subject.trim();
$('#subject').html(linkify(Util.escapeHtml(subject)));
if(subject == "")
{
$("#subject").css({display: "none"});
}
else
{
$("#subject").css({display: "block"});
}
}
/**
* Opens / closes the chat area.
*/
@ -242,7 +290,7 @@ var Chat = (function (my) {
if (unreadMessages) {
unreadMsgElement.innerHTML = unreadMessages.toString();
showToolbar();
Toolbar.showToolbar();
var chatButtonElement
= document.getElementById('chatButton').parentNode;

@ -0,0 +1,98 @@
/**
* Handles commands received via chat messages.
*/
var CommandsProcessor = (function()
{
/**
* Constructs new CommandProccessor instance from a message.
* @param message the message
* @constructor
*/
function CommandsPrototype(message)
{
/**
* Extracts the command from the message.
* @param message the received message
* @returns {string} the command
*/
function getCommand(message)
{
if(message)
{
for(var command in commands)
{
if(message.indexOf("/" + command) == 0)
return command;
}
}
return "";
};
var command = getCommand(message);
/**
* Returns the name of the command.
* @returns {String} the command
*/
this.getCommand = function()
{
return command;
}
var messageArgument = message.substr(command.length + 2);
/**
* Returns the arguments of the command.
* @returns {string}
*/
this.getArgument = function()
{
return messageArgument;
}
}
/**
* Checks whether this instance is valid command or not.
* @returns {boolean}
*/
CommandsPrototype.prototype.isCommand = function()
{
if(this.getCommand())
return true;
return false;
}
/**
* Processes the command.
*/
CommandsPrototype.prototype.processCommand = function()
{
if(!this.isCommand())
return;
commands[this.getCommand()](this.getArgument());
}
/**
* Processes the data for topic command.
* @param commandArguments the arguments of the topic command.
*/
var processTopic = function(commandArguments)
{
var topic = Util.escapeHtml(commandArguments);
connection.emuc.setSubject(topic);
}
/**
* List with supported commands. The keys are the names of the commands and
* the value is the function that processes the message.
* @type {{String: function}}
*/
var commands = {
"topic" : processTopic
};
return CommandsPrototype;
})();

@ -11,5 +11,7 @@ var config = {
bosh: '//lambada.jitsi.net/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
minChromeExtVersion: '0.1', // Required version of Chrome extension
enableRtpStats: false, // Enables RTP stats processing
openSctp: true //Toggle to enable/disable SCTP channels
};

@ -43,6 +43,10 @@ html, body{
color: #087dba;
}
.errorMessage {
color: red;
}
.remoteuser {
color: #424242;
}

@ -0,0 +1,124 @@
.popover {
position: absolute;
top: 0;
left: 0;
z-index: 1010;
display: none;
max-width: 300px;
min-width: 100px;
padding: 1px;
text-align: left;
color: #428bca;
background-color: #ffffff;
background-clip: padding-box;
border: 1px solid #cccccc;
border: 1px solid rgba(0, 0, 0, 0.2);
border-radius: 6px;
-webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.4);
white-space: normal;
}
.popover.top {
margin-top: -10px;
}
.popover.right {
margin-left: 10px;
}
.popover.bottom {
margin-top: 10px;
}
.popover.left {
margin-left: -10px;
}
.popover-title {
margin: 0;
padding: 8px 14px;
font-size: 11pt;
font-weight: normal;
line-height: 18px;
background-color: #f7f7f7;
border-bottom: 1px solid #ebebeb;
border-radius: 5px 5px 0 0;
}
.popover-content {
padding: 9px 14px;
font-size: 10pt;
white-space:pre-wrap;
text-align: center;
}
.popover > .arrow,
.popover > .arrow:after {
position: absolute;
display: block;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
}
.popover > .arrow {
border-width: 11px;
}
.popover > .arrow:after {
border-width: 10px;
content: "";
}
.popover.top > .arrow {
left: 50%;
margin-left: -11px;
border-bottom-width: 0;
border-top-color: #999999;
border-top-color: rgba(0, 0, 0, 0.25);
bottom: -11px;
}
.popover.top > .arrow:after {
content: " ";
bottom: 1px;
margin-left: -10px;
border-bottom-width: 0;
border-top-color: #ffffff;
}
.popover.right > .arrow {
top: 50%;
left: -11px;
margin-top: -11px;
border-left-width: 0;
border-right-color: #999999;
border-right-color: rgba(0, 0, 0, 0.25);
}
.popover.right > .arrow:after {
content: " ";
left: 1px;
bottom: -10px;
border-left-width: 0;
border-right-color: #ffffff;
}
.popover.bottom > .arrow {
left: 50%;
margin-left: -11px;
border-top-width: 0;
border-bottom-color: #999999;
border-bottom-color: rgba(0, 0, 0, 0.25);
top: -11px;
}
.popover.bottom > .arrow:after {
content: " ";
top: 1px;
margin-left: -10px;
border-top-width: 0;
border-bottom-color: #ffffff;
}
.popover.left > .arrow {
top: 50%;
right: -11px;
margin-top: -11px;
border-right-width: 0;
border-left-color: #999999;
border-left-color: rgba(0, 0, 0, 0.25);
}
.popover.left > .arrow:after {
content: " ";
right: 1px;
border-right-width: 0;
border-left-color: #ffffff;
bottom: -10px;
}

@ -9,7 +9,7 @@ ul.popupmenu {
padding-bottom: 5px;
padding-top: 5px;
right: 10px;
left: 0px;
left: -5px;
width: 100px;
background-color: rgba(0,0,0,1);
-webkit-box-shadow: 0 0 2px #000000, 0 0 10px #000000;
@ -21,7 +21,7 @@ ul.popupmenu:after {
display: block;
position: absolute;
bottom: -9px;
left: 13px;
left: 11px;
}
ul.popupmenu li {

@ -32,9 +32,11 @@
background-size: contain;
border-radius:8px;
border: 2px solid #212425;
margin-right: 3px;
}
#remoteVideos .videocontainer:hover {
#remoteVideos .videocontainer:hover,
#remoteVideos .videocontainer.videoContainerFocused {
width: 100%;
height: 100%;
content:"";
@ -49,7 +51,6 @@
-webkit-animation-iteration-count: 1;
-webkit-box-shadow: 0 0 18px #388396;
border: 2px solid #388396;
z-index: 3;
}
#localVideoWrapper {
@ -93,6 +94,11 @@
height: 100%;
}
.activespeaker {
-webkit-filter: grayscale(1);
filter: grayscale(1);
}
#etherpad,
#presentation {
text-align: center;
@ -119,6 +125,7 @@
text-shadow: 0px 1px 0px rgba(255,255,255,.3), 0px -1px 0px rgba(0,0,0,.7);
border: 0px;
z-index: 2;
text-align: center;
}
#remoteVideos .nick {
@ -133,25 +140,22 @@
.videocontainer>span.displayname,
.videocontainer>input.displayname {
display: inline-block;
display: none;
position: absolute;
background: -webkit-linear-gradient(left, rgba(0,0,0,.7), rgba(0,0,0,0));
color: #FFFFFF;
bottom: 0;
left: 0;
padding: 3px 5px;
width: 100%;
height: auto;
max-height: 18px;
font-size: 9pt;
text-align: left;
background: rgba(0,0,0,.7);
text-align: center;
text-overflow: ellipsis;
width: 70%;
height: 20%;
left: 15%;
top: 40%;
padding: 5px;
font-size: 11pt;
overflow: hidden;
white-space: nowrap;
z-index: 2;
box-sizing: border-box;
border-bottom-left-radius:4px;
border-bottom-right-radius:4px;
border-radius:20px;
}
#localVideoContainer>span.displayname:hover {
@ -162,6 +166,10 @@
pointer-events: none;
}
.videocontainer>input.displayname {
height: auto;
}
#localDisplayName {
pointer-events: auto !important;
}
@ -190,6 +198,7 @@
text-shadow: 0px 1px 0px rgba(255,255,255,.3), 0px -1px 0px rgba(0,0,0,.7);
border: 0px;
z-index: 3;
text-align: center;
}
.videocontainer>span.videoMuted {
@ -226,7 +235,6 @@
#header{
display:none;
position:absolute;
height: 0px;
text-align:center;
top:0;
left:0;
@ -241,13 +249,27 @@
margin-right:auto;
height:39px;
width:auto;
overflow: hidden;
background: linear-gradient(to bottom, rgba(103,103,103,.65) , rgba(0,0,0,.65));
-webkit-box-shadow: 0 0 2px #000000, 0 0 10px #000000;
border-bottom-left-radius: 12px;
border-bottom-right-radius: 12px;
}
#subject {
position: relative;
z-index: 3;
width: auto;
padding: 5px;
margin-left: 40%;
margin-right: 40%;
text-align: center;
background: linear-gradient(to bottom, rgba(255,255,255,.85) , rgba(255,255,255,.35));
-webkit-box-shadow: 0 0 2px #000000, 0 0 10px #000000;
border-bottom-left-radius: 12px;
border-bottom-right-radius: 12px;
display: none;
}
.watermark {
display: block;
position: absolute;

@ -0,0 +1,80 @@
/* global connection, Strophe, updateLargeVideo, focusedVideoSrc*/
/**
* 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);
// Code sample for sending string and/or binary data
// Sends String message to the bridge
//dataChannel.send("Hello bridge!");
// Sends 12 bytes binary message to the bridge
//dataChannel.send(new ArrayBuffer(12));
};
dataChannel.onerror = function (error)
{
console.error("Data Channel Error:", error, dataChannel);
};
dataChannel.onmessage = function (event)
{
var msgData = event.data;
console.info("Got Data Channel Message:", msgData, dataChannel);
// Active speaker event
if (msgData.indexOf('activeSpeaker') === 0)
{
// Endpoint ID from the bridge
var resourceJid = msgData.split(":")[1];
console.info(
"Data channel new active speaker event: " + resourceJid);
$(document).trigger('activespeakerchanged', [resourceJid]);
}
};
dataChannel.onclose = function ()
{
console.info("The Data Channel closed", dataChannel);
};
}
/**
* Binds "ondatachannel" event listener to given PeerConnection instance.
* @param peerConnection WebRTC peer connection instance.
*/
function bindDataChannelListener(peerConnection)
{
peerConnection.ondatachannel = onDataChannel;
// Sample code for opening new data channel from Jitsi Meet to the bridge.
// Although it's not a requirement to open separate channels from both bridge
// and peer as single channel can be used for sending and receiving data.
// So either channel opened by the bridge or the one opened here is enough
// for communication with the bridge.
/*var dataChannelOptions =
{
reliable: true
};
var dataChannel
= peerConnection.createDataChannel("myChannel", dataChannelOptions);
// Can be used only when is in open state
dataChannel.onopen = function ()
{
dataChannel.send("My channel !!!");
};
dataChannel.onmessage = function (event)
{
var msgData = event.data;
console.info("Got My Data Channel Message:", msgData, dataChannel);
};*/
}

@ -1,4 +1,4 @@
/* global $, config, connection, chrome, alert, getUserMediaWithConstraints, change_local_video, getConferenceHandler */
/* global $, config, connection, chrome, alert, getUserMediaWithConstraints, changeLocalVideo, getConferenceHandler */
/**
* Indicates that desktop stream is currently in use(for toggle purpose).
* @type {boolean}
@ -251,7 +251,9 @@ function newStreamCreated(stream) {
var oldStream = connection.jingle.localVideo;
change_local_video(stream, !isUsingScreenStream);
connection.jingle.localVideo = stream;
VideoLayout.changeLocalVideo(stream, !isUsingScreenStream);
var conferenceHandler = getConferenceHandler();
if (conferenceHandler) {

@ -45,8 +45,8 @@ var Etherpad = (function (my) {
if (Prezi.isPresentationVisible()) {
largeVideo.css({opacity: '0'});
} else {
setLargeVideoVisible(false);
dockToolbar(true);
VideoLayout.setLargeVideoVisible(false);
Toolbar.dockToolbar(true);
}
$('#etherpad>iframe').fadeIn(300, function () {
@ -63,8 +63,8 @@ var Etherpad = (function (my) {
document.body.style.background = 'black';
if (!isPresentation) {
$('#largeVideo').fadeIn(300, function () {
setLargeVideoVisible(true);
dockToolbar(false);
VideoLayout.setLargeVideoVisible(true);
Toolbar.dockToolbar(false);
});
}
});

@ -20,26 +20,35 @@
<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="config.js"></script><!-- adapt to your needs, i.e. set hosts and bosh path -->
<script src="muc.js?v=10"></script><!-- simple MUC library -->
<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="estos_log.js?v=2"></script><!-- simple stanza logger -->
<script src="desktopsharing.js?v=1"></script><!-- desktop sharing -->
<script src="app.js?v=26"></script><!-- application logic -->
<script src="chat.js?v=4"></script><!-- chat logic -->
<script src="util.js?v=3"></script><!-- utility functions -->
<script src="etherpad.js?v=7"></script><!-- etherpad plugin -->
<script src="prezi.js?v=2"></script><!-- prezi plugin -->
<script src="smileys.js?v=1"></script><!-- smiley images -->
<script src="replacement.js?v=5"></script><!-- link and smiley replacement -->
<script src="moderatemuc.js?v=1"></script><!-- moderator plugin -->
<script src="desktopsharing.js?v=2"></script><!-- desktop sharing -->
<script src="data_channels.js?v=2"></script><!-- data channels -->
<script src="app.js?v=29"></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="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 -->
<script src="replacement.js?v=6"></script><!-- link and smiley replacement -->
<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="local_stats.js?v=1"></script><!-- Local stats processing -->
<script src="videolayout.js?v=4"></script><!-- video ui -->
<script src="toolbar.js?v=2"></script><!-- toolbar ui -->
<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=20"/>
<link rel="stylesheet" type="text/css" media="screen" href="css/videolayout_default.css?v=4" id="videolayout_default"/>
<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/jquery-impromptu.css?v=4">
<link rel="stylesheet" href="css/modaldialog.css?v=3">
<link rel="stylesheet" href="css/popup_menu.css?v=1">
<link rel="stylesheet" href="css/popup_menu.css?v=2">
<link rel="stylesheet" href="css/popover.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 -->
@ -49,36 +58,41 @@
<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" onclick='toggleAudio();'>
<i id="mute" title="Mute / unmute" class="icon-microphone"></i></a>
<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" onclick='buttonClick("#video", "icon-camera icon-camera-disabled");toggleVideo();'>
<i id="video" title="Start / stop camera" class="icon-camera"></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>
<div class="header_button_separator"></div>
<a class="button" onclick="openLockDialog();" title="Lock/unlock room"><i id="lockIcon" class="icon-security"></i></a>
<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" onclick="openLinkDialog();" title="Invite others"><i class="icon-link"></i></a>
<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" onclick='Chat.toggleChat();' title="Open chat"><i id="chatButton" class="icon-chat"></i></a>
<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" onclick='Prezi.openPreziDialog();' title="Share Prezi"><i class="icon-prezi"></i></a>
<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" onclick='Etherpad.toggleEtherpad(0);' title="Open shared document"><i class="icon-share-doc"></i></a>
<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" onclick="toggleScreenSharing();" title="Share screen"><i class="icon-share-desktop"></i></a>
<a class="button" data-toggle="popover" data-placement="bottom" data-content="Share screen" onclick="toggleScreenSharing();"><i class="icon-share-desktop"></i></a>
<div class="header_button_separator"></div>
</span>
<a class="button" onclick='buttonClick("#fullScreen", "icon-full-screen icon-exit-full-screen");toggleFullScreen();'><i id="fullScreen" title="Enter / Exit Full Screen" class="icon-full-screen"></i></a>
<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="settings">
<h1>Connection Settings</h1>
<form id="loginInfo">
@ -89,7 +103,7 @@
</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="showToolbar();">
<div id="videospace" onmousemove="Toolbar.showToolbar();">
<div id="largeVideoContainer" class="videocontainer">
<div id="presentation"></div>
<div id="etherpad"></div>
@ -104,7 +118,7 @@
<!--<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 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>
@ -123,6 +137,6 @@
<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);'><i title="Download support information" class="fa fa-cloud-download"></i></a>
<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>
</html>

@ -44,8 +44,27 @@ function ColibriFocus(connection, bridgejid) {
this.peers = [];
this.confid = null;
/**
* Local XMPP resource used to join the multi user chat.
* @type {*}
*/
this.myMucResource = Strophe.getResourceFromJid(connection.emuc.myroomjid);
/**
* Default channel expire value in seconds.
* @type {number}
*/
this.channelExpire = 60;
// 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.mychannel = [];
@ -151,17 +170,29 @@ ColibriFocus.prototype._makeConference = function () {
elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri'});
this.media.forEach(function (name) {
var isData = name === 'data';
var channel = isData ? 'sctpconnection' : 'channel';
elem.c('content', {name: name});
elem.c('channel', {
elem.c(channel, {
initiator: 'true',
expire: '15',
endpoint: 'fix_me_focus_endpoint'}).up();
endpoint: self.myMucResource
});
if (isData)
elem.attrs({port: 5000});
elem.up();// end of channel
for (var j = 0; j < self.peers.length; j++) {
elem.c('channel', {
elem.c(channel, {
initiator: 'true',
expire: '15',
endpoint: self.peers[j].substr(1 + self.peers[j].lastIndexOf('/'))
}).up();
});
if (isData)
elem.attrs({port: 5000});
elem.up(); // end of channel
}
elem.up(); // end of content
});
@ -209,8 +240,13 @@ ColibriFocus.prototype.createdConference = function (result) {
this.confid = $(result).find('>conference').attr('id');
var remotecontents = $(result).find('>conference>content').get();
var numparticipants = 0;
for (var i = 0; i < remotecontents.length; i++) {
tmp = $(remotecontents[i]).find('>channel').get();
for (var i = 0; i < remotecontents.length; i++)
{
var contentName = $(remotecontents[i]).attr('name');
var channelName
= contentName !== 'data' ? '>channel' : '>sctpconnection';
tmp = $(remotecontents[i]).find(channelName).get();
this.mychannel.push($(tmp.shift()));
numparticipants = tmp.length;
for (j = 0; j < tmp.length; j++) {
@ -223,7 +259,55 @@ ColibriFocus.prototype.createdConference = function (result) {
console.log('remote channels', this.channels);
var bridgeSDP = new SDP('v=0\r\no=- 5151055458874951233 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\nm=audio 1 RTP/SAVPF 111 103 104 0 8 106 105 13 126\r\nc=IN IP4 0.0.0.0\r\na=rtcp:1 IN IP4 0.0.0.0\r\na=mid:audio\r\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\na=sendrecv\r\na=rtpmap:111 opus/48000/2\r\na=fmtp:111 minptime=10\r\na=rtpmap:103 ISAC/16000\r\na=rtpmap:104 ISAC/32000\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:106 CN/32000\r\na=rtpmap:105 CN/16000\r\na=rtpmap:13 CN/8000\r\na=rtpmap:126 telephone-event/8000\r\na=maxptime:60\r\nm=video 1 RTP/SAVPF 100 116 117\r\nc=IN IP4 0.0.0.0\r\na=rtcp:1 IN IP4 0.0.0.0\r\na=mid:video\r\na=extmap:2 urn:ietf:params:rtp-hdrext:toffset\r\na=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=sendrecv\r\na=rtpmap:100 VP8/90000\r\na=rtcp-fb:100 ccm fir\r\na=rtcp-fb:100 nack\r\na=rtcp-fb:100 goog-remb\r\na=rtpmap:116 red/90000\r\na=rtpmap:117 ulpfec/90000\r\n');
// Notify that the focus has created the conference on the bridge
$(document).trigger('conferenceCreated.jingle', [self]);
var bridgeSDP = new SDP(
'v=0\r\n' +
'o=- 5151055458874951233 2 IN IP4 127.0.0.1\r\n' +
's=-\r\n' +
't=0 0\r\n' +
/* Audio */
'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' +
'a=mid:audio\r\n' +
'a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n' +
'a=sendrecv\r\n' +
'a=rtpmap:111 opus/48000/2\r\n' +
'a=fmtp:111 minptime=10\r\n' +
'a=rtpmap:103 ISAC/16000\r\n' +
'a=rtpmap:104 ISAC/32000\r\n' +
'a=rtpmap:0 PCMU/8000\r\n' +
'a=rtpmap:8 PCMA/8000\r\n' +
'a=rtpmap:106 CN/32000\r\n' +
'a=rtpmap:105 CN/16000\r\n' +
'a=rtpmap:13 CN/8000\r\n' +
'a=rtpmap:126 telephone-event/8000\r\n' +
'a=maxptime:60\r\n' +
/* Video */
'm=video 1 RTP/SAVPF 100 116 117\r\n' +
'c=IN IP4 0.0.0.0\r\n' +
'a=rtcp:1 IN IP4 0.0.0.0\r\n' +
'a=mid:video\r\n' +
'a=extmap:2 urn:ietf:params:rtp-hdrext:toffset\r\n' +
'a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n' +
'a=sendrecv\r\n' +
'a=rtpmap:100 VP8/90000\r\n' +
'a=rtcp-fb:100 ccm fir\r\n' +
'a=rtcp-fb:100 nack\r\n' +
'a=rtcp-fb:100 goog-remb\r\n' +
'a=rtpmap:116 red/90000\r\n' +
'a=rtpmap:117 ulpfec/90000\r\n' +
/* Data SCTP */
(config.openSctp ?
'm=application 1 DTLS/SCTP 5000\r\n' +
'c=IN IP4 0.0.0.0\r\n' +
'a=sctpmap:5000 webrtc-datachannel\r\n' +
'a=mid:data\r\n'
: '')
);
bridgeSDP.media.length = this.mychannel.length;
var channel;
/*
@ -262,12 +346,17 @@ ColibriFocus.prototype.createdConference = function (result) {
// get the mixed ssrc
tmp = $(this.mychannel[channel]).find('>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
// FIXME: check rtp-level-relay-type
if (tmp.length) {
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 {
}
else if (!isData)
{
// 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';
@ -308,22 +397,42 @@ ColibriFocus.prototype.createdConference = function (result) {
elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: self.confid});
var localSDP = new SDP(self.peerconnection.localDescription.sdp);
localSDP.media.forEach(function (media, channel) {
var name = SDPUtil.parse_mline(media.split('\r\n')[0]).media;
var name = SDPUtil.parse_mid(SDPUtil.find_line(media, 'a=mid:'));
elem.c('content', {name: name});
var mline = SDPUtil.parse_mline(media.split('\r\n')[0]);
if (name !== 'data')
{
elem.c('channel', {
initiator: 'true',
expire: '15',
expire: self.channelExpire,
id: self.mychannel[channel].attr('id'),
endpoint: 'fix_me_focus_endpoint'
endpoint: self.myMucResource
});
// FIXME: should reuse code from .toJingle
var mline = SDPUtil.parse_mline(media.split('\r\n')[0]);
for (var j = 0; j < mline.fmt.length; j++) {
for (var j = 0; j < mline.fmt.length; j++)
{
var rtpmap = SDPUtil.find_line(media, 'a=rtpmap:' + mline.fmt[j]);
if (rtpmap)
{
elem.c('payload-type', SDPUtil.parse_rtpmap(rtpmap));
elem.up();
}
}
}
else
{
var sctpmap = SDPUtil.find_line(media, 'a=sctpmap:' + mline.fmt[0]);
var sctpPort = SDPUtil.parse_sctpmap(sctpmap)[0];
elem.c("sctpconnection",
{
initiator: 'true',
expire: self.channelExpire,
endpoint: self.myMucResource,
port: sctpPort
}
);
}
localSDP.TransportToJingle(channel, elem);
@ -336,7 +445,9 @@ ColibriFocus.prototype.createdConference = function (result) {
// ...
},
function (error) {
console.warn(error);
console.error(
"ERROR setLocalDescription succeded",
error, elem);
}
);
@ -344,6 +455,10 @@ ColibriFocus.prototype.createdConference = function (result) {
for (var i = 0; i < numparticipants; i++) {
self.initiate(self.peers[i], true);
}
// Notify we've created the conference
$(document).trigger(
'conferenceCreated.jingle', self);
},
function (error) {
console.warn('setLocalDescription failed.', error);
@ -417,7 +532,10 @@ ColibriFocus.prototype.initiate = function (peer, isInitiator) {
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';
} else {
}
// No SSRCs for 'data', comes when j == 2
else if (j < 2)
{
// 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';
@ -486,9 +604,17 @@ ColibriFocus.prototype.initiate = function (peer, isInitiator) {
// pull in a new participant into the conference
ColibriFocus.prototype.addNewParticipant = function (peer) {
var self = this;
if (this.confid === 0) {
if (this.confid === 0 || !this.peerconnection.localDescription)
{
// bad state
console.log('confid does not exist yet, postponing', peer);
if (this.confid === 0)
{
console.error('confid does not exist yet, postponing', peer);
}
else
{
console.error('local description not ready yet, postponing', peer);
}
window.setTimeout(function () {
self.addNewParticipant(peer);
}, 250);
@ -502,14 +628,26 @@ ColibriFocus.prototype.addNewParticipant = function (peer) {
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_mline(media.split('\r\n')[0]).media;
var name = SDPUtil.parse_mid(SDPUtil.find_line(media, 'a=mid:'));
elem.c('content', {name: name});
if (name !== 'data')
{
elem.c('channel', {
initiator: 'true',
expire:'15',
expire: self.channelExpire,
endpoint: peer.substr(1 + peer.lastIndexOf('/'))
});
elem.up(); // end of channel
}
else
{
elem.c('sctpconnection', {
endpoint: peer.substr(1 + peer.lastIndexOf('/')),
initiator: 'true',
expire: self.channelExpire,
port: 5000
});
}
elem.up(); // end of channel/sctpconnection
elem.up(); // end of content
});
@ -517,7 +655,15 @@ ColibriFocus.prototype.addNewParticipant = function (peer) {
function (result) {
var contents = $(result).find('>conference>content').get();
for (var i = 0; i < contents.length; i++) {
tmp = $(contents[i]).find('>channel').get();
var channelXml = $(contents[i]).find('>channel');
if (channelXml.length)
{
tmp = channelXml.get();
}
else
{
tmp = $(contents[i]).find('>sctpconnection').get();
}
self.channels[index][i] = tmp[0];
}
self.initiate(peer, true);
@ -531,14 +677,19 @@ ColibriFocus.prototype.addNewParticipant = function (peer) {
// update the channel description (payload-types + dtls fp) for a participant
ColibriFocus.prototype.updateChannel = function (remoteSDP, participant) {
console.log('change allocation for', this.confid);
var self = this;
var change = $iq({to: this.bridgejid, type: 'set'});
change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
for (channel = 0; channel < this.channels[participant].length; channel++) {
change.c('content', {name: channel === 0 ? 'audio' : 'video'});
for (channel = 0; channel < this.channels[participant].length; channel++)
{
var name = SDPUtil.parse_mid(SDPUtil.find_line(remoteSDP.media[channel], 'a=mid:'));
change.c('content', {name: name});
if (name !== 'data')
{
change.c('channel', {
id: $(this.channels[participant][channel]).attr('id'),
endpoint: $(this.channels[participant][channel]).attr('endpoint'),
expire: '15'
expire: self.channelExpire
});
var rtpmap = SDPUtil.find_lines(remoteSDP.media[channel], 'a=rtpmap:');
@ -558,10 +709,20 @@ ColibriFocus.prototype.updateChannel = function (remoteSDP, participant) {
*/
change.up();
});
}
else
{
var sctpmap = SDPUtil.find_line(remoteSDP.media[channel], 'a=sctpmap:');
change.c('sctpconnection', {
endpoint: $(this.channels[participant][channel]).attr('endpoint'),
expire: self.channelExpire,
port: SDPUtil.parse_sctpmap(sctpmap)[0]
});
}
// now add transport
remoteSDP.TransportToJingle(channel, change);
change.up(); // end of channel
change.up(); // end of channel/sctpconnection
change.up(); // end of content
}
this.connection.sendIQ(change,
@ -605,6 +766,19 @@ ColibriFocus.prototype.sendSSRCUpdate = function (sdpMediaSsrcs, fromJid, isadd)
ColibriFocus.prototype.addSource = function (elem, fromJid) {
var self = this;
// FIXME: dirty waiting
if (!this.peerconnection.localDescription)
{
console.warn("addSource - localDescription not ready yet")
setTimeout(function()
{
self.addSource(elem, fromJid);
},
200
);
return;
}
this.peerconnection.addSource(elem);
var peerSsrc = this.remotessrc[fromJid];
@ -638,6 +812,19 @@ ColibriFocus.prototype.addSource = function (elem, fromJid) {
ColibriFocus.prototype.removeSource = function (elem, fromJid) {
var self = this;
// FIXME: dirty waiting
if (!self.peerconnection.localDescription)
{
console.warn("removeSource - localDescription not ready yet");
setTimeout(function()
{
self.removeSource(elem, fromJid);
},
200
);
return;
}
this.peerconnection.removeSource(elem);
var peerSsrc = this.remotessrc[fromJid];
@ -675,8 +862,11 @@ 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 (SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:').length) {
this.remotessrc[session.peerjid][channel] = SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:').join('\r\n') + '\r\n';
if (SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:').length)
{
this.remotessrc[session.peerjid][channel] =
SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:')
.join('\r\n') + '\r\n';
}
}
@ -702,14 +892,27 @@ ColibriFocus.prototype.addIceCandidate = function (session, elem) {
change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
$(elem).each(function () {
var name = $(this).attr('name');
var channel = name == 'audio' ? 0 : 1; // FIXME: search mlineindex in localdesc
if (name != 'audio' && name != 'video')
channel = 2; // name == 'data'
change.c('content', {name: name});
if (name !== 'data')
{
change.c('channel', {
id: $(self.channels[participant][channel]).attr('id'),
endpoint: $(self.channels[participant][channel]).attr('endpoint'),
expire: '15'
expire: self.channelExpire
});
}
else
{
change.c('sctpconnection', {
endpoint: $(self.channels[participant][channel]).attr('endpoint'),
expire: self.channelExpire
});
}
$(this).find('>transport').each(function () {
change.c('transport', {
ufrag: $(this).attr('ufrag'),
@ -729,7 +932,7 @@ ColibriFocus.prototype.addIceCandidate = function (session, elem) {
});
change.up(); // end of transport
});
change.up(); // end of channel
change.up(); // end of channel/sctpconnection
change.up(); // end of content
});
// FIXME: need to check if there is at least one candidate when filtering TCP ones
@ -769,21 +972,35 @@ ColibriFocus.prototype.sendIceCandidates = function (candidates) {
mycands.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
// FIXME: multi-candidate logic is taken from strophe.jingle, should be refactored there
var localSDP = new SDP(this.peerconnection.localDescription.sdp);
for (var mid = 0; mid < localSDP.media.length; mid++) {
for (var mid = 0; mid < localSDP.media.length; mid++)
{
var cands = candidates.filter(function (el) { return el.sdpMLineIndex == mid; });
if (cands.length > 0) {
mycands.c('content', {name: cands[0].sdpMid });
if (cands.length > 0)
{
var name = cands[0].sdpMid;
mycands.c('content', {name: name });
if (name !== 'data')
{
mycands.c('channel', {
id: $(this.mychannel[cands[0].sdpMLineIndex]).attr('id'),
endpoint: $(this.mychannel[cands[0].sdpMLineIndex]).attr('endpoint'),
expire: '15'
expire: self.channelExpire
});
}
else
{
mycands.c('sctpconnection', {
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'});
for (var i = 0; i < cands.length; i++) {
mycands.c('candidate', SDPUtil.candidateToJingle(cands[i].candidate)).up();
}
mycands.up(); // transport
mycands.up(); // channel
mycands.up(); // channel / sctpconnection
mycands.up(); // content
}
}
@ -814,13 +1031,26 @@ ColibriFocus.prototype.terminate = function (session, reason) {
var change = $iq({to: this.bridgejid, type: 'set'});
change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
for (var channel = 0; channel < this.channels[participant].length; channel++) {
change.c('content', {name: channel === 0 ? 'audio' : 'video'});
var name = channel === 0 ? 'audio' : 'video';
if (channel == 2)
name = 'data';
change.c('content', {name: name});
if (name !== 'data')
{
change.c('channel', {
id: $(this.channels[participant][channel]).attr('id'),
endpoint: $(this.channels[participant][channel]).attr('endpoint'),
expire: '0'
});
change.up(); // end of channel
}
else
{
change.c('sctpconnection', {
endpoint: $(this.channels[participant][channel]).attr('endpoint'),
expire: '0'
});
}
change.up(); // end of channel/sctpconnection
change.up(); // end of content
}
this.connection.sendIQ(change,

@ -0,0 +1,110 @@
/* ========================================================================
* Bootstrap: popover.js v3.1.1
* http://getbootstrap.com/javascript/#popovers
* ========================================================================
* Copyright 2011-2014 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
+function ($) {
'use strict';
// POPOVER PUBLIC CLASS DEFINITION
// ===============================
var Popover = function (element, options) {
this.init('popover', element, options)
}
if (!$.fn.tooltip) throw new Error('Popover requires tooltip.js')
Popover.DEFAULTS = $.extend({}, $.fn.tooltip.Constructor.DEFAULTS, {
placement: 'right',
trigger: 'click',
content: '',
template: '<div class="popover"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>'
})
// NOTE: POPOVER EXTENDS tooltip.js
// ================================
Popover.prototype = $.extend({}, $.fn.tooltip.Constructor.prototype)
Popover.prototype.constructor = Popover
Popover.prototype.getDefaults = function () {
return Popover.DEFAULTS
}
Popover.prototype.setContent = function () {
var $tip = this.tip()
var title = this.getTitle()
var content = this.getContent()
$tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title)
$tip.find('.popover-content')[ // we use append for html objects to maintain js events
this.options.html ? (typeof content == 'string' ? 'html' : 'append') : 'text'
](content)
$tip.removeClass('fade top bottom left right in')
// IE8 doesn't accept hiding via the `:empty` pseudo selector, we have to do
// this manually by checking the contents.
if (!$tip.find('.popover-title').html()) $tip.find('.popover-title').hide()
}
Popover.prototype.hasContent = function () {
return this.getTitle() || this.getContent()
}
Popover.prototype.getContent = function () {
var $e = this.$element
var o = this.options
return $e.attr('data-content')
|| (typeof o.content == 'function' ?
o.content.call($e[0]) :
o.content)
}
Popover.prototype.arrow = function () {
return this.$arrow = this.$arrow || this.tip().find('.arrow')
}
Popover.prototype.tip = function () {
if (!this.$tip) this.$tip = $(this.options.template)
return this.$tip
}
// POPOVER PLUGIN DEFINITION
// =========================
var old = $.fn.popover
$.fn.popover = function (option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('bs.popover')
var options = typeof option == 'object' && option
if (!data && option == 'destroy') return
if (!data) $this.data('bs.popover', (data = new Popover(this, options)))
if (typeof option == 'string') data[option]()
})
}
$.fn.popover.Constructor = Popover
// POPOVER NO CONFLICT
// ===================
$.fn.popover.noConflict = function () {
$.fn.popover = old
return this
}
}(jQuery);

@ -5,7 +5,7 @@ function TraceablePeerConnection(ice_config, constraints) {
this.updateLog = [];
this.stats = {};
this.statsinterval = null;
this.maxstats = 300; // limit to 300 values, i.e. 5 minutes; set to 0 to disable
this.maxstats = 0; // limit to 300 values, i.e. 5 minutes; set to 0 to disable
/**
* Array of ssrcs that will be added on next modifySources call.
@ -88,8 +88,8 @@ function TraceablePeerConnection(ice_config, constraints) {
if (self.ondatachannel !== null) {
self.ondatachannel(event);
}
}
if (!navigator.mozGetUserMedia) {
};
if (!navigator.mozGetUserMedia && this.maxstats) {
this.statsinterval = window.setInterval(function() {
self.peerconnection.getStats(function(stats) {
var results = stats.result();
@ -144,8 +144,8 @@ TraceablePeerConnection.prototype.removeStream = function (stream) {
TraceablePeerConnection.prototype.createDataChannel = function (label, opts) {
this.trace('createDataChannel', label, opts);
this.peerconnection.createDataChannel(label, opts);
}
return this.peerconnection.createDataChannel(label, opts);
};
TraceablePeerConnection.prototype.setLocalDescription = function (description, successCallback, failureCallback) {
var self = this;

@ -155,7 +155,10 @@ SDP.prototype.toJingle = function (elem, thecreator) {
}
for (i = 0; i < this.media.length; i++) {
mline = SDPUtil.parse_mline(this.media[i].split('\r\n')[0]);
if (!(mline.media == 'audio' || mline.media == 'video')) {
if (!(mline.media === 'audio' ||
mline.media === 'video' ||
mline.media === 'application'))
{
continue;
}
if (SDPUtil.find_line(this.media[i], 'a=ssrc:')) {
@ -171,12 +174,14 @@ SDP.prototype.toJingle = function (elem, thecreator) {
elem.attrs({ name: mid });
// old BUNDLE plan, to be removed
if (bundle.indexOf(mid) != -1) {
if (bundle.indexOf(mid) !== -1) {
elem.c('bundle', {xmlns: 'http://estos.de/ns/bundle'}).up();
bundle.splice(bundle.indexOf(mid), 1);
}
}
if (SDPUtil.find_line(this.media[i], 'a=rtpmap:').length) {
if (SDPUtil.find_line(this.media[i], 'a=rtpmap:').length)
{
elem.c('description',
{xmlns: 'urn:xmpp:jingle:apps:rtp:1',
media: mline.media });
@ -304,6 +309,26 @@ SDP.prototype.TransportToJingle = function (mediaindex, elem) {
var self = this;
elem.c('transport');
// XEP-0343 DTLS/SCTP
if (SDPUtil.find_line(this.media[mediaindex], 'a=sctpmap:').length)
{
var sctpmap = SDPUtil.find_line(
this.media[i], 'a=sctpmap:', self.session);
if (sctpmap)
{
var sctpAttrs = SDPUtil.parse_sctpmap(sctpmap);
elem.c('sctpmap',
{
xmlns: 'urn:xmpp:jingle:transports:dtls-sctp:1',
number: sctpAttrs[0], /* SCTP port */
protocol: sctpAttrs[1], /* protocol */
});
// Optional stream count attribute
if (sctpAttrs.length > 2)
elem.attrs({ streams: sctpAttrs[2]});
elem.up();
}
}
// XEP-0320
var fingerprints = SDPUtil.find_lines(this.media[mediaindex], 'a=fingerprint:', this.session);
fingerprints.forEach(function(line) {
@ -438,6 +463,8 @@ SDP.prototype.jingle2media = function (content) {
ssrc = desc.attr('ssrc'),
self = this,
tmp;
var sctp = content.find(
'>transport>sctpmap[xmlns="urn:xmpp:jingle:transports:dtls-sctp:1"]');
tmp = { media: desc.attr('media') };
tmp.port = '1';
@ -446,13 +473,34 @@ SDP.prototype.jingle2media = function (content) {
tmp.port = '0';
}
if (content.find('>transport>fingerprint').length || desc.find('encryption').length) {
if (sctp.length)
tmp.proto = 'DTLS/SCTP';
else
tmp.proto = 'RTP/SAVPF';
} else {
tmp.proto = 'RTP/AVPF';
}
tmp.fmt = desc.find('payload-type').map(function () { return this.getAttribute('id'); }).get();
if (!sctp.length)
{
tmp.fmt = desc.find('payload-type').map(
function () { return this.getAttribute('id'); }).get();
media += SDPUtil.build_mline(tmp) + '\r\n';
}
else
{
media += 'm=application 1 DTLS/SCTP ' + sctp.attr('number') + '\r\n';
media += 'a=sctpmap:' + sctp.attr('number') +
' ' + sctp.attr('protocol');
var streamCount = sctp.attr('streams');
if (streamCount)
media += ' ' + streamCount + '\r\n';
else
media += '\r\n';
}
media += 'c=IN IP4 0.0.0.0\r\n';
if (!sctp.length)
media += 'a=rtcp:1 IN IP4 0.0.0.0\r\n';
tmp = content.find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]');
if (tmp.length) {

@ -90,6 +90,20 @@ SDPUtil = {
data.channels = parts.length ? parts.shift() : '1';
return data;
},
/**
* Parses SDP line "a=sctpmap:..." and extracts SCTP port from it.
* @param line eg. "a=sctpmap:5000 webrtc-datachannel"
* @returns [SCTP port number, protocol, streams]
*/
parse_sctpmap: function (line)
{
var parts = line.substring(10).split(' ');
var sctpPort = parts[0];
var protocol = parts[1];
// Stream count is optional
var streamCount = parts.length > 2 ? parts[2] : null;
return [sctpPort, protocol, streamCount];// SCTP port
},
build_rtpmap: function (el) {
var line = 'a=rtpmap:' + el.getAttribute('id') + ' ' + el.getAttribute('name') + '/' + el.getAttribute('clockrate');
if (el.getAttribute('channels') && el.getAttribute('channels') != '1') {
@ -269,7 +283,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;

@ -420,6 +420,7 @@ JingleSession.prototype.setRemoteDescription = function (elem, desctype) {
},
function (e) {
console.error('setRemoteDescription error', e);
$(document).trigger('fatalError.jingle', [self, e]);
}
);
};

@ -23,6 +23,20 @@ SessionBase.prototype.modifySources = function (successCallback) {
SessionBase.prototype.addSource = function (elem, fromJid) {
var self = this;
// FIXME: dirty waiting
if (!this.peerconnection.localDescription)
{
console.warn("addSource - localDescription not ready yet")
setTimeout(function()
{
self.addSource(elem, fromJid);
},
200
);
return;
}
this.peerconnection.addSource(elem);
this.modifySources();
@ -30,6 +44,20 @@ SessionBase.prototype.addSource = function (elem, fromJid) {
SessionBase.prototype.removeSource = function (elem, fromJid) {
var self = this;
// FIXME: dirty waiting
if (!this.peerconnection.localDescription)
{
console.warn("removeSource - localDescription not ready yet")
setTimeout(function()
{
self.removeSource(elem, fromJid);
},
200
);
return;
}
this.peerconnection.removeSource(elem);
this.modifySources();

@ -0,0 +1,399 @@
/* ========================================================================
* Bootstrap: tooltip.js v3.1.1
* http://getbootstrap.com/javascript/#tooltip
* Inspired by the original jQuery.tipsy by Jason Frame
* ========================================================================
* Copyright 2011-2014 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
+function ($) {
'use strict';
// TOOLTIP PUBLIC CLASS DEFINITION
// ===============================
var Tooltip = function (element, options) {
this.type =
this.options =
this.enabled =
this.timeout =
this.hoverState =
this.$element = null
this.init('tooltip', element, options)
}
Tooltip.DEFAULTS = {
animation: true,
placement: 'top',
selector: false,
template: '<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',
trigger: 'hover focus',
title: '',
delay: 0,
html: false,
container: false
}
Tooltip.prototype.init = function (type, element, options) {
this.enabled = true
this.type = type
this.$element = $(element)
this.options = this.getOptions(options)
var triggers = this.options.trigger.split(' ')
for (var i = triggers.length; i--;) {
var trigger = triggers[i]
if (trigger == 'click') {
this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this))
} else if (trigger != 'manual') {
var eventIn = trigger == 'hover' ? 'mouseenter' : 'focusin'
var eventOut = trigger == 'hover' ? 'mouseleave' : 'focusout'
this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this))
this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this))
}
}
this.options.selector ?
(this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) :
this.fixTitle()
}
Tooltip.prototype.getDefaults = function () {
return Tooltip.DEFAULTS
}
Tooltip.prototype.getOptions = function (options) {
options = $.extend({}, this.getDefaults(), this.$element.data(), options)
if (options.delay && typeof options.delay == 'number') {
options.delay = {
show: options.delay,
hide: options.delay
}
}
return options
}
Tooltip.prototype.getDelegateOptions = function () {
var options = {}
var defaults = this.getDefaults()
this._options && $.each(this._options, function (key, value) {
if (defaults[key] != value) options[key] = value
})
return options
}
Tooltip.prototype.enter = function (obj) {
var self = obj instanceof this.constructor ?
obj : $(obj.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type)
clearTimeout(self.timeout)
self.hoverState = 'in'
if (!self.options.delay || !self.options.delay.show) return self.show()
self.timeout = setTimeout(function () {
if (self.hoverState == 'in') self.show()
}, self.options.delay.show)
}
Tooltip.prototype.leave = function (obj) {
var self = obj instanceof this.constructor ?
obj : $(obj.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type)
clearTimeout(self.timeout)
self.hoverState = 'out'
if (!self.options.delay || !self.options.delay.hide) return self.hide()
self.timeout = setTimeout(function () {
if (self.hoverState == 'out') self.hide()
}, self.options.delay.hide)
}
Tooltip.prototype.show = function () {
var e = $.Event('show.bs.' + this.type)
if (this.hasContent() && this.enabled) {
this.$element.trigger(e)
if (e.isDefaultPrevented()) return
var that = this;
var $tip = this.tip()
this.setContent()
if (this.options.animation) $tip.addClass('fade')
var placement = typeof this.options.placement == 'function' ?
this.options.placement.call(this, $tip[0], this.$element[0]) :
this.options.placement
var autoToken = /\s?auto?\s?/i
var autoPlace = autoToken.test(placement)
if (autoPlace) placement = placement.replace(autoToken, '') || 'top'
$tip
.detach()
.css({ top: 0, left: 0, display: 'block' })
.addClass(placement)
this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element)
var pos = this.getPosition()
var actualWidth = $tip[0].offsetWidth
var actualHeight = $tip[0].offsetHeight
if (autoPlace) {
var $parent = this.$element.parent()
var orgPlacement = placement
var docScroll = document.documentElement.scrollTop || document.body.scrollTop
var parentWidth = this.options.container == 'body' ? window.innerWidth : $parent.outerWidth()
var parentHeight = this.options.container == 'body' ? window.innerHeight : $parent.outerHeight()
var parentLeft = this.options.container == 'body' ? 0 : $parent.offset().left
placement = placement == 'bottom' && pos.top + pos.height + actualHeight - docScroll > parentHeight ? 'top' :
placement == 'top' && pos.top - docScroll - actualHeight < 0 ? 'bottom' :
placement == 'right' && pos.right + actualWidth > parentWidth ? 'left' :
placement == 'left' && pos.left - actualWidth < parentLeft ? 'right' :
placement
$tip
.removeClass(orgPlacement)
.addClass(placement)
}
var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight)
this.applyPlacement(calculatedOffset, placement)
this.hoverState = null
var complete = function() {
that.$element.trigger('shown.bs.' + that.type)
}
$.support.transition && this.$tip.hasClass('fade') ?
$tip
.one($.support.transition.end, complete)
.emulateTransitionEnd(150) :
complete()
}
}
Tooltip.prototype.applyPlacement = function (offset, placement) {
var replace
var $tip = this.tip()
var width = $tip[0].offsetWidth
var height = $tip[0].offsetHeight
// manually read margins because getBoundingClientRect includes difference
var marginTop = parseInt($tip.css('margin-top'), 10)
var marginLeft = parseInt($tip.css('margin-left'), 10)
// we must check for NaN for ie 8/9
if (isNaN(marginTop)) marginTop = 0
if (isNaN(marginLeft)) marginLeft = 0
offset.top = offset.top + marginTop
offset.left = offset.left + marginLeft
// $.fn.offset doesn't round pixel values
// so we use setOffset directly with our own function B-0
$.offset.setOffset($tip[0], $.extend({
using: function (props) {
$tip.css({
top: Math.round(props.top),
left: Math.round(props.left)
})
}
}, offset), 0)
$tip.addClass('in')
// check to see if placing tip in new offset caused the tip to resize itself
var actualWidth = $tip[0].offsetWidth
var actualHeight = $tip[0].offsetHeight
if (placement == 'top' && actualHeight != height) {
replace = true
offset.top = offset.top + height - actualHeight
}
if (/bottom|top/.test(placement)) {
var delta = 0
if (offset.left < 0) {
delta = offset.left * -2
offset.left = 0
$tip.offset(offset)
actualWidth = $tip[0].offsetWidth
actualHeight = $tip[0].offsetHeight
}
this.replaceArrow(delta - width + actualWidth, actualWidth, 'left')
} else {
this.replaceArrow(actualHeight - height, actualHeight, 'top')
}
if (replace) $tip.offset(offset)
}
Tooltip.prototype.replaceArrow = function (delta, dimension, position) {
this.arrow().css(position, delta ? (50 * (1 - delta / dimension) + '%') : '')
}
Tooltip.prototype.setContent = function () {
var $tip = this.tip()
var title = this.getTitle()
$tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title)
$tip.removeClass('fade in top bottom left right')
}
Tooltip.prototype.hide = function () {
var that = this
var $tip = this.tip()
var e = $.Event('hide.bs.' + this.type)
function complete() {
if (that.hoverState != 'in') $tip.detach()
that.$element.trigger('hidden.bs.' + that.type)
}
this.$element.trigger(e)
if (e.isDefaultPrevented()) return
$tip.removeClass('in')
$.support.transition && this.$tip.hasClass('fade') ?
$tip
.one($.support.transition.end, complete)
.emulateTransitionEnd(150) :
complete()
this.hoverState = null
return this
}
Tooltip.prototype.fixTitle = function () {
var $e = this.$element
if ($e.attr('title') || typeof($e.attr('data-original-title')) != 'string') {
$e.attr('data-original-title', $e.attr('title') || '').attr('title', '')
}
}
Tooltip.prototype.hasContent = function () {
return this.getTitle()
}
Tooltip.prototype.getPosition = function () {
var el = this.$element[0]
return $.extend({}, (typeof el.getBoundingClientRect == 'function') ? el.getBoundingClientRect() : {
width: el.offsetWidth,
height: el.offsetHeight
}, this.$element.offset())
}
Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth, actualHeight) {
return placement == 'bottom' ? { top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2 } :
placement == 'top' ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2 } :
placement == 'left' ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } :
/* placement == 'right' */ { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width }
}
Tooltip.prototype.getTitle = function () {
var title
var $e = this.$element
var o = this.options
title = $e.attr('data-original-title')
|| (typeof o.title == 'function' ? o.title.call($e[0]) : o.title)
return title
}
Tooltip.prototype.tip = function () {
return this.$tip = this.$tip || $(this.options.template)
}
Tooltip.prototype.arrow = function () {
return this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow')
}
Tooltip.prototype.validate = function () {
if (!this.$element[0].parentNode) {
this.hide()
this.$element = null
this.options = null
}
}
Tooltip.prototype.enable = function () {
this.enabled = true
}
Tooltip.prototype.disable = function () {
this.enabled = false
}
Tooltip.prototype.toggleEnabled = function () {
this.enabled = !this.enabled
}
Tooltip.prototype.toggle = function (e) {
var self = e ? $(e.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type) : this
self.tip().hasClass('in') ? self.leave(self) : self.enter(self)
}
Tooltip.prototype.destroy = function () {
clearTimeout(this.timeout)
this.hide().$element.off('.' + this.type).removeData('bs.' + this.type)
}
// TOOLTIP PLUGIN DEFINITION
// =========================
var old = $.fn.tooltip
$.fn.tooltip = function (option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('bs.tooltip')
var options = typeof option == 'object' && option
if (!data && option == 'destroy') return
if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options)))
if (typeof option == 'string') data[option]()
})
}
$.fn.tooltip.Constructor = Tooltip
// TOOLTIP NO CONFLICT
// ===================
$.fn.tooltip.noConflict = function () {
$.fn.tooltip = old
return this
}
}(jQuery);

@ -0,0 +1,97 @@
/**
* Provides statistics for the local stream.
*/
var LocalStatsCollector = (function() {
/**
* Size of the webaudio analizer buffer.
* @type {number}
*/
var WEBAUDIO_ANALIZER_FFT_SIZE = 512;
/**
* Value of the webaudio analizer smoothing time parameter.
* @type {number}
*/
var WEBAUDIO_ANALIZER_SMOOTING_TIME = 0.1;
/**
* <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.getByteFrequencyData(array);
self.audioLevel = FrequencyDataToAudioLevel(array);
self.updateCallback(self);
},
this.intervalMilis
);
}
/**
* Stops collecting the statistics.
*/
LocalStatsCollectorProto.prototype.stop = function () {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
}
}
/**
* Converts frequency data array to audio level.
* @param array the frequency data array.
* @returns {number} the audio level
*/
var FrequencyDataToAudioLevel = function (array) {
var maxVolume = 0;
var length = array.length;
for (var i = 0; i < length; i++) {
if (maxVolume < array[i])
maxVolume = array[i];
}
return maxVolume / 255;
}
return LocalStatsCollectorProto;
})();

@ -21,6 +21,9 @@ Strophe.addConnectionPlugin('emuc', {
},
doJoin: function (jid, password) {
this.myroomjid = jid;
console.info("Joined MUC as " + this.myroomjid);
this.initPresenceMap(this.myroomjid);
if (!this.roomjid) {
@ -167,12 +170,36 @@ Strophe.addConnectionPlugin('emuc', {
}
this.connection.send(msg);
},
setSubject: function (subject){
var msg = $msg({to: this.roomjid, type: 'groupchat'});
msg.c('subject', subject);
this.connection.send(msg);
console.log("topic changed to " + subject);
},
onMessage: function (msg) {
var txt = $(msg).find('>body').text();
// TODO: <subject/>
// FIXME: this is a hack. but jingle on muc makes nickchanges hard
var from = msg.getAttribute('from');
var nick = $(msg).find('>nick[xmlns="http://jabber.org/protocol/nick"]').text() || Strophe.getResourceFromJid(from);
var txt = $(msg).find('>body').text();
var type = msg.getAttribute("type");
if(type == "error")
{
Chat.chatAddError($(msg).find('>text').text(), txt);
return true;
}
var subject = $(msg).find('>subject');
if(subject.length)
{
var subjectText = subject.text();
if(subjectText || subjectText == "") {
Chat.chatSetSubject(subjectText);
console.log("Subject is changed to " + subjectText);
}
}
if (txt) {
console.log('chat', nick, txt);

@ -19,10 +19,10 @@ var Prezi = (function (my) {
$(document).trigger("video.selected", [true]);
$('#largeVideo').fadeOut(300, function () {
setLargeVideoVisible(false);
VideoLayout.setLargeVideoVisible(false);
$('#presentation>iframe').fadeIn(300, function() {
$('#presentation>iframe').css({opacity:'1'});
dockToolbar(true);
Toolbar.dockToolbar(true);
});
});
}
@ -32,8 +32,8 @@ var Prezi = (function (my) {
$('#presentation>iframe').css({opacity:'0'});
$('#reloadPresentation').css({display:'none'});
$('#largeVideo').fadeIn(300, function() {
setLargeVideoVisible(true);
dockToolbar(false);
VideoLayout.setLargeVideoVisible(true);
Toolbar.dockToolbar(false);
});
});
}
@ -177,8 +177,8 @@ var Prezi = (function (my) {
// We explicitly don't specify the peer jid here, because we don't want
// this video to be dealt with as a peer related one (for example we
// don't want to show a mute/kick menu for this one, etc.).
addRemoteVideoContainer(null, elementId);
resizeThumbnails();
VideoLayout.addRemoteVideoContainer(null, elementId);
VideoLayout.resizeThumbnails();
var controlsEnabled = false;
if (jid === connection.emuc.myroomjid)

@ -0,0 +1,287 @@
/* global ssrc2jid */
/**
* Function object which once created can be used to calculate moving average of
* given period. Example for SMA3:</br>
* var sma3 = new SimpleMovingAverager(3);
* while(true) // some update loop
* {
* var currentSma3Value = sma3(nextInputValue);
* }
*
* @param period moving average period that will be used by created instance.
* @returns {Function} SMA calculator function of given <tt>period</tt>.
* @constructor
*/
function SimpleMovingAverager(period)
{
var nums = [];
return function (num)
{
nums.push(num);
if (nums.length > period)
nums.splice(0, 1);
var sum = 0;
for (var i in nums)
sum += nums[i];
var n = period;
if (nums.length < period)
n = nums.length;
return (sum / n);
};
}
/**
* Peer statistics data holder.
* @constructor
*/
function PeerStats()
{
this.ssrc2Loss = {};
this.ssrc2AudioLevel = {};
}
/**
* Sets packets loss rate for given <tt>ssrc</tt> that blong to the peer
* represented by this instance.
* @param ssrc audio or video RTP stream SSRC.
* @param lossRate new packet loss rate value to be set.
*/
PeerStats.prototype.setSsrcLoss = function (ssrc, lossRate)
{
this.ssrc2Loss[ssrc] = lossRate;
};
/**
* Sets new audio level(input or output) for given <tt>ssrc</tt> that identifies
* the stream which belongs to the peer represented by this instance.
* @param ssrc RTP stream SSRC for which current audio level value will be
* updated.
* @param audioLevel the new audio level value to be set. Value is truncated to
* fit the range from 0 to 1.
*/
PeerStats.prototype.setSsrcAudioLevel = function (ssrc, audioLevel)
{
// Range limit 0 - 1
this.ssrc2AudioLevel[ssrc] = Math.min(Math.max(audioLevel, 0), 1);
};
/**
* Calculates average packet loss for all streams that belong to the peer
* represented by this instance.
* @returns {number} average packet loss for all streams that belong to the peer
* represented by this instance.
*/
PeerStats.prototype.getAvgLoss = function ()
{
var self = this;
var avg = 0;
var count = Object.keys(this.ssrc2Loss).length;
Object.keys(this.ssrc2Loss).forEach(
function (ssrc)
{
avg += self.ssrc2Loss[ssrc];
}
);
return count > 0 ? avg / count : 0;
};
/**
* <tt>StatsCollector</tt> registers for stats updates of given
* <tt>peerconnection</tt> in given <tt>interval</tt>. On each update particular
* stats are extracted and put in {@link PeerStats} objects. Once the processing
* is done <tt>updateCallback</tt> is called with <tt>this</tt> instance as
* an event source.
*
* @param peerconnection webRTC peer connection object.
* @param interval stats refresh interval given in ms.
* @param {function(StatsCollector)} updateCallback the callback called on stats
* update.
* @constructor
*/
function StatsCollector(peerconnection, interval, updateCallback)
{
this.peerconnection = peerconnection;
this.baselineReport = null;
this.currentReport = null;
this.intervalId = null;
// Updates stats interval
this.intervalMilis = interval;
// Use SMA 3 to average packet loss changes over time
this.sma3 = new SimpleMovingAverager(3);
// Map of jids to PeerStats
this.jid2stats = {};
this.updateCallback = updateCallback;
}
/**
* Stops stats updates.
*/
StatsCollector.prototype.stop = function ()
{
if (this.intervalId)
{
clearInterval(this.intervalId);
this.intervalId = null;
}
};
/**
* Callback passed to <tt>getStats</tt> method.
* @param error an error that occurred on <tt>getStats</tt> call.
*/
StatsCollector.prototype.errorCallback = function (error)
{
console.error("Get stats error", error);
this.stop();
};
/**
* Starts stats updates.
*/
StatsCollector.prototype.start = function ()
{
var self = this;
this.intervalId = setInterval(
function ()
{
// Interval updates
self.peerconnection.getStats(
function (report)
{
var results = report.result();
//console.error("Got interval report", results);
self.currentReport = results;
self.processReport();
self.baselineReport = self.currentReport;
},
self.errorCallback
);
},
self.intervalMilis
);
};
/**
* Stats processing logic.
*/
StatsCollector.prototype.processReport = function ()
{
if (!this.baselineReport)
{
return;
}
for (var idx in this.currentReport)
{
var now = this.currentReport[idx];
if (now.type != 'ssrc')
{
continue;
}
var before = this.baselineReport[idx];
if (!before)
{
console.warn(now.stat('ssrc') + ' not enough data');
continue;
}
var ssrc = now.stat('ssrc');
var jid = ssrc2jid[ssrc];
if (!jid)
{
console.warn("No jid for ssrc: " + ssrc);
continue;
}
var jidStats = this.jid2stats[jid];
if (!jidStats)
{
jidStats = new PeerStats();
this.jid2stats[jid] = jidStats;
}
// Audio level
var audioLevel = now.stat('audioInputLevel');
if (!audioLevel)
audioLevel = now.stat('audioOutputLevel');
if (audioLevel)
{
// TODO: can't find specs about what this value really is,
// but it seems to vary between 0 and around 32k.
audioLevel = audioLevel / 32767;
jidStats.setSsrcAudioLevel(ssrc, audioLevel);
}
var key = 'packetsReceived';
if (!now.stat(key))
{
key = 'packetsSent';
if (!now.stat(key))
{
console.error("No packetsReceived nor packetSent stat found");
this.stop();
return;
}
}
var packetsNow = now.stat(key);
var packetsBefore = before.stat(key);
var packetRate = packetsNow - packetsBefore;
var currentLoss = now.stat('packetsLost');
var previousLoss = before.stat('packetsLost');
var lossRate = currentLoss - previousLoss;
var packetsTotal = (packetRate + lossRate);
var lossPercent;
if (packetsTotal > 0)
lossPercent = lossRate / packetsTotal;
else
lossPercent = 0;
//console.info(jid + " ssrc: " + ssrc + " " + key + ": " + packetsNow);
jidStats.setSsrcLoss(ssrc, lossPercent);
}
var self = this;
// Jid stats
var allPeersAvg = 0;
var jids = Object.keys(this.jid2stats);
jids.forEach(
function (jid)
{
var peerAvg = self.jid2stats[jid].getAvgLoss(
function (avg)
{
//console.info(jid + " stats: " + (avg * 100) + " %");
allPeersAvg += avg;
}
);
}
);
if (jids.length > 1)
{
// Our streams loss is reported as 0 always, so -1 to length
allPeersAvg = allPeersAvg / (jids.length - 1);
/**
* Calculates number of connection quality bars from 4(hi) to 0(lo).
*/
var outputAvg = self.sma3(allPeersAvg);
// Linear from 4(0%) to 0(25%).
var quality = Math.round(4 - outputAvg * 16);
quality = Math.max(quality, 0); // lower limit 0
quality = Math.min(quality, 4); // upper limit 4
// TODO: quality can be used to indicate connection quality using 4 step
// bar indicator
//console.info("Loss SMA3: " + outputAvg + " Q: " + quality);
}
self.updateCallback(self);
};

@ -0,0 +1,234 @@
var Toolbar = (function (my) {
var INITIAL_TOOLBAR_TIMEOUT = 20000;
var TOOLBAR_TIMEOUT = INITIAL_TOOLBAR_TIMEOUT;
/**
* Opens the lock room dialog.
*/
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",
persistent: false
}
);
else
$.prompt("This conversation isn't currently protected by"
+ " a secret key. Only the owner of the conference" +
+ " could set a shared key.",
{
title: "Secrect key",
persistent: false
}
);
} else {
if (sharedKey) {
$.prompt("Are you sure you would like to remove your secret key?",
{
title: "Remove secrect key",
persistent: false,
buttons: { "Remove": true, "Cancel": false},
defaultButton: 1,
submit: function (e, v, m, f) {
if (v) {
setSharedKey('');
lockRoom(false);
}
}
}
);
} else {
$.prompt('<h2>Set a secrect key to lock your room</h2>' +
'<input id="lockKey" type="text" placeholder="your shared key" autofocus>',
{
persistent: false,
buttons: { "Save": true, "Cancel": false},
defaultButton: 1,
loaded: function (event) {
document.getElementById('lockKey').focus();
},
submit: function (e, v, m, f) {
if (v) {
var lockKey = document.getElementById('lockKey');
if (lockKey.value) {
setSharedKey(Util.escapeHtml(lockKey.value));
lockRoom(true);
}
}
}
}
);
}
}
};
/**
* Opens the invite link dialog.
*/
my.openLinkDialog = function() {
$.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();
}
}
);
};
/**
* Opens the settings dialog.
*/
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>',
{
persistent: false,
buttons: { "Save": true, "Cancel": false},
defaultButton: 1,
loaded: function (event) {
document.getElementById('lockKey').focus();
},
submit: function (e, v, m, f) {
if (v) {
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);
lockRoom(true);
}
*/
}
}
}
);
};
/**
* Toggles the application in and out of full screen mode
* (a.k.a. presentation mode in Chrome).
*/
my.toggleFullScreen = function() {
var fsElement = document.documentElement;
if (!document.mozFullScreen && !document.webkitIsFullScreen) {
//Enter Full Screen
if (fsElement.mozRequestFullScreen) {
fsElement.mozRequestFullScreen();
}
else {
fsElement.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT);
}
} else {
//Exit Full Screen
if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else {
document.webkitCancelFullScreen();
}
}
};
/**
* Shows the main toolbar.
*/
my.showToolbar = function() {
if (!$('#header').is(':visible')) {
$('#header').show("slide", { direction: "up", duration: 300});
$('#subject').animate({top: "+=40"}, 300);
if (toolbarTimeout) {
clearTimeout(toolbarTimeout);
toolbarTimeout = null;
}
toolbarTimeout = setTimeout(hideToolbar, TOOLBAR_TIMEOUT);
TOOLBAR_TIMEOUT = 4000;
}
if (focus != null)
{
// TODO: Enable settings functionality. Need to uncomment the settings button in index.html.
// $('#settingsButton').css({visibility:"visible"});
}
// Show/hide desktop sharing button
showDesktopSharingButton();
};
/**
* Docks/undocks the toolbar.
*
* @param isDock indicates what operation to perform
*/
my.dockToolbar = function(isDock) {
if (isDock) {
// First make sure the toolbar is shown.
if (!$('#header').is(':visible')) {
Toolbar.showToolbar();
}
// Then clear the time out, to dock the toolbar.
clearTimeout(toolbarTimeout);
toolbarTimeout = null;
}
else {
if (!$('#header').is(':visible')) {
Toolbar.showToolbar();
}
else {
toolbarTimeout = setTimeout(hideToolbar, TOOLBAR_TIMEOUT);
}
}
};
/**
* Updates the lock button state.
*/
my.updateLockButton = function() {
buttonClick("#lockIcon", "icon-security icon-security-locked");
};
/**
* Hides the toolbar.
*/
var hideToolbar = function () {
var isToolbarHover = false;
$('#header').find('*').each(function () {
var id = $(this).attr('id');
if ($("#" + id + ":hover").length > 0) {
isToolbarHover = true;
}
});
clearTimeout(toolbarTimeout);
toolbarTimeout = null;
if (!isToolbarHover) {
$('#header').hide("slide", { direction: "up", duration: 300});
$('#subject').animate({top: "-=40"}, 300);
}
else {
toolbarTimeout = setTimeout(hideToolbar, TOOLBAR_TIMEOUT);
}
};
return my;
}(Toolbar || {}));

@ -51,10 +51,35 @@ var Util = (function (my) {
* Returns the available video width.
*/
my.getAvailableVideoWidth = function () {
var chatspaceWidth = $('#chatspace').is(":visible") ? $('#chatspace').width() : 0;
var chatspaceWidth
= $('#chatspace').is(":visible") ? $('#chatspace').width() : 0;
return window.innerWidth - chatspaceWidth;
};
my.imageToGrayScale = function (canvas) {
var context = canvas.getContext('2d');
var imgData = context.getImageData(0, 0, canvas.width, canvas.height);
var pixels = imgData.data;
for (var i = 0, n = pixels.length; i < n; i += 4) {
var grayscale
= pixels[i] * .3 + pixels[i+1] * .59 + pixels[i+2] * .11;
pixels[i ] = grayscale; // red
pixels[i+1] = grayscale; // green
pixels[i+2] = grayscale; // blue
// pixels[i+3] is alpha
}
// redraw the image in black & white
context.putImageData(imgData, 0, 0);
};
my.setTooltip = function (element, tooltipText, position) {
element.setAttribute("data-content", tooltipText);
element.setAttribute("data-toggle", "popover");
element.setAttribute("data-placement", position);
element.setAttribute("data-html", true);
};
return my;
}(Util || {}));

@ -0,0 +1,901 @@
var VideoLayout = (function (my) {
var preMuted = false;
var currentActiveSpeaker = null;
my.changeLocalAudio = function(stream) {
connection.jingle.localAudio = stream;
RTC.attachMediaStream($('#localAudio'), stream);
document.getElementById('localAudio').autoplay = true;
document.getElementById('localAudio').volume = 0;
if (preMuted) {
toggleAudio();
preMuted = false;
}
};
my.changeLocalVideo = function(stream, flipX) {
connection.jingle.localVideo = stream;
var localVideo = document.createElement('video');
localVideo.id = 'localVideo_' + stream.id;
localVideo.autoplay = true;
localVideo.volume = 0; // is it required if audio is separated ?
localVideo.oncontextmenu = function () { return false; };
var localVideoContainer = document.getElementById('localVideoWrapper');
localVideoContainer.appendChild(localVideo);
var localVideoSelector = $('#' + localVideo.id);
// Add click handler
localVideoSelector.click(function () {
VideoLayout.handleVideoThumbClicked(localVideo.src);
});
// Add hover handler
$('#localVideoContainer').hover(
function() {
VideoLayout.showDisplayName('localVideoContainer', true);
},
function() {
if (focusedVideoSrc !== localVideo.src)
VideoLayout.showDisplayName('localVideoContainer', false);
}
);
// Add stream ended handler
stream.onended = function () {
localVideoContainer.removeChild(localVideo);
VideoLayout.checkChangeLargeVideo(localVideo.src);
};
// Flip video x axis if needed
flipXLocalVideo = flipX;
if (flipX) {
localVideoSelector.addClass("flipVideoX");
}
// Attach WebRTC stream
RTC.attachMediaStream(localVideoSelector, stream);
localVideoSrc = localVideo.src;
VideoLayout.updateLargeVideo(localVideoSrc, 0);
};
/**
* Checks if removed video is currently displayed and tries to display
* another one instead.
* @param removedVideoSrc src stream identifier of the video.
*/
my.checkChangeLargeVideo = function(removedVideoSrc) {
if (removedVideoSrc === $('#largeVideo').attr('src')) {
// this is currently displayed as large
// pick the last visible video in the row
// if nobody else is left, this picks the local video
var pick
= $('#remoteVideos>span[id!="mixedstream"]:visible:last>video')
.get(0);
if (!pick) {
console.info("Last visible video no longer exists");
pick = $('#remoteVideos>span[id!="mixedstream"]>video').get(0);
if (!pick) {
// Try local video
console.info("Fallback to local video...");
pick = $('#remoteVideos>span>span>video').get(0);
}
}
// mute if localvideo
if (pick) {
VideoLayout.updateLargeVideo(pick.src, pick.volume);
} else {
console.warn("Failed to elect large video");
}
}
};
/**
* Updates the large video with the given new video source.
*/
my.updateLargeVideo = function(newSrc, vol) {
console.log('hover in', newSrc);
if ($('#largeVideo').attr('src') != newSrc) {
var isVisible = $('#largeVideo').is(':visible');
$('#largeVideo').fadeOut(300, function () {
$(this).attr('src', newSrc);
// Screen stream is already rotated
var flipX = (newSrc === localVideoSrc) && flipXLocalVideo;
var videoTransform = document.getElementById('largeVideo')
.style.webkitTransform;
if (flipX && videoTransform !== 'scaleX(-1)') {
document.getElementById('largeVideo').style.webkitTransform
= "scaleX(-1)";
}
else if (!flipX && videoTransform === 'scaleX(-1)') {
document.getElementById('largeVideo').style.webkitTransform
= "none";
}
// Change the way we'll be measuring and positioning large video
var isDesktop = isVideoSrcDesktop(newSrc);
getVideoSize = isDesktop
? getDesktopVideoSize
: getCameraVideoSize;
getVideoPosition = isDesktop
? getDesktopVideoPosition
: getCameraVideoPosition;
if (isVisible)
$(this).fadeIn(300);
});
}
};
my.handleVideoThumbClicked = function(videoSrc) {
// Restore style for previously focused video
var focusJid = getJidFromVideoSrc(focusedVideoSrc);
var oldContainer = getParticipantContainer(focusJid);
if (oldContainer) {
oldContainer.removeClass("videoContainerFocused");
VideoLayout.enableActiveSpeaker(
Strophe.getResourceFromJid(focusJid), false);
}
// Unlock current focused.
if (focusedVideoSrc === videoSrc)
{
focusedVideoSrc = null;
// Enable the currently set active speaker.
if (currentActiveSpeaker) {
VideoLayout.enableActiveSpeaker(currentActiveSpeaker, true);
}
return;
}
// Remove style for current active speaker if we're going to lock
// another video.
else if (currentActiveSpeaker) {
VideoLayout.enableActiveSpeaker(currentActiveSpeaker, false);
}
// Lock new video
focusedVideoSrc = videoSrc;
var userJid = getJidFromVideoSrc(videoSrc);
if (userJid)
{
var container = getParticipantContainer(userJid);
container.addClass("videoContainerFocused");
var resourceJid = Strophe.getResourceFromJid(userJid);
VideoLayout.enableActiveSpeaker(resourceJid, true);
}
$(document).trigger("video.selected", [false]);
VideoLayout.updateLargeVideo(videoSrc, 1);
$('audio').each(function (idx, el) {
if (el.id.indexOf('mixedmslabel') !== -1) {
el.volume = 0;
el.volume = 1;
}
});
};
/**
* Positions the large video.
*
* @param videoWidth the stream video width
* @param videoHeight the stream video height
*/
my.positionLarge = function (videoWidth, videoHeight) {
var videoSpaceWidth = $('#videospace').width();
var videoSpaceHeight = window.innerHeight;
var videoSize = getVideoSize(videoWidth,
videoHeight,
videoSpaceWidth,
videoSpaceHeight);
var largeVideoWidth = videoSize[0];
var largeVideoHeight = videoSize[1];
var videoPosition = getVideoPosition(largeVideoWidth,
largeVideoHeight,
videoSpaceWidth,
videoSpaceHeight);
var horizontalIndent = videoPosition[0];
var verticalIndent = videoPosition[1];
positionVideo($('#largeVideo'),
largeVideoWidth,
largeVideoHeight,
horizontalIndent, verticalIndent);
};
/**
* Shows/hides the large video.
*/
my.setLargeVideoVisible = function(isVisible) {
if (isVisible) {
$('#largeVideo').css({visibility: 'visible'});
$('.watermark').css({visibility: 'visible'});
}
else {
$('#largeVideo').css({visibility: 'hidden'});
$('.watermark').css({visibility: 'hidden'});
}
};
/**
* Checks if container for participant identified by given peerJid exists
* in the document and creates it eventually.
*
* @param peerJid peer Jid to check.
*/
my.ensurePeerContainerExists = function(peerJid) {
var peerResource = Strophe.getResourceFromJid(peerJid);
var videoSpanId = 'participant_' + peerResource;
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)
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();
};
my.addRemoteVideoContainer = function(peerJid, spanId) {
var container = document.createElement('span');
container.id = spanId;
container.className = 'videocontainer';
var remotes = document.getElementById('remoteVideos');
// If the peerJid is null then this video span couldn't be directly
// associated with a participant (this could happen in the case of prezi).
if (focus && peerJid != null)
addRemoteVideoMenu(peerJid, container);
remotes.appendChild(container);
return container;
};
/**
* Shows the display name for the given video.
*/
my.setDisplayName = function(videoSpanId, displayName) {
var nameSpan = $('#' + videoSpanId + '>span.displayname');
// If we already have a display name for this video.
if (nameSpan.length > 0) {
var nameSpanElement = nameSpan.get(0);
if (nameSpanElement.id === 'localDisplayName' &&
$('#localDisplayName').text() !== displayName) {
$('#localDisplayName').text(displayName);
} else {
$('#' + videoSpanId + '_name').text(displayName);
}
} else {
var editButton = null;
if (videoSpanId === 'localVideoContainer') {
editButton = createEditDisplayNameButton();
}
if (displayName.length) {
nameSpan = document.createElement('span');
nameSpan.className = 'displayname';
nameSpan.innerText = displayName;
$('#' + videoSpanId)[0].appendChild(nameSpan);
}
if (!editButton) {
nameSpan.id = videoSpanId + '_name';
} else {
nameSpan.id = 'localDisplayName';
$('#' + videoSpanId)[0].appendChild(editButton);
var editableText = document.createElement('input');
editableText.className = 'displayname';
editableText.id = 'editDisplayName';
if (displayName.length) {
editableText.value
= displayName.substring(0, displayName.indexOf(' (me)'));
}
editableText.setAttribute('style', 'display:none;');
editableText.setAttribute('placeholder', 'ex. Jane Pink');
$('#' + videoSpanId)[0].appendChild(editableText);
$('#localVideoContainer .displayname').bind("click", function (e) {
e.preventDefault();
$('#localDisplayName').hide();
$('#editDisplayName').show();
$('#editDisplayName').focus();
$('#editDisplayName').select();
var inputDisplayNameHandler = function (name) {
if (nickname !== name) {
nickname = name;
window.localStorage.displayname = nickname;
connection.emuc.addDisplayNameToPresence(nickname);
connection.emuc.sendPresence();
Chat.setChatConversationMode(true);
}
if (!$('#localDisplayName').is(":visible")) {
if (nickname) {
$('#localDisplayName').text(nickname + " (me)");
$('#localDisplayName').show();
}
else {
$('#localDisplayName').text(nickname);
}
$('#editDisplayName').hide();
}
};
$('#editDisplayName').one("focusout", function (e) {
inputDisplayNameHandler(this.value);
});
$('#editDisplayName').on('keydown', function (e) {
if (e.keyCode === 13) {
e.preventDefault();
inputDisplayNameHandler(this.value);
}
});
});
}
}
};
/**
* Shows/hides the display name on the remote video.
* @param videoSpanId the identifier of the video span element
* @param isShow indicates if the display name should be shown or hidden
*/
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;");
}
else {
if (nameSpan)
nameSpan.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
* from the connection.jingle.sessions.
*/
my.showFocusIndicator = function() {
if (focus !== null) {
var indicatorSpan = $('#localVideoContainer .focusindicator');
if (indicatorSpan.children().length === 0)
{
createFocusIndicatorElement(indicatorSpan[0]);
}
}
else if (Object.keys(connection.jingle.sessions).length > 0) {
// If we're only a participant the focus will be the only session we have.
var session
= connection.jingle.sessions
[Object.keys(connection.jingle.sessions)[0]];
var focusId
= 'participant_' + Strophe.getResourceFromJid(session.peerjid);
var focusContainer = document.getElementById(focusId);
if (!focusContainer) {
console.error("No focus container!");
return;
}
var indicatorSpan = $('#' + focusId + ' .focusindicator');
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);
}
}
};
/**
* Shows video muted indicator over small videos.
*/
my.showVideoIndicator = function(videoSpanId, isMuted) {
var videoMutedSpan = $('#' + videoSpanId + '>span.videoMuted');
if (isMuted === 'false') {
if (videoMutedSpan.length > 0) {
videoMutedSpan.remove();
}
}
else {
var audioMutedSpan = $('#' + videoSpanId + '>span.audioMuted');
videoMutedSpan = document.createElement('span');
videoMutedSpan.className = 'videoMuted';
if (audioMutedSpan) {
videoMutedSpan.right = '30px';
}
$('#' + videoSpanId)[0].appendChild(videoMutedSpan);
var mutedIndicator = document.createElement('i');
mutedIndicator.className = 'icon-camera-disabled';
Util.setTooltip(mutedIndicator,
"Participant has<br/>stopped the camera.",
"top");
videoMutedSpan.appendChild(mutedIndicator);
}
};
/**
* Shows audio muted indicator over small videos.
*/
my.showAudioIndicator = function(videoSpanId, isMuted) {
var audioMutedSpan = $('#' + videoSpanId + '>span.audioMuted');
if (isMuted === 'false') {
if (audioMutedSpan.length > 0) {
audioMutedSpan.remove();
}
}
else {
var videoMutedSpan = $('#' + videoSpanId + '>span.videoMuted');
audioMutedSpan = document.createElement('span');
audioMutedSpan.className = 'audioMuted';
Util.setTooltip(audioMutedSpan,
"Participant is muted",
"top");
if (videoMutedSpan) {
audioMutedSpan.right = '30px';
}
$('#' + videoSpanId)[0].appendChild(audioMutedSpan);
var mutedIndicator = document.createElement('i');
mutedIndicator.className = 'icon-mic-disabled';
audioMutedSpan.appendChild(mutedIndicator);
}
};
/**
* Resizes the large video container.
*/
my.resizeLargeVideoContainer = function () {
Chat.resizeChat();
var availableHeight = window.innerHeight;
var availableWidth = Util.getAvailableVideoWidth();
if (availableWidth < 0 || availableHeight < 0) return;
$('#videospace').width(availableWidth);
$('#videospace').height(availableHeight);
$('#largeVideoContainer').width(availableWidth);
$('#largeVideoContainer').height(availableHeight);
VideoLayout.resizeThumbnails();
};
/**
* Resizes thumbnails.
*/
my.resizeThumbnails = function() {
var thumbnailSize = calculateThumbnailSize();
var width = thumbnailSize[0];
var height = thumbnailSize[1];
// size videos so that while keeping AR and max height, we have a
// nice fit
$('#remoteVideos').height(height);
$('#remoteVideos>span').width(width);
$('#remoteVideos>span').height(height);
};
/**
* Enables the active speaker UI.
*
* @param resourceJid the jid indicating the video element to
* activate/deactivate
* @param isEnable indicates if the active speaker should be enabled or
* disabled
*/
my.enableActiveSpeaker = function(resourceJid, isEnable) {
var displayName = resourceJid;
var nameSpan = $('#participant_' + resourceJid + '>span.displayname');
if (nameSpan.length > 0)
displayName = nameSpan.text();
console.log("Enable active speaker", displayName, isEnable);
var videoSpanId = null;
if (resourceJid
=== Strophe.getResourceFromJid(connection.emuc.myroomjid))
videoSpanId = 'localVideoWrapper';
else
videoSpanId = 'participant_' + resourceJid;
videoSpan = document.getElementById(videoSpanId);
if (!videoSpan) {
console.error("No video element for jid", resourceJid);
return;
}
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(videoSpanId, true);
}
else {
VideoLayout.showDisplayName(videoSpanId, false);
if (videoElement.classList.contains("activespeaker"))
videoElement.classList.remove("activespeaker");
}
}
};
/**
* Gets the selector of video thumbnail container for the user identified by
* given <tt>userJid</tt>
* @param userJid user's Jid for whom we want to get the video container.
*/
function getParticipantContainer(userJid)
{
if (!userJid)
return null;
if (userJid === connection.emuc.myroomjid)
return $("#localVideoContainer");
else
return $("#participant_" + Strophe.getResourceFromJid(userJid));
}
/**
* Sets the size and position of the given video element.
*
* @param video the video element to position
* @param width the desired video width
* @param height the desired video height
* @param horizontalIndent the left and right indent
* @param verticalIndent the top and bottom indent
*/
function positionVideo(video,
width,
height,
horizontalIndent,
verticalIndent) {
video.width(width);
video.height(height);
video.css({ top: verticalIndent + 'px',
bottom: verticalIndent + 'px',
left: horizontalIndent + 'px',
right: horizontalIndent + 'px'});
}
/**
* Calculates the thumbnail size.
*/
var calculateThumbnailSize = function () {
// 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;
// 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);
availableHeight = Math.min(maxHeight, availableWidth / aspectRatio);
if (availableHeight < availableWidth / aspectRatio) {
availableWidth = Math.floor(availableHeight * aspectRatio);
}
return [availableWidth, availableHeight];
};
/**
* Returns an array of the video dimensions, so that it keeps it's aspect
* ratio and fits available area with it's larger dimension. This method
* ensures that whole video will be visible and can leave empty areas.
*
* @return an array with 2 elements, the video width and the video height
*/
function getDesktopVideoSize(videoWidth,
videoHeight,
videoSpaceWidth,
videoSpaceHeight) {
if (!videoWidth)
videoWidth = currentVideoWidth;
if (!videoHeight)
videoHeight = currentVideoHeight;
var aspectRatio = videoWidth / videoHeight;
var availableWidth = Math.max(videoWidth, videoSpaceWidth);
var availableHeight = Math.max(videoHeight, videoSpaceHeight);
videoSpaceHeight -= $('#remoteVideos').outerHeight();
if (availableWidth / aspectRatio >= videoSpaceHeight)
{
availableHeight = videoSpaceHeight;
availableWidth = availableHeight * aspectRatio;
}
if (availableHeight * aspectRatio >= videoSpaceWidth)
{
availableWidth = videoSpaceWidth;
availableHeight = availableWidth / aspectRatio;
}
return [availableWidth, availableHeight];
}
/**
* Creates the edit display name button.
*
* @returns the edit button
*/
function createEditDisplayNameButton() {
var editButton = document.createElement('a');
editButton.className = 'displayname';
Util.setTooltip(editButton,
'Click to edit your<br/>display name',
"top");
editButton.innerHTML = '<i class="fa fa-pencil"></i>';
return editButton;
}
/**
* Creates the element indicating the focus of the conference.
*
* @param parentElement the parent element where the focus indicator will
* be added
*/
function createFocusIndicatorElement(parentElement) {
var focusIndicator = document.createElement('i');
focusIndicator.className = 'fa fa-star';
parentElement.appendChild(focusIndicator);
}
/**
* Updates the remote video menu.
*
* @param jid the jid indicating the video for which we're adding a menu.
* @param isMuted indicates the current mute state
*/
my.updateRemoteVideoMenu = function(jid, isMuted) {
var muteMenuItem
= $('#remote_popupmenu_'
+ Strophe.getResourceFromJid(jid)
+ '>li>a.mutelink');
var mutedIndicator = "<i class='icon-mic-disabled'></i>";
if (muteMenuItem.length) {
var muteLink = muteMenuItem.get(0);
if (isMuted === 'true') {
muteLink.innerHTML = mutedIndicator + ' Muted';
muteLink.className = 'mutelink disabled';
}
else {
muteLink.innerHTML = mutedIndicator + ' Mute';
muteLink.className = 'mutelink';
}
}
};
/**
* Returns the current active speaker.
*/
my.getActiveSpeakerContainerId = function () {
return 'participant_' + currentActiveSpeaker;
};
/**
* Adds the remote video menu element for the given <tt>jid</tt> in the
* given <tt>parentElement</tt>.
*
* @param jid the jid indicating the video for which we're adding a menu.
* @param parentElement the parent element where this menu will be added
*/
function addRemoteVideoMenu(jid, parentElement) {
var spanElement = document.createElement('span');
spanElement.className = 'remotevideomenu';
parentElement.appendChild(spanElement);
var menuElement = document.createElement('i');
menuElement.className = 'fa fa-angle-down';
menuElement.title = 'Remote user controls';
spanElement.appendChild(menuElement);
// <ul class="popupmenu">
// <li><a href="#">Mute</a></li>
// <li><a href="#">Eject</a></li>
// </ul>
var popupmenuElement = document.createElement('ul');
popupmenuElement.className = 'popupmenu';
popupmenuElement.id
= 'remote_popupmenu_' + Strophe.getResourceFromJid(jid);
spanElement.appendChild(popupmenuElement);
var muteMenuItem = document.createElement('li');
var muteLinkItem = document.createElement('a');
var mutedIndicator = "<i class='icon-mic-disabled'></i>";
if (!mutedAudios[jid]) {
muteLinkItem.innerHTML = mutedIndicator + 'Mute';
muteLinkItem.className = 'mutelink';
}
else {
muteLinkItem.innerHTML = mutedIndicator + ' Muted';
muteLinkItem.className = 'mutelink disabled';
}
muteLinkItem.onclick = function(){
if ($(this).attr('disabled') != undefined) {
event.preventDefault();
}
var isMute = !mutedAudios[jid];
connection.moderate.setMute(jid, isMute);
popupmenuElement.setAttribute('style', 'display:none;');
if (isMute) {
this.innerHTML = mutedIndicator + ' Muted';
this.className = 'mutelink disabled';
}
else {
this.innerHTML = mutedIndicator + ' Mute';
this.className = 'mutelink';
}
};
muteMenuItem.appendChild(muteLinkItem);
popupmenuElement.appendChild(muteMenuItem);
var ejectIndicator = "<i class='fa fa-eject'></i>";
var ejectMenuItem = document.createElement('li');
var ejectLinkItem = document.createElement('a');
ejectLinkItem.innerHTML = ejectIndicator + ' Kick out';
ejectLinkItem.onclick = function(){
connection.moderate.eject(jid);
popupmenuElement.setAttribute('style', 'display:none;');
};
ejectMenuItem.appendChild(ejectLinkItem);
popupmenuElement.appendChild(ejectMenuItem);
}
/**
* On audio muted event.
*/
$(document).bind('audiomuted.muc', function (event, jid, isMuted) {
var videoSpanId = null;
if (jid === connection.emuc.myroomjid) {
videoSpanId = 'localVideoContainer';
} else {
VideoLayout.ensurePeerContainerExists(jid);
videoSpanId = 'participant_' + Strophe.getResourceFromJid(jid);
}
if (focus) {
mutedAudios[jid] = isMuted;
VideoLayout.updateRemoteVideoMenu(jid, isMuted);
}
if (videoSpanId)
VideoLayout.showAudioIndicator(videoSpanId, isMuted);
});
/**
* On video muted event.
*/
$(document).bind('videomuted.muc', function (event, jid, isMuted) {
var videoSpanId = null;
if (jid === connection.emuc.myroomjid) {
videoSpanId = 'localVideoContainer';
} else {
VideoLayout.ensurePeerContainerExists(jid);
videoSpanId = 'participant_' + Strophe.getResourceFromJid(jid);
}
if (videoSpanId)
VideoLayout.showVideoIndicator(videoSpanId, isMuted);
});
/**
* On active speaker changed event.
*/
$(document).bind('activespeakerchanged', function (event, resourceJid) {
// We ignore local user events.
if (resourceJid
=== Strophe.getResourceFromJid(connection.emuc.myroomjid))
return;
// Disable style for previous active speaker.
if (currentActiveSpeaker
&& currentActiveSpeaker !== resourceJid
&& !focusedVideoSrc) {
var oldContainer = document.getElementById(
'participant_' + currentActiveSpeaker);
if (oldContainer) {
VideoLayout.enableActiveSpeaker(currentActiveSpeaker, false);
}
}
// Obtain container for new active speaker.
var container = document.getElementById(
'participant_' + resourceJid);
// Update the current active speaker.
if (resourceJid !== currentActiveSpeaker)
currentActiveSpeaker = resourceJid;
else
return;
// 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");
if (video.length)
{
VideoLayout.updateLargeVideo(video[0].src);
VideoLayout.enableActiveSpeaker(resourceJid, true);
}
}
});
return my;
}(VideoLayout || {}));
Loading…
Cancel
Save