Commit 6faa9c5a authored by Dele Olajide's avatar Dele Olajide

Openfire Meetings plugin. Initial release

parent 971f90c8
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title>Openfire Meetings Plugin Changelog</title>
<style type="text/css">
BODY {
font-size: 100%;
}
BODY, TD, TH {
font-family: tahoma, verdana, arial, helvetica, sans-serif;
font-size: 0.8em;
}
H2 {
font-size: 10pt;
font-weight: bold;
padding-left: 1em;
}
A:hover {
text-decoration: none;
}
H1 {
font-family: tahoma, arial, helvetica, sans-serif;
font-size: 1.4em;
font-weight: bold;
border-bottom: 1px #ccc solid;
padding-bottom: 2px;
}
TT {
font-family: courier new;
font-weight: bold;
color: #060;
}
PRE {
font-family: courier new;
font-size: 100%;
}
</style>
</head>
<body>
<h1>
Openfire Meetings Plugin Changelog
</h1>
<p><b>0.0.1</b> -- Nov 27th, 2014</p>
<ul>
<li>Initial release.</li>
</ul>
</body>
</html>
Some of the files here, notably libjitsi.jar, are OSGi bundles from the Jitsi
project. In Jitsi they are generated in the 'sc-bundles' directory by the
'bundles' ant target. To update the files here, run 'ant rebuild' in Jitsi and
then copy the files here, e.g.:
for f in *.jar; do
if [ -e ../../jitsi/sc-bundles/$f ]; then
cp ../../jitsi/sc-bundles/$f .;
fi;
done
handlers= java.util.logging.ConsoleHandler
java.util.logging.ConsoleHandler.level = ALL
java.util.logging.ConsoleHandler.formatter = net.java.sip.communicator.util.ScLogFormatter
.level=INFO
# FIXME: remove once RTT issue is fixed
org.jitsi.impl.neomedia.MediaStreamStatsImpl.level=WARNING
\ No newline at end of file
# The amount of memory that the java process can consume.
# Use the format that is in use for java's -Xmx switch
# The default is 3072m
# Only effective on linux, 64 bit
# VIDEOBRIDGE_MAX_MEMORY=3072m
# Uncomment the next line to enable the remote debugging of the video bridge
# VIDEOBRIDGE_DEBUG_OPTIONS="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8000"
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<plugin>
<class>org.jivesoftware.openfire.plugin.ofmeet.OfMeetPlugin</class>
<name>ofmeet</name>
<description>Openfire Meetings</description>
<author></author>
<version>0.0.1</version>
<date>27/11/2014</date>
<minServerVersion>3.10.0</minServerVersion>
<adminconsole>
<tab id="tab-ofmeet" name="${plugin.title}" url="ofmeet-summary.jsp" description="${plugin.description}">
<sidebar id="siderbar-ofmeet" name="${plugin.title}">
<item id="ofmeet-summary" name="${config.page.summary.title}" description="${config.page.summary.description}" url="ofmeet-summary.jsp"/>
<item id="ofmeet-settings" name="${config.page.settings.title}" description="${config.page.settings.description}" url="ofmeet-settings.jsp"/>
</sidebar>
</tab>
</adminconsole>
</plugin>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title>Openfire Meetings Plugin Readme</title>
<style type="text/css">
BODY {
font-size: 100%;
}
BODY, TD, TH {
font-family: tahoma, verdana, arial, helvetica, sans-serif;
font-size: 0.8em;
}
H2 {
font-size: 10pt;
font-weight: bold;
}
A:hover {
text-decoration: none;
}
H1 {
font-family: tahoma, arial, helvetica, sans-serif;
font-size: 1.4em;
font-weight: bold;
border-bottom: 1px #ccc solid;
padding-bottom: 2px;
}
TT {
font-family: courier new;
font-weight: bold;
color: #060;
}
PRE {
font-family: courier new;
font-size: 100%;
}
</style>
</head>
<body>
<h1>
Openfire Meetings Plugin Readme
</h1>
<h2>Overview</h2>
<p>
Openfire Meetings
</p>
<h2>Installation</h2>
</body>
</html>
# Server Installation for Jitsi Meet
This describes configuring a server `jitsi.example.com`. You will need to
change references to that to match your host, and generate some passwords for
`YOURSECRET1` and `YOURSECRET2`.
There are also some complete [example config files](https://github.com/jitsi/jitsi-meet/tree/master/doc/example-config-files/) available, mentioned in each section.
## Install prosody and otalk modules
```sh
apt-get install lsb-release
echo deb http://packages.prosody.im/debian $(lsb_release -sc) main | sudo tee -a /etc/apt/sources.list
wget --no-check-certificate https://prosody.im/files/prosody-debian-packages.key -O- | sudo apt-key add -
apt-get update
apt-get install prosody-trunk
apt-get install git lua-zlib lua-sec-prosody lua-dbi-sqlite3 liblua5.1-bitop-dev liblua5.1-bitop0
git clone https://github.com/andyet/otalk-server.git
cd otalk-server
cp -r mod* /usr/lib/prosody/modules
```
## Configure prosody
Modify the config file in `/etc/prosody/prosody.cfg.lua` (see also the example config file):
- modules to enable/add: compression, bosh, smacks, carbons, mam, lastactivity, offline, pubsub, adhoc, websocket, http_altconnect
- comment out: `c2s_require_encryption = true`, and `s2s_secure_auth = false`
- change `authentication = "internal_hashed"`
- add this:
```
daemonize = true
cross_domain_bosh = true;
storage = {archive2 = "sql2"}
sql = { driver = "SQLite3", database = "prosody.sqlite" }
default_archive_policy = "roster"
```
- configure your domain by editing the example.com virtual host section section:
```
VirtualHost "jitsi.example.com"
authentication = "anonymous"
ssl = {
key = "/var/lib/prosody/jitsi.example.com.key";
certificate = "/var/lib/prosody/jitsi.example.com.crt";
}
```
- and finally configure components:
```
Component "conference.jitsi.example.com" "muc"
Component "jitsi-videobridge.jitsi.example.com"
component_secret = "YOURSECRET1"
```
Generate certs for the domain:
```sh
prosodyctl cert generate jitsi.example.com
```
Restart prosody XMPP server with the new config
```sh
prosodyctl restart
```
## Install nginx
```sh
apt-get install nginx
```
Add nginx config for domain in `/etc/nginx/nginx.conf`:
```
tcp_nopush on;
types_hash_max_size 2048;
server_names_hash_bucket_size 64;
```
Add a new file `jitsi.example.com` in `/etc/nginx/sites-available` (see also the example config file):
```
server {
listen 80;
server_name jitsi.example.com;
# set the root
root /srv/jitsi.example.com;
index index.html;
location ~ ^/([a-zA-Z0-9]+)$ {
rewrite ^/(.*)$ / break;
}
# BOSH
location /http-bind {
proxy_pass http://localhost:5280/http-bind;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Host $http_host;
}
# xmpp websockets
location /xmpp-websocket {
proxy_pass http://localhost:5280;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
tcp_nodelay on;
}
}
```
Add link for the added configuration
```sh
cd /etc/nginx/sites-enabled
ln -s ../sites-available/jitsi.example.com jitsi.example.com
```
## Fix firewall if needed
```sh
ufw allow 80
ufw allow 5222
```
## Install Jitsi Videobridge
```sh
wget https://download.jitsi.org/jitsi-videobridge/linux/jitsi-videobridge-linux-{arch-buildnum}.zip
unzip jitsi-videobridge-linux-{arch-buildnum}.zip
```
Install JRE if missing:
```
apt-get install default-jre
```
In the user home that will be starting Jitsi Videobridge create `.sip-communicator` folder and add the file `sip-communicator.properties` with one line in it:
```
org.jitsi.impl.neomedia.transform.srtp.SRTPCryptoContext.checkReplay=false
```
Start the videobridge with:
```sh
./jvb.sh --host=localhost --domain=jitsi.example.com --port=5347 --secret=YOURSECRET1 &
```
Or autostart it by adding the line in `/etc/rc.local`:
```sh
/bin/bash /root/jitsi-videobridge-linux-{arch-buildnum}/jvb.sh --host=localhost --domain=jitsi.example.com --port=5347 --secret=YOURSECRET1 </dev/null >> /var/log/jvb.log 2>&1
```
## Deploy Jitsi Meet
Checkout and configure Jitsi Meet:
```sh
cd /srv
git clone https://github.com/jitsi/jitsi-meet.git
mv jitsi-meet/ jitsi.example.com
```
Edit host names in `/srv/jitsi.example.com/config.js` (see also the example config file):
```
var config = {
hosts: {
domain: 'jitsi.example.com',
muc: 'conference.jitsi.example.com',
bridge: 'jitsi-videobridge.jitsi.example.com'
},
useNicks: false,
bosh: '//jitsi.example.com/http-bind', // FIXME: use xep-0156 for that
desktopSharing: 'false' // 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
};
```
Restart nginx to get the new configuration:
```sh
invoke-rc.d nginx restart
```
## Install [Turn server](https://github.com/andyet/otalk-server/tree/master/restund)
```sh
apt-get install make gcc
wget http://creytiv.com/pub/re-0.4.7.tar.gz
tar zxvf re-0.4.7.tar.gz
ln -s re-0.4.7 re
cd re-0.4.7
sudo make install PREFIX=/usr
cd ..
wget http://creytiv.com/pub/restund-0.4.2.tar.gz
wget https://raw.github.com/andyet/otalk-server/master/restund/restund-auth.patch
tar zxvf restund-0.4.2.tar.gz
cd restund-0.4.2/
patch -p1 < ../restund-auth.patch
sudo make install PREFIX=/usr
cp debian/restund.init /etc/init.d/restund
chmod +x /etc/init.d/restund
cd /etc
wget https://raw.github.com/andyet/otalk-server/master/restund/restund.conf
```
Configure addresses and ports as desired, and the password to be configured in prosody:
```
realm jitsi.example.com
# share this with your prosody server
auth_shared YOURSECRET2
# modules
module_path /usr/lib/restund/modules
turn_relay_addr [turn ip address]
```
Configure prosody to use it in `/etc/prosody/prosody.cfg.lua`. Add to your virtual host:
```
turncredentials_secret = "YOURSECRET2";
turncredentials = {
{ type = "turn", host = "turn.address.ip.configured", port = 3478, transport = "tcp" }
}
```
Add turncredentials module in the "modules_enabled" section
Reload prosody if needed
```
prosodyctl restart
```
## Running behind NAT
In case of videobridge being installed on a machine behind NAT, add the following extra lines to the file `~/.sip-communicator/sip-communicator.properties` (in the home of user running the videobridge):
```
org.jitsi.videobridge.NAT_HARVESTER_LOCAL_ADDRESS=<Local.IP.Address>
org.jitsi.videobridge.NAT_HARVESTER_PUBLIC_ADDRESS=<Public.IP.Address>
```
So the file should look like this at the end:
```
org.jitsi.impl.neomedia.transform.srtp.SRTPCryptoContext.checkReplay=false
org.jitsi.videobridge.NAT_HARVESTER_LOCAL_ADDRESS=<Local.IP.Address>
org.jitsi.videobridge.NAT_HARVESTER_PUBLIC_ADDRESS=<Public.IP.Address>
```
# Hold your first conference
You are now all set and ready to have your first meet by going to http://jitsi.example.com
## Enabling recording
Currently recording is only supported for linux-64 and macos. To enable it, add
the following properties to sip-communicator.properties:
```
org.jitsi.videobridge.ENABLE_MEDIA_RECORDING=true
org.jitsi.videobridge.MEDIA_RECORDING_PATH=/path/to/recordings/dir
org.jitsi.videobridge.MEDIA_RECORDING_TOKEN=secret
```
where /path/to/recordings/dir is the path to a pre-existing directory where recordings
will be stored (needs to be writeable by the user running jitsi-videobridge),
and "secret" is a string which will be used for authentication.
Then, edit the Jitsi-Meet config.js file and set:
```
enableRecording: true
```
Restart jitsi-videobridge and start a new conference (making sure that the page
is reloaded with the new config.js) -- the organizer of the conference should
now have a "recoriding" button in the floating menu, near the "mute" button.
The MIT License (MIT)
Copyright (c) 2013 ESTOS GmbH
Copyright (c) 2013 BlueJimp SARL
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Jitsi Meet - Secure, Simple and Scalable Video Conferences
====
Jitsi Meet is an OpenSource (MIT) WebRTC JavaScript application that uses [Jitsi Videobridge](https://jitsi.org/videobridge) to provide high quality, scalable video conferences. You can see [Jitsi Meet in action](http://youtu.be/7vFUVClsNh0) here at the 482 session of the VoIP Users Conference.
You can also try it out yourself at https://meet.jit.si .
Jitsi Meet allows for very efficient collaboration. It allows users to stream their desktop or only some windows. It also supports shared document editing with Etherpad and remote presentations with Prezi.
## Install
Installing Jitsi Meet is quite a simple experience even though it requires installing a few other components first, such as Jitsi Videobridge, a web server such as Nginx and an XMPP one like Prosody.
You can find information on how to deploy Jitsi Meet in the [installation instructions](https://jitsi.org/meet/deploy)
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!
<?xml version="1.0" encoding="UTF-8"?>
<web-app>
<servlet>
<servlet-name>proxy</servlet-name>
<servlet-class>org.jitsi.videobridge.openfire.HttpProxy</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>proxy</servlet-name>
<url-pattern>/proxy</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>config</servlet-name>
<servlet-class>org.jitsi.videobridge.openfire.Config</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>config</servlet-name>
<url-pattern>/config</url-pattern>
</servlet-mapping>
</web-app>
\ No newline at end of file
/**
* Google Analytics
*/
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-319188-14', 'jit.si');
ga('send', 'pageview');
\ No newline at end of file
/**
* Implements API class that communicates with external api class
* and provides interface to access Jitsi Meet features by external
* applications that embed Jitsi Meet
*/
var APIConnector = (function () {
function APIConnector() { }
/**
* List of the available commands.
* @type {{
* displayName: inputDisplayNameHandler,
* muteAudio: toggleAudio,
* muteVideo: toggleVideo,
* filmStrip: toggleFilmStrip
* }}
*/
var commands =
{
displayName: VideoLayout.inputDisplayNameHandler,
muteAudio: toggleAudio,
muteVideo: toggleVideo,
toggleFilmStrip: BottomToolbar.toggleFilmStrip,
toggleChat: BottomToolbar.toggleChat,
toggleContactList: BottomToolbar.toggleContactList
};
/**
* Maps the supported events and their status
* (true it the event is enabled and false if it is disabled)
* @type {{
* incomingMessage: boolean,
* outgoingMessage: boolean,
* displayNameChange: boolean,
* participantJoined: boolean,
* participantLeft: boolean
* }}
*/
var events =
{
incomingMessage: false,
outgoingMessage:false,
displayNameChange: false,
participantJoined: false,
participantLeft: false
};
/**
* Check whether the API should be enabled or not.
* @returns {boolean}
*/
APIConnector.isEnabled = function () {
var hash = location.hash;
if(hash && hash.indexOf("external") > -1 && window.postMessage)
return true;
return false;
};
/**
* Initializes the APIConnector. Setups message event listeners that will
* receive information from external applications that embed Jitsi Meet.
* It also sends a message to the external application that APIConnector
* is initialized.
*/
APIConnector.init = function () {
if (window.addEventListener)
{
window.addEventListener('message',
APIConnector.processMessage, false);
}
else
{
window.attachEvent('onmessage', APIConnector.processMessage);
}
APIConnector.sendMessage({type: "system", loaded: true});
};
/**
* Sends message to the external application.
* @param object
*/
APIConnector.sendMessage = function (object) {
window.parent.postMessage(JSON.stringify(object), "*");
};
/**
* Processes a message event from the external application
* @param event the message event
*/
APIConnector.processMessage = function(event)
{
var message;
try {
message = JSON.parse(event.data);
} catch (e) {}
if(!message.type)
return;
switch (message.type)
{
case "command":
APIConnector.processCommand(message);
break;
case "event":
APIConnector.processEvent(message);
break;
default:
console.error("Unknown type of the message");
return;
}
};
/**
* Processes commands from external applicaiton.
* @param message the object with the command
*/
APIConnector.processCommand = function (message)
{
if(message.action != "execute")
{
console.error("Unknown action of the message");
return;
}
for(var key in message)
{
if(commands[key])
commands[key].apply(null, message[key]);
}
};
/**
* Processes events objects from external applications
* @param event the event
*/
APIConnector.processEvent = function (event) {
if(!event.action)
{
console.error("Event with no action is received.");
return;
}
switch(event.action)
{
case "add":
for(var i = 0; i < event.events.length; i++)
{
events[event.events[i]] = true;
}
break;
case "remove":
for(var i = 0; i < event.events.length; i++)
{
events[event.events[i]] = false;
}
break;
default:
console.error("Unknown action for event.");
}
};
/**
* Checks whether the event is enabled ot not.
* @param name the name of the event.
* @returns {*}
*/
APIConnector.isEventEnabled = function (name) {
return events[name];
};
/**
* Sends event object to the external application that has been subscribed
* for that event.
* @param name the name event
* @param object data associated with the event
*/
APIConnector.triggerEvent = function (name, object) {
APIConnector.sendMessage({
type: "event", action: "result", event: name, result: object});
};
/**
* Removes the listeners.
*/
APIConnector.dispose = function () {
if(window.removeEventListener)
{
window.removeEventListener("message",
APIConnector.processMessage, false);
}
else
{
window.detachEvent('onmessage', APIConnector.processMessage);
}
};
return APIConnector;
})();
\ No newline at end of file
This diff is collapsed.
/**
* The audio Levels plugin.
*/
var AudioLevels = (function(my) {
var audioLevelCanvasCache = {};
my.LOCAL_LEVEL = 'local';
/**
* Updates the audio level canvas for the given peerJid. If the canvas
* didn't exist we create it.
*/
my.updateAudioLevelCanvas = function (peerJid) {
var resourceJid = null;
var videoSpanId = null;
if (!peerJid)
videoSpanId = 'localVideoContainer';
else {
resourceJid = Strophe.getResourceFromJid(peerJid);
videoSpanId = 'participant_' + resourceJid;
}
videoSpan = document.getElementById(videoSpanId);
if (!videoSpan) {
if (resourceJid)
console.error("No video element for jid", resourceJid);
else
console.error("No video element for local video.");
return;
}
var audioLevelCanvas = $('#' + videoSpanId + '>canvas');
var videoSpaceWidth = $('#remoteVideos').width();
var thumbnailSize
= VideoLayout.calculateThumbnailSize(videoSpaceWidth);
var thumbnailWidth = thumbnailSize[0];
var thumbnailHeight = thumbnailSize[1];
if (!audioLevelCanvas || audioLevelCanvas.length === 0) {
audioLevelCanvas = document.createElement('canvas');
audioLevelCanvas.className = "audiolevel";
audioLevelCanvas.style.bottom = "-" + interfaceConfig.CANVAS_EXTRA/2 + "px";
audioLevelCanvas.style.left = "-" + interfaceConfig.CANVAS_EXTRA/2 + "px";
resizeAudioLevelCanvas( audioLevelCanvas,
thumbnailWidth,
thumbnailHeight);
videoSpan.appendChild(audioLevelCanvas);
} else {
audioLevelCanvas = audioLevelCanvas.get(0);
resizeAudioLevelCanvas( audioLevelCanvas,
thumbnailWidth,
thumbnailHeight);
}
};
/**
* Updates the audio level UI for the given resourceJid.
*
* @param resourceJid the resource jid indicating the video element for
* which we draw the audio level
* @param audioLevel the newAudio level to render
*/
my.updateAudioLevel = function (resourceJid, audioLevel) {
drawAudioLevelCanvas(resourceJid, audioLevel);
var videoSpanId = getVideoSpanId(resourceJid);
var audioLevelCanvas = $('#' + videoSpanId + '>canvas').get(0);
if (!audioLevelCanvas)
return;
var drawContext = audioLevelCanvas.getContext('2d');
var canvasCache = audioLevelCanvasCache[resourceJid];
drawContext.clearRect (0, 0,
audioLevelCanvas.width, audioLevelCanvas.height);
drawContext.drawImage(canvasCache, 0, 0);
};
/**
* Resizes the given audio level canvas to match the given thumbnail size.
*/
function resizeAudioLevelCanvas(audioLevelCanvas,
thumbnailWidth,
thumbnailHeight) {
audioLevelCanvas.width = thumbnailWidth + interfaceConfig.CANVAS_EXTRA;
audioLevelCanvas.height = thumbnailHeight + interfaceConfig.CANVAS_EXTRA;
};
/**
* Draws the audio level canvas into the cached canvas object.
*
* @param resourceJid the resource jid indicating the video element for
* which we draw the audio level
* @param audioLevel the newAudio level to render
*/
function drawAudioLevelCanvas(resourceJid, audioLevel) {
if (!audioLevelCanvasCache[resourceJid]) {
var videoSpanId = getVideoSpanId(resourceJid);
var audioLevelCanvasOrig = $('#' + videoSpanId + '>canvas').get(0);
/*
* FIXME Testing has shown that audioLevelCanvasOrig may not exist.
* In such a case, the method CanvasUtil.cloneCanvas may throw an
* error. Since audio levels are frequently updated, the errors have
* been observed to pile into the console, strain the CPU.
*/
if (audioLevelCanvasOrig)
{
audioLevelCanvasCache[resourceJid]
= CanvasUtil.cloneCanvas(audioLevelCanvasOrig);
}
}
var canvas = audioLevelCanvasCache[resourceJid];
if (!canvas)
return;
var drawContext = canvas.getContext('2d');
drawContext.clearRect(0, 0, canvas.width, canvas.height);
var shadowLevel = getShadowLevel(audioLevel);
if (shadowLevel > 0)
// drawContext, x, y, w, h, r, shadowColor, shadowLevel
CanvasUtil.drawRoundRectGlow( drawContext,
interfaceConfig.CANVAS_EXTRA/2, interfaceConfig.CANVAS_EXTRA/2,
canvas.width - interfaceConfig.CANVAS_EXTRA,
canvas.height - interfaceConfig.CANVAS_EXTRA,
interfaceConfig.CANVAS_RADIUS,
interfaceConfig.SHADOW_COLOR,
shadowLevel);
};
/**
* Returns the shadow/glow level for the given audio level.
*
* @param audioLevel the audio level from which we determine the shadow
* level
*/
function getShadowLevel (audioLevel) {
var shadowLevel = 0;
if (audioLevel <= 0.3) {
shadowLevel = Math.round(interfaceConfig.CANVAS_EXTRA/2*(audioLevel/0.3));
}
else if (audioLevel <= 0.6) {
shadowLevel = Math.round(interfaceConfig.CANVAS_EXTRA/2*((audioLevel - 0.3) / 0.3));
}
else {
shadowLevel = Math.round(interfaceConfig.CANVAS_EXTRA/2*((audioLevel - 0.6) / 0.4));
}
return shadowLevel;
};
/**
* Returns the video span id corresponding to the given resourceJid or local
* user.
*/
function getVideoSpanId(resourceJid) {
var videoSpanId = null;
if (resourceJid === AudioLevels.LOCAL_LEVEL
|| (connection.emuc.myroomjid && resourceJid
=== Strophe.getResourceFromJid(connection.emuc.myroomjid)))
videoSpanId = 'localVideoContainer';
else
videoSpanId = 'participant_' + resourceJid;
return videoSpanId;
};
/**
* Indicates that the remote video has been resized.
*/
$(document).bind('remotevideo.resized', function (event, width, height) {
var resized = false;
$('#remoteVideos>span>canvas').each(function() {
var canvas = $(this).get(0);
if (canvas.width !== width + interfaceConfig.CANVAS_EXTRA) {
canvas.width = width + interfaceConfig.CANVAS_EXTRA;
resized = true;
}
if (canvas.heigh !== height + interfaceConfig.CANVAS_EXTRA) {
canvas.height = height + interfaceConfig.CANVAS_EXTRA;
resized = true;
}
});
if (resized)
Object.keys(audioLevelCanvasCache).forEach(function (resourceJid) {
audioLevelCanvasCache[resourceJid].width
= width + interfaceConfig.CANVAS_EXTRA;
audioLevelCanvasCache[resourceJid].height
= height + interfaceConfig.CANVAS_EXTRA;
});
});
return my;
})(AudioLevels || {});
var Avatar = (function(my) {
var users = {};
var activeSpeakerJid;
/**
* Sets the user's avatar in the settings menu(if local user), contact list
* and thumbnail
* @param jid jid of the user
* @param id email or userID to be used as a hash
*/
my.setUserAvatar = function(jid, id) {
if(id) {
if(users[jid] === id) {
return;
}
users[jid] = id;
}
var url = getGravatarUrl(users[jid] || jid);
var resourceJid = Strophe.getResourceFromJid(jid);
var thumbnail = $('#participant_' + resourceJid);
var avatar = $('#avatar_' + resourceJid);
// set the avatar in the settings menu if it is local user and get the
// local video container
if(jid === connection.emuc.myroomjid) {
$('#avatar').get(0).src = url;
thumbnail = $('#localVideoContainer');
}
// set the avatar in the contact list
var contact = $('#' + resourceJid + '>img');
if(contact && contact.length > 0) {
contact.get(0).src = url;
}
// set the avatar in the thumbnail
if(avatar && avatar.length > 0) {
avatar[0].src = url;
} else {
if (thumbnail && thumbnail.length > 0) {
avatar = document.createElement('img');
avatar.id = 'avatar_' + resourceJid;
avatar.className = 'userAvatar';
avatar.src = url;
thumbnail.append(avatar);
}
}
//if the user is the current active speaker - update the active speaker
// avatar
if(jid === activeSpeakerJid) {
Avatar.updateActiveSpeakerAvatarSrc(jid);
}
};
/**
* Hides or shows the user's avatar
* @param jid jid of the user
* @param show whether we should show the avatar or not
* video because there is no dominant speaker and no focused speaker
*/
my.showUserAvatar = function(jid, show) {
if(users[jid]) {
var resourceJid = Strophe.getResourceFromJid(jid);
var video = $('#participant_' + resourceJid + '>video');
var avatar = $('#avatar_' + resourceJid);
if(jid === connection.emuc.myroomjid) {
video = $('#localVideoWrapper>video');
}
if(show === undefined || show === null) {
show = isUserMuted(jid);
}
//if the user is the currently focused, the dominant speaker or if
//there is no focused and no dominant speaker
if (activeSpeakerJid === jid) {
setVisibility($("#largeVideo"), !show);
setVisibility($('#activeSpeakerAvatar'), show);
setVisibility(avatar, false);
setVisibility(video, false);
} else {
if (video && video.length > 0) {
setVisibility(video, !show);
setVisibility(avatar, show);
}
}
}
};
/**
* Updates the src of the active speaker avatar
* @param jid of the current active speaker
*/
my.updateActiveSpeakerAvatarSrc = function(jid) {
if(!jid) {
if (focusedVideoSrc) {
jid = getJidFromVideoSrc(focusedVideoSrc);
} else {
jid = connection.emuc.findJidFromResource(
VideoLayout.getDominantSpeakerResourceJid());
}
}
var avatar = $("#activeSpeakerAvatar")[0];
var url = getGravatarUrl(users[jid],
interfaceConfig.ACTIVE_SPEAKER_AVATAR_SIZE);
if(jid === activeSpeakerJid && avatar.src === url) {
return;
}
activeSpeakerJid = jid;
var isMuted = isUserMuted(jid);
if(jid && isMuted !== null) {
avatar.src = url;
setVisibility($("#largeVideo"), !isMuted);
Avatar.showUserAvatar(jid, isMuted);
}
};
function setVisibility(selector, show) {
if (selector && selector.length > 0) {
selector.css("visibility", show ? "visible" : "hidden");
}
}
function isUserMuted(jid) {
if(!mediaStreams[jid] || !mediaStreams[jid][MediaStream.VIDEO_TYPE]) {
return null;
}
return mediaStreams[jid][MediaStream.VIDEO_TYPE].muted;
}
function getGravatarUrl(id, size) {
if(id === connection.emuc.myroomjid || !id) {
id = SettingsMenu.getUID();
}
return 'https://www.gravatar.com/avatar/' +
MD5.hexdigest(id.trim().toLowerCase()) +
"?d=retro&size=" + (size || "30");
}
return my;
}(Avatar || {}));
var BottomToolbar = (function (my) {
my.toggleChat = function() {
if (ContactList.isVisible()) {
buttonClick("#contactListButton", "active");
$('#contactlist').css('z-index', 4);
setTimeout(function() {
$('#contactlist').css('display', 'none');
$('#contactlist').css('z-index', 5);
}, 500);
}
Chat.toggleChat();
buttonClick("#chatBottomButton", "active");
};
my.toggleContactList = function() {
if (Chat.isVisible()) {
buttonClick("#chatBottomButton", "active");
setTimeout(function() {
$('#chatspace').css('display', 'none');
}, 500);
}
buttonClick("#contactListButton", "active");
ContactList.toggleContactList();
};
my.toggleFilmStrip = function() {
var filmstrip = $("#remoteVideos");
filmstrip.toggleClass("hidden");
};
$(document).bind("remotevideo.resized", function (event, width, height) {
var bottom = (height - $('#bottomToolbar').outerHeight())/2 + 18;
$('#bottomToolbar').css({bottom: bottom + 'px'});
});
return my;
}(BottomToolbar || {}));
/**
* Utility class for drawing canvas shapes.
*/
var CanvasUtil = (function(my) {
/**
* Draws a round rectangle with a glow. The glowWidth indicates the depth
* of the glow.
*
* @param drawContext the context of the canvas to draw to
* @param x the x coordinate of the round rectangle
* @param y the y coordinate of the round rectangle
* @param w the width of the round rectangle
* @param h the height of the round rectangle
* @param glowColor the color of the glow
* @param glowWidth the width of the glow
*/
my.drawRoundRectGlow
= function(drawContext, x, y, w, h, r, glowColor, glowWidth) {
// Save the previous state of the context.
drawContext.save();
if (w < 2 * r) r = w / 2;
if (h < 2 * r) r = h / 2;
// Draw a round rectangle.
drawContext.beginPath();
drawContext.moveTo(x+r, y);
drawContext.arcTo(x+w, y, x+w, y+h, r);
drawContext.arcTo(x+w, y+h, x, y+h, r);
drawContext.arcTo(x, y+h, x, y, r);
drawContext.arcTo(x, y, x+w, y, r);
drawContext.closePath();
// Add a shadow around the rectangle
drawContext.shadowColor = glowColor;
drawContext.shadowBlur = glowWidth;
drawContext.shadowOffsetX = 0;
drawContext.shadowOffsetY = 0;
// Fill the shape.
drawContext.fill();
drawContext.save();
drawContext.restore();
// 1) Uncomment this line to use Composite Operation, which is doing the
// same as the clip function below and is also antialiasing the round
// border, but is said to be less fast performance wise.
// drawContext.globalCompositeOperation='destination-out';
drawContext.beginPath();
drawContext.moveTo(x+r, y);
drawContext.arcTo(x+w, y, x+w, y+h, r);
drawContext.arcTo(x+w, y+h, x, y+h, r);
drawContext.arcTo(x, y+h, x, y, r);
drawContext.arcTo(x, y, x+w, y, r);
drawContext.closePath();
// 2) Uncomment this line to use Composite Operation, which is doing the
// same as the clip function below and is also antialiasing the round
// border, but is said to be less fast performance wise.
// drawContext.fill();
// Comment these two lines if choosing to do the same with composite
// operation above 1 and 2.
drawContext.clip();
drawContext.clearRect(0, 0, 277, 200);
// Restore the previous context state.
drawContext.restore();
};
/**
* Clones the given canvas.
*
* @return the new cloned canvas.
*/
my.cloneCanvas = function (oldCanvas) {
/*
* FIXME Testing has shown that oldCanvas may not exist. In such a case,
* the method CanvasUtil.cloneCanvas may throw an error. Since audio
* levels are frequently updated, the errors have been observed to pile
* into the console, strain the CPU.
*/
if (!oldCanvas)
return oldCanvas;
//create a new canvas
var newCanvas = document.createElement('canvas');
var context = newCanvas.getContext('2d');
//set dimensions
newCanvas.width = oldCanvas.width;
newCanvas.height = oldCanvas.height;
//apply the old canvas to the new one
context.drawImage(oldCanvas, 0, 0);
//return the new canvas
return newCanvas;
};
return my;
})(CanvasUtil || {});
This diff is collapsed.
/* Background page, responsible for actually choosing media */
chrome.runtime.onConnect.addListener(function (channel) {
channel.onMessage.addListener(function (message) {
switch(message.type) {
case 'ofmeetGetScreen':
var pending = chrome.desktopCapture.chooseDesktopMedia(message.options || ['screen', 'window'],
channel.sender.tab, function (streamid) {
// Communicate this string to the app so it can call getUserMedia with it
message.type = 'ofmeetGotScreen';
message.sourceId = streamid;
channel.postMessage(message);
});
// Let the app know that it can cancel the timeout
message.type = 'ofmeetGetScreenPending';
message.request = pending;
channel.postMessage(message);
break;
case 'ofmeetCancelGetScreen':
chrome.desktopCapture.cancelChooseDesktopMedia(message.request);
message.type = 'ofmeetCanceledGetScreen';
channel.postMessage(message);
break;
}
});
});
/* The chrome content script which can listen to the page dom events */
var channel = chrome.runtime.connect();
channel.onMessage.addListener(function (message) {
console.log('ofmeet extension channel message', message);
window.postMessage(message, '*');
});
window.addEventListener('message', function (event) {
if (event.source != window)
return;
if (!event.data && (
event.data.type == 'ofmeetGetScreen' ||
event.data.type == 'ofmeetCancelGetScreen'))
return;
channel.postMessage(event.data);
});
var div = document.createElement('div');
div.id = "ofmeet-extension-installed";
div.style = "display: none;";
document.body.appendChild(div);
{
"background": {
"scripts": [ "background.js" ]
},
"content_scripts": [ {
"js": [ "content.js" ],
"matches": [ "https://*/*" ]
} ],
"description": "Openfire Meetings Chrome Extension required for screen sharing",
"icons": {
"128": "icon128.png",
"16": "icon16.png",
"48": "icon48.png"
},
"manifest_version": 2,
"minimum_chrome_version": "34",
"name": "Openfire Meetings Screen Share",
"permissions": [ "desktopCapture" ],
"short_name": "ofmeet screensharing",
"version": "0.0.1"
}
<html>
<head>
<title>Jitsi Meet: Unsupported Browser</title>
<link rel="stylesheet" type="text/css" media="screen" href="css/chromeonly.css" />
</head>
<body>
<!-- wrap starts here -->
<div id="wrap">
<a href="http://google.com/chrome"><div id="left"></div></a>
<div id="middle"></div>
<div id="text">
<p>This application is currently only supported by <a href="http://google.com/chrome">Chrome</a>, <a href="http://www.chromium.org/">Chromium</a> and <a href="http://www.opera.com">Opera</a></p>
<p><a href="http://google.com/chrome">Download Chrome</a></p>
<p class="firefox">We are hoping that <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=977864">multistream support</a> for Firefox would not be long so that we could all use this application with our favorite browser.</p>
</div>
<!-- wrap ends here -->
</div>
</body>
</html>
/**
* 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;
})();
var config = {
hosts: {
domain: 'btg199251',
//anonymousdomain: 'guest.example.com',
muc: 'conference.btg199251', // FIXME: use XEP-0030
bridge: 'jitsi-videobridge.btg199251', // FIXME: use XEP-0030
//call_control: 'callcontrol.jitsi-meet.example.com'
},
getroomnode: function (path)
{
console.log('getroomnode', path);
var name = "r";
var roomnode = null;
var results = new RegExp('[\\?&]' + name + '=([^&#]*)').exec(window.location.href);
if (!results)
roomnode = null;
else roomnode = results[1] || undefined;
if (!roomnode) {
roomnode = Math.random().toString(36).substr(2, 20);
window.history.pushState('VideoChat', 'Room: ' + roomnode, path + "?r=" + roomnode);
}
return roomnode;
},
// useStunTurn: true, // use XEP-0215 to fetch STUN and TURN server
// useIPv6: true, // ipv6 support. use at your own risk
useNicks: false,
bosh: '//btg199251:7443/http-bind/', // FIXME: use xep-0156 for that
clientNode: 'http://jitsi.org/jitsimeet', // The name of client node advertised in XEP-0115 'c' stanza
//defaultSipNumber: '', // Default SIP number
desktopSharing: 'ext', // Desktop sharing method. Can be set to 'ext', 'webrtc' or false to disable.
chromeExtensionId: 'diibjkoicjeejcmhdnailmkgecihlobk', // Id of desktop streamer Chrome extension
desktopSharingSources: ['screen', 'window'],
minChromeExtVersion: '0.1', // Required version of Chrome extension
enableRtpStats: true, // Enables RTP stats processing
openSctp: true, // Toggle to enable/disable SCTP channels
channelLastN: -1, // The default value of the channel attribute last-n.
adaptiveLastN: false,
adaptiveSimulcast: false,
useRtcpMux: true,
useBundle: true,
enableRecording: false,
enableWelcomePage: true,
enableSimulcast: false
};
var ConnectionQuality = (function () {
/**
* Constructs new ConnectionQuality object
* @constructor
*/
function ConnectionQuality() {
}
/**
* local stats
* @type {{}}
*/
var stats = {};
/**
* remote stats
* @type {{}}
*/
var remoteStats = {};
/**
* Interval for sending statistics to other participants
* @type {null}
*/
var sendIntervalId = null;
/**
* Updates the local statistics
* @param data new statistics
*/
ConnectionQuality.updateLocalStats = function (data) {
stats = data;
VideoLayout.updateLocalConnectionStats(100 - stats.packetLoss.total,stats);
if(sendIntervalId == null)
{
startSendingStats();
}
};
/**
* Start statistics sending.
*/
function startSendingStats() {
sendStats();
sendIntervalId = setInterval(sendStats, 10000);
}
/**
* Sends statistics to other participants
*/
function sendStats() {
connection.emuc.addConnectionInfoToPresence(convertToMUCStats(stats));
connection.emuc.sendPresence();
}
/**
* Converts statistics to format for sending through XMPP
* @param stats the statistics
* @returns {{bitrate_donwload: *, bitrate_uplpoad: *, packetLoss_total: *, packetLoss_download: *, packetLoss_upload: *}}
*/
function convertToMUCStats(stats) {
return {
"bitrate_download": stats.bitrate.download,
"bitrate_upload": stats.bitrate.upload,
"packetLoss_total": stats.packetLoss.total,
"packetLoss_download": stats.packetLoss.download,
"packetLoss_upload": stats.packetLoss.upload
};
}
/**
* Converts statitistics to format used by VideoLayout
* @param stats
* @returns {{bitrate: {download: *, upload: *}, packetLoss: {total: *, download: *, upload: *}}}
*/
function parseMUCStats(stats) {
return {
bitrate: {
download: stats.bitrate_download,
upload: stats.bitrate_upload
},
packetLoss: {
total: stats.packetLoss_total,
download: stats.packetLoss_download,
upload: stats.packetLoss_upload
}
};
}
/**
* Updates remote statistics
* @param jid the jid associated with the statistics
* @param data the statistics
*/
ConnectionQuality.updateRemoteStats = function (jid, data) {
if(data == null || data.packetLoss_total == null)
{
VideoLayout.updateConnectionStats(jid, null, null);
return;
}
remoteStats[jid] = parseMUCStats(data);
VideoLayout.updateConnectionStats(jid, 100 - data.packetLoss_total,remoteStats[jid]);
};
/**
* Stops statistics sending.
*/
ConnectionQuality.stopSendingStats = function () {
clearInterval(sendIntervalId);
sendIntervalId = null;
//notify UI about stopping statistics gathering
VideoLayout.onStatsStop();
};
/**
* Returns the local statistics.
*/
ConnectionQuality.getStats = function () {
return stats;
}
return ConnectionQuality;
})();
\ No newline at end of file
/**
* Contact list.
*/
var ContactList = (function (my) {
var numberOfContacts = 0;
var notificationInterval;
/**
* Indicates if the chat is currently visible.
*
* @return <tt>true</tt> if the chat is currently visible, <tt>false</tt> -
* otherwise
*/
my.isVisible = function () {
return $('#contactlist').is(":visible");
};
/**
* Adds a contact for the given peerJid if such doesn't yet exist.
*
* @param peerJid the peerJid corresponding to the contact
*/
my.ensureAddContact = function(peerJid) {
var resourceJid = Strophe.getResourceFromJid(peerJid);
var contact = $('#contactlist>ul>li[id="' + resourceJid + '"]');
if (!contact || contact.length <= 0)
ContactList.addContact(peerJid);
};
/**
* Adds a contact for the given peer jid.
*
* @param peerJid the jid of the contact to add
*/
my.addContact = function(peerJid) {
var resourceJid = Strophe.getResourceFromJid(peerJid);
var contactlist = $('#contactlist>ul');
var newContact = document.createElement('li');
// XXX(gp) contact click event handling is now in videolayout.js. Is the
// following statement (newContact.id = resourceJid) still relevant?
newContact.id = resourceJid;
newContact.className = "clickable";
newContact.onclick = function(event) {
if(event.currentTarget.className === "clickable") {
$(ContactList).trigger('contactclicked', [peerJid]);
}
};
newContact.appendChild(createAvatar());
newContact.appendChild(createDisplayNameParagraph("Participant"));
var clElement = contactlist.get(0);
if (resourceJid === Strophe.getResourceFromJid(connection.emuc.myroomjid)
&& $('#contactlist>ul .title')[0].nextSibling.nextSibling)
{
clElement.insertBefore(newContact,
$('#contactlist>ul .title')[0].nextSibling.nextSibling);
}
else {
clElement.appendChild(newContact);
}
updateNumberOfParticipants(1);
};
/**
* Removes a contact for the given peer jid.
*
* @param peerJid the peerJid corresponding to the contact to remove
*/
my.removeContact = function(peerJid) {
var resourceJid = Strophe.getResourceFromJid(peerJid);
var contact = $('#contactlist>ul>li[id="' + resourceJid + '"]');
if (contact && contact.length > 0) {
var contactlist = $('#contactlist>ul');
contactlist.get(0).removeChild(contact.get(0));
updateNumberOfParticipants(-1);
}
};
/**
* Opens / closes the contact list area.
*/
my.toggleContactList = function () {
var contactlist = $('#contactlist');
var videospace = $('#videospace');
var chatSize = (ContactList.isVisible()) ? [0, 0] : Chat.getChatSize();
var videospaceWidth = window.innerWidth - chatSize[0];
var videospaceHeight = window.innerHeight;
var videoSize
= getVideoSize(null, null, videospaceWidth, videospaceHeight);
var videoWidth = videoSize[0];
var videoHeight = videoSize[1];
var videoPosition = getVideoPosition(videoWidth,
videoHeight,
videospaceWidth,
videospaceHeight);
var horizontalIndent = videoPosition[0];
var verticalIndent = videoPosition[1];
var thumbnailSize = VideoLayout.calculateThumbnailSize(videospaceWidth);
var thumbnailsWidth = thumbnailSize[0];
var thumbnailsHeight = thumbnailSize[1];
var completeFunction = ContactList.isVisible() ?
function() {} : function () { contactlist.trigger('shown');};
videospace.animate({right: chatSize[0],
width: videospaceWidth,
height: videospaceHeight},
{queue: false,
duration: 500,
complete: completeFunction
});
$('#remoteVideos').animate({height: thumbnailsHeight},
{queue: false,
duration: 500});
$('#remoteVideos>span').animate({height: thumbnailsHeight,
width: thumbnailsWidth},
{queue: false,
duration: 500,
complete: function() {
$(document).trigger(
"remotevideo.resized",
[thumbnailsWidth,
thumbnailsHeight]);
}});
$('#largeVideoContainer').animate({ width: videospaceWidth,
height: videospaceHeight},
{queue: false,
duration: 500
});
$('#largeVideo').animate({ width: videoWidth,
height: videoHeight,
top: verticalIndent,
bottom: verticalIndent,
left: horizontalIndent,
right: horizontalIndent},
{ queue: false,
duration: 500
});
if (ContactList.isVisible()) {
$("#toast-container").animate({right: '12px'},
{queue: false,
duration: 500});
$('#contactlist').hide("slide", { direction: "right",
queue: false,
duration: 500});
} else {
// Undock the toolbar when the chat is shown and if we're in a
// video mode.
if (VideoLayout.isLargeVideoVisible())
ToolbarToggler.dockToolbar(false);
$("#toast-container").animate({right: '212px'},
{queue: false,
duration: 500});
$('#contactlist').show("slide", { direction: "right",
queue: false,
duration: 500});
//stop the glowing of the contact list icon
setVisualNotification(false);
}
};
/**
* Updates the number of participants in the contact list button and sets
* the glow
* @param delta indicates whether a new user has joined (1) or someone has
* left(-1)
*/
function updateNumberOfParticipants(delta) {
//when the user is alone we don't show the number of participants
if(numberOfContacts === 0) {
$("#numberOfParticipants").text('');
numberOfContacts += delta;
} else if(numberOfContacts !== 0 && !ContactList.isVisible()) {
setVisualNotification(true);
numberOfContacts += delta;
$("#numberOfParticipants").text(numberOfContacts);
}
};
/**
* Creates the avatar element.
*
* @return the newly created avatar element
*/
function createAvatar() {
var avatar = document.createElement('i');
avatar.className = "icon-avatar avatar";
return avatar;
}
/**
* Creates the display name paragraph.
*
* @param displayName the display name to set
*/
function createDisplayNameParagraph(displayName) {
var p = document.createElement('p');
p.innerText = displayName;
return p;
}
/**
* Shows/hides a visual notification, indicating that a new user has joined
* the conference.
*/
function setVisualNotification(show, stopGlowingIn) {
var glower = $('#contactListButton');
function stopGlowing() {
window.clearInterval(notificationInterval);
notificationInterval = false;
glower.removeClass('glowing');
if(!ContactList.isVisible()) {
glower.removeClass('active');
}
}
if (show && !notificationInterval) {
notificationInterval = window.setInterval(function () {
glower.toggleClass('active glowing');
}, 800);
}
else if (!show && notificationInterval) {
stopGlowing();
}
if(stopGlowingIn) {
setTimeout(stopGlowing, stopGlowingIn);
}
}
/**
* Indicates that the display name has changed.
*/
$(document).bind( 'displaynamechanged',
function (event, peerJid, displayName) {
if (peerJid === 'localVideoContainer')
peerJid = connection.emuc.myroomjid;
var resourceJid = Strophe.getResourceFromJid(peerJid);
var contactName = $('#contactlist #' + resourceJid + '>p');
if (contactName && displayName && displayName.length > 0)
contactName.text(displayName);
});
my.setClickable = function(resourceJid, isClickable) {
var contact = $('#contactlist>ul>li[id="' + resourceJid + '"]');
if(isClickable) {
contact.addClass('clickable');
} else {
contact.removeClass('clickable');
}
};
return my;
}(ContactList || {}));
#chatspace {
background-color: black;
border-left: 1px solid #424242;
}
#chatspace * {
-webkit-user-select: text;
user-select: text;
}
#chatconversation {
visibility: hidden;
position: relative;
top: 5px;
padding: 5px;
text-align: left;
line-height: 20px;
font-size: 10pt;
width: 100%;
height: 90%;
overflow-y: scroll;
overflow-x: hidden;
word-wrap: break-word;
}
.localuser {
color: #087dba;
}
.errorMessage {
color: red;
}
.remoteuser {
color: white;
}
#usermsg {
visibility:hidden;
position: absolute;
bottom: 0px;
right: 0px;
width: 83%;
height: 30px;
padding: 5px 5px 5px 0px;
max-height:150px;
min-height:35px;
border: 0px none;
background: #3a3a3a;
color: #a7a7a7;
box-shadow: none;
border-radius:0;
font-size: 10pt;
line-height: 30px;
overflow: hidden;
resize: none;
}
#usermsg:hover {
border: 0px none;
box-shadow: none;
}
#nickname {
position: absolute;
text-align: center;
color: #9d9d9d;
font-size: 18px;
top: 100px;
left: 5px;
right: 5px;
width: 95%;
}
#nickinput {
margin-top: 20px;
font-size: 14px;
background: #3a3a3a;
box-shadow: inset 0 0 3px 2px #a7a7a7;
border: 1px solid #a7a7a7;
color: #a7a7a7;
}
#unreadMessages {
font-size: 8px;
position: absolute;
left: 46%;
top: 27%
}
#bottomUnreadMessages {
top: 5px;
left: 10px;
position: absolute;
font-size: 8px;
}
#chatspace .username {
float: left;
padding-left: 5px;
font-weight: bold;
}
#chatspace .timestamp {
float: right;
padding-right: 5px;
font-size: 11px;
}
#chatspace .usermessage {
padding-top: 20px;
padding-left: 5px;
}
.chatArrow {
position: absolute;
height: 15px;
left: 5px;
}
.chatmessage {
background: #3a3a3a;
width: 93%;
margin-left: 5%;
margin-right: auto;
border-radius: 5px;
border-top-left-radius: 0px;
margin-top: 3px;
left: 5px;
color: #a7a7a7;
overflow: hidden;
padding-bottom: 3px;
}
.smiley {
height: 26px;
}
#smileys {
position: absolute;
bottom: 7px;
right: 5px;
background: white;
border-radius: 50px;
height: 26px;
margin: auto;
cursor: pointer;
}
#smileys img {
width: 22px;
padding: 2px;
}
#smileysarea {
position: absolute;
bottom: 0px;
left: 0px;
width: 17%;
min-width: 31px;
height: 40px;
padding: 0px;
max-height:150px;
min-height:35px;
border: 0px none;
background: #3a3a3a;
overflow: hidden;
visibility: hidden;
}
#smileysContainer {
display: none;
position: absolute;
background: #3a3a3a;
border-bottom: 1px solid;
border-top: 1px solid;
width: 100%;
bottom: 10%;
}
#smileysContainer .smiley {
padding: 7px;
}
.smileyContainer {
width: 40px;
height: 40px;
display: inline-block;
}
.smileyContainer:hover {
background: #3e3e3e;
}
#usermsg::-webkit-input-placeholder {
line-height: 30px;
}
::-webkit-scrollbar {
background: #06a5df;
width: 7px;
}
::-webkit-scrollbar-button {
display: none;
}
::-webkit-scrollbar-track {
background: black;
}
::-webkit-scrollbar-track-piece {
background: black;
}
::-webkit-scrollbar-thumb {
background: #06a5df;
border-radius: 4px;
}
#usermsg::-webkit-scrollbar-track-piece {
background: #3a3a3a;
}
a:link {
color: rgb(184, 184, 184);
}
a:visited {
color: white;
}
a:hover {
color: rgb(213, 213, 213);
}
a:active {
color: black;
}
\ No newline at end of file
body {
width:100%;
height:100%;
background-color: white;
color: #424242;
font-family:'YanoneKaffeesatzLight',Verdana,Tahoma,Arial;
margin:0;
padding:0;
}
#wrap{
display: block;
position: absolute;
width:900px;
height: 262px;
overflow:hidden;
text-align: center;
margin: auto;
top: 0; left: 0; bottom: 0; right: 0;
}
#left{
display:inline-block;
background-image:url(../images/chromelogo.png);
background-repeat:no-repeat;
width:246px;
height:262px;
float: left;
}
.firefox{
font-size: 11pt;
color: #c8c8c8;
}
#middle{
display:inline-block;
background-image:url(../images/chromepointer.png);
background-repeat:no-repeat;
width:53px;
height:262px;
float: left;
}
#text{
display:inline-block;
font-size: 18pt;
width: 560px;
vertical-align:middle;
padding-top: 30px;
}
a {
color: #087dba;
text-decoration:none;
}
#contactlist {
background-color: black;
cursor: default;
}
#contactlist>ul {
margin: 0px;
padding: 0px;
}
#contactlist>ul>li {
list-style-type: none;
text-align: left;
color: #FFF;
font-size: 10pt;
padding: 7px 10px;
margin: 2px;
}
#contactlist>ul>li>p {
display: inline-block;
vertical-align: middle;
margin: 0px;
}
#contactlist>ul>li.title {
color: #00ccff;
font-size: 11pt;
border-bottom: 1px solid #676767;
}
.avatar {
padding: 0px;
margin-right: 10px;
vertical-align: middle;
font-size: 22pt;
}
#contactlist .clickable {
cursor: pointer;
}
\ No newline at end of file
@font-face {
font-family: 'jitsi';
src:url('../fonts/jitsi.eot?94d075');
src:url('../fonts/jitsi.eot?#iefix94d075') format('embedded-opentype'),
url('../fonts/jitsi.woff?94d075') format('woff'),
url('../fonts/jitsi.ttf?94d075') format('truetype'),
url('../fonts/jitsi.svg?94d075#jitsi') format('svg');
font-weight: normal;
font-style: normal;
}
[class^="icon-"], [class*=" icon-"] {
font-family: 'jitsi';
speak: none;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 0.75em;
font-size: 1.22em;
/* Better Font Rendering =========== */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-contactList:before {
content: "\e615";
}
.icon-avatar:before {
content: "\e616";
}
.icon-callRetro:before {
content: "\e611";
}
.icon-callModern:before {
content: "\e612";
}
.icon-recDisable:before {
content: "\e613";
}
.icon-recEnable:before {
content: "\e614";
}
.icon-kick1:before {
content: "\e60f";
}
.icon-kick:before {
content: "\e610";
}
.icon-share-desktop:before {
content: "\e602";
}
.icon-chat-simple:before {
content: "\e606";
}
.icon-full-screen:before {
content: "\e60d";
}
.icon-exit-full-screen:before {
content: "\e60e";
}
.icon-prezi:before {
content: "\e60c";
}
.icon-link:before {
content: "\e600";
}
.icon-chat:before {
content: "\e601";
}
.icon-presentation:before {
content: "\e603";
}
.icon-security:before {
content: "\e604";
}
.icon-share-doc:before {
content: "\e605";
}
.icon-telephone:before {
content: "\e611";
}
.icon-security-locked:before {
content: "\e607";
}
.icon-camera:before {
content: "\e608";
}
.icon-camera-disabled:before {
content: "\e609";
}
.icon-mic-disabled:before {
content: "\e60a";
}
.icon-microphone:before {
content: "\e60b";
}
.icon-hangup:before {
content: "\e617";
}
.icon-reload:before {
content: "\e618";
}
.icon-filmstrip:before {
content: "\e619";
}
.icon-connection:before {
line-height: normal;
content: "\e61a";
}
\ No newline at end of file
.jitsipopover {
position: absolute;
top: 0;
left: 0;
z-index: 1010;
display: none;
max-width: 300px;
min-width: 100px;
padding: 1px;
text-align: left;
color: #333333;
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;
margin-top: -10px;
margin-bottom: 35px;
}
.jitsipopover.black
{
background-color: rgba(0,0,0,0.8);
color: #ffffff;
}
.jitsipopover-content {
padding: 9px 14px;
font-size: 10pt;
white-space:pre-wrap;
text-align: center;
}
.jitsipopover > .arrow,
.jitsipopover > .arrow:after {
position: absolute;
display: block;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
}
.jitsipopover > .arrow {
border-width: 11px;
left: 50%;
margin-left: -11px;
border-bottom-width: 0;
border-top-color: #999999;
border-top-color: rgba(0, 0, 0, 0.25);
bottom: -11px;
}
.jitsipopover > .arrow:after {
border-width: 10px;
content: " ";
bottom: 1px;
margin-left: -10px;
border-bottom-width: 0;
border-top-color: #ffffff;
}
.jitsipopover.black > .arrow:after
{
border-top-color: rgba(0, 0, 0, 0.8);
}
.jitsiPopupmenuPadding {
height: 35px;
width: 100px;
position: absolute;
bottom: -35;
}
.jitsipopover_green
{
color: #4abd04;
}
.jitsipopover_orange
{
color: #ffa800;
}
.jitsipopover_blue
{
color: #06a5df;
}
.jitsipopover_showmore
{
background-color: #06a5df;
color: #ffffff;
cursor: pointer;
border-radius: 3px;
text-align: center;
width: 90px;
height: 16px;
padding-top: 4px;
margin: 15px auto 0px auto;
}
/*
------------------------------
Impromptu
------------------------------
*/
.jqifade{
position: absolute;
background-color: #000;
}
div.jqi{
width: 400px;
position: absolute;
background-color: #ffffff;
font-size: 11px;
text-align: left;
border: solid 1px #eeeeee;
border-radius: 6px;
-moz-border-radius: 6px;
-webkit-border-radius: 6px;
padding: 7px;
}
div.jqi .jqicontainer{
}
div.jqi .jqiclose{
position: absolute;
top: 4px; right: -2px;
width: 18px;
cursor: default;
color: #bbbbbb;
font-weight: bold;
}
div.jqi .jqistate{
background-color: #fff;
}
div.jqi .jqititle{
padding: 5px 10px;
font-size: 16px;
line-height: 20px;
border-bottom: solid 1px #eeeeee;
}
div.jqi .jqimessage{
padding: 10px;
line-height: 20px;
color: #444444;
}
div.jqi .jqibuttons{
text-align: right;
margin: 0 -7px -7px -7px;
border-top: solid 1px #e4e4e4;
background-color: #f4f4f4;
border-radius: 0 0 6px 6px;
-moz-border-radius: 0 0 6px 6px;
-webkit-border-radius: 0 0 6px 6px;
}
div.jqi .jqibuttons button{
margin: 0;
padding: 5px 20px;
background-color: transparent;
font-weight: normal;
border: none;
border-left: solid 1px #e4e4e4;
color: #777;
font-weight: bold;
font-size: 12px;
}
div.jqi .jqibuttons button.jqidefaultbutton{
color: #489afe;
}
div.jqi .jqibuttons button:disabled {
color: #b6b6b6 !important;
}
div.jqi .jqibuttons button:hover,
div.jqi .jqibuttons button:focus{
color: #287ade;
outline: none;
}
.jqiwarning .jqi .jqibuttons{
background-color: #b95656;
}
/* sub states */
div.jqi .jqiparentstate::after{
background-color: #777;
opacity: 0.6;
filter: alpha(opacity=60);
content: '';
position: absolute;
top:0;left:0;bottom:0;right:0;
border-radius: 6px;
-moz-border-radius: 6px;
-webkit-border-radius: 6px;
}
div.jqi .jqisubstate{
position: absolute;
top:0;
left: 20%;
width: 60%;
padding: 7px;
border: solid 1px #eeeeee;
border-top: none;
border-radius: 0 0 6px 6px;
-moz-border-radius: 0 0 6px 6px;
-webkit-border-radius: 0 0 6px 6px;
}
div.jqi .jqisubstate .jqibuttons button{
padding: 10px 18px;
}
/* arrows for tooltips/tours */
.jqi .jqiarrow{ position: absolute; height: 0; width:0; line-height: 0; font-size: 0; border: solid 10px transparent;}
.jqi .jqiarrowtl{ left: 10px; top: -20px; border-bottom-color: #ffffff; }
.jqi .jqiarrowtc{ left: 50%; top: -20px; border-bottom-color: #ffffff; margin-left: -10px; }
.jqi .jqiarrowtr{ right: 10px; top: -20px; border-bottom-color: #ffffff; }
.jqi .jqiarrowbl{ left: 10px; bottom: -20px; border-top-color: #ffffff; }
.jqi .jqiarrowbc{ left: 50%; bottom: -20px; border-top-color: #ffffff; margin-left: -10px; }
.jqi .jqiarrowbr{ right: 10px; bottom: -20px; border-top-color: #ffffff; }
.jqi .jqiarrowlt{ left: -20px; top: 10px; border-right-color: #ffffff; }
.jqi .jqiarrowlm{ left: -20px; top: 50%; border-right-color: #ffffff; margin-top: -10px; }
.jqi .jqiarrowlb{ left: -20px; bottom: 10px; border-right-color: #ffffff; }
.jqi .jqiarrowrt{ right: -20px; top: 10px; border-left-color: #ffffff; }
.jqi .jqiarrowrm{ right: -20px; top: 50%; border-left-color: #ffffff; margin-top: -10px; }
.jqi .jqiarrowrb{ right: -20px; bottom: 10px; border-left-color: #ffffff; }
This diff is collapsed.
.jqistates {
font-size: 14px;
}
.jqistates h2 {
padding-bottom: 10px;
border-bottom: 1px solid #eee;
font-size: 18px;
line-height: 25px;
text-align: center;
color: #424242;
}
.jqistates input {
margin: 10px 0;
}
.jqistates input[type='text'], input[type='password'] {
width: 100%;
}
.jqibuttons button {
margin-right: 5px;
float:right;
}
button.jqidefaultbutton #inviteLinkRef {
color: #2c8ad2;
}
\ No newline at end of file
.popover {
position: absolute;
top: 0;
left: 0;
z-index: 1010;
display: none;
max-width: 300px;
min-width: 100px;
padding: 1px;
text-align: left;
color: #333333;
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;
}
\ No newline at end of file
/*Initialize*/
ul.popupmenu {
display:none;
position: absolute;
padding:10px;
margin: 0;
bottom: 0;
margin-bottom: 35px;
padding-bottom: 10px;
padding-top: 10px;
right: 10px;
left: -5px;
width: 100px;
background-color: rgba(0,0,0,0.9);
border: 1px solid rgba(256, 256, 256, 0.2);
border-radius:8px;
}
ul.popupmenu:after {
content: url('../images/popupPointer.png');
display: block;
position: absolute;
bottom: -8px;
left: 11px;
}
ul.popupmenu li {
list-style-type: none;
text-align: left;
}
ul.popupmenu li:hover {
background-color: rgba(256, 256, 256, .2);
border-radius:6px;
}
/*Link Appearance*/
ul.popupmenu li a {
text-decoration: none;
color: #fff;
padding: 5px;
display: inline-block;
font-size: 9pt;
}
ul.popupmenu li a i.icon-kick {
font-size: 8pt;
}
ul.popupmenu li a span {
display: inline-block;
width: 20px;
height: 16px;
text-align: center;
}
span.remotevideomenu:hover ul.popupmenu, ul.popupmenu:hover {
display:block !important;
}
a.disabled {
color: gray !important;
pointer-events: none;
}
.popupmenuPadding {
height: 35px;
width: 100px;
position: absolute;
bottom: -35;
left: 0px;
}
\ No newline at end of file
#settingsmenu {
background: black;
color: #00ccff;
}
#settingsmenu input {
margin-top: 10px;
margin-left: 10%;
width: 80%;
font-size: 14px;
background: #3a3a3a;
border: none;
box-shadow: none;
color: #a7a7a7;
}
#settingsmenu .arrow-up {
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-bottom: 5px solid #3a3a3a;
position: relative;
top: 10px;
margin-left: auto;
margin-right: auto;
}
#settingsmenu button {
width: 36%;
left: 32%;
padding: 0;
margin-top: 10px;
}
#settingsmenu #avatar {
width: 24%;
left: 38%;
border-radius: 25px;
position: relative;
}
#settingsmenu .icon-settings {
padding: 34px;
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
jitsi-meet (1.0.1-1) unstable; urgency=low
* Initial release. (Closes: #760485)
-- Damian Minkov <damencho@jitsi.org> Wed, 22 Oct 2014 10:30:00 +0200
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment