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/)
|
||||
|
||||
## Discuss
|
||||
Please use the [Jitsi dev mailing list](http://lists.jitsi.org/pipermail/dev/) to discuss feature requests before opening an issue on github.
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
Jitsi Meet started out as a sample conferencing application using Jitsi Videobridge. It was originally developed by Philipp Hancke who then contributed it to the community where development continues with joint forces!
|
||||
|
|
54
chat.js
54
chat.js
|
@ -39,10 +39,19 @@ var Chat = (function (my) {
|
|||
$('#usermsg').keydown(function (event) {
|
||||
if (event.keyCode === 13) {
|
||||
event.preventDefault();
|
||||
var message = Util.escapeHtml(this.value);
|
||||
var value = this.value;
|
||||
$('#usermsg').val('').trigger('autosize.resize');
|
||||
this.focus();
|
||||
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);
|
||||
};
|
||||
|
||||
/**
|
||||
* Appends error message to the conversation
|
||||
* @param errorMessage the received error message.
|
||||
* @param originalText the original message.
|
||||
*/
|
||||
my.chatAddError = function(errorMessage, originalText)
|
||||
{
|
||||
errorMessage = Util.escapeHtml(errorMessage);
|
||||
originalText = Util.escapeHtml(originalText);
|
||||
|
||||
$('#chatconversation').append('<div class="errorMessage"><b>Error: </b>'
|
||||
+ 'Your message' + (originalText? (' \"'+ originalText + '\"') : "")
|
||||
+ ' was not sent.' + (errorMessage? (' Reason: ' + errorMessage) : '')
|
||||
+ '</div>');
|
||||
$('#chatconversation').animate(
|
||||
{ scrollTop: $('#chatconversation')[0].scrollHeight}, 1000);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the subject to the UI
|
||||
* @param subject the subject
|
||||
*/
|
||||
my.chatSetSubject = function(subject)
|
||||
{
|
||||
if(subject)
|
||||
subject = subject.trim();
|
||||
$('#subject').html(linkify(Util.escapeHtml(subject)));
|
||||
if(subject == "")
|
||||
{
|
||||
$("#subject").css({display: "none"});
|
||||
}
|
||||
else
|
||||
{
|
||||
$("#subject").css({display: "block"});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Opens / closes the chat area.
|
||||
*/
|
||||
|
@ -242,7 +290,7 @@ var Chat = (function (my) {
|
|||
if (unreadMessages) {
|
||||
unreadMsgElement.innerHTML = unreadMessages.toString();
|
||||
|
||||
showToolbar();
|
||||
Toolbar.showToolbar();
|
||||
|
||||
var chatButtonElement
|
||||
= document.getElementById('chatButton').parentNode;
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
/**
|
||||
* Handles commands received via chat messages.
|
||||
*/
|
||||
var CommandsProcessor = (function()
|
||||
{
|
||||
/**
|
||||
* Constructs new CommandProccessor instance from a message.
|
||||
* @param message the message
|
||||
* @constructor
|
||||
*/
|
||||
function CommandsPrototype(message)
|
||||
{
|
||||
/**
|
||||
* Extracts the command from the message.
|
||||
* @param message the received message
|
||||
* @returns {string} the command
|
||||
*/
|
||||
function getCommand(message)
|
||||
{
|
||||
if(message)
|
||||
{
|
||||
for(var command in commands)
|
||||
{
|
||||
if(message.indexOf("/" + command) == 0)
|
||||
return command;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
var command = getCommand(message);
|
||||
|
||||
/**
|
||||
* Returns the name of the command.
|
||||
* @returns {String} the command
|
||||
*/
|
||||
this.getCommand = function()
|
||||
{
|
||||
return command;
|
||||
}
|
||||
|
||||
|
||||
var messageArgument = message.substr(command.length + 2);
|
||||
|
||||
/**
|
||||
* Returns the arguments of the command.
|
||||
* @returns {string}
|
||||
*/
|
||||
this.getArgument = function()
|
||||
{
|
||||
return messageArgument;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether this instance is valid command or not.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
CommandsPrototype.prototype.isCommand = function()
|
||||
{
|
||||
if(this.getCommand())
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the command.
|
||||
*/
|
||||
CommandsPrototype.prototype.processCommand = function()
|
||||
{
|
||||
if(!this.isCommand())
|
||||
return;
|
||||
|
||||
commands[this.getCommand()](this.getArgument());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the data for topic command.
|
||||
* @param commandArguments the arguments of the topic command.
|
||||
*/
|
||||
var processTopic = function(commandArguments)
|
||||
{
|
||||
var topic = Util.escapeHtml(commandArguments);
|
||||
connection.emuc.setSubject(topic);
|
||||
}
|
||||
|
||||
/**
|
||||
* List with supported commands. The keys are the names of the commands and
|
||||
* the value is the function that processes the message.
|
||||
* @type {{String: function}}
|
||||
*/
|
||||
var commands = {
|
||||
"topic" : processTopic
|
||||
};
|
||||
|
||||
return CommandsPrototype;
|
||||
})();
|
|
@ -11,5 +11,7 @@ var config = {
|
|||
bosh: '//lambada.jitsi.net/http-bind', // FIXME: use xep-0156 for that
|
||||
desktopSharing: 'ext', // Desktop sharing method. Can be set to 'ext', 'webrtc' or false to disable.
|
||||
chromeExtensionId: 'diibjkoicjeejcmhdnailmkgecihlobk', // Id of desktop streamer Chrome extension
|
||||
minChromeExtVersion: '0.1' // Required version of Chrome extension
|
||||
minChromeExtVersion: '0.1', // Required version of Chrome extension
|
||||
enableRtpStats: false, // Enables RTP stats processing
|
||||
openSctp: true //Toggle to enable/disable SCTP channels
|
||||
};
|
|
@ -43,6 +43,10 @@ html, body{
|
|||
color: #087dba;
|
||||
}
|
||||
|
||||
.errorMessage {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.remoteuser {
|
||||
color: #424242;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,124 @@
|
|||
.popover {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1010;
|
||||
display: none;
|
||||
max-width: 300px;
|
||||
min-width: 100px;
|
||||
padding: 1px;
|
||||
text-align: left;
|
||||
color: #428bca;
|
||||
background-color: #ffffff;
|
||||
background-clip: padding-box;
|
||||
border: 1px solid #cccccc;
|
||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||
border-radius: 6px;
|
||||
-webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
|
||||
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.4);
|
||||
white-space: normal;
|
||||
}
|
||||
.popover.top {
|
||||
margin-top: -10px;
|
||||
}
|
||||
.popover.right {
|
||||
margin-left: 10px;
|
||||
}
|
||||
.popover.bottom {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.popover.left {
|
||||
margin-left: -10px;
|
||||
}
|
||||
.popover-title {
|
||||
margin: 0;
|
||||
padding: 8px 14px;
|
||||
font-size: 11pt;
|
||||
font-weight: normal;
|
||||
line-height: 18px;
|
||||
background-color: #f7f7f7;
|
||||
border-bottom: 1px solid #ebebeb;
|
||||
border-radius: 5px 5px 0 0;
|
||||
}
|
||||
.popover-content {
|
||||
padding: 9px 14px;
|
||||
font-size: 10pt;
|
||||
white-space:pre-wrap;
|
||||
text-align: center;
|
||||
}
|
||||
.popover > .arrow,
|
||||
.popover > .arrow:after {
|
||||
position: absolute;
|
||||
display: block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-color: transparent;
|
||||
border-style: solid;
|
||||
}
|
||||
.popover > .arrow {
|
||||
border-width: 11px;
|
||||
}
|
||||
.popover > .arrow:after {
|
||||
border-width: 10px;
|
||||
content: "";
|
||||
}
|
||||
.popover.top > .arrow {
|
||||
left: 50%;
|
||||
margin-left: -11px;
|
||||
border-bottom-width: 0;
|
||||
border-top-color: #999999;
|
||||
border-top-color: rgba(0, 0, 0, 0.25);
|
||||
bottom: -11px;
|
||||
}
|
||||
.popover.top > .arrow:after {
|
||||
content: " ";
|
||||
bottom: 1px;
|
||||
margin-left: -10px;
|
||||
border-bottom-width: 0;
|
||||
border-top-color: #ffffff;
|
||||
}
|
||||
.popover.right > .arrow {
|
||||
top: 50%;
|
||||
left: -11px;
|
||||
margin-top: -11px;
|
||||
border-left-width: 0;
|
||||
border-right-color: #999999;
|
||||
border-right-color: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
.popover.right > .arrow:after {
|
||||
content: " ";
|
||||
left: 1px;
|
||||
bottom: -10px;
|
||||
border-left-width: 0;
|
||||
border-right-color: #ffffff;
|
||||
}
|
||||
.popover.bottom > .arrow {
|
||||
left: 50%;
|
||||
margin-left: -11px;
|
||||
border-top-width: 0;
|
||||
border-bottom-color: #999999;
|
||||
border-bottom-color: rgba(0, 0, 0, 0.25);
|
||||
top: -11px;
|
||||
}
|
||||
.popover.bottom > .arrow:after {
|
||||
content: " ";
|
||||
top: 1px;
|
||||
margin-left: -10px;
|
||||
border-top-width: 0;
|
||||
border-bottom-color: #ffffff;
|
||||
}
|
||||
.popover.left > .arrow {
|
||||
top: 50%;
|
||||
right: -11px;
|
||||
margin-top: -11px;
|
||||
border-right-width: 0;
|
||||
border-left-color: #999999;
|
||||
border-left-color: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
.popover.left > .arrow:after {
|
||||
content: " ";
|
||||
right: 1px;
|
||||
border-right-width: 0;
|
||||
border-left-color: #ffffff;
|
||||
bottom: -10px;
|
||||
}
|
|
@ -9,7 +9,7 @@ ul.popupmenu {
|
|||
padding-bottom: 5px;
|
||||
padding-top: 5px;
|
||||
right: 10px;
|
||||
left: 0px;
|
||||
left: -5px;
|
||||
width: 100px;
|
||||
background-color: rgba(0,0,0,1);
|
||||
-webkit-box-shadow: 0 0 2px #000000, 0 0 10px #000000;
|
||||
|
@ -21,7 +21,7 @@ ul.popupmenu:after {
|
|||
display: block;
|
||||
position: absolute;
|
||||
bottom: -9px;
|
||||
left: 13px;
|
||||
left: 11px;
|
||||
}
|
||||
|
||||
ul.popupmenu li {
|
||||
|
|
|
@ -32,9 +32,11 @@
|
|||
background-size: contain;
|
||||
border-radius:8px;
|
||||
border: 2px solid #212425;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
#remoteVideos .videocontainer:hover {
|
||||
#remoteVideos .videocontainer:hover,
|
||||
#remoteVideos .videocontainer.videoContainerFocused {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
content:"";
|
||||
|
@ -49,7 +51,6 @@
|
|||
-webkit-animation-iteration-count: 1;
|
||||
-webkit-box-shadow: 0 0 18px #388396;
|
||||
border: 2px solid #388396;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
#localVideoWrapper {
|
||||
|
@ -93,6 +94,11 @@
|
|||
height: 100%;
|
||||
}
|
||||
|
||||
.activespeaker {
|
||||
-webkit-filter: grayscale(1);
|
||||
filter: grayscale(1);
|
||||
}
|
||||
|
||||
#etherpad,
|
||||
#presentation {
|
||||
text-align: center;
|
||||
|
@ -119,6 +125,7 @@
|
|||
text-shadow: 0px 1px 0px rgba(255,255,255,.3), 0px -1px 0px rgba(0,0,0,.7);
|
||||
border: 0px;
|
||||
z-index: 2;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#remoteVideos .nick {
|
||||
|
@ -133,25 +140,22 @@
|
|||
|
||||
.videocontainer>span.displayname,
|
||||
.videocontainer>input.displayname {
|
||||
display: inline-block;
|
||||
display: none;
|
||||
position: absolute;
|
||||
background: -webkit-linear-gradient(left, rgba(0,0,0,.7), rgba(0,0,0,0));
|
||||
color: #FFFFFF;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding: 3px 5px;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-height: 18px;
|
||||
font-size: 9pt;
|
||||
text-align: left;
|
||||
background: rgba(0,0,0,.7);
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
width: 70%;
|
||||
height: 20%;
|
||||
left: 15%;
|
||||
top: 40%;
|
||||
padding: 5px;
|
||||
font-size: 11pt;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
z-index: 2;
|
||||
box-sizing: border-box;
|
||||
border-bottom-left-radius:4px;
|
||||
border-bottom-right-radius:4px;
|
||||
border-radius:20px;
|
||||
}
|
||||
|
||||
#localVideoContainer>span.displayname:hover {
|
||||
|
@ -162,6 +166,10 @@
|
|||
pointer-events: none;
|
||||
}
|
||||
|
||||
.videocontainer>input.displayname {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
#localDisplayName {
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
@ -190,6 +198,7 @@
|
|||
text-shadow: 0px 1px 0px rgba(255,255,255,.3), 0px -1px 0px rgba(0,0,0,.7);
|
||||
border: 0px;
|
||||
z-index: 3;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.videocontainer>span.videoMuted {
|
||||
|
@ -226,7 +235,6 @@
|
|||
#header{
|
||||
display:none;
|
||||
position:absolute;
|
||||
height: 0px;
|
||||
text-align:center;
|
||||
top:0;
|
||||
left:0;
|
||||
|
@ -241,13 +249,27 @@
|
|||
margin-right:auto;
|
||||
height:39px;
|
||||
width:auto;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(to bottom, rgba(103,103,103,.65) , rgba(0,0,0,.65));
|
||||
-webkit-box-shadow: 0 0 2px #000000, 0 0 10px #000000;
|
||||
border-bottom-left-radius: 12px;
|
||||
border-bottom-right-radius: 12px;
|
||||
}
|
||||
|
||||
#subject {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
width: auto;
|
||||
padding: 5px;
|
||||
margin-left: 40%;
|
||||
margin-right: 40%;
|
||||
text-align: center;
|
||||
background: linear-gradient(to bottom, rgba(255,255,255,.85) , rgba(255,255,255,.35));
|
||||
-webkit-box-shadow: 0 0 2px #000000, 0 0 10px #000000;
|
||||
border-bottom-left-radius: 12px;
|
||||
border-bottom-right-radius: 12px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.watermark {
|
||||
display: block;
|
||||
position: absolute;
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
/* global connection, Strophe, updateLargeVideo, focusedVideoSrc*/
|
||||
/**
|
||||
* Callback triggered by PeerConnection when new data channel is opened
|
||||
* on the bridge.
|
||||
* @param event the event info object.
|
||||
*/
|
||||
function onDataChannel(event)
|
||||
{
|
||||
var dataChannel = event.channel;
|
||||
|
||||
dataChannel.onopen = function ()
|
||||
{
|
||||
console.info("Data channel opened by the bridge !!!", dataChannel);
|
||||
|
||||
// Code sample for sending string and/or binary data
|
||||
// Sends String message to the bridge
|
||||
//dataChannel.send("Hello bridge!");
|
||||
// Sends 12 bytes binary message to the bridge
|
||||
//dataChannel.send(new ArrayBuffer(12));
|
||||
};
|
||||
|
||||
dataChannel.onerror = function (error)
|
||||
{
|
||||
console.error("Data Channel Error:", error, dataChannel);
|
||||
};
|
||||
|
||||
dataChannel.onmessage = function (event)
|
||||
{
|
||||
var msgData = event.data;
|
||||
console.info("Got Data Channel Message:", msgData, dataChannel);
|
||||
|
||||
// Active speaker event
|
||||
if (msgData.indexOf('activeSpeaker') === 0)
|
||||
{
|
||||
// Endpoint ID from the bridge
|
||||
var resourceJid = msgData.split(":")[1];
|
||||
|
||||
console.info(
|
||||
"Data channel new active speaker event: " + resourceJid);
|
||||
$(document).trigger('activespeakerchanged', [resourceJid]);
|
||||
}
|
||||
};
|
||||
|
||||
dataChannel.onclose = function ()
|
||||
{
|
||||
console.info("The Data Channel closed", dataChannel);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds "ondatachannel" event listener to given PeerConnection instance.
|
||||
* @param peerConnection WebRTC peer connection instance.
|
||||
*/
|
||||
function bindDataChannelListener(peerConnection)
|
||||
{
|
||||
peerConnection.ondatachannel = onDataChannel;
|
||||
|
||||
// Sample code for opening new data channel from Jitsi Meet to the bridge.
|
||||
// Although it's not a requirement to open separate channels from both bridge
|
||||
// and peer as single channel can be used for sending and receiving data.
|
||||
// So either channel opened by the bridge or the one opened here is enough
|
||||
// for communication with the bridge.
|
||||
/*var dataChannelOptions =
|
||||
{
|
||||
reliable: true
|
||||
};
|
||||
var dataChannel
|
||||
= peerConnection.createDataChannel("myChannel", dataChannelOptions);
|
||||
|
||||
// Can be used only when is in open state
|
||||
dataChannel.onopen = function ()
|
||||
{
|
||||
dataChannel.send("My channel !!!");
|
||||
};
|
||||
dataChannel.onmessage = function (event)
|
||||
{
|
||||
var msgData = event.data;
|
||||
console.info("Got My Data Channel Message:", msgData, dataChannel);
|
||||
};*/
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
/* global $, config, connection, chrome, alert, getUserMediaWithConstraints, change_local_video, getConferenceHandler */
|
||||
/* global $, config, connection, chrome, alert, getUserMediaWithConstraints, changeLocalVideo, getConferenceHandler */
|
||||
/**
|
||||
* Indicates that desktop stream is currently in use(for toggle purpose).
|
||||
* @type {boolean}
|
||||
|
@ -251,7 +251,9 @@ function newStreamCreated(stream) {
|
|||
|
||||
var oldStream = connection.jingle.localVideo;
|
||||
|
||||
change_local_video(stream, !isUsingScreenStream);
|
||||
connection.jingle.localVideo = stream;
|
||||
|
||||
VideoLayout.changeLocalVideo(stream, !isUsingScreenStream);
|
||||
|
||||
var conferenceHandler = getConferenceHandler();
|
||||
if (conferenceHandler) {
|
||||
|
|
|
@ -45,8 +45,8 @@ var Etherpad = (function (my) {
|
|||
if (Prezi.isPresentationVisible()) {
|
||||
largeVideo.css({opacity: '0'});
|
||||
} else {
|
||||
setLargeVideoVisible(false);
|
||||
dockToolbar(true);
|
||||
VideoLayout.setLargeVideoVisible(false);
|
||||
Toolbar.dockToolbar(true);
|
||||
}
|
||||
|
||||
$('#etherpad>iframe').fadeIn(300, function () {
|
||||
|
@ -63,8 +63,8 @@ var Etherpad = (function (my) {
|
|||
document.body.style.background = 'black';
|
||||
if (!isPresentation) {
|
||||
$('#largeVideo').fadeIn(300, function () {
|
||||
setLargeVideoVisible(true);
|
||||
dockToolbar(false);
|
||||
VideoLayout.setLargeVideoVisible(true);
|
||||
Toolbar.dockToolbar(false);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
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.session.js?v=1"></script>
|
||||
<script src="//code.jquery.com/ui/1.10.4/jquery-ui.js"></script>
|
||||
<script src="config.js"></script><!-- adapt to your needs, i.e. set hosts and bosh path -->
|
||||
<script src="muc.js?v=10"></script><!-- simple MUC library -->
|
||||
<script src="libs/tooltip.js?v=1"></script><!-- bootstrap tooltip lib -->
|
||||
<script src="libs/popover.js?v=1"></script><!-- bootstrap tooltip lib -->
|
||||
<script src="config.js?v=2"></script><!-- adapt to your needs, i.e. set hosts and bosh path -->
|
||||
<script src="muc.js?v=12"></script><!-- simple MUC library -->
|
||||
<script src="estos_log.js?v=2"></script><!-- simple stanza logger -->
|
||||
<script src="desktopsharing.js?v=1"></script><!-- desktop sharing -->
|
||||
<script src="app.js?v=26"></script><!-- application logic -->
|
||||
<script src="chat.js?v=4"></script><!-- chat logic -->
|
||||
<script src="util.js?v=3"></script><!-- utility functions -->
|
||||
<script src="etherpad.js?v=7"></script><!-- etherpad plugin -->
|
||||
<script src="prezi.js?v=2"></script><!-- prezi plugin -->
|
||||
<script src="smileys.js?v=1"></script><!-- smiley images -->
|
||||
<script src="replacement.js?v=5"></script><!-- link and smiley replacement -->
|
||||
<script src="moderatemuc.js?v=1"></script><!-- moderator plugin -->
|
||||
<script src="desktopsharing.js?v=2"></script><!-- desktop sharing -->
|
||||
<script src="data_channels.js?v=2"></script><!-- data channels -->
|
||||
<script src="app.js?v=29"></script><!-- application logic -->
|
||||
<script src="commands.js?v=1"></script><!-- application logic -->
|
||||
<script src="chat.js?v=6"></script><!-- chat logic -->
|
||||
<script src="util.js?v=5"></script><!-- utility functions -->
|
||||
<script src="etherpad.js?v=8"></script><!-- etherpad plugin -->
|
||||
<script src="prezi.js?v=4"></script><!-- prezi plugin -->
|
||||
<script src="smileys.js?v=2"></script><!-- smiley images -->
|
||||
<script src="replacement.js?v=6"></script><!-- link and smiley replacement -->
|
||||
<script src="moderatemuc.js?v=3"></script><!-- moderator plugin -->
|
||||
<script src="analytics.js?v=1"></script><!-- google analytics plugin -->
|
||||
<script src="rtp_stats.js?v=1"></script><!-- RTP stats processing -->
|
||||
<script src="local_stats.js?v=1"></script><!-- Local stats processing -->
|
||||
<script src="videolayout.js?v=4"></script><!-- video ui -->
|
||||
<script src="toolbar.js?v=2"></script><!-- toolbar ui -->
|
||||
<link href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="css/font.css"/>
|
||||
<link rel="stylesheet" type="text/css" media="screen" href="css/main.css?v=20"/>
|
||||
<link rel="stylesheet" type="text/css" media="screen" href="css/videolayout_default.css?v=4" id="videolayout_default"/>
|
||||
<link rel="stylesheet" type="text/css" media="screen" href="css/main.css?v=21"/>
|
||||
<link rel="stylesheet" type="text/css" media="screen" href="css/videolayout_default.css?v=7" id="videolayout_default"/>
|
||||
<link rel="stylesheet" href="css/jquery-impromptu.css?v=4">
|
||||
<link rel="stylesheet" href="css/modaldialog.css?v=3">
|
||||
<link rel="stylesheet" href="css/popup_menu.css?v=1">
|
||||
<link rel="stylesheet" href="css/popup_menu.css?v=2">
|
||||
<link rel="stylesheet" href="css/popover.css?v=1">
|
||||
<!--
|
||||
Link used for inline installation of chrome desktop streaming extension,
|
||||
is updated automatically from the code with the value defined in config.js -->
|
||||
|
@ -49,35 +58,40 @@
|
|||
<script src="libs/prezi_player.js?v=2"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="header">
|
||||
<span id="toolbar">
|
||||
<a class="button" onclick='toggleAudio();'>
|
||||
<i id="mute" title="Mute / unmute" class="icon-microphone"></i></a>
|
||||
<div class="header_button_separator"></div>
|
||||
<a class="button" onclick='buttonClick("#video", "icon-camera icon-camera-disabled");toggleVideo();'>
|
||||
<i id="video" title="Start / stop camera" class="icon-camera"></i></a>
|
||||
<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 style="position: relative;" id="header_container">
|
||||
<div id="header">
|
||||
<span id="toolbar">
|
||||
<a class="button" data-toggle="popover" data-placement="bottom" data-content="Mute / Unmute" onclick='toggleAudio();'>
|
||||
<i id="mute" class="icon-microphone"></i></a>
|
||||
<div class="header_button_separator"></div>
|
||||
<a class="button" onclick='Etherpad.toggleEtherpad(0);' title="Open shared document"><i class="icon-share-doc"></i></a>
|
||||
</span>
|
||||
<div class="header_button_separator"></div>
|
||||
<span id="desktopsharing" style="display: none">
|
||||
<a class="button" onclick="toggleScreenSharing();" title="Share screen"><i class="icon-share-desktop"></i></a>
|
||||
<a class="button" data-toggle="popover" data-placement="bottom" data-content="Start / stop camera" onclick='buttonClick("#video", "icon-camera icon-camera-disabled");toggleVideo();'>
|
||||
<i id="video" class="icon-camera"></i></a>
|
||||
<div class="header_button_separator"></div>
|
||||
<a class="button" data-toggle="popover" data-placement="bottom" data-content="Lock / unlock room" onclick="Toolbar.openLockDialog();">
|
||||
<i id="lockIcon" class="icon-security"></i></a>
|
||||
<div class="header_button_separator"></div>
|
||||
<a class="button" data-toggle="popover" data-placement="bottom" data-content="Invite others" onclick="Toolbar.openLinkDialog();"><i class="icon-link"></i></a>
|
||||
<div class="header_button_separator"></div>
|
||||
<span class="toolbar_span">
|
||||
<a class="button" data-toggle="popover" data-placement="bottom" data-content="Open / close chat" onclick='Chat.toggleChat();'><i id="chatButton" class="icon-chat"></i></a>
|
||||
<span id="unreadMessages"></span>
|
||||
</span>
|
||||
<div class="header_button_separator"></div>
|
||||
<a class="button" data-toggle="popover" data-placement="bottom" data-content="Share Prezi" onclick='Prezi.openPreziDialog();'><i class="icon-prezi"></i></a>
|
||||
<span id="etherpadButton">
|
||||
<div 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>
|
||||
<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>
|
||||
</span>
|
||||
</div>
|
||||
<div id="subject"></div>
|
||||
</div>
|
||||
<div id="settings">
|
||||
<h1>Connection Settings</h1>
|
||||
|
@ -89,7 +103,7 @@
|
|||
</form>
|
||||
</div>
|
||||
<div id="reloadPresentation"><a onclick='Prezi.reloadPresentation();'><i title="Reload Prezi" class="fa fa-repeat fa-lg"></i></a></div>
|
||||
<div id="videospace" onmousemove="showToolbar();">
|
||||
<div id="videospace" onmousemove="Toolbar.showToolbar();">
|
||||
<div id="largeVideoContainer" class="videocontainer">
|
||||
<div id="presentation"></div>
|
||||
<div id="etherpad"></div>
|
||||
|
@ -104,7 +118,7 @@
|
|||
<!--<video id="localVideo" autoplay oncontextmenu="return false;" muted></video> - is now per stream generated -->
|
||||
</span>
|
||||
<audio id="localAudio" autoplay oncontextmenu="return false;" muted></audio>
|
||||
<span class="focusindicator"></span>
|
||||
<span class="focusindicator" data-content="The owner of this conference" data-toggle="popover" data-placement="top"></span>
|
||||
</span>
|
||||
<audio id="userJoined" src="sounds/joined.wav" preload="auto"></audio>
|
||||
<audio id="userLeft" src="sounds/left.wav" preload="auto"></audio>
|
||||
|
@ -123,6 +137,6 @@
|
|||
<audio id="chatNotification" src="sounds/incomingMessage.wav" preload="auto"></audio>
|
||||
<textarea id="usermsg" placeholder='Enter text...' autofocus></textarea>
|
||||
</div>
|
||||
<a id="downloadlog" onclick='dump(event.target);'><i title="Download support information" class="fa fa-cloud-download"></i></a>
|
||||
<a id="downloadlog" onclick='dump(event.target);' data-toggle="popover" data-placement="right" data-content="Download logs" ><i class="fa fa-cloud-download"></i></a>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -44,8 +44,27 @@ function ColibriFocus(connection, bridgejid) {
|
|||
this.peers = [];
|
||||
this.confid = null;
|
||||
|
||||
/**
|
||||
* Local XMPP resource used to join the multi user chat.
|
||||
* @type {*}
|
||||
*/
|
||||
this.myMucResource = Strophe.getResourceFromJid(connection.emuc.myroomjid);
|
||||
|
||||
/**
|
||||
* Default channel expire value in seconds.
|
||||
* @type {number}
|
||||
*/
|
||||
this.channelExpire = 60;
|
||||
|
||||
// media types of the conference
|
||||
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.mychannel = [];
|
||||
|
@ -151,17 +170,29 @@ ColibriFocus.prototype._makeConference = function () {
|
|||
elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri'});
|
||||
|
||||
this.media.forEach(function (name) {
|
||||
var isData = name === 'data';
|
||||
var channel = isData ? 'sctpconnection' : 'channel';
|
||||
|
||||
elem.c('content', {name: name});
|
||||
elem.c('channel', {
|
||||
|
||||
elem.c(channel, {
|
||||
initiator: 'true',
|
||||
expire: '15',
|
||||
endpoint: 'fix_me_focus_endpoint'}).up();
|
||||
endpoint: self.myMucResource
|
||||
});
|
||||
if (isData)
|
||||
elem.attrs({port: 5000});
|
||||
elem.up();// end of channel
|
||||
|
||||
for (var j = 0; j < self.peers.length; j++) {
|
||||
elem.c('channel', {
|
||||
elem.c(channel, {
|
||||
initiator: 'true',
|
||||
expire: '15',
|
||||
endpoint: self.peers[j].substr(1 + self.peers[j].lastIndexOf('/'))
|
||||
}).up();
|
||||
});
|
||||
if (isData)
|
||||
elem.attrs({port: 5000});
|
||||
elem.up(); // end of channel
|
||||
}
|
||||
elem.up(); // end of content
|
||||
});
|
||||
|
@ -209,8 +240,13 @@ ColibriFocus.prototype.createdConference = function (result) {
|
|||
this.confid = $(result).find('>conference').attr('id');
|
||||
var remotecontents = $(result).find('>conference>content').get();
|
||||
var numparticipants = 0;
|
||||
for (var i = 0; i < remotecontents.length; i++) {
|
||||
tmp = $(remotecontents[i]).find('>channel').get();
|
||||
for (var i = 0; i < remotecontents.length; i++)
|
||||
{
|
||||
var contentName = $(remotecontents[i]).attr('name');
|
||||
var channelName
|
||||
= contentName !== 'data' ? '>channel' : '>sctpconnection';
|
||||
|
||||
tmp = $(remotecontents[i]).find(channelName).get();
|
||||
this.mychannel.push($(tmp.shift()));
|
||||
numparticipants = tmp.length;
|
||||
for (j = 0; j < tmp.length; j++) {
|
||||
|
@ -223,7 +259,55 @@ ColibriFocus.prototype.createdConference = function (result) {
|
|||
|
||||
console.log('remote channels', this.channels);
|
||||
|
||||
var bridgeSDP = new SDP('v=0\r\no=- 5151055458874951233 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\nm=audio 1 RTP/SAVPF 111 103 104 0 8 106 105 13 126\r\nc=IN IP4 0.0.0.0\r\na=rtcp:1 IN IP4 0.0.0.0\r\na=mid:audio\r\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\na=sendrecv\r\na=rtpmap:111 opus/48000/2\r\na=fmtp:111 minptime=10\r\na=rtpmap:103 ISAC/16000\r\na=rtpmap:104 ISAC/32000\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:106 CN/32000\r\na=rtpmap:105 CN/16000\r\na=rtpmap:13 CN/8000\r\na=rtpmap:126 telephone-event/8000\r\na=maxptime:60\r\nm=video 1 RTP/SAVPF 100 116 117\r\nc=IN IP4 0.0.0.0\r\na=rtcp:1 IN IP4 0.0.0.0\r\na=mid:video\r\na=extmap:2 urn:ietf:params:rtp-hdrext:toffset\r\na=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=sendrecv\r\na=rtpmap:100 VP8/90000\r\na=rtcp-fb:100 ccm fir\r\na=rtcp-fb:100 nack\r\na=rtcp-fb:100 goog-remb\r\na=rtpmap:116 red/90000\r\na=rtpmap:117 ulpfec/90000\r\n');
|
||||
// Notify that the focus has created the conference on the bridge
|
||||
$(document).trigger('conferenceCreated.jingle', [self]);
|
||||
|
||||
var bridgeSDP = new SDP(
|
||||
'v=0\r\n' +
|
||||
'o=- 5151055458874951233 2 IN IP4 127.0.0.1\r\n' +
|
||||
's=-\r\n' +
|
||||
't=0 0\r\n' +
|
||||
/* Audio */
|
||||
'm=audio 1 RTP/SAVPF 111 103 104 0 8 106 105 13 126\r\n' +
|
||||
'c=IN IP4 0.0.0.0\r\n' +
|
||||
'a=rtcp:1 IN IP4 0.0.0.0\r\n' +
|
||||
'a=mid:audio\r\n' +
|
||||
'a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n' +
|
||||
'a=sendrecv\r\n' +
|
||||
'a=rtpmap:111 opus/48000/2\r\n' +
|
||||
'a=fmtp:111 minptime=10\r\n' +
|
||||
'a=rtpmap:103 ISAC/16000\r\n' +
|
||||
'a=rtpmap:104 ISAC/32000\r\n' +
|
||||
'a=rtpmap:0 PCMU/8000\r\n' +
|
||||
'a=rtpmap:8 PCMA/8000\r\n' +
|
||||
'a=rtpmap:106 CN/32000\r\n' +
|
||||
'a=rtpmap:105 CN/16000\r\n' +
|
||||
'a=rtpmap:13 CN/8000\r\n' +
|
||||
'a=rtpmap:126 telephone-event/8000\r\n' +
|
||||
'a=maxptime:60\r\n' +
|
||||
/* Video */
|
||||
'm=video 1 RTP/SAVPF 100 116 117\r\n' +
|
||||
'c=IN IP4 0.0.0.0\r\n' +
|
||||
'a=rtcp:1 IN IP4 0.0.0.0\r\n' +
|
||||
'a=mid:video\r\n' +
|
||||
'a=extmap:2 urn:ietf:params:rtp-hdrext:toffset\r\n' +
|
||||
'a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n' +
|
||||
'a=sendrecv\r\n' +
|
||||
'a=rtpmap:100 VP8/90000\r\n' +
|
||||
'a=rtcp-fb:100 ccm fir\r\n' +
|
||||
'a=rtcp-fb:100 nack\r\n' +
|
||||
'a=rtcp-fb:100 goog-remb\r\n' +
|
||||
'a=rtpmap:116 red/90000\r\n' +
|
||||
'a=rtpmap:117 ulpfec/90000\r\n' +
|
||||
/* Data SCTP */
|
||||
(config.openSctp ?
|
||||
'm=application 1 DTLS/SCTP 5000\r\n' +
|
||||
'c=IN IP4 0.0.0.0\r\n' +
|
||||
'a=sctpmap:5000 webrtc-datachannel\r\n' +
|
||||
'a=mid:data\r\n'
|
||||
: '')
|
||||
);
|
||||
|
||||
bridgeSDP.media.length = this.mychannel.length;
|
||||
var channel;
|
||||
/*
|
||||
|
@ -262,12 +346,17 @@ ColibriFocus.prototype.createdConference = function (result) {
|
|||
// get the mixed ssrc
|
||||
tmp = $(this.mychannel[channel]).find('>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
|
||||
// FIXME: check rtp-level-relay-type
|
||||
if (tmp.length) {
|
||||
|
||||
var isData = bridgeSDP.media[channel].indexOf('application') !== -1;
|
||||
if (!isData && tmp.length)
|
||||
{
|
||||
bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'cname:mixed' + '\r\n';
|
||||
bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'label:mixedlabela0' + '\r\n';
|
||||
bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'msid:mixedmslabel mixedlabela0' + '\r\n';
|
||||
bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'mslabel:mixedmslabel' + '\r\n';
|
||||
} else {
|
||||
}
|
||||
else if (!isData)
|
||||
{
|
||||
// make chrome happy... '3735928559' == 0xDEADBEEF
|
||||
// FIXME: this currently appears as two streams, should be one
|
||||
bridgeSDP.media[channel] += 'a=ssrc:' + '3735928559' + ' ' + 'cname:mixed' + '\r\n';
|
||||
|
@ -308,21 +397,41 @@ ColibriFocus.prototype.createdConference = function (result) {
|
|||
elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: self.confid});
|
||||
var localSDP = new SDP(self.peerconnection.localDescription.sdp);
|
||||
localSDP.media.forEach(function (media, channel) {
|
||||
var name = SDPUtil.parse_mline(media.split('\r\n')[0]).media;
|
||||
var name = SDPUtil.parse_mid(SDPUtil.find_line(media, 'a=mid:'));
|
||||
elem.c('content', {name: name});
|
||||
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]);
|
||||
for (var j = 0; j < mline.fmt.length; j++) {
|
||||
var rtpmap = SDPUtil.find_line(media, 'a=rtpmap:' + mline.fmt[j]);
|
||||
elem.c('payload-type', SDPUtil.parse_rtpmap(rtpmap));
|
||||
elem.up();
|
||||
if (name !== 'data')
|
||||
{
|
||||
elem.c('channel', {
|
||||
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);
|
||||
|
@ -336,7 +445,9 @@ ColibriFocus.prototype.createdConference = function (result) {
|
|||
// ...
|
||||
},
|
||||
function (error) {
|
||||
console.warn(error);
|
||||
console.error(
|
||||
"ERROR setLocalDescription succeded",
|
||||
error, elem);
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -344,6 +455,10 @@ ColibriFocus.prototype.createdConference = function (result) {
|
|||
for (var i = 0; i < numparticipants; i++) {
|
||||
self.initiate(self.peers[i], true);
|
||||
}
|
||||
|
||||
// Notify we've created the conference
|
||||
$(document).trigger(
|
||||
'conferenceCreated.jingle', self);
|
||||
},
|
||||
function (error) {
|
||||
console.warn('setLocalDescription failed.', error);
|
||||
|
@ -417,7 +532,10 @@ ColibriFocus.prototype.initiate = function (peer, isInitiator) {
|
|||
sdp.media[j] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'label:mixedlabela0' + '\r\n';
|
||||
sdp.media[j] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'msid:mixedmslabel mixedlabela0' + '\r\n';
|
||||
sdp.media[j] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'mslabel:mixedmslabel' + '\r\n';
|
||||
} else {
|
||||
}
|
||||
// No SSRCs for 'data', comes when j == 2
|
||||
else if (j < 2)
|
||||
{
|
||||
// make chrome happy... '3735928559' == 0xDEADBEEF
|
||||
sdp.media[j] += 'a=ssrc:' + '3735928559' + ' ' + 'cname:mixed' + '\r\n';
|
||||
sdp.media[j] += 'a=ssrc:' + '3735928559' + ' ' + 'label:mixedlabelv0' + '\r\n';
|
||||
|
@ -486,9 +604,17 @@ ColibriFocus.prototype.initiate = function (peer, isInitiator) {
|
|||
// pull in a new participant into the conference
|
||||
ColibriFocus.prototype.addNewParticipant = function (peer) {
|
||||
var self = this;
|
||||
if (this.confid === 0) {
|
||||
if (this.confid === 0 || !this.peerconnection.localDescription)
|
||||
{
|
||||
// bad state
|
||||
console.log('confid does not exist yet, postponing', peer);
|
||||
if (this.confid === 0)
|
||||
{
|
||||
console.error('confid does not exist yet, postponing', peer);
|
||||
}
|
||||
else
|
||||
{
|
||||
console.error('local description not ready yet, postponing', peer);
|
||||
}
|
||||
window.setTimeout(function () {
|
||||
self.addNewParticipant(peer);
|
||||
}, 250);
|
||||
|
@ -502,14 +628,26 @@ ColibriFocus.prototype.addNewParticipant = function (peer) {
|
|||
elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
|
||||
var localSDP = new SDP(this.peerconnection.localDescription.sdp);
|
||||
localSDP.media.forEach(function (media, channel) {
|
||||
var name = SDPUtil.parse_mline(media.split('\r\n')[0]).media;
|
||||
var name = SDPUtil.parse_mid(SDPUtil.find_line(media, 'a=mid:'));
|
||||
elem.c('content', {name: name});
|
||||
elem.c('channel', {
|
||||
if (name !== 'data')
|
||||
{
|
||||
elem.c('channel', {
|
||||
initiator: 'true',
|
||||
expire:'15',
|
||||
expire: self.channelExpire,
|
||||
endpoint: peer.substr(1 + peer.lastIndexOf('/'))
|
||||
});
|
||||
elem.up(); // end of channel
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
elem.c('sctpconnection', {
|
||||
endpoint: peer.substr(1 + peer.lastIndexOf('/')),
|
||||
initiator: 'true',
|
||||
expire: self.channelExpire,
|
||||
port: 5000
|
||||
});
|
||||
}
|
||||
elem.up(); // end of channel/sctpconnection
|
||||
elem.up(); // end of content
|
||||
});
|
||||
|
||||
|
@ -517,7 +655,15 @@ ColibriFocus.prototype.addNewParticipant = function (peer) {
|
|||
function (result) {
|
||||
var contents = $(result).find('>conference>content').get();
|
||||
for (var i = 0; i < contents.length; i++) {
|
||||
tmp = $(contents[i]).find('>channel').get();
|
||||
var channelXml = $(contents[i]).find('>channel');
|
||||
if (channelXml.length)
|
||||
{
|
||||
tmp = channelXml.get();
|
||||
}
|
||||
else
|
||||
{
|
||||
tmp = $(contents[i]).find('>sctpconnection').get();
|
||||
}
|
||||
self.channels[index][i] = tmp[0];
|
||||
}
|
||||
self.initiate(peer, true);
|
||||
|
@ -531,37 +677,52 @@ ColibriFocus.prototype.addNewParticipant = function (peer) {
|
|||
// update the channel description (payload-types + dtls fp) for a participant
|
||||
ColibriFocus.prototype.updateChannel = function (remoteSDP, participant) {
|
||||
console.log('change allocation for', this.confid);
|
||||
var self = this;
|
||||
var change = $iq({to: this.bridgejid, type: 'set'});
|
||||
change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
|
||||
for (channel = 0; channel < this.channels[participant].length; channel++) {
|
||||
change.c('content', {name: channel === 0 ? 'audio' : 'video'});
|
||||
change.c('channel', {
|
||||
id: $(this.channels[participant][channel]).attr('id'),
|
||||
endpoint: $(this.channels[participant][channel]).attr('endpoint'),
|
||||
expire: '15'
|
||||
});
|
||||
for (channel = 0; channel < this.channels[participant].length; channel++)
|
||||
{
|
||||
var name = SDPUtil.parse_mid(SDPUtil.find_line(remoteSDP.media[channel], 'a=mid:'));
|
||||
change.c('content', {name: name});
|
||||
if (name !== 'data')
|
||||
{
|
||||
change.c('channel', {
|
||||
id: $(this.channels[participant][channel]).attr('id'),
|
||||
endpoint: $(this.channels[participant][channel]).attr('endpoint'),
|
||||
expire: self.channelExpire
|
||||
});
|
||||
|
||||
var rtpmap = SDPUtil.find_lines(remoteSDP.media[channel], 'a=rtpmap:');
|
||||
rtpmap.forEach(function (val) {
|
||||
// TODO: too much copy-paste
|
||||
var rtpmap = SDPUtil.parse_rtpmap(val);
|
||||
change.c('payload-type', rtpmap);
|
||||
//
|
||||
// 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)) {
|
||||
tmp = SDPUtil.parse_fmtp(SDPUtil.find_line(remoteSDP.media[channel], 'a=fmtp:' + rtpmap.id));
|
||||
for (var k = 0; k < tmp.length; k++) {
|
||||
change.c('parameter', tmp[k]).up();
|
||||
var rtpmap = SDPUtil.find_lines(remoteSDP.media[channel], 'a=rtpmap:');
|
||||
rtpmap.forEach(function (val) {
|
||||
// TODO: too much copy-paste
|
||||
var rtpmap = SDPUtil.parse_rtpmap(val);
|
||||
change.c('payload-type', rtpmap);
|
||||
//
|
||||
// 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)) {
|
||||
tmp = SDPUtil.parse_fmtp(SDPUtil.find_line(remoteSDP.media[channel], 'a=fmtp:' + rtpmap.id));
|
||||
for (var k = 0; k < tmp.length; k++) {
|
||||
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
|
||||
remoteSDP.TransportToJingle(channel, change);
|
||||
|
||||
change.up(); // end of channel
|
||||
change.up(); // end of channel/sctpconnection
|
||||
change.up(); // end of content
|
||||
}
|
||||
this.connection.sendIQ(change,
|
||||
|
@ -605,6 +766,19 @@ ColibriFocus.prototype.sendSSRCUpdate = function (sdpMediaSsrcs, fromJid, isadd)
|
|||
ColibriFocus.prototype.addSource = function (elem, fromJid) {
|
||||
|
||||
var self = this;
|
||||
// FIXME: dirty waiting
|
||||
if (!this.peerconnection.localDescription)
|
||||
{
|
||||
console.warn("addSource - localDescription not ready yet")
|
||||
setTimeout(function()
|
||||
{
|
||||
self.addSource(elem, fromJid);
|
||||
},
|
||||
200
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.peerconnection.addSource(elem);
|
||||
|
||||
var peerSsrc = this.remotessrc[fromJid];
|
||||
|
@ -638,6 +812,19 @@ ColibriFocus.prototype.addSource = function (elem, fromJid) {
|
|||
ColibriFocus.prototype.removeSource = function (elem, fromJid) {
|
||||
|
||||
var self = this;
|
||||
// FIXME: dirty waiting
|
||||
if (!self.peerconnection.localDescription)
|
||||
{
|
||||
console.warn("removeSource - localDescription not ready yet");
|
||||
setTimeout(function()
|
||||
{
|
||||
self.removeSource(elem, fromJid);
|
||||
},
|
||||
200
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.peerconnection.removeSource(elem);
|
||||
|
||||
var peerSsrc = this.remotessrc[fromJid];
|
||||
|
@ -675,8 +862,11 @@ ColibriFocus.prototype.setRemoteDescription = function (session, elem, desctype)
|
|||
this.remotessrc[session.peerjid] = [];
|
||||
for (channel = 0; channel < this.channels[participant].length; channel++) {
|
||||
//if (channel == 0) continue; FIXME: does not work as intended
|
||||
if (SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:').length) {
|
||||
this.remotessrc[session.peerjid][channel] = SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:').join('\r\n') + '\r\n';
|
||||
if (SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:').length)
|
||||
{
|
||||
this.remotessrc[session.peerjid][channel] =
|
||||
SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:')
|
||||
.join('\r\n') + '\r\n';
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -702,14 +892,27 @@ ColibriFocus.prototype.addIceCandidate = function (session, elem) {
|
|||
change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
|
||||
$(elem).each(function () {
|
||||
var name = $(this).attr('name');
|
||||
|
||||
var channel = name == 'audio' ? 0 : 1; // FIXME: search mlineindex in localdesc
|
||||
if (name != 'audio' && name != 'video')
|
||||
channel = 2; // name == 'data'
|
||||
|
||||
change.c('content', {name: name});
|
||||
change.c('channel', {
|
||||
id: $(self.channels[participant][channel]).attr('id'),
|
||||
endpoint: $(self.channels[participant][channel]).attr('endpoint'),
|
||||
expire: '15'
|
||||
});
|
||||
if (name !== 'data')
|
||||
{
|
||||
change.c('channel', {
|
||||
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 () {
|
||||
change.c('transport', {
|
||||
ufrag: $(this).attr('ufrag'),
|
||||
|
@ -729,7 +932,7 @@ ColibriFocus.prototype.addIceCandidate = function (session, elem) {
|
|||
});
|
||||
change.up(); // end of transport
|
||||
});
|
||||
change.up(); // end of channel
|
||||
change.up(); // end of channel/sctpconnection
|
||||
change.up(); // end of content
|
||||
});
|
||||
// FIXME: need to check if there is at least one candidate when filtering TCP ones
|
||||
|
@ -769,21 +972,35 @@ ColibriFocus.prototype.sendIceCandidates = function (candidates) {
|
|||
mycands.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
|
||||
// FIXME: multi-candidate logic is taken from strophe.jingle, should be refactored there
|
||||
var localSDP = new SDP(this.peerconnection.localDescription.sdp);
|
||||
for (var mid = 0; mid < localSDP.media.length; mid++) {
|
||||
for (var mid = 0; mid < localSDP.media.length; mid++)
|
||||
{
|
||||
var cands = candidates.filter(function (el) { return el.sdpMLineIndex == mid; });
|
||||
if (cands.length > 0) {
|
||||
mycands.c('content', {name: cands[0].sdpMid });
|
||||
mycands.c('channel', {
|
||||
id: $(this.mychannel[cands[0].sdpMLineIndex]).attr('id'),
|
||||
endpoint: $(this.mychannel[cands[0].sdpMLineIndex]).attr('endpoint'),
|
||||
expire: '15'
|
||||
});
|
||||
if (cands.length > 0)
|
||||
{
|
||||
var name = cands[0].sdpMid;
|
||||
mycands.c('content', {name: name });
|
||||
if (name !== 'data')
|
||||
{
|
||||
mycands.c('channel', {
|
||||
id: $(this.mychannel[cands[0].sdpMLineIndex]).attr('id'),
|
||||
endpoint: $(this.mychannel[cands[0].sdpMLineIndex]).attr('endpoint'),
|
||||
expire: self.channelExpire
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
mycands.c('sctpconnection', {
|
||||
endpoint: $(this.mychannel[cands[0].sdpMLineIndex]).attr('endpoint'),
|
||||
port: $(this.mychannel[cands[0].sdpMLineIndex]).attr('port'),
|
||||
expire: self.channelExpire
|
||||
});
|
||||
}
|
||||
mycands.c('transport', {xmlns: 'urn:xmpp:jingle:transports:ice-udp:1'});
|
||||
for (var i = 0; i < cands.length; i++) {
|
||||
mycands.c('candidate', SDPUtil.candidateToJingle(cands[i].candidate)).up();
|
||||
}
|
||||
mycands.up(); // transport
|
||||
mycands.up(); // channel
|
||||
mycands.up(); // channel / sctpconnection
|
||||
mycands.up(); // content
|
||||
}
|
||||
}
|
||||
|
@ -814,13 +1031,26 @@ ColibriFocus.prototype.terminate = function (session, reason) {
|
|||
var change = $iq({to: this.bridgejid, type: 'set'});
|
||||
change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
|
||||
for (var channel = 0; channel < this.channels[participant].length; channel++) {
|
||||
change.c('content', {name: channel === 0 ? 'audio' : 'video'});
|
||||
change.c('channel', {
|
||||
id: $(this.channels[participant][channel]).attr('id'),
|
||||
endpoint: $(this.channels[participant][channel]).attr('endpoint'),
|
||||
expire: '0'
|
||||
});
|
||||
change.up(); // end of channel
|
||||
var name = channel === 0 ? 'audio' : 'video';
|
||||
if (channel == 2)
|
||||
name = 'data';
|
||||
change.c('content', {name: name});
|
||||
if (name !== 'data')
|
||||
{
|
||||
change.c('channel', {
|
||||
id: $(this.channels[participant][channel]).attr('id'),
|
||||
endpoint: $(this.channels[participant][channel]).attr('endpoint'),
|
||||
expire: '0'
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
change.c('sctpconnection', {
|
||||
endpoint: $(this.channels[participant][channel]).attr('endpoint'),
|
||||
expire: '0'
|
||||
});
|
||||
}
|
||||
change.up(); // end of channel/sctpconnection
|
||||
change.up(); // end of content
|
||||
}
|
||||
this.connection.sendIQ(change,
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
/* ========================================================================
|
||||
* Bootstrap: popover.js v3.1.1
|
||||
* http://getbootstrap.com/javascript/#popovers
|
||||
* ========================================================================
|
||||
* Copyright 2011-2014 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||
* ======================================================================== */
|
||||
|
||||
|
||||
+function ($) {
|
||||
'use strict';
|
||||
|
||||
// POPOVER PUBLIC CLASS DEFINITION
|
||||
// ===============================
|
||||
|
||||
var Popover = function (element, options) {
|
||||
this.init('popover', element, options)
|
||||
}
|
||||
|
||||
if (!$.fn.tooltip) throw new Error('Popover requires tooltip.js')
|
||||
|
||||
Popover.DEFAULTS = $.extend({}, $.fn.tooltip.Constructor.DEFAULTS, {
|
||||
placement: 'right',
|
||||
trigger: 'click',
|
||||
content: '',
|
||||
template: '<div class="popover"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>'
|
||||
})
|
||||
|
||||
|
||||
// NOTE: POPOVER EXTENDS tooltip.js
|
||||
// ================================
|
||||
|
||||
Popover.prototype = $.extend({}, $.fn.tooltip.Constructor.prototype)
|
||||
|
||||
Popover.prototype.constructor = Popover
|
||||
|
||||
Popover.prototype.getDefaults = function () {
|
||||
return Popover.DEFAULTS
|
||||
}
|
||||
|
||||
Popover.prototype.setContent = function () {
|
||||
var $tip = this.tip()
|
||||
var title = this.getTitle()
|
||||
var content = this.getContent()
|
||||
|
||||
$tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title)
|
||||
$tip.find('.popover-content')[ // we use append for html objects to maintain js events
|
||||
this.options.html ? (typeof content == 'string' ? 'html' : 'append') : 'text'
|
||||
](content)
|
||||
|
||||
$tip.removeClass('fade top bottom left right in')
|
||||
|
||||
// IE8 doesn't accept hiding via the `:empty` pseudo selector, we have to do
|
||||
// this manually by checking the contents.
|
||||
if (!$tip.find('.popover-title').html()) $tip.find('.popover-title').hide()
|
||||
}
|
||||
|
||||
Popover.prototype.hasContent = function () {
|
||||
return this.getTitle() || this.getContent()
|
||||
}
|
||||
|
||||
Popover.prototype.getContent = function () {
|
||||
var $e = this.$element
|
||||
var o = this.options
|
||||
|
||||
return $e.attr('data-content')
|
||||
|| (typeof o.content == 'function' ?
|
||||
o.content.call($e[0]) :
|
||||
o.content)
|
||||
}
|
||||
|
||||
Popover.prototype.arrow = function () {
|
||||
return this.$arrow = this.$arrow || this.tip().find('.arrow')
|
||||
}
|
||||
|
||||
Popover.prototype.tip = function () {
|
||||
if (!this.$tip) this.$tip = $(this.options.template)
|
||||
return this.$tip
|
||||
}
|
||||
|
||||
|
||||
// POPOVER PLUGIN DEFINITION
|
||||
// =========================
|
||||
|
||||
var old = $.fn.popover
|
||||
|
||||
$.fn.popover = function (option) {
|
||||
return this.each(function () {
|
||||
var $this = $(this)
|
||||
var data = $this.data('bs.popover')
|
||||
var options = typeof option == 'object' && option
|
||||
|
||||
if (!data && option == 'destroy') return
|
||||
if (!data) $this.data('bs.popover', (data = new Popover(this, options)))
|
||||
if (typeof option == 'string') data[option]()
|
||||
})
|
||||
}
|
||||
|
||||
$.fn.popover.Constructor = Popover
|
||||
|
||||
|
||||
// POPOVER NO CONFLICT
|
||||
// ===================
|
||||
|
||||
$.fn.popover.noConflict = function () {
|
||||
$.fn.popover = old
|
||||
return this
|
||||
}
|
||||
|
||||
}(jQuery);
|
|
@ -5,7 +5,7 @@ function TraceablePeerConnection(ice_config, constraints) {
|
|||
this.updateLog = [];
|
||||
this.stats = {};
|
||||
this.statsinterval = null;
|
||||
this.maxstats = 300; // limit to 300 values, i.e. 5 minutes; set to 0 to disable
|
||||
this.maxstats = 0; // limit to 300 values, i.e. 5 minutes; set to 0 to disable
|
||||
|
||||
/**
|
||||
* Array of ssrcs that will be added on next modifySources call.
|
||||
|
@ -32,7 +32,7 @@ function TraceablePeerConnection(ice_config, constraints) {
|
|||
this.switchstreams = false;
|
||||
|
||||
// override as desired
|
||||
this.trace = function(what, info) {
|
||||
this.trace = function (what, info) {
|
||||
//console.warn('WTRACE', what, info);
|
||||
self.updateLog.push({
|
||||
time: new Date(),
|
||||
|
@ -88,8 +88,8 @@ function TraceablePeerConnection(ice_config, constraints) {
|
|||
if (self.ondatachannel !== null) {
|
||||
self.ondatachannel(event);
|
||||
}
|
||||
}
|
||||
if (!navigator.mozGetUserMedia) {
|
||||
};
|
||||
if (!navigator.mozGetUserMedia && this.maxstats) {
|
||||
this.statsinterval = window.setInterval(function() {
|
||||
self.peerconnection.getStats(function(stats) {
|
||||
var results = stats.result();
|
||||
|
@ -144,8 +144,8 @@ TraceablePeerConnection.prototype.removeStream = function (stream) {
|
|||
|
||||
TraceablePeerConnection.prototype.createDataChannel = function (label, opts) {
|
||||
this.trace('createDataChannel', label, opts);
|
||||
this.peerconnection.createDataChannel(label, opts);
|
||||
}
|
||||
return this.peerconnection.createDataChannel(label, opts);
|
||||
};
|
||||
|
||||
TraceablePeerConnection.prototype.setLocalDescription = function (description, successCallback, failureCallback) {
|
||||
var self = this;
|
||||
|
|
|
@ -155,7 +155,10 @@ SDP.prototype.toJingle = function (elem, thecreator) {
|
|||
}
|
||||
for (i = 0; i < this.media.length; i++) {
|
||||
mline = SDPUtil.parse_mline(this.media[i].split('\r\n')[0]);
|
||||
if (!(mline.media == 'audio' || mline.media == 'video')) {
|
||||
if (!(mline.media === 'audio' ||
|
||||
mline.media === 'video' ||
|
||||
mline.media === 'application'))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (SDPUtil.find_line(this.media[i], 'a=ssrc:')) {
|
||||
|
@ -171,12 +174,14 @@ SDP.prototype.toJingle = function (elem, thecreator) {
|
|||
elem.attrs({ name: mid });
|
||||
|
||||
// old BUNDLE plan, to be removed
|
||||
if (bundle.indexOf(mid) != -1) {
|
||||
if (bundle.indexOf(mid) !== -1) {
|
||||
elem.c('bundle', {xmlns: 'http://estos.de/ns/bundle'}).up();
|
||||
bundle.splice(bundle.indexOf(mid), 1);
|
||||
}
|
||||
}
|
||||
if (SDPUtil.find_line(this.media[i], 'a=rtpmap:').length) {
|
||||
|
||||
if (SDPUtil.find_line(this.media[i], 'a=rtpmap:').length)
|
||||
{
|
||||
elem.c('description',
|
||||
{xmlns: 'urn:xmpp:jingle:apps:rtp:1',
|
||||
media: mline.media });
|
||||
|
@ -304,6 +309,26 @@ SDP.prototype.TransportToJingle = function (mediaindex, elem) {
|
|||
var self = this;
|
||||
elem.c('transport');
|
||||
|
||||
// XEP-0343 DTLS/SCTP
|
||||
if (SDPUtil.find_line(this.media[mediaindex], 'a=sctpmap:').length)
|
||||
{
|
||||
var sctpmap = SDPUtil.find_line(
|
||||
this.media[i], 'a=sctpmap:', self.session);
|
||||
if (sctpmap)
|
||||
{
|
||||
var sctpAttrs = SDPUtil.parse_sctpmap(sctpmap);
|
||||
elem.c('sctpmap',
|
||||
{
|
||||
xmlns: 'urn:xmpp:jingle:transports:dtls-sctp:1',
|
||||
number: sctpAttrs[0], /* SCTP port */
|
||||
protocol: sctpAttrs[1], /* protocol */
|
||||
});
|
||||
// Optional stream count attribute
|
||||
if (sctpAttrs.length > 2)
|
||||
elem.attrs({ streams: sctpAttrs[2]});
|
||||
elem.up();
|
||||
}
|
||||
}
|
||||
// XEP-0320
|
||||
var fingerprints = SDPUtil.find_lines(this.media[mediaindex], 'a=fingerprint:', this.session);
|
||||
fingerprints.forEach(function(line) {
|
||||
|
@ -438,6 +463,8 @@ SDP.prototype.jingle2media = function (content) {
|
|||
ssrc = desc.attr('ssrc'),
|
||||
self = this,
|
||||
tmp;
|
||||
var sctp = content.find(
|
||||
'>transport>sctpmap[xmlns="urn:xmpp:jingle:transports:dtls-sctp:1"]');
|
||||
|
||||
tmp = { media: desc.attr('media') };
|
||||
tmp.port = '1';
|
||||
|
@ -446,14 +473,35 @@ SDP.prototype.jingle2media = function (content) {
|
|||
tmp.port = '0';
|
||||
}
|
||||
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 {
|
||||
tmp.proto = 'RTP/AVPF';
|
||||
}
|
||||
tmp.fmt = desc.find('payload-type').map(function () { return this.getAttribute('id'); }).get();
|
||||
media += SDPUtil.build_mline(tmp) + '\r\n';
|
||||
if (!sctp.length)
|
||||
{
|
||||
tmp.fmt = desc.find('payload-type').map(
|
||||
function () { return this.getAttribute('id'); }).get();
|
||||
media += SDPUtil.build_mline(tmp) + '\r\n';
|
||||
}
|
||||
else
|
||||
{
|
||||
media += 'm=application 1 DTLS/SCTP ' + sctp.attr('number') + '\r\n';
|
||||
media += 'a=sctpmap:' + sctp.attr('number') +
|
||||
' ' + sctp.attr('protocol');
|
||||
|
||||
var streamCount = sctp.attr('streams');
|
||||
if (streamCount)
|
||||
media += ' ' + streamCount + '\r\n';
|
||||
else
|
||||
media += '\r\n';
|
||||
}
|
||||
|
||||
media += 'c=IN IP4 0.0.0.0\r\n';
|
||||
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"]');
|
||||
if (tmp.length) {
|
||||
if (tmp.attr('ufrag')) {
|
||||
|
|
|
@ -90,6 +90,20 @@ SDPUtil = {
|
|||
data.channels = parts.length ? parts.shift() : '1';
|
||||
return data;
|
||||
},
|
||||
/**
|
||||
* Parses SDP line "a=sctpmap:..." and extracts SCTP port from it.
|
||||
* @param line eg. "a=sctpmap:5000 webrtc-datachannel"
|
||||
* @returns [SCTP port number, protocol, streams]
|
||||
*/
|
||||
parse_sctpmap: function (line)
|
||||
{
|
||||
var parts = line.substring(10).split(' ');
|
||||
var sctpPort = parts[0];
|
||||
var protocol = parts[1];
|
||||
// Stream count is optional
|
||||
var streamCount = parts.length > 2 ? parts[2] : null;
|
||||
return [sctpPort, protocol, streamCount];// SCTP port
|
||||
},
|
||||
build_rtpmap: function (el) {
|
||||
var line = 'a=rtpmap:' + el.getAttribute('id') + ' ' + el.getAttribute('name') + '/' + el.getAttribute('clockrate');
|
||||
if (el.getAttribute('channels') && el.getAttribute('channels') != '1') {
|
||||
|
@ -269,7 +283,9 @@ SDPUtil = {
|
|||
candidateToJingle: function (line) {
|
||||
// a=candidate:2979166662 1 udp 2113937151 192.168.2.100 57698 typ host generation 0
|
||||
// <candidate component=... foundation=... generation=... id=... ip=... network=... port=... priority=... protocol=... type=.../>
|
||||
if (line.substring(0, 12) != 'a=candidate:') {
|
||||
if (line.indexOf('candidate:') == 0) {
|
||||
line = 'a=' + line;
|
||||
} else if (line.substring(0, 12) != 'a=candidate:') {
|
||||
console.log('parseCandidate called with a line that is not a candidate line');
|
||||
console.log(line);
|
||||
return null;
|
||||
|
|
|
@ -420,6 +420,7 @@ JingleSession.prototype.setRemoteDescription = function (elem, desctype) {
|
|||
},
|
||||
function (e) {
|
||||
console.error('setRemoteDescription error', e);
|
||||
$(document).trigger('fatalError.jingle', [self, e]);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
@ -23,6 +23,20 @@ SessionBase.prototype.modifySources = function (successCallback) {
|
|||
|
||||
SessionBase.prototype.addSource = function (elem, fromJid) {
|
||||
|
||||
var self = this;
|
||||
// FIXME: dirty waiting
|
||||
if (!this.peerconnection.localDescription)
|
||||
{
|
||||
console.warn("addSource - localDescription not ready yet")
|
||||
setTimeout(function()
|
||||
{
|
||||
self.addSource(elem, fromJid);
|
||||
},
|
||||
200
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.peerconnection.addSource(elem);
|
||||
|
||||
this.modifySources();
|
||||
|
@ -30,6 +44,20 @@ SessionBase.prototype.addSource = function (elem, fromJid) {
|
|||
|
||||
SessionBase.prototype.removeSource = function (elem, fromJid) {
|
||||
|
||||
var self = this;
|
||||
// FIXME: dirty waiting
|
||||
if (!this.peerconnection.localDescription)
|
||||
{
|
||||
console.warn("removeSource - localDescription not ready yet")
|
||||
setTimeout(function()
|
||||
{
|
||||
self.removeSource(elem, fromJid);
|
||||
},
|
||||
200
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.peerconnection.removeSource(elem);
|
||||
|
||||
this.modifySources();
|
||||
|
|
|
@ -0,0 +1,399 @@
|
|||
/* ========================================================================
|
||||
* Bootstrap: tooltip.js v3.1.1
|
||||
* http://getbootstrap.com/javascript/#tooltip
|
||||
* Inspired by the original jQuery.tipsy by Jason Frame
|
||||
* ========================================================================
|
||||
* Copyright 2011-2014 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
|
||||
* ======================================================================== */
|
||||
|
||||
|
||||
+function ($) {
|
||||
'use strict';
|
||||
|
||||
// TOOLTIP PUBLIC CLASS DEFINITION
|
||||
// ===============================
|
||||
|
||||
var Tooltip = function (element, options) {
|
||||
this.type =
|
||||
this.options =
|
||||
this.enabled =
|
||||
this.timeout =
|
||||
this.hoverState =
|
||||
this.$element = null
|
||||
|
||||
this.init('tooltip', element, options)
|
||||
}
|
||||
|
||||
Tooltip.DEFAULTS = {
|
||||
animation: true,
|
||||
placement: 'top',
|
||||
selector: false,
|
||||
template: '<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',
|
||||
trigger: 'hover focus',
|
||||
title: '',
|
||||
delay: 0,
|
||||
html: false,
|
||||
container: false
|
||||
}
|
||||
|
||||
Tooltip.prototype.init = function (type, element, options) {
|
||||
this.enabled = true
|
||||
this.type = type
|
||||
this.$element = $(element)
|
||||
this.options = this.getOptions(options)
|
||||
|
||||
var triggers = this.options.trigger.split(' ')
|
||||
|
||||
for (var i = triggers.length; i--;) {
|
||||
var trigger = triggers[i]
|
||||
|
||||
if (trigger == 'click') {
|
||||
this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this))
|
||||
} else if (trigger != 'manual') {
|
||||
var eventIn = trigger == 'hover' ? 'mouseenter' : 'focusin'
|
||||
var eventOut = trigger == 'hover' ? 'mouseleave' : 'focusout'
|
||||
|
||||
this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this))
|
||||
this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this))
|
||||
}
|
||||
}
|
||||
|
||||
this.options.selector ?
|
||||
(this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) :
|
||||
this.fixTitle()
|
||||
}
|
||||
|
||||
Tooltip.prototype.getDefaults = function () {
|
||||
return Tooltip.DEFAULTS
|
||||
}
|
||||
|
||||
Tooltip.prototype.getOptions = function (options) {
|
||||
options = $.extend({}, this.getDefaults(), this.$element.data(), options)
|
||||
|
||||
if (options.delay && typeof options.delay == 'number') {
|
||||
options.delay = {
|
||||
show: options.delay,
|
||||
hide: options.delay
|
||||
}
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
Tooltip.prototype.getDelegateOptions = function () {
|
||||
var options = {}
|
||||
var defaults = this.getDefaults()
|
||||
|
||||
this._options && $.each(this._options, function (key, value) {
|
||||
if (defaults[key] != value) options[key] = value
|
||||
})
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
Tooltip.prototype.enter = function (obj) {
|
||||
var self = obj instanceof this.constructor ?
|
||||
obj : $(obj.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type)
|
||||
|
||||
clearTimeout(self.timeout)
|
||||
|
||||
self.hoverState = 'in'
|
||||
|
||||
if (!self.options.delay || !self.options.delay.show) return self.show()
|
||||
|
||||
self.timeout = setTimeout(function () {
|
||||
if (self.hoverState == 'in') self.show()
|
||||
}, self.options.delay.show)
|
||||
}
|
||||
|
||||
Tooltip.prototype.leave = function (obj) {
|
||||
var self = obj instanceof this.constructor ?
|
||||
obj : $(obj.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type)
|
||||
|
||||
clearTimeout(self.timeout)
|
||||
|
||||
self.hoverState = 'out'
|
||||
|
||||
if (!self.options.delay || !self.options.delay.hide) return self.hide()
|
||||
|
||||
self.timeout = setTimeout(function () {
|
||||
if (self.hoverState == 'out') self.hide()
|
||||
}, self.options.delay.hide)
|
||||
}
|
||||
|
||||
Tooltip.prototype.show = function () {
|
||||
var e = $.Event('show.bs.' + this.type)
|
||||
|
||||
if (this.hasContent() && this.enabled) {
|
||||
this.$element.trigger(e)
|
||||
|
||||
if (e.isDefaultPrevented()) return
|
||||
var that = this;
|
||||
|
||||
var $tip = this.tip()
|
||||
|
||||
this.setContent()
|
||||
|
||||
if (this.options.animation) $tip.addClass('fade')
|
||||
|
||||
var placement = typeof this.options.placement == 'function' ?
|
||||
this.options.placement.call(this, $tip[0], this.$element[0]) :
|
||||
this.options.placement
|
||||
|
||||
var autoToken = /\s?auto?\s?/i
|
||||
var autoPlace = autoToken.test(placement)
|
||||
if (autoPlace) placement = placement.replace(autoToken, '') || 'top'
|
||||
|
||||
$tip
|
||||
.detach()
|
||||
.css({ top: 0, left: 0, display: 'block' })
|
||||
.addClass(placement)
|
||||
|
||||
this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element)
|
||||
|
||||
var pos = this.getPosition()
|
||||
var actualWidth = $tip[0].offsetWidth
|
||||
var actualHeight = $tip[0].offsetHeight
|
||||
|
||||
if (autoPlace) {
|
||||
var $parent = this.$element.parent()
|
||||
|
||||
var orgPlacement = placement
|
||||
var docScroll = document.documentElement.scrollTop || document.body.scrollTop
|
||||
var parentWidth = this.options.container == 'body' ? window.innerWidth : $parent.outerWidth()
|
||||
var parentHeight = this.options.container == 'body' ? window.innerHeight : $parent.outerHeight()
|
||||
var parentLeft = this.options.container == 'body' ? 0 : $parent.offset().left
|
||||
|
||||
placement = placement == 'bottom' && pos.top + pos.height + actualHeight - docScroll > parentHeight ? 'top' :
|
||||
placement == 'top' && pos.top - docScroll - actualHeight < 0 ? 'bottom' :
|
||||
placement == 'right' && pos.right + actualWidth > parentWidth ? 'left' :
|
||||
placement == 'left' && pos.left - actualWidth < parentLeft ? 'right' :
|
||||
placement
|
||||
|
||||
$tip
|
||||
.removeClass(orgPlacement)
|
||||
.addClass(placement)
|
||||
}
|
||||
|
||||
var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight)
|
||||
|
||||
this.applyPlacement(calculatedOffset, placement)
|
||||
this.hoverState = null
|
||||
|
||||
var complete = function() {
|
||||
that.$element.trigger('shown.bs.' + that.type)
|
||||
}
|
||||
|
||||
$.support.transition && this.$tip.hasClass('fade') ?
|
||||
$tip
|
||||
.one($.support.transition.end, complete)
|
||||
.emulateTransitionEnd(150) :
|
||||
complete()
|
||||
}
|
||||
}
|
||||
|
||||
Tooltip.prototype.applyPlacement = function (offset, placement) {
|
||||
var replace
|
||||
var $tip = this.tip()
|
||||
var width = $tip[0].offsetWidth
|
||||
var height = $tip[0].offsetHeight
|
||||
|
||||
// manually read margins because getBoundingClientRect includes difference
|
||||
var marginTop = parseInt($tip.css('margin-top'), 10)
|
||||
var marginLeft = parseInt($tip.css('margin-left'), 10)
|
||||
|
||||
// we must check for NaN for ie 8/9
|
||||
if (isNaN(marginTop)) marginTop = 0
|
||||
if (isNaN(marginLeft)) marginLeft = 0
|
||||
|
||||
offset.top = offset.top + marginTop
|
||||
offset.left = offset.left + marginLeft
|
||||
|
||||
// $.fn.offset doesn't round pixel values
|
||||
// so we use setOffset directly with our own function B-0
|
||||
$.offset.setOffset($tip[0], $.extend({
|
||||
using: function (props) {
|
||||
$tip.css({
|
||||
top: Math.round(props.top),
|
||||
left: Math.round(props.left)
|
||||
})
|
||||
}
|
||||
}, offset), 0)
|
||||
|
||||
$tip.addClass('in')
|
||||
|
||||
// check to see if placing tip in new offset caused the tip to resize itself
|
||||
var actualWidth = $tip[0].offsetWidth
|
||||
var actualHeight = $tip[0].offsetHeight
|
||||
|
||||
if (placement == 'top' && actualHeight != height) {
|
||||
replace = true
|
||||
offset.top = offset.top + height - actualHeight
|
||||
}
|
||||
|
||||
if (/bottom|top/.test(placement)) {
|
||||
var delta = 0
|
||||
|
||||
if (offset.left < 0) {
|
||||
delta = offset.left * -2
|
||||
offset.left = 0
|
||||
|
||||
$tip.offset(offset)
|
||||
|
||||
actualWidth = $tip[0].offsetWidth
|
||||
actualHeight = $tip[0].offsetHeight
|
||||
}
|
||||
|
||||
this.replaceArrow(delta - width + actualWidth, actualWidth, 'left')
|
||||
} else {
|
||||
this.replaceArrow(actualHeight - height, actualHeight, 'top')
|
||||
}
|
||||
|
||||
if (replace) $tip.offset(offset)
|
||||
}
|
||||
|
||||
Tooltip.prototype.replaceArrow = function (delta, dimension, position) {
|
||||
this.arrow().css(position, delta ? (50 * (1 - delta / dimension) + '%') : '')
|
||||
}
|
||||
|
||||
Tooltip.prototype.setContent = function () {
|
||||
var $tip = this.tip()
|
||||
var title = this.getTitle()
|
||||
|
||||
$tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title)
|
||||
$tip.removeClass('fade in top bottom left right')
|
||||
}
|
||||
|
||||
Tooltip.prototype.hide = function () {
|
||||
var that = this
|
||||
var $tip = this.tip()
|
||||
var e = $.Event('hide.bs.' + this.type)
|
||||
|
||||
function complete() {
|
||||
if (that.hoverState != 'in') $tip.detach()
|
||||
that.$element.trigger('hidden.bs.' + that.type)
|
||||
}
|
||||
|
||||
this.$element.trigger(e)
|
||||
|
||||
if (e.isDefaultPrevented()) return
|
||||
|
||||
$tip.removeClass('in')
|
||||
|
||||
$.support.transition && this.$tip.hasClass('fade') ?
|
||||
$tip
|
||||
.one($.support.transition.end, complete)
|
||||
.emulateTransitionEnd(150) :
|
||||
complete()
|
||||
|
||||
this.hoverState = null
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
Tooltip.prototype.fixTitle = function () {
|
||||
var $e = this.$element
|
||||
if ($e.attr('title') || typeof($e.attr('data-original-title')) != 'string') {
|
||||
$e.attr('data-original-title', $e.attr('title') || '').attr('title', '')
|
||||
}
|
||||
}
|
||||
|
||||
Tooltip.prototype.hasContent = function () {
|
||||
return this.getTitle()
|
||||
}
|
||||
|
||||
Tooltip.prototype.getPosition = function () {
|
||||
var el = this.$element[0]
|
||||
return $.extend({}, (typeof el.getBoundingClientRect == 'function') ? el.getBoundingClientRect() : {
|
||||
width: el.offsetWidth,
|
||||
height: el.offsetHeight
|
||||
}, this.$element.offset())
|
||||
}
|
||||
|
||||
Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth, actualHeight) {
|
||||
return placement == 'bottom' ? { top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2 } :
|
||||
placement == 'top' ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2 } :
|
||||
placement == 'left' ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } :
|
||||
/* placement == 'right' */ { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width }
|
||||
}
|
||||
|
||||
Tooltip.prototype.getTitle = function () {
|
||||
var title
|
||||
var $e = this.$element
|
||||
var o = this.options
|
||||
|
||||
title = $e.attr('data-original-title')
|
||||
|| (typeof o.title == 'function' ? o.title.call($e[0]) : o.title)
|
||||
|
||||
return title
|
||||
}
|
||||
|
||||
Tooltip.prototype.tip = function () {
|
||||
return this.$tip = this.$tip || $(this.options.template)
|
||||
}
|
||||
|
||||
Tooltip.prototype.arrow = function () {
|
||||
return this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow')
|
||||
}
|
||||
|
||||
Tooltip.prototype.validate = function () {
|
||||
if (!this.$element[0].parentNode) {
|
||||
this.hide()
|
||||
this.$element = null
|
||||
this.options = null
|
||||
}
|
||||
}
|
||||
|
||||
Tooltip.prototype.enable = function () {
|
||||
this.enabled = true
|
||||
}
|
||||
|
||||
Tooltip.prototype.disable = function () {
|
||||
this.enabled = false
|
||||
}
|
||||
|
||||
Tooltip.prototype.toggleEnabled = function () {
|
||||
this.enabled = !this.enabled
|
||||
}
|
||||
|
||||
Tooltip.prototype.toggle = function (e) {
|
||||
var self = e ? $(e.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type) : this
|
||||
self.tip().hasClass('in') ? self.leave(self) : self.enter(self)
|
||||
}
|
||||
|
||||
Tooltip.prototype.destroy = function () {
|
||||
clearTimeout(this.timeout)
|
||||
this.hide().$element.off('.' + this.type).removeData('bs.' + this.type)
|
||||
}
|
||||
|
||||
|
||||
// TOOLTIP PLUGIN DEFINITION
|
||||
// =========================
|
||||
|
||||
var old = $.fn.tooltip
|
||||
|
||||
$.fn.tooltip = function (option) {
|
||||
return this.each(function () {
|
||||
var $this = $(this)
|
||||
var data = $this.data('bs.tooltip')
|
||||
var options = typeof option == 'object' && option
|
||||
|
||||
if (!data && option == 'destroy') return
|
||||
if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options)))
|
||||
if (typeof option == 'string') data[option]()
|
||||
})
|
||||
}
|
||||
|
||||
$.fn.tooltip.Constructor = Tooltip
|
||||
|
||||
|
||||
// TOOLTIP NO CONFLICT
|
||||
// ===================
|
||||
|
||||
$.fn.tooltip.noConflict = function () {
|
||||
$.fn.tooltip = old
|
||||
return this
|
||||
}
|
||||
|
||||
}(jQuery);
|
|
@ -0,0 +1,97 @@
|
|||
/**
|
||||
* Provides statistics for the local stream.
|
||||
*/
|
||||
var LocalStatsCollector = (function() {
|
||||
/**
|
||||
* Size of the webaudio analizer buffer.
|
||||
* @type {number}
|
||||
*/
|
||||
var WEBAUDIO_ANALIZER_FFT_SIZE = 512;
|
||||
|
||||
/**
|
||||
* Value of the webaudio analizer smoothing time parameter.
|
||||
* @type {number}
|
||||
*/
|
||||
var WEBAUDIO_ANALIZER_SMOOTING_TIME = 0.1;
|
||||
|
||||
/**
|
||||
* <tt>LocalStatsCollector</tt> calculates statistics for the local stream.
|
||||
*
|
||||
* @param stream the local stream
|
||||
* @param interval stats refresh interval given in ms.
|
||||
* @param {function(LocalStatsCollector)} updateCallback the callback called on stats
|
||||
* update.
|
||||
* @constructor
|
||||
*/
|
||||
function LocalStatsCollectorProto(stream, interval, updateCallback) {
|
||||
window.AudioContext = window.AudioContext || window.webkitAudioContext;
|
||||
this.stream = stream;
|
||||
this.intervalId = null;
|
||||
this.intervalMilis = interval;
|
||||
this.updateCallback = updateCallback;
|
||||
this.audioLevel = 0;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Starts the collecting the statistics.
|
||||
*/
|
||||
LocalStatsCollectorProto.prototype.start = function () {
|
||||
if (!window.AudioContext)
|
||||
return;
|
||||
|
||||
var context = new AudioContext();
|
||||
var analyser = context.createAnalyser();
|
||||
analyser.smoothingTimeConstant = WEBAUDIO_ANALIZER_SMOOTING_TIME;
|
||||
analyser.fftSize = WEBAUDIO_ANALIZER_FFT_SIZE;
|
||||
|
||||
|
||||
var source = context.createMediaStreamSource(this.stream);
|
||||
source.connect(analyser);
|
||||
|
||||
|
||||
var self = this;
|
||||
|
||||
this.intervalId = setInterval(
|
||||
function () {
|
||||
var array = new Uint8Array(analyser.frequencyBinCount);
|
||||
analyser.getByteFrequencyData(array);
|
||||
self.audioLevel = FrequencyDataToAudioLevel(array);
|
||||
self.updateCallback(self);
|
||||
},
|
||||
this.intervalMilis
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops collecting the statistics.
|
||||
*/
|
||||
LocalStatsCollectorProto.prototype.stop = function () {
|
||||
if (this.intervalId) {
|
||||
clearInterval(this.intervalId);
|
||||
this.intervalId = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Converts frequency data array to audio level.
|
||||
* @param array the frequency data array.
|
||||
* @returns {number} the audio level
|
||||
*/
|
||||
var FrequencyDataToAudioLevel = function (array) {
|
||||
var maxVolume = 0;
|
||||
|
||||
var length = array.length;
|
||||
|
||||
for (var i = 0; i < length; i++) {
|
||||
if (maxVolume < array[i])
|
||||
maxVolume = array[i];
|
||||
}
|
||||
|
||||
return maxVolume / 255;
|
||||
}
|
||||
|
||||
return LocalStatsCollectorProto;
|
||||
})();
|
31
muc.js
31
muc.js
|
@ -21,6 +21,9 @@ Strophe.addConnectionPlugin('emuc', {
|
|||
},
|
||||
doJoin: function (jid, password) {
|
||||
this.myroomjid = jid;
|
||||
|
||||
console.info("Joined MUC as " + this.myroomjid);
|
||||
|
||||
this.initPresenceMap(this.myroomjid);
|
||||
|
||||
if (!this.roomjid) {
|
||||
|
@ -167,12 +170,36 @@ Strophe.addConnectionPlugin('emuc', {
|
|||
}
|
||||
this.connection.send(msg);
|
||||
},
|
||||
setSubject: function (subject){
|
||||
var msg = $msg({to: this.roomjid, type: 'groupchat'});
|
||||
msg.c('subject', subject);
|
||||
this.connection.send(msg);
|
||||
console.log("topic changed to " + subject);
|
||||
},
|
||||
onMessage: function (msg) {
|
||||
var txt = $(msg).find('>body').text();
|
||||
// TODO: <subject/>
|
||||
// FIXME: this is a hack. but jingle on muc makes nickchanges hard
|
||||
var from = msg.getAttribute('from');
|
||||
var nick = $(msg).find('>nick[xmlns="http://jabber.org/protocol/nick"]').text() || Strophe.getResourceFromJid(from);
|
||||
|
||||
var txt = $(msg).find('>body').text();
|
||||
var type = msg.getAttribute("type");
|
||||
if(type == "error")
|
||||
{
|
||||
Chat.chatAddError($(msg).find('>text').text(), txt);
|
||||
return true;
|
||||
}
|
||||
|
||||
var subject = $(msg).find('>subject');
|
||||
if(subject.length)
|
||||
{
|
||||
var subjectText = subject.text();
|
||||
if(subjectText || subjectText == "") {
|
||||
Chat.chatSetSubject(subjectText);
|
||||
console.log("Subject is changed to " + subjectText);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (txt) {
|
||||
console.log('chat', nick, txt);
|
||||
|
||||
|
|
12
prezi.js
12
prezi.js
|
@ -19,10 +19,10 @@ var Prezi = (function (my) {
|
|||
$(document).trigger("video.selected", [true]);
|
||||
|
||||
$('#largeVideo').fadeOut(300, function () {
|
||||
setLargeVideoVisible(false);
|
||||
VideoLayout.setLargeVideoVisible(false);
|
||||
$('#presentation>iframe').fadeIn(300, function() {
|
||||
$('#presentation>iframe').css({opacity:'1'});
|
||||
dockToolbar(true);
|
||||
Toolbar.dockToolbar(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -32,8 +32,8 @@ var Prezi = (function (my) {
|
|||
$('#presentation>iframe').css({opacity:'0'});
|
||||
$('#reloadPresentation').css({display:'none'});
|
||||
$('#largeVideo').fadeIn(300, function() {
|
||||
setLargeVideoVisible(true);
|
||||
dockToolbar(false);
|
||||
VideoLayout.setLargeVideoVisible(true);
|
||||
Toolbar.dockToolbar(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -177,8 +177,8 @@ var Prezi = (function (my) {
|
|||
// We explicitly don't specify the peer jid here, because we don't want
|
||||
// this video to be dealt with as a peer related one (for example we
|
||||
// don't want to show a mute/kick menu for this one, etc.).
|
||||
addRemoteVideoContainer(null, elementId);
|
||||
resizeThumbnails();
|
||||
VideoLayout.addRemoteVideoContainer(null, elementId);
|
||||
VideoLayout.resizeThumbnails();
|
||||
|
||||
var controlsEnabled = false;
|
||||
if (jid === connection.emuc.myroomjid)
|
||||
|
|
|
@ -0,0 +1,287 @@
|
|||
/* global ssrc2jid */
|
||||
|
||||
/**
|
||||
* Function object which once created can be used to calculate moving average of
|
||||
* given period. Example for SMA3:</br>
|
||||
* var sma3 = new SimpleMovingAverager(3);
|
||||
* while(true) // some update loop
|
||||
* {
|
||||
* var currentSma3Value = sma3(nextInputValue);
|
||||
* }
|
||||
*
|
||||
* @param period moving average period that will be used by created instance.
|
||||
* @returns {Function} SMA calculator function of given <tt>period</tt>.
|
||||
* @constructor
|
||||
*/
|
||||
function SimpleMovingAverager(period)
|
||||
{
|
||||
var nums = [];
|
||||
return function (num)
|
||||
{
|
||||
nums.push(num);
|
||||
if (nums.length > period)
|
||||
nums.splice(0, 1);
|
||||
var sum = 0;
|
||||
for (var i in nums)
|
||||
sum += nums[i];
|
||||
var n = period;
|
||||
if (nums.length < period)
|
||||
n = nums.length;
|
||||
return (sum / n);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Peer statistics data holder.
|
||||
* @constructor
|
||||
*/
|
||||
function PeerStats()
|
||||
{
|
||||
this.ssrc2Loss = {};
|
||||
this.ssrc2AudioLevel = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets packets loss rate for given <tt>ssrc</tt> that blong to the peer
|
||||
* represented by this instance.
|
||||
* @param ssrc audio or video RTP stream SSRC.
|
||||
* @param lossRate new packet loss rate value to be set.
|
||||
*/
|
||||
PeerStats.prototype.setSsrcLoss = function (ssrc, lossRate)
|
||||
{
|
||||
this.ssrc2Loss[ssrc] = lossRate;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets new audio level(input or output) for given <tt>ssrc</tt> that identifies
|
||||
* the stream which belongs to the peer represented by this instance.
|
||||
* @param ssrc RTP stream SSRC for which current audio level value will be
|
||||
* updated.
|
||||
* @param audioLevel the new audio level value to be set. Value is truncated to
|
||||
* fit the range from 0 to 1.
|
||||
*/
|
||||
PeerStats.prototype.setSsrcAudioLevel = function (ssrc, audioLevel)
|
||||
{
|
||||
// Range limit 0 - 1
|
||||
this.ssrc2AudioLevel[ssrc] = Math.min(Math.max(audioLevel, 0), 1);
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates average packet loss for all streams that belong to the peer
|
||||
* represented by this instance.
|
||||
* @returns {number} average packet loss for all streams that belong to the peer
|
||||
* represented by this instance.
|
||||
*/
|
||||
PeerStats.prototype.getAvgLoss = function ()
|
||||
{
|
||||
var self = this;
|
||||
var avg = 0;
|
||||
var count = Object.keys(this.ssrc2Loss).length;
|
||||
Object.keys(this.ssrc2Loss).forEach(
|
||||
function (ssrc)
|
||||
{
|
||||
avg += self.ssrc2Loss[ssrc];
|
||||
}
|
||||
);
|
||||
return count > 0 ? avg / count : 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* <tt>StatsCollector</tt> registers for stats updates of given
|
||||
* <tt>peerconnection</tt> in given <tt>interval</tt>. On each update particular
|
||||
* stats are extracted and put in {@link PeerStats} objects. Once the processing
|
||||
* is done <tt>updateCallback</tt> is called with <tt>this</tt> instance as
|
||||
* an event source.
|
||||
*
|
||||
* @param peerconnection webRTC peer connection object.
|
||||
* @param interval stats refresh interval given in ms.
|
||||
* @param {function(StatsCollector)} updateCallback the callback called on stats
|
||||
* update.
|
||||
* @constructor
|
||||
*/
|
||||
function StatsCollector(peerconnection, interval, updateCallback)
|
||||
{
|
||||
this.peerconnection = peerconnection;
|
||||
this.baselineReport = null;
|
||||
this.currentReport = null;
|
||||
this.intervalId = null;
|
||||
// Updates stats interval
|
||||
this.intervalMilis = interval;
|
||||
// Use SMA 3 to average packet loss changes over time
|
||||
this.sma3 = new SimpleMovingAverager(3);
|
||||
// Map of jids to PeerStats
|
||||
this.jid2stats = {};
|
||||
|
||||
this.updateCallback = updateCallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops stats updates.
|
||||
*/
|
||||
StatsCollector.prototype.stop = function ()
|
||||
{
|
||||
if (this.intervalId)
|
||||
{
|
||||
clearInterval(this.intervalId);
|
||||
this.intervalId = null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback passed to <tt>getStats</tt> method.
|
||||
* @param error an error that occurred on <tt>getStats</tt> call.
|
||||
*/
|
||||
StatsCollector.prototype.errorCallback = function (error)
|
||||
{
|
||||
console.error("Get stats error", error);
|
||||
this.stop();
|
||||
};
|
||||
|
||||
/**
|
||||
* Starts stats updates.
|
||||
*/
|
||||
StatsCollector.prototype.start = function ()
|
||||
{
|
||||
var self = this;
|
||||
this.intervalId = setInterval(
|
||||
function ()
|
||||
{
|
||||
// Interval updates
|
||||
self.peerconnection.getStats(
|
||||
function (report)
|
||||
{
|
||||
var results = report.result();
|
||||
//console.error("Got interval report", results);
|
||||
self.currentReport = results;
|
||||
self.processReport();
|
||||
self.baselineReport = self.currentReport;
|
||||
},
|
||||
self.errorCallback
|
||||
);
|
||||
},
|
||||
self.intervalMilis
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Stats processing logic.
|
||||
*/
|
||||
StatsCollector.prototype.processReport = function ()
|
||||
{
|
||||
if (!this.baselineReport)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (var idx in this.currentReport)
|
||||
{
|
||||
var now = this.currentReport[idx];
|
||||
if (now.type != 'ssrc')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var before = this.baselineReport[idx];
|
||||
if (!before)
|
||||
{
|
||||
console.warn(now.stat('ssrc') + ' not enough data');
|
||||
continue;
|
||||
}
|
||||
|
||||
var ssrc = now.stat('ssrc');
|
||||
var jid = ssrc2jid[ssrc];
|
||||
if (!jid)
|
||||
{
|
||||
console.warn("No jid for ssrc: " + ssrc);
|
||||
continue;
|
||||
}
|
||||
|
||||
var jidStats = this.jid2stats[jid];
|
||||
if (!jidStats)
|
||||
{
|
||||
jidStats = new PeerStats();
|
||||
this.jid2stats[jid] = jidStats;
|
||||
}
|
||||
|
||||
// Audio level
|
||||
var audioLevel = now.stat('audioInputLevel');
|
||||
if (!audioLevel)
|
||||
audioLevel = now.stat('audioOutputLevel');
|
||||
if (audioLevel)
|
||||
{
|
||||
// TODO: can't find specs about what this value really is,
|
||||
// but it seems to vary between 0 and around 32k.
|
||||
audioLevel = audioLevel / 32767;
|
||||
jidStats.setSsrcAudioLevel(ssrc, audioLevel);
|
||||
}
|
||||
|
||||
var key = 'packetsReceived';
|
||||
if (!now.stat(key))
|
||||
{
|
||||
key = 'packetsSent';
|
||||
if (!now.stat(key))
|
||||
{
|
||||
console.error("No packetsReceived nor packetSent stat found");
|
||||
this.stop();
|
||||
return;
|
||||
}
|
||||
}
|
||||
var packetsNow = now.stat(key);
|
||||
var packetsBefore = before.stat(key);
|
||||
var packetRate = packetsNow - packetsBefore;
|
||||
|
||||
var currentLoss = now.stat('packetsLost');
|
||||
var previousLoss = before.stat('packetsLost');
|
||||
var lossRate = currentLoss - previousLoss;
|
||||
|
||||
var packetsTotal = (packetRate + lossRate);
|
||||
var lossPercent;
|
||||
|
||||
if (packetsTotal > 0)
|
||||
lossPercent = lossRate / packetsTotal;
|
||||
else
|
||||
lossPercent = 0;
|
||||
|
||||
//console.info(jid + " ssrc: " + ssrc + " " + key + ": " + packetsNow);
|
||||
|
||||
jidStats.setSsrcLoss(ssrc, lossPercent);
|
||||
}
|
||||
|
||||
var self = this;
|
||||
// Jid stats
|
||||
var allPeersAvg = 0;
|
||||
var jids = Object.keys(this.jid2stats);
|
||||
jids.forEach(
|
||||
function (jid)
|
||||
{
|
||||
var peerAvg = self.jid2stats[jid].getAvgLoss(
|
||||
function (avg)
|
||||
{
|
||||
//console.info(jid + " stats: " + (avg * 100) + " %");
|
||||
allPeersAvg += avg;
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
if (jids.length > 1)
|
||||
{
|
||||
// Our streams loss is reported as 0 always, so -1 to length
|
||||
allPeersAvg = allPeersAvg / (jids.length - 1);
|
||||
|
||||
/**
|
||||
* Calculates number of connection quality bars from 4(hi) to 0(lo).
|
||||
*/
|
||||
var outputAvg = self.sma3(allPeersAvg);
|
||||
// Linear from 4(0%) to 0(25%).
|
||||
var quality = Math.round(4 - outputAvg * 16);
|
||||
quality = Math.max(quality, 0); // lower limit 0
|
||||
quality = Math.min(quality, 4); // upper limit 4
|
||||
// TODO: quality can be used to indicate connection quality using 4 step
|
||||
// bar indicator
|
||||
//console.info("Loss SMA3: " + outputAvg + " Q: " + quality);
|
||||
}
|
||||
|
||||
self.updateCallback(self);
|
||||
};
|
||||
|
|
@ -0,0 +1,234 @@
|
|||
var Toolbar = (function (my) {
|
||||
var INITIAL_TOOLBAR_TIMEOUT = 20000;
|
||||
var TOOLBAR_TIMEOUT = INITIAL_TOOLBAR_TIMEOUT;
|
||||
|
||||
/**
|
||||
* Opens the lock room dialog.
|
||||
*/
|
||||
my.openLockDialog = function() {
|
||||
// Only the focus is able to set a shared key.
|
||||
if (focus === null) {
|
||||
if (sharedKey)
|
||||
$.prompt("This conversation is currently protected by"
|
||||
+ " a shared secret key.",
|
||||
{
|
||||
title: "Secrect key",
|
||||
persistent: false
|
||||
}
|
||||
);
|
||||
else
|
||||
$.prompt("This conversation isn't currently protected by"
|
||||
+ " a secret key. Only the owner of the conference" +
|
||||
+ " could set a shared key.",
|
||||
{
|
||||
title: "Secrect key",
|
||||
persistent: false
|
||||
}
|
||||
);
|
||||
} else {
|
||||
if (sharedKey) {
|
||||
$.prompt("Are you sure you would like to remove your secret key?",
|
||||
{
|
||||
title: "Remove secrect key",
|
||||
persistent: false,
|
||||
buttons: { "Remove": true, "Cancel": false},
|
||||
defaultButton: 1,
|
||||
submit: function (e, v, m, f) {
|
||||
if (v) {
|
||||
setSharedKey('');
|
||||
lockRoom(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
$.prompt('<h2>Set a secrect key to lock your room</h2>' +
|
||||
'<input id="lockKey" type="text" placeholder="your shared key" autofocus>',
|
||||
{
|
||||
persistent: false,
|
||||
buttons: { "Save": true, "Cancel": false},
|
||||
defaultButton: 1,
|
||||
loaded: function (event) {
|
||||
document.getElementById('lockKey').focus();
|
||||
},
|
||||
submit: function (e, v, m, f) {
|
||||
if (v) {
|
||||
var lockKey = document.getElementById('lockKey');
|
||||
|
||||
if (lockKey.value) {
|
||||
setSharedKey(Util.escapeHtml(lockKey.value));
|
||||
lockRoom(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Opens the invite link dialog.
|
||||
*/
|
||||
my.openLinkDialog = function() {
|
||||
$.prompt('<input id="inviteLinkRef" type="text" value="' +
|
||||
encodeURI(roomUrl) + '" onclick="this.select();" readonly>',
|
||||
{
|
||||
title: "Share this link with everyone you want to invite",
|
||||
persistent: false,
|
||||
buttons: { "Cancel": false},
|
||||
loaded: function (event) {
|
||||
document.getElementById('inviteLinkRef').select();
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Opens the settings dialog.
|
||||
*/
|
||||
my.openSettingsDialog = function() {
|
||||
$.prompt('<h2>Configure your conference</h2>' +
|
||||
'<input type="checkbox" id="initMuted"> Participants join muted<br/>' +
|
||||
'<input type="checkbox" id="requireNicknames"> Require nicknames<br/><br/>' +
|
||||
'Set a secrect key to lock your room: <input id="lockKey" type="text" placeholder="your shared key" autofocus>',
|
||||
{
|
||||
persistent: false,
|
||||
buttons: { "Save": true, "Cancel": false},
|
||||
defaultButton: 1,
|
||||
loaded: function (event) {
|
||||
document.getElementById('lockKey').focus();
|
||||
},
|
||||
submit: function (e, v, m, f) {
|
||||
if (v) {
|
||||
if ($('#initMuted').is(":checked")) {
|
||||
// it is checked
|
||||
}
|
||||
|
||||
if ($('#requireNicknames').is(":checked")) {
|
||||
// it is checked
|
||||
}
|
||||
/*
|
||||
var lockKey = document.getElementById('lockKey');
|
||||
|
||||
if (lockKey.value)
|
||||
{
|
||||
setSharedKey(lockKey.value);
|
||||
lockRoom(true);
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggles the application in and out of full screen mode
|
||||
* (a.k.a. presentation mode in Chrome).
|
||||
*/
|
||||
my.toggleFullScreen = function() {
|
||||
var fsElement = document.documentElement;
|
||||
|
||||
if (!document.mozFullScreen && !document.webkitIsFullScreen) {
|
||||
//Enter Full Screen
|
||||
if (fsElement.mozRequestFullScreen) {
|
||||
fsElement.mozRequestFullScreen();
|
||||
}
|
||||
else {
|
||||
fsElement.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT);
|
||||
}
|
||||
} else {
|
||||
//Exit Full Screen
|
||||
if (document.mozCancelFullScreen) {
|
||||
document.mozCancelFullScreen();
|
||||
} else {
|
||||
document.webkitCancelFullScreen();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows the main toolbar.
|
||||
*/
|
||||
my.showToolbar = function() {
|
||||
if (!$('#header').is(':visible')) {
|
||||
$('#header').show("slide", { direction: "up", duration: 300});
|
||||
$('#subject').animate({top: "+=40"}, 300);
|
||||
|
||||
if (toolbarTimeout) {
|
||||
clearTimeout(toolbarTimeout);
|
||||
toolbarTimeout = null;
|
||||
}
|
||||
toolbarTimeout = setTimeout(hideToolbar, TOOLBAR_TIMEOUT);
|
||||
TOOLBAR_TIMEOUT = 4000;
|
||||
}
|
||||
|
||||
if (focus != null)
|
||||
{
|
||||
// TODO: Enable settings functionality. Need to uncomment the settings button in index.html.
|
||||
// $('#settingsButton').css({visibility:"visible"});
|
||||
}
|
||||
|
||||
// Show/hide desktop sharing button
|
||||
showDesktopSharingButton();
|
||||
};
|
||||
|
||||
/**
|
||||
* Docks/undocks the toolbar.
|
||||
*
|
||||
* @param isDock indicates what operation to perform
|
||||
*/
|
||||
my.dockToolbar = function(isDock) {
|
||||
if (isDock) {
|
||||
// First make sure the toolbar is shown.
|
||||
if (!$('#header').is(':visible')) {
|
||||
Toolbar.showToolbar();
|
||||
}
|
||||
// Then clear the time out, to dock the toolbar.
|
||||
clearTimeout(toolbarTimeout);
|
||||
toolbarTimeout = null;
|
||||
}
|
||||
else {
|
||||
if (!$('#header').is(':visible')) {
|
||||
Toolbar.showToolbar();
|
||||
}
|
||||
else {
|
||||
toolbarTimeout = setTimeout(hideToolbar, TOOLBAR_TIMEOUT);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the lock button state.
|
||||
*/
|
||||
my.updateLockButton = function() {
|
||||
buttonClick("#lockIcon", "icon-security icon-security-locked");
|
||||
};
|
||||
|
||||
/**
|
||||
* Hides the toolbar.
|
||||
*/
|
||||
var hideToolbar = function () {
|
||||
var isToolbarHover = false;
|
||||
$('#header').find('*').each(function () {
|
||||
var id = $(this).attr('id');
|
||||
if ($("#" + id + ":hover").length > 0) {
|
||||
isToolbarHover = true;
|
||||
}
|
||||
});
|
||||
|
||||
clearTimeout(toolbarTimeout);
|
||||
toolbarTimeout = null;
|
||||
|
||||
if (!isToolbarHover) {
|
||||
$('#header').hide("slide", { direction: "up", duration: 300});
|
||||
$('#subject').animate({top: "-=40"}, 300);
|
||||
}
|
||||
else {
|
||||
toolbarTimeout = setTimeout(hideToolbar, TOOLBAR_TIMEOUT);
|
||||
}
|
||||
};
|
||||
|
||||
return my;
|
||||
}(Toolbar || {}));
|
27
util.js
27
util.js
|
@ -51,10 +51,35 @@ var Util = (function (my) {
|
|||
* Returns the available video width.
|
||||
*/
|
||||
my.getAvailableVideoWidth = function () {
|
||||
var chatspaceWidth = $('#chatspace').is(":visible") ? $('#chatspace').width() : 0;
|
||||
var chatspaceWidth
|
||||
= $('#chatspace').is(":visible") ? $('#chatspace').width() : 0;
|
||||
|
||||
return window.innerWidth - chatspaceWidth;
|
||||
};
|
||||
|
||||
my.imageToGrayScale = function (canvas) {
|
||||
var context = canvas.getContext('2d');
|
||||
var imgData = context.getImageData(0, 0, canvas.width, canvas.height);
|
||||
var pixels = imgData.data;
|
||||
|
||||
for (var i = 0, n = pixels.length; i < n; i += 4) {
|
||||
var grayscale
|
||||
= pixels[i] * .3 + pixels[i+1] * .59 + pixels[i+2] * .11;
|
||||
pixels[i ] = grayscale; // red
|
||||
pixels[i+1] = grayscale; // green
|
||||
pixels[i+2] = grayscale; // blue
|
||||
// pixels[i+3] is alpha
|
||||
}
|
||||
// redraw the image in black & white
|
||||
context.putImageData(imgData, 0, 0);
|
||||
};
|
||||
|
||||
my.setTooltip = function (element, tooltipText, position) {
|
||||
element.setAttribute("data-content", tooltipText);
|
||||
element.setAttribute("data-toggle", "popover");
|
||||
element.setAttribute("data-placement", position);
|
||||
element.setAttribute("data-html", true);
|
||||
};
|
||||
|
||||
return my;
|
||||
}(Util || {}));
|
||||
|
|
|
@ -0,0 +1,901 @@
|
|||
var VideoLayout = (function (my) {
|
||||
var preMuted = false;
|
||||
var currentActiveSpeaker = null;
|
||||
|
||||
my.changeLocalAudio = function(stream) {
|
||||
connection.jingle.localAudio = stream;
|
||||
|
||||
RTC.attachMediaStream($('#localAudio'), stream);
|
||||
document.getElementById('localAudio').autoplay = true;
|
||||
document.getElementById('localAudio').volume = 0;
|
||||
if (preMuted) {
|
||||
toggleAudio();
|
||||
preMuted = false;
|
||||
}
|
||||
};
|
||||
|
||||
my.changeLocalVideo = function(stream, flipX) {
|
||||
connection.jingle.localVideo = stream;
|
||||
|
||||
var localVideo = document.createElement('video');
|
||||
localVideo.id = 'localVideo_' + stream.id;
|
||||
localVideo.autoplay = true;
|
||||
localVideo.volume = 0; // is it required if audio is separated ?
|
||||
localVideo.oncontextmenu = function () { return false; };
|
||||
|
||||
var localVideoContainer = document.getElementById('localVideoWrapper');
|
||||
localVideoContainer.appendChild(localVideo);
|
||||
|
||||
var localVideoSelector = $('#' + localVideo.id);
|
||||
// Add click handler
|
||||
localVideoSelector.click(function () {
|
||||
VideoLayout.handleVideoThumbClicked(localVideo.src);
|
||||
});
|
||||
// Add hover handler
|
||||
$('#localVideoContainer').hover(
|
||||
function() {
|
||||
VideoLayout.showDisplayName('localVideoContainer', true);
|
||||
},
|
||||
function() {
|
||||
if (focusedVideoSrc !== localVideo.src)
|
||||
VideoLayout.showDisplayName('localVideoContainer', false);
|
||||
}
|
||||
);
|
||||
// Add stream ended handler
|
||||
stream.onended = function () {
|
||||
localVideoContainer.removeChild(localVideo);
|
||||
VideoLayout.checkChangeLargeVideo(localVideo.src);
|
||||
};
|
||||
// Flip video x axis if needed
|
||||
flipXLocalVideo = flipX;
|
||||
if (flipX) {
|
||||
localVideoSelector.addClass("flipVideoX");
|
||||
}
|
||||
// Attach WebRTC stream
|
||||
RTC.attachMediaStream(localVideoSelector, stream);
|
||||
|
||||
localVideoSrc = localVideo.src;
|
||||
VideoLayout.updateLargeVideo(localVideoSrc, 0);
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if removed video is currently displayed and tries to display
|
||||
* another one instead.
|
||||
* @param removedVideoSrc src stream identifier of the video.
|
||||
*/
|
||||
my.checkChangeLargeVideo = function(removedVideoSrc) {
|
||||
if (removedVideoSrc === $('#largeVideo').attr('src')) {
|
||||
// this is currently displayed as large
|
||||
// pick the last visible video in the row
|
||||
// if nobody else is left, this picks the local video
|
||||
var pick
|
||||
= $('#remoteVideos>span[id!="mixedstream"]:visible:last>video')
|
||||
.get(0);
|
||||
|
||||
if (!pick) {
|
||||
console.info("Last visible video no longer exists");
|
||||
pick = $('#remoteVideos>span[id!="mixedstream"]>video').get(0);
|
||||
if (!pick) {
|
||||
// Try local video
|
||||
console.info("Fallback to local video...");
|
||||
pick = $('#remoteVideos>span>span>video').get(0);
|
||||
}
|
||||
}
|
||||
|
||||
// mute if localvideo
|
||||
if (pick) {
|
||||
VideoLayout.updateLargeVideo(pick.src, pick.volume);
|
||||
} else {
|
||||
console.warn("Failed to elect large video");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Updates the large video with the given new video source.
|
||||
*/
|
||||
my.updateLargeVideo = function(newSrc, vol) {
|
||||
console.log('hover in', newSrc);
|
||||
|
||||
if ($('#largeVideo').attr('src') != newSrc) {
|
||||
|
||||
var isVisible = $('#largeVideo').is(':visible');
|
||||
|
||||
$('#largeVideo').fadeOut(300, function () {
|
||||
$(this).attr('src', newSrc);
|
||||
|
||||
// Screen stream is already rotated
|
||||
var flipX = (newSrc === localVideoSrc) && flipXLocalVideo;
|
||||
|
||||
var videoTransform = document.getElementById('largeVideo')
|
||||
.style.webkitTransform;
|
||||
|
||||
if (flipX && videoTransform !== 'scaleX(-1)') {
|
||||
document.getElementById('largeVideo').style.webkitTransform
|
||||
= "scaleX(-1)";
|
||||
}
|
||||
else if (!flipX && videoTransform === 'scaleX(-1)') {
|
||||
document.getElementById('largeVideo').style.webkitTransform
|
||||
= "none";
|
||||
}
|
||||
|
||||
// Change the way we'll be measuring and positioning large video
|
||||
var isDesktop = isVideoSrcDesktop(newSrc);
|
||||
getVideoSize = isDesktop
|
||||
? getDesktopVideoSize
|
||||
: getCameraVideoSize;
|
||||
getVideoPosition = isDesktop
|
||||
? getDesktopVideoPosition
|
||||
: getCameraVideoPosition;
|
||||
|
||||
if (isVisible)
|
||||
$(this).fadeIn(300);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
my.handleVideoThumbClicked = function(videoSrc) {
|
||||
// Restore style for previously focused video
|
||||
var focusJid = getJidFromVideoSrc(focusedVideoSrc);
|
||||
var oldContainer = getParticipantContainer(focusJid);
|
||||
|
||||
if (oldContainer) {
|
||||
oldContainer.removeClass("videoContainerFocused");
|
||||
VideoLayout.enableActiveSpeaker(
|
||||
Strophe.getResourceFromJid(focusJid), false);
|
||||
}
|
||||
|
||||
// Unlock current focused.
|
||||
if (focusedVideoSrc === videoSrc)
|
||||
{
|
||||
focusedVideoSrc = null;
|
||||
// Enable the currently set active speaker.
|
||||
if (currentActiveSpeaker) {
|
||||
VideoLayout.enableActiveSpeaker(currentActiveSpeaker, true);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
// Remove style for current active speaker if we're going to lock
|
||||
// another video.
|
||||
else if (currentActiveSpeaker) {
|
||||
VideoLayout.enableActiveSpeaker(currentActiveSpeaker, false);
|
||||
}
|
||||
|
||||
// Lock new video
|
||||
focusedVideoSrc = videoSrc;
|
||||
|
||||
var userJid = getJidFromVideoSrc(videoSrc);
|
||||
if (userJid)
|
||||
{
|
||||
var container = getParticipantContainer(userJid);
|
||||
container.addClass("videoContainerFocused");
|
||||
|
||||
var resourceJid = Strophe.getResourceFromJid(userJid);
|
||||
VideoLayout.enableActiveSpeaker(resourceJid, true);
|
||||
}
|
||||
|
||||
$(document).trigger("video.selected", [false]);
|
||||
|
||||
VideoLayout.updateLargeVideo(videoSrc, 1);
|
||||
|
||||
$('audio').each(function (idx, el) {
|
||||
if (el.id.indexOf('mixedmslabel') !== -1) {
|
||||
el.volume = 0;
|
||||
el.volume = 1;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Positions the large video.
|
||||
*
|
||||
* @param videoWidth the stream video width
|
||||
* @param videoHeight the stream video height
|
||||
*/
|
||||
my.positionLarge = function (videoWidth, videoHeight) {
|
||||
var videoSpaceWidth = $('#videospace').width();
|
||||
var videoSpaceHeight = window.innerHeight;
|
||||
|
||||
var videoSize = getVideoSize(videoWidth,
|
||||
videoHeight,
|
||||
videoSpaceWidth,
|
||||
videoSpaceHeight);
|
||||
|
||||
var largeVideoWidth = videoSize[0];
|
||||
var largeVideoHeight = videoSize[1];
|
||||
|
||||
var videoPosition = getVideoPosition(largeVideoWidth,
|
||||
largeVideoHeight,
|
||||
videoSpaceWidth,
|
||||
videoSpaceHeight);
|
||||
|
||||
var horizontalIndent = videoPosition[0];
|
||||
var verticalIndent = videoPosition[1];
|
||||
|
||||
positionVideo($('#largeVideo'),
|
||||
largeVideoWidth,
|
||||
largeVideoHeight,
|
||||
horizontalIndent, verticalIndent);
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows/hides the large video.
|
||||
*/
|
||||
my.setLargeVideoVisible = function(isVisible) {
|
||||
if (isVisible) {
|
||||
$('#largeVideo').css({visibility: 'visible'});
|
||||
$('.watermark').css({visibility: 'visible'});
|
||||
}
|
||||
else {
|
||||
$('#largeVideo').css({visibility: 'hidden'});
|
||||
$('.watermark').css({visibility: 'hidden'});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Checks if container for participant identified by given peerJid exists
|
||||
* in the document and creates it eventually.
|
||||
*
|
||||
* @param peerJid peer Jid to check.
|
||||
*/
|
||||
my.ensurePeerContainerExists = function(peerJid) {
|
||||
var peerResource = Strophe.getResourceFromJid(peerJid);
|
||||
var videoSpanId = 'participant_' + peerResource;
|
||||
|
||||
if ($('#' + videoSpanId).length > 0) {
|
||||
// If there's been a focus change, make sure we add focus related
|
||||
// interface!!
|
||||
if (focus && $('#remote_popupmenu_' + peerResource).length <= 0)
|
||||
addRemoteVideoMenu( peerJid,
|
||||
document.getElementById(videoSpanId));
|
||||
return;
|
||||
}
|
||||
|
||||
var container
|
||||
= VideoLayout.addRemoteVideoContainer(peerJid, videoSpanId);
|
||||
|
||||
var nickfield = document.createElement('span');
|
||||
nickfield.className = "nick";
|
||||
nickfield.appendChild(document.createTextNode(peerResource));
|
||||
container.appendChild(nickfield);
|
||||
VideoLayout.resizeThumbnails();
|
||||
};
|
||||
|
||||
my.addRemoteVideoContainer = function(peerJid, spanId) {
|
||||
var container = document.createElement('span');
|
||||
container.id = spanId;
|
||||
container.className = 'videocontainer';
|
||||
var remotes = document.getElementById('remoteVideos');
|
||||
|
||||
// If the peerJid is null then this video span couldn't be directly
|
||||
// associated with a participant (this could happen in the case of prezi).
|
||||
if (focus && peerJid != null)
|
||||
addRemoteVideoMenu(peerJid, container);
|
||||
|
||||
remotes.appendChild(container);
|
||||
return container;
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows the display name for the given video.
|
||||
*/
|
||||
my.setDisplayName = function(videoSpanId, displayName) {
|
||||
var nameSpan = $('#' + videoSpanId + '>span.displayname');
|
||||
|
||||
// If we already have a display name for this video.
|
||||
if (nameSpan.length > 0) {
|
||||
var nameSpanElement = nameSpan.get(0);
|
||||
|
||||
if (nameSpanElement.id === 'localDisplayName' &&
|
||||
$('#localDisplayName').text() !== displayName) {
|
||||
$('#localDisplayName').text(displayName);
|
||||
} else {
|
||||
$('#' + videoSpanId + '_name').text(displayName);
|
||||
}
|
||||
} else {
|
||||
var editButton = null;
|
||||
|
||||
if (videoSpanId === 'localVideoContainer') {
|
||||
editButton = createEditDisplayNameButton();
|
||||
}
|
||||
if (displayName.length) {
|
||||
nameSpan = document.createElement('span');
|
||||
nameSpan.className = 'displayname';
|
||||
nameSpan.innerText = displayName;
|
||||
$('#' + videoSpanId)[0].appendChild(nameSpan);
|
||||
}
|
||||
|
||||
if (!editButton) {
|
||||
nameSpan.id = videoSpanId + '_name';
|
||||
} else {
|
||||
nameSpan.id = 'localDisplayName';
|
||||
$('#' + videoSpanId)[0].appendChild(editButton);
|
||||
|
||||
var editableText = document.createElement('input');
|
||||
editableText.className = 'displayname';
|
||||
editableText.id = 'editDisplayName';
|
||||
|
||||
if (displayName.length) {
|
||||
editableText.value
|
||||
= displayName.substring(0, displayName.indexOf(' (me)'));
|
||||
}
|
||||
|
||||
editableText.setAttribute('style', 'display:none;');
|
||||
editableText.setAttribute('placeholder', 'ex. Jane Pink');
|
||||
$('#' + videoSpanId)[0].appendChild(editableText);
|
||||
|
||||
$('#localVideoContainer .displayname').bind("click", function (e) {
|
||||
e.preventDefault();
|
||||
$('#localDisplayName').hide();
|
||||
$('#editDisplayName').show();
|
||||
$('#editDisplayName').focus();
|
||||
$('#editDisplayName').select();
|
||||
|
||||
var inputDisplayNameHandler = function (name) {
|
||||
if (nickname !== name) {
|
||||
nickname = name;
|
||||
window.localStorage.displayname = nickname;
|
||||
connection.emuc.addDisplayNameToPresence(nickname);
|
||||
connection.emuc.sendPresence();
|
||||
|
||||
Chat.setChatConversationMode(true);
|
||||
}
|
||||
|
||||
if (!$('#localDisplayName').is(":visible")) {
|
||||
if (nickname) {
|
||||
$('#localDisplayName').text(nickname + " (me)");
|
||||
$('#localDisplayName').show();
|
||||
}
|
||||
else {
|
||||
$('#localDisplayName').text(nickname);
|
||||
}
|
||||
|
||||
$('#editDisplayName').hide();
|
||||
}
|
||||
};
|
||||
|
||||
$('#editDisplayName').one("focusout", function (e) {
|
||||
inputDisplayNameHandler(this.value);
|
||||
});
|
||||
|
||||
$('#editDisplayName').on('keydown', function (e) {
|
||||
if (e.keyCode === 13) {
|
||||
e.preventDefault();
|
||||
inputDisplayNameHandler(this.value);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows/hides the display name on the remote video.
|
||||
* @param videoSpanId the identifier of the video span element
|
||||
* @param isShow indicates if the display name should be shown or hidden
|
||||
*/
|
||||
my.showDisplayName = function(videoSpanId, isShow) {
|
||||
var nameSpan = $('#' + videoSpanId + '>span.displayname').get(0);
|
||||
|
||||
if (isShow) {
|
||||
if (nameSpan && nameSpan.innerHTML && nameSpan.innerHTML.length)
|
||||
nameSpan.setAttribute("style", "display:inline-block;");
|
||||
}
|
||||
else {
|
||||
if (nameSpan)
|
||||
nameSpan.setAttribute("style", "display:none;");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows a visual indicator for the focus of the conference.
|
||||
* Currently if we're not the owner of the conference we obtain the focus
|
||||
* from the connection.jingle.sessions.
|
||||
*/
|
||||
my.showFocusIndicator = function() {
|
||||
if (focus !== null) {
|
||||
var indicatorSpan = $('#localVideoContainer .focusindicator');
|
||||
|
||||
if (indicatorSpan.children().length === 0)
|
||||
{
|
||||
createFocusIndicatorElement(indicatorSpan[0]);
|
||||
}
|
||||
}
|
||||
else if (Object.keys(connection.jingle.sessions).length > 0) {
|
||||
// If we're only a participant the focus will be the only session we have.
|
||||
var session
|
||||
= connection.jingle.sessions
|
||||
[Object.keys(connection.jingle.sessions)[0]];
|
||||
var focusId
|
||||
= 'participant_' + Strophe.getResourceFromJid(session.peerjid);
|
||||
|
||||
var focusContainer = document.getElementById(focusId);
|
||||
if (!focusContainer) {
|
||||
console.error("No focus container!");
|
||||
return;
|
||||
}
|
||||
var indicatorSpan = $('#' + focusId + ' .focusindicator');
|
||||
|
||||
if (!indicatorSpan || indicatorSpan.length === 0) {
|
||||
indicatorSpan = document.createElement('span');
|
||||
indicatorSpan.className = 'focusindicator';
|
||||
Util.setTooltip(indicatorSpan,
|
||||
"The owner of<br/>this conference",
|
||||
"top");
|
||||
focusContainer.appendChild(indicatorSpan);
|
||||
|
||||
createFocusIndicatorElement(indicatorSpan);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows video muted indicator over small videos.
|
||||
*/
|
||||
my.showVideoIndicator = function(videoSpanId, isMuted) {
|
||||
var videoMutedSpan = $('#' + videoSpanId + '>span.videoMuted');
|
||||
|
||||
if (isMuted === 'false') {
|
||||
if (videoMutedSpan.length > 0) {
|
||||
videoMutedSpan.remove();
|
||||
}
|
||||
}
|
||||
else {
|
||||
var audioMutedSpan = $('#' + videoSpanId + '>span.audioMuted');
|
||||
|
||||
videoMutedSpan = document.createElement('span');
|
||||
videoMutedSpan.className = 'videoMuted';
|
||||
if (audioMutedSpan) {
|
||||
videoMutedSpan.right = '30px';
|
||||
}
|
||||
$('#' + videoSpanId)[0].appendChild(videoMutedSpan);
|
||||
|
||||
var mutedIndicator = document.createElement('i');
|
||||
mutedIndicator.className = 'icon-camera-disabled';
|
||||
Util.setTooltip(mutedIndicator,
|
||||
"Participant has<br/>stopped the camera.",
|
||||
"top");
|
||||
videoMutedSpan.appendChild(mutedIndicator);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows audio muted indicator over small videos.
|
||||
*/
|
||||
my.showAudioIndicator = function(videoSpanId, isMuted) {
|
||||
var audioMutedSpan = $('#' + videoSpanId + '>span.audioMuted');
|
||||
|
||||
if (isMuted === 'false') {
|
||||
if (audioMutedSpan.length > 0) {
|
||||
audioMutedSpan.remove();
|
||||
}
|
||||
}
|
||||
else {
|
||||
var videoMutedSpan = $('#' + videoSpanId + '>span.videoMuted');
|
||||
|
||||
audioMutedSpan = document.createElement('span');
|
||||
audioMutedSpan.className = 'audioMuted';
|
||||
Util.setTooltip(audioMutedSpan,
|
||||
"Participant is muted",
|
||||
"top");
|
||||
|
||||
if (videoMutedSpan) {
|
||||
audioMutedSpan.right = '30px';
|
||||
}
|
||||
$('#' + videoSpanId)[0].appendChild(audioMutedSpan);
|
||||
|
||||
var mutedIndicator = document.createElement('i');
|
||||
mutedIndicator.className = 'icon-mic-disabled';
|
||||
audioMutedSpan.appendChild(mutedIndicator);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Resizes the large video container.
|
||||
*/
|
||||
my.resizeLargeVideoContainer = function () {
|
||||
Chat.resizeChat();
|
||||
var availableHeight = window.innerHeight;
|
||||
var availableWidth = Util.getAvailableVideoWidth();
|
||||
|
||||
if (availableWidth < 0 || availableHeight < 0) return;
|
||||
|
||||
$('#videospace').width(availableWidth);
|
||||
$('#videospace').height(availableHeight);
|
||||
$('#largeVideoContainer').width(availableWidth);
|
||||
$('#largeVideoContainer').height(availableHeight);
|
||||
|
||||
VideoLayout.resizeThumbnails();
|
||||
};
|
||||
|
||||
/**
|
||||
* Resizes thumbnails.
|
||||
*/
|
||||
my.resizeThumbnails = function() {
|
||||
var thumbnailSize = calculateThumbnailSize();
|
||||
var width = thumbnailSize[0];
|
||||
var height = thumbnailSize[1];
|
||||
|
||||
// size videos so that while keeping AR and max height, we have a
|
||||
// nice fit
|
||||
$('#remoteVideos').height(height);
|
||||
$('#remoteVideos>span').width(width);
|
||||
$('#remoteVideos>span').height(height);
|
||||
};
|
||||
|
||||
/**
|
||||
* Enables the active speaker UI.
|
||||
*
|
||||
* @param resourceJid the jid indicating the video element to
|
||||
* activate/deactivate
|
||||
* @param isEnable indicates if the active speaker should be enabled or
|
||||
* disabled
|
||||
*/
|
||||
my.enableActiveSpeaker = function(resourceJid, isEnable) {
|
||||
var displayName = resourceJid;
|
||||
var nameSpan = $('#participant_' + resourceJid + '>span.displayname');
|
||||
if (nameSpan.length > 0)
|
||||
displayName = nameSpan.text();
|
||||
|
||||
console.log("Enable active speaker", displayName, isEnable);
|
||||
|
||||
var videoSpanId = null;
|
||||
if (resourceJid
|
||||
=== Strophe.getResourceFromJid(connection.emuc.myroomjid))
|
||||
videoSpanId = 'localVideoWrapper';
|
||||
else
|
||||
videoSpanId = 'participant_' + resourceJid;
|
||||
|
||||
videoSpan = document.getElementById(videoSpanId);
|
||||
|
||||
if (!videoSpan) {
|
||||
console.error("No video element for jid", resourceJid);
|
||||
return;
|
||||
}
|
||||
|
||||
var video = $('#' + videoSpanId + '>video');
|
||||
|
||||
if (video && video.length > 0) {
|
||||
var videoElement = video.get(0);
|
||||
if (isEnable) {
|
||||
if (!videoElement.classList.contains("activespeaker"))
|
||||
videoElement.classList.add("activespeaker");
|
||||
|
||||
VideoLayout.showDisplayName(videoSpanId, true);
|
||||
}
|
||||
else {
|
||||
VideoLayout.showDisplayName(videoSpanId, false);
|
||||
|
||||
if (videoElement.classList.contains("activespeaker"))
|
||||
videoElement.classList.remove("activespeaker");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the selector of video thumbnail container for the user identified by
|
||||
* given <tt>userJid</tt>
|
||||
* @param userJid user's Jid for whom we want to get the video container.
|
||||
*/
|
||||
function getParticipantContainer(userJid)
|
||||
{
|
||||
if (!userJid)
|
||||
return null;
|
||||
|
||||
if (userJid === connection.emuc.myroomjid)
|
||||
return $("#localVideoContainer");
|
||||
else
|
||||
return $("#participant_" + Strophe.getResourceFromJid(userJid));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the size and position of the given video element.
|
||||
*
|
||||
* @param video the video element to position
|
||||
* @param width the desired video width
|
||||
* @param height the desired video height
|
||||
* @param horizontalIndent the left and right indent
|
||||
* @param verticalIndent the top and bottom indent
|
||||
*/
|
||||
function positionVideo(video,
|
||||
width,
|
||||
height,
|
||||
horizontalIndent,
|
||||
verticalIndent) {
|
||||
video.width(width);
|
||||
video.height(height);
|
||||
video.css({ top: verticalIndent + 'px',
|
||||
bottom: verticalIndent + 'px',
|
||||
left: horizontalIndent + 'px',
|
||||
right: horizontalIndent + 'px'});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the thumbnail size.
|
||||
*/
|
||||
var calculateThumbnailSize = function () {
|
||||
// Calculate the available height, which is the inner window height minus
|
||||
// 39px for the header minus 2px for the delimiter lines on the top and
|
||||
// bottom of the large video, minus the 36px space inside the remoteVideos
|
||||
// container used for highlighting shadow.
|
||||
var availableHeight = 100;
|
||||
|
||||
var numvids = $('#remoteVideos>span:visible').length;
|
||||
|
||||
// Remove the 1px borders arround videos and the chat width.
|
||||
var availableWinWidth = $('#remoteVideos').width() - 2 * numvids - 50;
|
||||
var availableWidth = availableWinWidth / numvids;
|
||||
var aspectRatio = 16.0 / 9.0;
|
||||
var maxHeight = Math.min(160, availableHeight);
|
||||
availableHeight = Math.min(maxHeight, availableWidth / aspectRatio);
|
||||
if (availableHeight < availableWidth / aspectRatio) {
|
||||
availableWidth = Math.floor(availableHeight * aspectRatio);
|
||||
}
|
||||
|
||||
return [availableWidth, availableHeight];
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns an array of the video dimensions, so that it keeps it's aspect
|
||||
* ratio and fits available area with it's larger dimension. This method
|
||||
* ensures that whole video will be visible and can leave empty areas.
|
||||
*
|
||||
* @return an array with 2 elements, the video width and the video height
|
||||
*/
|
||||
function getDesktopVideoSize(videoWidth,
|
||||
videoHeight,
|
||||
videoSpaceWidth,
|
||||
videoSpaceHeight) {
|
||||
if (!videoWidth)
|
||||
videoWidth = currentVideoWidth;
|
||||
if (!videoHeight)
|
||||
videoHeight = currentVideoHeight;
|
||||
|
||||
var aspectRatio = videoWidth / videoHeight;
|
||||
|
||||
var availableWidth = Math.max(videoWidth, videoSpaceWidth);
|
||||
var availableHeight = Math.max(videoHeight, videoSpaceHeight);
|
||||
|
||||
videoSpaceHeight -= $('#remoteVideos').outerHeight();
|
||||
|
||||
if (availableWidth / aspectRatio >= videoSpaceHeight)
|
||||
{
|
||||
availableHeight = videoSpaceHeight;
|
||||
availableWidth = availableHeight * aspectRatio;
|
||||
}
|
||||
|
||||
if (availableHeight * aspectRatio >= videoSpaceWidth)
|
||||
{
|
||||
availableWidth = videoSpaceWidth;
|
||||
availableHeight = availableWidth / aspectRatio;
|
||||
}
|
||||
|
||||
return [availableWidth, availableHeight];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the edit display name button.
|
||||
*
|
||||
* @returns the edit button
|
||||
*/
|
||||
function createEditDisplayNameButton() {
|
||||
var editButton = document.createElement('a');
|
||||
editButton.className = 'displayname';
|
||||
Util.setTooltip(editButton,
|
||||
'Click to edit your<br/>display name',
|
||||
"top");
|
||||
editButton.innerHTML = '<i class="fa fa-pencil"></i>';
|
||||
|
||||
return editButton;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the element indicating the focus of the conference.
|
||||
*
|
||||
* @param parentElement the parent element where the focus indicator will
|
||||
* be added
|
||||
*/
|
||||
function createFocusIndicatorElement(parentElement) {
|
||||
var focusIndicator = document.createElement('i');
|
||||
focusIndicator.className = 'fa fa-star';
|
||||
parentElement.appendChild(focusIndicator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the remote video menu.
|
||||
*
|
||||
* @param jid the jid indicating the video for which we're adding a menu.
|
||||
* @param isMuted indicates the current mute state
|
||||
*/
|
||||
my.updateRemoteVideoMenu = function(jid, isMuted) {
|
||||
var muteMenuItem
|
||||
= $('#remote_popupmenu_'
|
||||
+ Strophe.getResourceFromJid(jid)
|
||||
+ '>li>a.mutelink');
|
||||
|
||||
var mutedIndicator = "<i class='icon-mic-disabled'></i>";
|
||||
|
||||
if (muteMenuItem.length) {
|
||||
var muteLink = muteMenuItem.get(0);
|
||||
|
||||
if (isMuted === 'true') {
|
||||
muteLink.innerHTML = mutedIndicator + ' Muted';
|
||||
muteLink.className = 'mutelink disabled';
|
||||
}
|
||||
else {
|
||||
muteLink.innerHTML = mutedIndicator + ' Mute';
|
||||
muteLink.className = 'mutelink';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the current active speaker.
|
||||
*/
|
||||
my.getActiveSpeakerContainerId = function () {
|
||||
return 'participant_' + currentActiveSpeaker;
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds the remote video menu element for the given <tt>jid</tt> in the
|
||||
* given <tt>parentElement</tt>.
|
||||
*
|
||||
* @param jid the jid indicating the video for which we're adding a menu.
|
||||
* @param parentElement the parent element where this menu will be added
|
||||
*/
|
||||
function addRemoteVideoMenu(jid, parentElement) {
|
||||
var spanElement = document.createElement('span');
|
||||
spanElement.className = 'remotevideomenu';
|
||||
|
||||
parentElement.appendChild(spanElement);
|
||||
|
||||
var menuElement = document.createElement('i');
|
||||
menuElement.className = 'fa fa-angle-down';
|
||||
menuElement.title = 'Remote user controls';
|
||||
spanElement.appendChild(menuElement);
|
||||
|
||||
// <ul class="popupmenu">
|
||||
// <li><a href="#">Mute</a></li>
|
||||
// <li><a href="#">Eject</a></li>
|
||||
// </ul>
|
||||
var popupmenuElement = document.createElement('ul');
|
||||
popupmenuElement.className = 'popupmenu';
|
||||
popupmenuElement.id
|
||||
= 'remote_popupmenu_' + Strophe.getResourceFromJid(jid);
|
||||
spanElement.appendChild(popupmenuElement);
|
||||
|
||||
var muteMenuItem = document.createElement('li');
|
||||
var muteLinkItem = document.createElement('a');
|
||||
|
||||
var mutedIndicator = "<i class='icon-mic-disabled'></i>";
|
||||
|
||||
if (!mutedAudios[jid]) {
|
||||
muteLinkItem.innerHTML = mutedIndicator + 'Mute';
|
||||
muteLinkItem.className = 'mutelink';
|
||||
}
|
||||
else {
|
||||
muteLinkItem.innerHTML = mutedIndicator + ' Muted';
|
||||
muteLinkItem.className = 'mutelink disabled';
|
||||
}
|
||||
|
||||
muteLinkItem.onclick = function(){
|
||||
if ($(this).attr('disabled') != undefined) {
|
||||
event.preventDefault();
|
||||
}
|
||||
var isMute = !mutedAudios[jid];
|
||||
connection.moderate.setMute(jid, isMute);
|
||||
popupmenuElement.setAttribute('style', 'display:none;');
|
||||
|
||||
if (isMute) {
|
||||
this.innerHTML = mutedIndicator + ' Muted';
|
||||
this.className = 'mutelink disabled';
|
||||
}
|
||||
else {
|
||||
this.innerHTML = mutedIndicator + ' Mute';
|
||||
this.className = 'mutelink';
|
||||
}
|
||||
};
|
||||
|
||||
muteMenuItem.appendChild(muteLinkItem);
|
||||
popupmenuElement.appendChild(muteMenuItem);
|
||||
|
||||
var ejectIndicator = "<i class='fa fa-eject'></i>";
|
||||
|
||||
var ejectMenuItem = document.createElement('li');
|
||||
var ejectLinkItem = document.createElement('a');
|
||||
ejectLinkItem.innerHTML = ejectIndicator + ' Kick out';
|
||||
ejectLinkItem.onclick = function(){
|
||||
connection.moderate.eject(jid);
|
||||
popupmenuElement.setAttribute('style', 'display:none;');
|
||||
};
|
||||
|
||||
ejectMenuItem.appendChild(ejectLinkItem);
|
||||
popupmenuElement.appendChild(ejectMenuItem);
|
||||
}
|
||||
|
||||
/**
|
||||
* On audio muted event.
|
||||
*/
|
||||
$(document).bind('audiomuted.muc', function (event, jid, isMuted) {
|
||||
var videoSpanId = null;
|
||||
if (jid === connection.emuc.myroomjid) {
|
||||
videoSpanId = 'localVideoContainer';
|
||||
} else {
|
||||
VideoLayout.ensurePeerContainerExists(jid);
|
||||
videoSpanId = 'participant_' + Strophe.getResourceFromJid(jid);
|
||||
}
|
||||
|
||||
if (focus) {
|
||||
mutedAudios[jid] = isMuted;
|
||||
VideoLayout.updateRemoteVideoMenu(jid, isMuted);
|
||||
}
|
||||
|
||||
if (videoSpanId)
|
||||
VideoLayout.showAudioIndicator(videoSpanId, isMuted);
|
||||
});
|
||||
|
||||
/**
|
||||
* On video muted event.
|
||||
*/
|
||||
$(document).bind('videomuted.muc', function (event, jid, isMuted) {
|
||||
var videoSpanId = null;
|
||||
if (jid === connection.emuc.myroomjid) {
|
||||
videoSpanId = 'localVideoContainer';
|
||||
} else {
|
||||
VideoLayout.ensurePeerContainerExists(jid);
|
||||
videoSpanId = 'participant_' + Strophe.getResourceFromJid(jid);
|
||||
}
|
||||
|
||||
if (videoSpanId)
|
||||
VideoLayout.showVideoIndicator(videoSpanId, isMuted);
|
||||
});
|
||||
|
||||
/**
|
||||
* On active speaker changed event.
|
||||
*/
|
||||
$(document).bind('activespeakerchanged', function (event, resourceJid) {
|
||||
// We ignore local user events.
|
||||
if (resourceJid
|
||||
=== Strophe.getResourceFromJid(connection.emuc.myroomjid))
|
||||
return;
|
||||
|
||||
// Disable style for previous active speaker.
|
||||
if (currentActiveSpeaker
|
||||
&& currentActiveSpeaker !== resourceJid
|
||||
&& !focusedVideoSrc) {
|
||||
var oldContainer = document.getElementById(
|
||||
'participant_' + currentActiveSpeaker);
|
||||
|
||||
if (oldContainer) {
|
||||
VideoLayout.enableActiveSpeaker(currentActiveSpeaker, false);
|
||||
}
|
||||
}
|
||||
|
||||
// Obtain container for new active speaker.
|
||||
var container = document.getElementById(
|
||||
'participant_' + resourceJid);
|
||||
|
||||
// Update the current active speaker.
|
||||
if (resourceJid !== currentActiveSpeaker)
|
||||
currentActiveSpeaker = resourceJid;
|
||||
else
|
||||
return;
|
||||
|
||||
// Local video will not have container found, but that's ok
|
||||
// since we don't want to switch to local video.
|
||||
if (container && !focusedVideoSrc)
|
||||
{
|
||||
var video = container.getElementsByTagName("video");
|
||||
if (video.length)
|
||||
{
|
||||
VideoLayout.updateLargeVideo(video[0].src);
|
||||
VideoLayout.enableActiveSpeaker(resourceJid, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return my;
|
||||
}(VideoLayout || {}));
|
Loading…
Reference in New Issue