mirror of https://github.com/jitsi/jitsi-meet
commit
77cb10d6a1
@ -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; |
||||
})(); |
@ -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; |
||||
} |
@ -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); |
||||
};*/ |
||||
} |
@ -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); |
@ -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; |
||||
})(); |
@ -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 || {})); |
@ -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