Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
77cb10d6a1
|
@ -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/)
|
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
|
## 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!
|
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!
|
||||||
|
|
54
chat.js
54
chat.js
|
@ -39,10 +39,19 @@ var Chat = (function (my) {
|
||||||
$('#usermsg').keydown(function (event) {
|
$('#usermsg').keydown(function (event) {
|
||||||
if (event.keyCode === 13) {
|
if (event.keyCode === 13) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
var message = Util.escapeHtml(this.value);
|
var value = this.value;
|
||||||
$('#usermsg').val('').trigger('autosize.resize');
|
$('#usermsg').val('').trigger('autosize.resize');
|
||||||
this.focus();
|
this.focus();
|
||||||
connection.emuc.sendMessage(message, nickname);
|
var command = new CommandsProcessor(value);
|
||||||
|
if(command.isCommand())
|
||||||
|
{
|
||||||
|
command.processCommand();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var message = Util.escapeHtml(value);
|
||||||
|
connection.emuc.sendMessage(message, nickname);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -90,6 +99,45 @@ var Chat = (function (my) {
|
||||||
{ scrollTop: $('#chatconversation')[0].scrollHeight}, 1000);
|
{ 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.
|
* Opens / closes the chat area.
|
||||||
*/
|
*/
|
||||||
|
@ -242,7 +290,7 @@ var Chat = (function (my) {
|
||||||
if (unreadMessages) {
|
if (unreadMessages) {
|
||||||
unreadMsgElement.innerHTML = unreadMessages.toString();
|
unreadMsgElement.innerHTML = unreadMessages.toString();
|
||||||
|
|
||||||
showToolbar();
|
Toolbar.showToolbar();
|
||||||
|
|
||||||
var chatButtonElement
|
var chatButtonElement
|
||||||
= document.getElementById('chatButton').parentNode;
|
= 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
|
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.
|
desktopSharing: 'ext', // Desktop sharing method. Can be set to 'ext', 'webrtc' or false to disable.
|
||||||
chromeExtensionId: 'diibjkoicjeejcmhdnailmkgecihlobk', // Id of desktop streamer Chrome extension
|
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;
|
color: #087dba;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.errorMessage {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
.remoteuser {
|
.remoteuser {
|
||||||
color: #424242;
|
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-bottom: 5px;
|
||||||
padding-top: 5px;
|
padding-top: 5px;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
left: 0px;
|
left: -5px;
|
||||||
width: 100px;
|
width: 100px;
|
||||||
background-color: rgba(0,0,0,1);
|
background-color: rgba(0,0,0,1);
|
||||||
-webkit-box-shadow: 0 0 2px #000000, 0 0 10px #000000;
|
-webkit-box-shadow: 0 0 2px #000000, 0 0 10px #000000;
|
||||||
|
@ -21,7 +21,7 @@ ul.popupmenu:after {
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: -9px;
|
bottom: -9px;
|
||||||
left: 13px;
|
left: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.popupmenu li {
|
ul.popupmenu li {
|
||||||
|
|
|
@ -32,9 +32,11 @@
|
||||||
background-size: contain;
|
background-size: contain;
|
||||||
border-radius:8px;
|
border-radius:8px;
|
||||||
border: 2px solid #212425;
|
border: 2px solid #212425;
|
||||||
|
margin-right: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#remoteVideos .videocontainer:hover {
|
#remoteVideos .videocontainer:hover,
|
||||||
|
#remoteVideos .videocontainer.videoContainerFocused {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
content:"";
|
content:"";
|
||||||
|
@ -49,7 +51,6 @@
|
||||||
-webkit-animation-iteration-count: 1;
|
-webkit-animation-iteration-count: 1;
|
||||||
-webkit-box-shadow: 0 0 18px #388396;
|
-webkit-box-shadow: 0 0 18px #388396;
|
||||||
border: 2px solid #388396;
|
border: 2px solid #388396;
|
||||||
z-index: 3;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#localVideoWrapper {
|
#localVideoWrapper {
|
||||||
|
@ -93,6 +94,11 @@
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.activespeaker {
|
||||||
|
-webkit-filter: grayscale(1);
|
||||||
|
filter: grayscale(1);
|
||||||
|
}
|
||||||
|
|
||||||
#etherpad,
|
#etherpad,
|
||||||
#presentation {
|
#presentation {
|
||||||
text-align: center;
|
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);
|
text-shadow: 0px 1px 0px rgba(255,255,255,.3), 0px -1px 0px rgba(0,0,0,.7);
|
||||||
border: 0px;
|
border: 0px;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#remoteVideos .nick {
|
#remoteVideos .nick {
|
||||||
|
@ -133,25 +140,22 @@
|
||||||
|
|
||||||
.videocontainer>span.displayname,
|
.videocontainer>span.displayname,
|
||||||
.videocontainer>input.displayname {
|
.videocontainer>input.displayname {
|
||||||
display: inline-block;
|
display: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background: -webkit-linear-gradient(left, rgba(0,0,0,.7), rgba(0,0,0,0));
|
|
||||||
color: #FFFFFF;
|
color: #FFFFFF;
|
||||||
bottom: 0;
|
background: rgba(0,0,0,.7);
|
||||||
left: 0;
|
text-align: center;
|
||||||
padding: 3px 5px;
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
max-height: 18px;
|
|
||||||
font-size: 9pt;
|
|
||||||
text-align: left;
|
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
width: 70%;
|
||||||
|
height: 20%;
|
||||||
|
left: 15%;
|
||||||
|
top: 40%;
|
||||||
|
padding: 5px;
|
||||||
|
font-size: 11pt;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
box-sizing: border-box;
|
border-radius:20px;
|
||||||
border-bottom-left-radius:4px;
|
|
||||||
border-bottom-right-radius:4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#localVideoContainer>span.displayname:hover {
|
#localVideoContainer>span.displayname:hover {
|
||||||
|
@ -162,6 +166,10 @@
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.videocontainer>input.displayname {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
#localDisplayName {
|
#localDisplayName {
|
||||||
pointer-events: auto !important;
|
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);
|
text-shadow: 0px 1px 0px rgba(255,255,255,.3), 0px -1px 0px rgba(0,0,0,.7);
|
||||||
border: 0px;
|
border: 0px;
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.videocontainer>span.videoMuted {
|
.videocontainer>span.videoMuted {
|
||||||
|
@ -226,7 +235,6 @@
|
||||||
#header{
|
#header{
|
||||||
display:none;
|
display:none;
|
||||||
position:absolute;
|
position:absolute;
|
||||||
height: 0px;
|
|
||||||
text-align:center;
|
text-align:center;
|
||||||
top:0;
|
top:0;
|
||||||
left:0;
|
left:0;
|
||||||
|
@ -241,13 +249,27 @@
|
||||||
margin-right:auto;
|
margin-right:auto;
|
||||||
height:39px;
|
height:39px;
|
||||||
width:auto;
|
width:auto;
|
||||||
overflow: hidden;
|
|
||||||
background: linear-gradient(to bottom, rgba(103,103,103,.65) , rgba(0,0,0,.65));
|
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;
|
-webkit-box-shadow: 0 0 2px #000000, 0 0 10px #000000;
|
||||||
border-bottom-left-radius: 12px;
|
border-bottom-left-radius: 12px;
|
||||||
border-bottom-right-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 {
|
.watermark {
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
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).
|
* Indicates that desktop stream is currently in use(for toggle purpose).
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
|
@ -251,7 +251,9 @@ function newStreamCreated(stream) {
|
||||||
|
|
||||||
var oldStream = connection.jingle.localVideo;
|
var oldStream = connection.jingle.localVideo;
|
||||||
|
|
||||||
change_local_video(stream, !isUsingScreenStream);
|
connection.jingle.localVideo = stream;
|
||||||
|
|
||||||
|
VideoLayout.changeLocalVideo(stream, !isUsingScreenStream);
|
||||||
|
|
||||||
var conferenceHandler = getConferenceHandler();
|
var conferenceHandler = getConferenceHandler();
|
||||||
if (conferenceHandler) {
|
if (conferenceHandler) {
|
||||||
|
|
|
@ -45,8 +45,8 @@ var Etherpad = (function (my) {
|
||||||
if (Prezi.isPresentationVisible()) {
|
if (Prezi.isPresentationVisible()) {
|
||||||
largeVideo.css({opacity: '0'});
|
largeVideo.css({opacity: '0'});
|
||||||
} else {
|
} else {
|
||||||
setLargeVideoVisible(false);
|
VideoLayout.setLargeVideoVisible(false);
|
||||||
dockToolbar(true);
|
Toolbar.dockToolbar(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
$('#etherpad>iframe').fadeIn(300, function () {
|
$('#etherpad>iframe').fadeIn(300, function () {
|
||||||
|
@ -63,8 +63,8 @@ var Etherpad = (function (my) {
|
||||||
document.body.style.background = 'black';
|
document.body.style.background = 'black';
|
||||||
if (!isPresentation) {
|
if (!isPresentation) {
|
||||||
$('#largeVideo').fadeIn(300, function () {
|
$('#largeVideo').fadeIn(300, function () {
|
||||||
setLargeVideoVisible(true);
|
VideoLayout.setLargeVideoVisible(true);
|
||||||
dockToolbar(false);
|
Toolbar.dockToolbar(false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
100
index.html
100
index.html
|
@ -20,26 +20,35 @@
|
||||||
<script src="libs/colibri/colibri.focus.js?v=8"></script><!-- colibri focus implementation -->
|
<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="libs/colibri/colibri.session.js?v=1"></script>
|
||||||
<script src="//code.jquery.com/ui/1.10.4/jquery-ui.js"></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="libs/tooltip.js?v=1"></script><!-- bootstrap tooltip lib -->
|
||||||
<script src="muc.js?v=10"></script><!-- simple MUC library -->
|
<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="estos_log.js?v=2"></script><!-- simple stanza logger -->
|
||||||
<script src="desktopsharing.js?v=1"></script><!-- desktop sharing -->
|
<script src="desktopsharing.js?v=2"></script><!-- desktop sharing -->
|
||||||
<script src="app.js?v=26"></script><!-- application logic -->
|
<script src="data_channels.js?v=2"></script><!-- data channels -->
|
||||||
<script src="chat.js?v=4"></script><!-- chat logic -->
|
<script src="app.js?v=29"></script><!-- application logic -->
|
||||||
<script src="util.js?v=3"></script><!-- utility functions -->
|
<script src="commands.js?v=1"></script><!-- application logic -->
|
||||||
<script src="etherpad.js?v=7"></script><!-- etherpad plugin -->
|
<script src="chat.js?v=6"></script><!-- chat logic -->
|
||||||
<script src="prezi.js?v=2"></script><!-- prezi plugin -->
|
<script src="util.js?v=5"></script><!-- utility functions -->
|
||||||
<script src="smileys.js?v=1"></script><!-- smiley images -->
|
<script src="etherpad.js?v=8"></script><!-- etherpad plugin -->
|
||||||
<script src="replacement.js?v=5"></script><!-- link and smiley replacement -->
|
<script src="prezi.js?v=4"></script><!-- prezi plugin -->
|
||||||
<script src="moderatemuc.js?v=1"></script><!-- moderator 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="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 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" 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/main.css?v=21"/>
|
||||||
<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/videolayout_default.css?v=7" id="videolayout_default"/>
|
||||||
<link rel="stylesheet" href="css/jquery-impromptu.css?v=4">
|
<link rel="stylesheet" href="css/jquery-impromptu.css?v=4">
|
||||||
<link rel="stylesheet" href="css/modaldialog.css?v=3">
|
<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,
|
Link used for inline installation of chrome desktop streaming extension,
|
||||||
is updated automatically from the code with the value defined in config.js -->
|
is updated automatically from the code with the value defined in config.js -->
|
||||||
|
@ -49,35 +58,40 @@
|
||||||
<script src="libs/prezi_player.js?v=2"></script>
|
<script src="libs/prezi_player.js?v=2"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="header">
|
<div style="position: relative;" id="header_container">
|
||||||
<span id="toolbar">
|
<div id="header">
|
||||||
<a class="button" onclick='toggleAudio();'>
|
<span id="toolbar">
|
||||||
<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();'>
|
||||||
<div class="header_button_separator"></div>
|
<i id="mute" class="icon-microphone"></i></a>
|
||||||
<a class="button" onclick='buttonClick("#video", "icon-camera icon-camera-disabled");toggleVideo();'>
|
|
||||||
<i id="video" title="Start / stop camera" 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>
|
|
||||||
<div class="header_button_separator"></div>
|
|
||||||
<a class="button" onclick="openLinkDialog();" title="Invite others"><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>
|
|
||||||
<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>
|
|
||||||
<span id="etherpadButton">
|
|
||||||
<div class="header_button_separator"></div>
|
<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="Start / stop camera" onclick='buttonClick("#video", "icon-camera icon-camera-disabled");toggleVideo();'>
|
||||||
</span>
|
<i id="video" class="icon-camera"></i></a>
|
||||||
<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>
|
|
||||||
<div class="header_button_separator"></div>
|
<div class="header_button_separator"></div>
|
||||||
|
<a class="button" data-toggle="popover" data-placement="bottom" data-content="Lock / unlock room" onclick="Toolbar.openLockDialog();">
|
||||||
|
<i id="lockIcon" class="icon-security"></i></a>
|
||||||
|
<div class="header_button_separator"></div>
|
||||||
|
<a class="button" data-toggle="popover" data-placement="bottom" data-content="Invite others" onclick="Toolbar.openLinkDialog();"><i class="icon-link"></i></a>
|
||||||
|
<div class="header_button_separator"></div>
|
||||||
|
<span class="toolbar_span">
|
||||||
|
<a class="button" data-toggle="popover" data-placement="bottom" data-content="Open / close chat" onclick='Chat.toggleChat();'><i id="chatButton" class="icon-chat"></i></a>
|
||||||
|
<span id="unreadMessages"></span>
|
||||||
|
</span>
|
||||||
|
<div class="header_button_separator"></div>
|
||||||
|
<a class="button" data-toggle="popover" data-placement="bottom" data-content="Share Prezi" onclick='Prezi.openPreziDialog();'><i class="icon-prezi"></i></a>
|
||||||
|
<span id="etherpadButton">
|
||||||
|
<div class="header_button_separator"></div>
|
||||||
|
<a class="button" data-toggle="popover" data-placement="bottom" data-content="Shared document" onclick='Etherpad.toggleEtherpad(0);'><i class="icon-share-doc"></i></a>
|
||||||
|
</span>
|
||||||
|
<div class="header_button_separator"></div>
|
||||||
|
<span id="desktopsharing" style="display: none">
|
||||||
|
<a class="button" data-toggle="popover" data-placement="bottom" data-content="Share screen" onclick="toggleScreenSharing();"><i class="icon-share-desktop"></i></a>
|
||||||
|
<div class="header_button_separator"></div>
|
||||||
|
</span>
|
||||||
|
<a class="button" data-toggle="popover" data-placement="bottom" data-content="Enter / Exit Full Screen" onclick='buttonClick("#fullScreen", "icon-full-screen icon-exit-full-screen");Toolbar.toggleFullScreen();'>
|
||||||
|
<i id="fullScreen" class="icon-full-screen"></i></a>
|
||||||
</span>
|
</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>
|
</div>
|
||||||
</span>
|
<div id="subject"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="settings">
|
<div id="settings">
|
||||||
<h1>Connection Settings</h1>
|
<h1>Connection Settings</h1>
|
||||||
|
@ -89,7 +103,7 @@
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div id="reloadPresentation"><a onclick='Prezi.reloadPresentation();'><i title="Reload Prezi" class="fa fa-repeat fa-lg"></i></a></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="largeVideoContainer" class="videocontainer">
|
||||||
<div id="presentation"></div>
|
<div id="presentation"></div>
|
||||||
<div id="etherpad"></div>
|
<div id="etherpad"></div>
|
||||||
|
@ -104,7 +118,7 @@
|
||||||
<!--<video id="localVideo" autoplay oncontextmenu="return false;" muted></video> - is now per stream generated -->
|
<!--<video id="localVideo" autoplay oncontextmenu="return false;" muted></video> - is now per stream generated -->
|
||||||
</span>
|
</span>
|
||||||
<audio id="localAudio" autoplay oncontextmenu="return false;" muted></audio>
|
<audio id="localAudio" autoplay oncontextmenu="return false;" muted></audio>
|
||||||
<span class="focusindicator"></span>
|
<span class="focusindicator" data-content="The owner of this conference" data-toggle="popover" data-placement="top"></span>
|
||||||
</span>
|
</span>
|
||||||
<audio id="userJoined" src="sounds/joined.wav" preload="auto"></audio>
|
<audio id="userJoined" src="sounds/joined.wav" preload="auto"></audio>
|
||||||
<audio id="userLeft" src="sounds/left.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>
|
<audio id="chatNotification" src="sounds/incomingMessage.wav" preload="auto"></audio>
|
||||||
<textarea id="usermsg" placeholder='Enter text...' autofocus></textarea>
|
<textarea id="usermsg" placeholder='Enter text...' autofocus></textarea>
|
||||||
</div>
|
</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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -44,8 +44,27 @@ function ColibriFocus(connection, bridgejid) {
|
||||||
this.peers = [];
|
this.peers = [];
|
||||||
this.confid = null;
|
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
|
// media types of the conference
|
||||||
this.media = ['audio', 'video'];
|
if (config.openSctp)
|
||||||
|
{
|
||||||
|
this.media = ['audio', 'video', 'data'];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.media = ['audio', 'video'];
|
||||||
|
}
|
||||||
|
|
||||||
this.connection.jingle.sessions[this.sid] = this;
|
this.connection.jingle.sessions[this.sid] = this;
|
||||||
this.mychannel = [];
|
this.mychannel = [];
|
||||||
|
@ -151,17 +170,29 @@ ColibriFocus.prototype._makeConference = function () {
|
||||||
elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri'});
|
elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri'});
|
||||||
|
|
||||||
this.media.forEach(function (name) {
|
this.media.forEach(function (name) {
|
||||||
|
var isData = name === 'data';
|
||||||
|
var channel = isData ? 'sctpconnection' : 'channel';
|
||||||
|
|
||||||
elem.c('content', {name: name});
|
elem.c('content', {name: name});
|
||||||
elem.c('channel', {
|
|
||||||
|
elem.c(channel, {
|
||||||
initiator: 'true',
|
initiator: 'true',
|
||||||
expire: '15',
|
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++) {
|
for (var j = 0; j < self.peers.length; j++) {
|
||||||
elem.c('channel', {
|
elem.c(channel, {
|
||||||
initiator: 'true',
|
initiator: 'true',
|
||||||
expire: '15',
|
expire: '15',
|
||||||
endpoint: self.peers[j].substr(1 + self.peers[j].lastIndexOf('/'))
|
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
|
elem.up(); // end of content
|
||||||
});
|
});
|
||||||
|
@ -209,8 +240,13 @@ ColibriFocus.prototype.createdConference = function (result) {
|
||||||
this.confid = $(result).find('>conference').attr('id');
|
this.confid = $(result).find('>conference').attr('id');
|
||||||
var remotecontents = $(result).find('>conference>content').get();
|
var remotecontents = $(result).find('>conference>content').get();
|
||||||
var numparticipants = 0;
|
var numparticipants = 0;
|
||||||
for (var i = 0; i < remotecontents.length; i++) {
|
for (var i = 0; i < remotecontents.length; i++)
|
||||||
tmp = $(remotecontents[i]).find('>channel').get();
|
{
|
||||||
|
var contentName = $(remotecontents[i]).attr('name');
|
||||||
|
var channelName
|
||||||
|
= contentName !== 'data' ? '>channel' : '>sctpconnection';
|
||||||
|
|
||||||
|
tmp = $(remotecontents[i]).find(channelName).get();
|
||||||
this.mychannel.push($(tmp.shift()));
|
this.mychannel.push($(tmp.shift()));
|
||||||
numparticipants = tmp.length;
|
numparticipants = tmp.length;
|
||||||
for (j = 0; j < tmp.length; j++) {
|
for (j = 0; j < tmp.length; j++) {
|
||||||
|
@ -223,7 +259,55 @@ ColibriFocus.prototype.createdConference = function (result) {
|
||||||
|
|
||||||
console.log('remote channels', this.channels);
|
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;
|
bridgeSDP.media.length = this.mychannel.length;
|
||||||
var channel;
|
var channel;
|
||||||
/*
|
/*
|
||||||
|
@ -262,12 +346,17 @@ ColibriFocus.prototype.createdConference = function (result) {
|
||||||
// get the mixed ssrc
|
// get the mixed ssrc
|
||||||
tmp = $(this.mychannel[channel]).find('>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
|
tmp = $(this.mychannel[channel]).find('>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
|
||||||
// FIXME: check rtp-level-relay-type
|
// 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') + ' ' + 'cname:mixed' + '\r\n';
|
||||||
bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'label:mixedlabela0' + '\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') + ' ' + 'msid:mixedmslabel mixedlabela0' + '\r\n';
|
||||||
bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'mslabel:mixedmslabel' + '\r\n';
|
bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'mslabel:mixedmslabel' + '\r\n';
|
||||||
} else {
|
}
|
||||||
|
else if (!isData)
|
||||||
|
{
|
||||||
// make chrome happy... '3735928559' == 0xDEADBEEF
|
// make chrome happy... '3735928559' == 0xDEADBEEF
|
||||||
// FIXME: this currently appears as two streams, should be one
|
// FIXME: this currently appears as two streams, should be one
|
||||||
bridgeSDP.media[channel] += 'a=ssrc:' + '3735928559' + ' ' + 'cname:mixed' + '\r\n';
|
bridgeSDP.media[channel] += 'a=ssrc:' + '3735928559' + ' ' + 'cname:mixed' + '\r\n';
|
||||||
|
@ -308,21 +397,41 @@ ColibriFocus.prototype.createdConference = function (result) {
|
||||||
elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: self.confid});
|
elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: self.confid});
|
||||||
var localSDP = new SDP(self.peerconnection.localDescription.sdp);
|
var localSDP = new SDP(self.peerconnection.localDescription.sdp);
|
||||||
localSDP.media.forEach(function (media, channel) {
|
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});
|
elem.c('content', {name: name});
|
||||||
elem.c('channel', {
|
|
||||||
initiator: 'true',
|
|
||||||
expire: '15',
|
|
||||||
id: self.mychannel[channel].attr('id'),
|
|
||||||
endpoint: 'fix_me_focus_endpoint'
|
|
||||||
});
|
|
||||||
|
|
||||||
// FIXME: should reuse code from .toJingle
|
|
||||||
var mline = SDPUtil.parse_mline(media.split('\r\n')[0]);
|
var mline = SDPUtil.parse_mline(media.split('\r\n')[0]);
|
||||||
for (var j = 0; j < mline.fmt.length; j++) {
|
if (name !== 'data')
|
||||||
var rtpmap = SDPUtil.find_line(media, 'a=rtpmap:' + mline.fmt[j]);
|
{
|
||||||
elem.c('payload-type', SDPUtil.parse_rtpmap(rtpmap));
|
elem.c('channel', {
|
||||||
elem.up();
|
initiator: 'true',
|
||||||
|
expire: self.channelExpire,
|
||||||
|
id: self.mychannel[channel].attr('id'),
|
||||||
|
endpoint: self.myMucResource
|
||||||
|
});
|
||||||
|
|
||||||
|
// FIXME: should reuse code from .toJingle
|
||||||
|
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);
|
localSDP.TransportToJingle(channel, elem);
|
||||||
|
@ -336,7 +445,9 @@ ColibriFocus.prototype.createdConference = function (result) {
|
||||||
// ...
|
// ...
|
||||||
},
|
},
|
||||||
function (error) {
|
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++) {
|
for (var i = 0; i < numparticipants; i++) {
|
||||||
self.initiate(self.peers[i], true);
|
self.initiate(self.peers[i], true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Notify we've created the conference
|
||||||
|
$(document).trigger(
|
||||||
|
'conferenceCreated.jingle', self);
|
||||||
},
|
},
|
||||||
function (error) {
|
function (error) {
|
||||||
console.warn('setLocalDescription failed.', 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') + ' ' + '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') + ' ' + 'msid:mixedmslabel mixedlabela0' + '\r\n';
|
||||||
sdp.media[j] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'mslabel:mixedmslabel' + '\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
|
// make chrome happy... '3735928559' == 0xDEADBEEF
|
||||||
sdp.media[j] += 'a=ssrc:' + '3735928559' + ' ' + 'cname:mixed' + '\r\n';
|
sdp.media[j] += 'a=ssrc:' + '3735928559' + ' ' + 'cname:mixed' + '\r\n';
|
||||||
sdp.media[j] += 'a=ssrc:' + '3735928559' + ' ' + 'label:mixedlabelv0' + '\r\n';
|
sdp.media[j] += 'a=ssrc:' + '3735928559' + ' ' + 'label:mixedlabelv0' + '\r\n';
|
||||||
|
@ -486,9 +604,17 @@ ColibriFocus.prototype.initiate = function (peer, isInitiator) {
|
||||||
// pull in a new participant into the conference
|
// pull in a new participant into the conference
|
||||||
ColibriFocus.prototype.addNewParticipant = function (peer) {
|
ColibriFocus.prototype.addNewParticipant = function (peer) {
|
||||||
var self = this;
|
var self = this;
|
||||||
if (this.confid === 0) {
|
if (this.confid === 0 || !this.peerconnection.localDescription)
|
||||||
|
{
|
||||||
// bad state
|
// 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 () {
|
window.setTimeout(function () {
|
||||||
self.addNewParticipant(peer);
|
self.addNewParticipant(peer);
|
||||||
}, 250);
|
}, 250);
|
||||||
|
@ -502,14 +628,26 @@ ColibriFocus.prototype.addNewParticipant = function (peer) {
|
||||||
elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
|
elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
|
||||||
var localSDP = new SDP(this.peerconnection.localDescription.sdp);
|
var localSDP = new SDP(this.peerconnection.localDescription.sdp);
|
||||||
localSDP.media.forEach(function (media, channel) {
|
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});
|
elem.c('content', {name: name});
|
||||||
elem.c('channel', {
|
if (name !== 'data')
|
||||||
|
{
|
||||||
|
elem.c('channel', {
|
||||||
initiator: 'true',
|
initiator: 'true',
|
||||||
expire:'15',
|
expire: self.channelExpire,
|
||||||
endpoint: peer.substr(1 + peer.lastIndexOf('/'))
|
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
|
elem.up(); // end of content
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -517,7 +655,15 @@ ColibriFocus.prototype.addNewParticipant = function (peer) {
|
||||||
function (result) {
|
function (result) {
|
||||||
var contents = $(result).find('>conference>content').get();
|
var contents = $(result).find('>conference>content').get();
|
||||||
for (var i = 0; i < contents.length; i++) {
|
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.channels[index][i] = tmp[0];
|
||||||
}
|
}
|
||||||
self.initiate(peer, true);
|
self.initiate(peer, true);
|
||||||
|
@ -531,37 +677,52 @@ ColibriFocus.prototype.addNewParticipant = function (peer) {
|
||||||
// update the channel description (payload-types + dtls fp) for a participant
|
// update the channel description (payload-types + dtls fp) for a participant
|
||||||
ColibriFocus.prototype.updateChannel = function (remoteSDP, participant) {
|
ColibriFocus.prototype.updateChannel = function (remoteSDP, participant) {
|
||||||
console.log('change allocation for', this.confid);
|
console.log('change allocation for', this.confid);
|
||||||
|
var self = this;
|
||||||
var change = $iq({to: this.bridgejid, type: 'set'});
|
var change = $iq({to: this.bridgejid, type: 'set'});
|
||||||
change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
|
change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
|
||||||
for (channel = 0; channel < this.channels[participant].length; channel++) {
|
for (channel = 0; channel < this.channels[participant].length; channel++)
|
||||||
change.c('content', {name: channel === 0 ? 'audio' : 'video'});
|
{
|
||||||
change.c('channel', {
|
var name = SDPUtil.parse_mid(SDPUtil.find_line(remoteSDP.media[channel], 'a=mid:'));
|
||||||
id: $(this.channels[participant][channel]).attr('id'),
|
change.c('content', {name: name});
|
||||||
endpoint: $(this.channels[participant][channel]).attr('endpoint'),
|
if (name !== 'data')
|
||||||
expire: '15'
|
{
|
||||||
});
|
change.c('channel', {
|
||||||
|
id: $(this.channels[participant][channel]).attr('id'),
|
||||||
|
endpoint: $(this.channels[participant][channel]).attr('endpoint'),
|
||||||
|
expire: self.channelExpire
|
||||||
|
});
|
||||||
|
|
||||||
var rtpmap = SDPUtil.find_lines(remoteSDP.media[channel], 'a=rtpmap:');
|
var rtpmap = SDPUtil.find_lines(remoteSDP.media[channel], 'a=rtpmap:');
|
||||||
rtpmap.forEach(function (val) {
|
rtpmap.forEach(function (val) {
|
||||||
// TODO: too much copy-paste
|
// TODO: too much copy-paste
|
||||||
var rtpmap = SDPUtil.parse_rtpmap(val);
|
var rtpmap = SDPUtil.parse_rtpmap(val);
|
||||||
change.c('payload-type', rtpmap);
|
change.c('payload-type', rtpmap);
|
||||||
//
|
//
|
||||||
// put any 'a=fmtp:' + mline.fmt[j] lines into <param name=foo value=bar/>
|
// put any 'a=fmtp:' + mline.fmt[j] lines into <param name=foo value=bar/>
|
||||||
/*
|
/*
|
||||||
if (SDPUtil.find_line(remoteSDP.media[channel], 'a=fmtp:' + rtpmap.id)) {
|
if (SDPUtil.find_line(remoteSDP.media[channel], 'a=fmtp:' + rtpmap.id)) {
|
||||||
tmp = SDPUtil.parse_fmtp(SDPUtil.find_line(remoteSDP.media[channel], 'a=fmtp:' + rtpmap.id));
|
tmp = SDPUtil.parse_fmtp(SDPUtil.find_line(remoteSDP.media[channel], 'a=fmtp:' + rtpmap.id));
|
||||||
for (var k = 0; k < tmp.length; k++) {
|
for (var k = 0; k < tmp.length; k++) {
|
||||||
change.c('parameter', tmp[k]).up();
|
change.c('parameter', tmp[k]).up();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
*/
|
||||||
*/
|
change.up();
|
||||||
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
|
// now add transport
|
||||||
remoteSDP.TransportToJingle(channel, change);
|
remoteSDP.TransportToJingle(channel, change);
|
||||||
|
|
||||||
change.up(); // end of channel
|
change.up(); // end of channel/sctpconnection
|
||||||
change.up(); // end of content
|
change.up(); // end of content
|
||||||
}
|
}
|
||||||
this.connection.sendIQ(change,
|
this.connection.sendIQ(change,
|
||||||
|
@ -605,6 +766,19 @@ ColibriFocus.prototype.sendSSRCUpdate = function (sdpMediaSsrcs, fromJid, isadd)
|
||||||
ColibriFocus.prototype.addSource = function (elem, fromJid) {
|
ColibriFocus.prototype.addSource = function (elem, fromJid) {
|
||||||
|
|
||||||
var self = this;
|
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.peerconnection.addSource(elem);
|
||||||
|
|
||||||
var peerSsrc = this.remotessrc[fromJid];
|
var peerSsrc = this.remotessrc[fromJid];
|
||||||
|
@ -638,6 +812,19 @@ ColibriFocus.prototype.addSource = function (elem, fromJid) {
|
||||||
ColibriFocus.prototype.removeSource = function (elem, fromJid) {
|
ColibriFocus.prototype.removeSource = function (elem, fromJid) {
|
||||||
|
|
||||||
var self = this;
|
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);
|
this.peerconnection.removeSource(elem);
|
||||||
|
|
||||||
var peerSsrc = this.remotessrc[fromJid];
|
var peerSsrc = this.remotessrc[fromJid];
|
||||||
|
@ -675,8 +862,11 @@ ColibriFocus.prototype.setRemoteDescription = function (session, elem, desctype)
|
||||||
this.remotessrc[session.peerjid] = [];
|
this.remotessrc[session.peerjid] = [];
|
||||||
for (channel = 0; channel < this.channels[participant].length; channel++) {
|
for (channel = 0; channel < this.channels[participant].length; channel++) {
|
||||||
//if (channel == 0) continue; FIXME: does not work as intended
|
//if (channel == 0) continue; FIXME: does not work as intended
|
||||||
if (SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:').length) {
|
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';
|
{
|
||||||
|
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});
|
change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
|
||||||
$(elem).each(function () {
|
$(elem).each(function () {
|
||||||
var name = $(this).attr('name');
|
var name = $(this).attr('name');
|
||||||
|
|
||||||
var channel = name == 'audio' ? 0 : 1; // FIXME: search mlineindex in localdesc
|
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});
|
change.c('content', {name: name});
|
||||||
change.c('channel', {
|
if (name !== 'data')
|
||||||
id: $(self.channels[participant][channel]).attr('id'),
|
{
|
||||||
endpoint: $(self.channels[participant][channel]).attr('endpoint'),
|
change.c('channel', {
|
||||||
expire: '15'
|
id: $(self.channels[participant][channel]).attr('id'),
|
||||||
});
|
endpoint: $(self.channels[participant][channel]).attr('endpoint'),
|
||||||
|
expire: self.channelExpire
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
change.c('sctpconnection', {
|
||||||
|
endpoint: $(self.channels[participant][channel]).attr('endpoint'),
|
||||||
|
expire: self.channelExpire
|
||||||
|
});
|
||||||
|
}
|
||||||
$(this).find('>transport').each(function () {
|
$(this).find('>transport').each(function () {
|
||||||
change.c('transport', {
|
change.c('transport', {
|
||||||
ufrag: $(this).attr('ufrag'),
|
ufrag: $(this).attr('ufrag'),
|
||||||
|
@ -729,7 +932,7 @@ ColibriFocus.prototype.addIceCandidate = function (session, elem) {
|
||||||
});
|
});
|
||||||
change.up(); // end of transport
|
change.up(); // end of transport
|
||||||
});
|
});
|
||||||
change.up(); // end of channel
|
change.up(); // end of channel/sctpconnection
|
||||||
change.up(); // end of content
|
change.up(); // end of content
|
||||||
});
|
});
|
||||||
// FIXME: need to check if there is at least one candidate when filtering TCP ones
|
// 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});
|
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
|
// FIXME: multi-candidate logic is taken from strophe.jingle, should be refactored there
|
||||||
var localSDP = new SDP(this.peerconnection.localDescription.sdp);
|
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; });
|
var cands = candidates.filter(function (el) { return el.sdpMLineIndex == mid; });
|
||||||
if (cands.length > 0) {
|
if (cands.length > 0)
|
||||||
mycands.c('content', {name: cands[0].sdpMid });
|
{
|
||||||
mycands.c('channel', {
|
var name = cands[0].sdpMid;
|
||||||
id: $(this.mychannel[cands[0].sdpMLineIndex]).attr('id'),
|
mycands.c('content', {name: name });
|
||||||
endpoint: $(this.mychannel[cands[0].sdpMLineIndex]).attr('endpoint'),
|
if (name !== 'data')
|
||||||
expire: '15'
|
{
|
||||||
});
|
mycands.c('channel', {
|
||||||
|
id: $(this.mychannel[cands[0].sdpMLineIndex]).attr('id'),
|
||||||
|
endpoint: $(this.mychannel[cands[0].sdpMLineIndex]).attr('endpoint'),
|
||||||
|
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'});
|
mycands.c('transport', {xmlns: 'urn:xmpp:jingle:transports:ice-udp:1'});
|
||||||
for (var i = 0; i < cands.length; i++) {
|
for (var i = 0; i < cands.length; i++) {
|
||||||
mycands.c('candidate', SDPUtil.candidateToJingle(cands[i].candidate)).up();
|
mycands.c('candidate', SDPUtil.candidateToJingle(cands[i].candidate)).up();
|
||||||
}
|
}
|
||||||
mycands.up(); // transport
|
mycands.up(); // transport
|
||||||
mycands.up(); // channel
|
mycands.up(); // channel / sctpconnection
|
||||||
mycands.up(); // content
|
mycands.up(); // content
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -814,13 +1031,26 @@ ColibriFocus.prototype.terminate = function (session, reason) {
|
||||||
var change = $iq({to: this.bridgejid, type: 'set'});
|
var change = $iq({to: this.bridgejid, type: 'set'});
|
||||||
change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
|
change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
|
||||||
for (var channel = 0; channel < this.channels[participant].length; channel++) {
|
for (var channel = 0; channel < this.channels[participant].length; channel++) {
|
||||||
change.c('content', {name: channel === 0 ? 'audio' : 'video'});
|
var name = channel === 0 ? 'audio' : 'video';
|
||||||
change.c('channel', {
|
if (channel == 2)
|
||||||
id: $(this.channels[participant][channel]).attr('id'),
|
name = 'data';
|
||||||
endpoint: $(this.channels[participant][channel]).attr('endpoint'),
|
change.c('content', {name: name});
|
||||||
expire: '0'
|
if (name !== 'data')
|
||||||
});
|
{
|
||||||
change.up(); // end of channel
|
change.c('channel', {
|
||||||
|
id: $(this.channels[participant][channel]).attr('id'),
|
||||||
|
endpoint: $(this.channels[participant][channel]).attr('endpoint'),
|
||||||
|
expire: '0'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
change.c('sctpconnection', {
|
||||||
|
endpoint: $(this.channels[participant][channel]).attr('endpoint'),
|
||||||
|
expire: '0'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
change.up(); // end of channel/sctpconnection
|
||||||
change.up(); // end of content
|
change.up(); // end of content
|
||||||
}
|
}
|
||||||
this.connection.sendIQ(change,
|
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.updateLog = [];
|
||||||
this.stats = {};
|
this.stats = {};
|
||||||
this.statsinterval = null;
|
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.
|
* Array of ssrcs that will be added on next modifySources call.
|
||||||
|
@ -32,7 +32,7 @@ function TraceablePeerConnection(ice_config, constraints) {
|
||||||
this.switchstreams = false;
|
this.switchstreams = false;
|
||||||
|
|
||||||
// override as desired
|
// override as desired
|
||||||
this.trace = function(what, info) {
|
this.trace = function (what, info) {
|
||||||
//console.warn('WTRACE', what, info);
|
//console.warn('WTRACE', what, info);
|
||||||
self.updateLog.push({
|
self.updateLog.push({
|
||||||
time: new Date(),
|
time: new Date(),
|
||||||
|
@ -88,8 +88,8 @@ function TraceablePeerConnection(ice_config, constraints) {
|
||||||
if (self.ondatachannel !== null) {
|
if (self.ondatachannel !== null) {
|
||||||
self.ondatachannel(event);
|
self.ondatachannel(event);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
if (!navigator.mozGetUserMedia) {
|
if (!navigator.mozGetUserMedia && this.maxstats) {
|
||||||
this.statsinterval = window.setInterval(function() {
|
this.statsinterval = window.setInterval(function() {
|
||||||
self.peerconnection.getStats(function(stats) {
|
self.peerconnection.getStats(function(stats) {
|
||||||
var results = stats.result();
|
var results = stats.result();
|
||||||
|
@ -144,8 +144,8 @@ TraceablePeerConnection.prototype.removeStream = function (stream) {
|
||||||
|
|
||||||
TraceablePeerConnection.prototype.createDataChannel = function (label, opts) {
|
TraceablePeerConnection.prototype.createDataChannel = function (label, opts) {
|
||||||
this.trace('createDataChannel', 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) {
|
TraceablePeerConnection.prototype.setLocalDescription = function (description, successCallback, failureCallback) {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
|
@ -155,7 +155,10 @@ SDP.prototype.toJingle = function (elem, thecreator) {
|
||||||
}
|
}
|
||||||
for (i = 0; i < this.media.length; i++) {
|
for (i = 0; i < this.media.length; i++) {
|
||||||
mline = SDPUtil.parse_mline(this.media[i].split('\r\n')[0]);
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
if (SDPUtil.find_line(this.media[i], 'a=ssrc:')) {
|
if (SDPUtil.find_line(this.media[i], 'a=ssrc:')) {
|
||||||
|
@ -171,12 +174,14 @@ SDP.prototype.toJingle = function (elem, thecreator) {
|
||||||
elem.attrs({ name: mid });
|
elem.attrs({ name: mid });
|
||||||
|
|
||||||
// old BUNDLE plan, to be removed
|
// 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();
|
elem.c('bundle', {xmlns: 'http://estos.de/ns/bundle'}).up();
|
||||||
bundle.splice(bundle.indexOf(mid), 1);
|
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',
|
elem.c('description',
|
||||||
{xmlns: 'urn:xmpp:jingle:apps:rtp:1',
|
{xmlns: 'urn:xmpp:jingle:apps:rtp:1',
|
||||||
media: mline.media });
|
media: mline.media });
|
||||||
|
@ -304,6 +309,26 @@ SDP.prototype.TransportToJingle = function (mediaindex, elem) {
|
||||||
var self = this;
|
var self = this;
|
||||||
elem.c('transport');
|
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
|
// XEP-0320
|
||||||
var fingerprints = SDPUtil.find_lines(this.media[mediaindex], 'a=fingerprint:', this.session);
|
var fingerprints = SDPUtil.find_lines(this.media[mediaindex], 'a=fingerprint:', this.session);
|
||||||
fingerprints.forEach(function(line) {
|
fingerprints.forEach(function(line) {
|
||||||
|
@ -438,6 +463,8 @@ SDP.prototype.jingle2media = function (content) {
|
||||||
ssrc = desc.attr('ssrc'),
|
ssrc = desc.attr('ssrc'),
|
||||||
self = this,
|
self = this,
|
||||||
tmp;
|
tmp;
|
||||||
|
var sctp = content.find(
|
||||||
|
'>transport>sctpmap[xmlns="urn:xmpp:jingle:transports:dtls-sctp:1"]');
|
||||||
|
|
||||||
tmp = { media: desc.attr('media') };
|
tmp = { media: desc.attr('media') };
|
||||||
tmp.port = '1';
|
tmp.port = '1';
|
||||||
|
@ -446,14 +473,35 @@ SDP.prototype.jingle2media = function (content) {
|
||||||
tmp.port = '0';
|
tmp.port = '0';
|
||||||
}
|
}
|
||||||
if (content.find('>transport>fingerprint').length || desc.find('encryption').length) {
|
if (content.find('>transport>fingerprint').length || desc.find('encryption').length) {
|
||||||
tmp.proto = 'RTP/SAVPF';
|
if (sctp.length)
|
||||||
|
tmp.proto = 'DTLS/SCTP';
|
||||||
|
else
|
||||||
|
tmp.proto = 'RTP/SAVPF';
|
||||||
} else {
|
} else {
|
||||||
tmp.proto = 'RTP/AVPF';
|
tmp.proto = 'RTP/AVPF';
|
||||||
}
|
}
|
||||||
tmp.fmt = desc.find('payload-type').map(function () { return this.getAttribute('id'); }).get();
|
if (!sctp.length)
|
||||||
media += SDPUtil.build_mline(tmp) + '\r\n';
|
{
|
||||||
|
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';
|
media += 'c=IN IP4 0.0.0.0\r\n';
|
||||||
media += 'a=rtcp:1 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"]');
|
tmp = content.find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]');
|
||||||
if (tmp.length) {
|
if (tmp.length) {
|
||||||
if (tmp.attr('ufrag')) {
|
if (tmp.attr('ufrag')) {
|
||||||
|
|
|
@ -90,6 +90,20 @@ SDPUtil = {
|
||||||
data.channels = parts.length ? parts.shift() : '1';
|
data.channels = parts.length ? parts.shift() : '1';
|
||||||
return data;
|
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) {
|
build_rtpmap: function (el) {
|
||||||
var line = 'a=rtpmap:' + el.getAttribute('id') + ' ' + el.getAttribute('name') + '/' + el.getAttribute('clockrate');
|
var line = 'a=rtpmap:' + el.getAttribute('id') + ' ' + el.getAttribute('name') + '/' + el.getAttribute('clockrate');
|
||||||
if (el.getAttribute('channels') && el.getAttribute('channels') != '1') {
|
if (el.getAttribute('channels') && el.getAttribute('channels') != '1') {
|
||||||
|
@ -269,7 +283,9 @@ SDPUtil = {
|
||||||
candidateToJingle: function (line) {
|
candidateToJingle: function (line) {
|
||||||
// a=candidate:2979166662 1 udp 2113937151 192.168.2.100 57698 typ host generation 0
|
// 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=.../>
|
// <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('parseCandidate called with a line that is not a candidate line');
|
||||||
console.log(line);
|
console.log(line);
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -420,6 +420,7 @@ JingleSession.prototype.setRemoteDescription = function (elem, desctype) {
|
||||||
},
|
},
|
||||||
function (e) {
|
function (e) {
|
||||||
console.error('setRemoteDescription error', 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) {
|
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.peerconnection.addSource(elem);
|
||||||
|
|
||||||
this.modifySources();
|
this.modifySources();
|
||||||
|
@ -30,6 +44,20 @@ SessionBase.prototype.addSource = function (elem, fromJid) {
|
||||||
|
|
||||||
SessionBase.prototype.removeSource = 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.peerconnection.removeSource(elem);
|
||||||
|
|
||||||
this.modifySources();
|
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;
|
||||||
|
})();
|
31
muc.js
31
muc.js
|
@ -21,6 +21,9 @@ Strophe.addConnectionPlugin('emuc', {
|
||||||
},
|
},
|
||||||
doJoin: function (jid, password) {
|
doJoin: function (jid, password) {
|
||||||
this.myroomjid = jid;
|
this.myroomjid = jid;
|
||||||
|
|
||||||
|
console.info("Joined MUC as " + this.myroomjid);
|
||||||
|
|
||||||
this.initPresenceMap(this.myroomjid);
|
this.initPresenceMap(this.myroomjid);
|
||||||
|
|
||||||
if (!this.roomjid) {
|
if (!this.roomjid) {
|
||||||
|
@ -167,12 +170,36 @@ Strophe.addConnectionPlugin('emuc', {
|
||||||
}
|
}
|
||||||
this.connection.send(msg);
|
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) {
|
onMessage: function (msg) {
|
||||||
var txt = $(msg).find('>body').text();
|
|
||||||
// TODO: <subject/>
|
|
||||||
// FIXME: this is a hack. but jingle on muc makes nickchanges hard
|
// FIXME: this is a hack. but jingle on muc makes nickchanges hard
|
||||||
var from = msg.getAttribute('from');
|
var from = msg.getAttribute('from');
|
||||||
var nick = $(msg).find('>nick[xmlns="http://jabber.org/protocol/nick"]').text() || Strophe.getResourceFromJid(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) {
|
if (txt) {
|
||||||
console.log('chat', nick, txt);
|
console.log('chat', nick, txt);
|
||||||
|
|
||||||
|
|
12
prezi.js
12
prezi.js
|
@ -19,10 +19,10 @@ var Prezi = (function (my) {
|
||||||
$(document).trigger("video.selected", [true]);
|
$(document).trigger("video.selected", [true]);
|
||||||
|
|
||||||
$('#largeVideo').fadeOut(300, function () {
|
$('#largeVideo').fadeOut(300, function () {
|
||||||
setLargeVideoVisible(false);
|
VideoLayout.setLargeVideoVisible(false);
|
||||||
$('#presentation>iframe').fadeIn(300, function() {
|
$('#presentation>iframe').fadeIn(300, function() {
|
||||||
$('#presentation>iframe').css({opacity:'1'});
|
$('#presentation>iframe').css({opacity:'1'});
|
||||||
dockToolbar(true);
|
Toolbar.dockToolbar(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -32,8 +32,8 @@ var Prezi = (function (my) {
|
||||||
$('#presentation>iframe').css({opacity:'0'});
|
$('#presentation>iframe').css({opacity:'0'});
|
||||||
$('#reloadPresentation').css({display:'none'});
|
$('#reloadPresentation').css({display:'none'});
|
||||||
$('#largeVideo').fadeIn(300, function() {
|
$('#largeVideo').fadeIn(300, function() {
|
||||||
setLargeVideoVisible(true);
|
VideoLayout.setLargeVideoVisible(true);
|
||||||
dockToolbar(false);
|
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
|
// 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
|
// 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.).
|
// don't want to show a mute/kick menu for this one, etc.).
|
||||||
addRemoteVideoContainer(null, elementId);
|
VideoLayout.addRemoteVideoContainer(null, elementId);
|
||||||
resizeThumbnails();
|
VideoLayout.resizeThumbnails();
|
||||||
|
|
||||||
var controlsEnabled = false;
|
var controlsEnabled = false;
|
||||||
if (jid === connection.emuc.myroomjid)
|
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 || {}));
|
27
util.js
27
util.js
|
@ -51,10 +51,35 @@ var Util = (function (my) {
|
||||||
* Returns the available video width.
|
* Returns the available video width.
|
||||||
*/
|
*/
|
||||||
my.getAvailableVideoWidth = function () {
|
my.getAvailableVideoWidth = function () {
|
||||||
var chatspaceWidth = $('#chatspace').is(":visible") ? $('#chatspace').width() : 0;
|
var chatspaceWidth
|
||||||
|
= $('#chatspace').is(":visible") ? $('#chatspace').width() : 0;
|
||||||
|
|
||||||
return window.innerWidth - chatspaceWidth;
|
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;
|
return my;
|
||||||
}(Util || {}));
|
}(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…
Reference in New Issue