Compare commits
306 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15f4f03ba3 | ||
|
|
4f9b6f7180 | ||
|
|
b36ec5fd01 | ||
|
|
ac95ea03fe | ||
|
|
ae535fcb7d | ||
|
|
957cc6afc1 | ||
|
|
16fdd59617 | ||
|
|
fabf8f42c6 | ||
|
|
c98a56dc37 | ||
|
|
deb68dd420 | ||
|
|
0fd1a7fa08 | ||
|
|
c6ff8aa5dd | ||
|
|
06f025e92a | ||
|
|
f14329f2cd | ||
|
|
53e525597a | ||
|
|
54b3cbcf94 | ||
|
|
2852740e71 | ||
|
|
5322ba086b | ||
|
|
d2f95f3c81 | ||
|
|
3747251821 | ||
|
|
159ba82167 | ||
|
|
e34a8e6b60 | ||
|
|
17a6e360a2 | ||
|
|
b690f5d4a1 | ||
|
|
30f3168bf7 | ||
|
|
115f2e4663 | ||
|
|
fa15a75928 | ||
|
|
4db75446f3 | ||
|
|
d9f7b8b6cc | ||
|
|
05bbfda5bb | ||
|
|
e465b3ed90 | ||
|
|
1825f47ef2 | ||
|
|
169d613ac4 | ||
|
|
3dac5eeff5 | ||
|
|
f79651f806 | ||
|
|
6048d0a325 | ||
|
|
6f12446c99 | ||
|
|
af682f8727 | ||
|
|
9123923818 | ||
|
|
aee7a8e1bd | ||
|
|
5b44edb3cc | ||
|
|
806d4ea443 | ||
|
|
1e35ca5e4d | ||
|
|
d4f00d76ab | ||
|
|
37282e63b3 | ||
|
|
4b218499ae | ||
|
|
f16a1cdf44 | ||
|
|
702f02568d | ||
|
|
b6808d87bc | ||
|
|
8042bd2aa6 | ||
|
|
053b2d5af2 | ||
|
|
222164333b | ||
|
|
db50810e4b | ||
|
|
720851dcb9 | ||
|
|
d7203b8b1a | ||
|
|
204ca29ed7 | ||
|
|
fdada53a4a | ||
|
|
81eb3754a0 | ||
|
|
d260f1db61 | ||
|
|
74f078f166 | ||
|
|
e16cee4187 | ||
|
|
a904e35c67 | ||
|
|
b87cd9f842 | ||
|
|
fed34e7671 | ||
|
|
ed57f72117 | ||
|
|
4d39d4ccc3 | ||
|
|
79cdd94833 | ||
|
|
e0645b41d3 | ||
|
|
aa7f0c8a0b | ||
|
|
2362770cce | ||
|
|
8334036cf4 | ||
|
|
eec513e9e3 | ||
|
|
f2a7a43ba7 | ||
|
|
61bbbaf6eb | ||
|
|
3519a6ec7b | ||
|
|
d21f994eee | ||
|
|
b32acf0dfb | ||
|
|
71a56e13d9 | ||
|
|
0f6d0a0439 | ||
|
|
3032ea7684 | ||
|
|
04cfbafc33 | ||
|
|
57fcee676a | ||
|
|
2f5d090ca5 | ||
|
|
8d796f328b | ||
|
|
ffb1d6ea17 | ||
|
|
4447e5dac6 | ||
|
|
dbed14db5e | ||
|
|
254103e21f | ||
|
|
d0b39e1c97 | ||
|
|
4bb555e4b2 | ||
|
|
8d0ee3ded9 | ||
|
|
98d1ca8505 | ||
|
|
e766bad4ce | ||
|
|
9eb2873cfa | ||
|
|
c7e2331284 | ||
|
|
02ca5e5732 | ||
|
|
bc2d72638b | ||
|
|
40de181959 | ||
|
|
70bc071cb8 | ||
|
|
567ac23c2c | ||
|
|
af50bd5b94 | ||
|
|
899f0ee83d | ||
|
|
29b3ea07e0 | ||
|
|
c0a316c7df | ||
|
|
f624833f1f | ||
|
|
4c661ffca6 | ||
|
|
0819f23049 | ||
|
|
1e9a463245 | ||
|
|
447d8f5677 | ||
|
|
d2453b1f1f | ||
|
|
9460138cc3 | ||
|
|
0063461858 | ||
|
|
248d7a3173 | ||
|
|
51277270fe | ||
|
|
394738394d | ||
|
|
6c4a5bd2bc | ||
|
|
6347730dc7 | ||
|
|
3da8e39745 | ||
|
|
f4acf97b00 | ||
|
|
e4e66a03d7 | ||
|
|
ed78c0053c | ||
|
|
398fd18b8e | ||
|
|
d3003d4fcd | ||
|
|
ee94eca733 | ||
|
|
0696fb2c5a | ||
|
|
e6fbb0934e | ||
|
|
faaf24d3c4 | ||
|
|
fcf785f32c | ||
|
|
0508628871 | ||
|
|
27502d3fa8 | ||
|
|
1057ff36cd | ||
|
|
8d5e50c0ca | ||
|
|
8db602c8bd | ||
|
|
06494cf821 | ||
|
|
0fe6a55700 | ||
|
|
e20274c2f7 | ||
|
|
b77106f61a | ||
|
|
4d25b139cc | ||
|
|
6ce48a5b7b | ||
|
|
69b0e2ad32 | ||
|
|
cd0c9393d8 | ||
|
|
2494444ca4 | ||
|
|
c76b78eb46 | ||
|
|
78fcc8b72c | ||
|
|
652412cd4f | ||
|
|
78801aa9e5 | ||
|
|
cd266f60d7 | ||
|
|
459891e647 | ||
|
|
6cc8b63104 | ||
|
|
1aed7e6237 | ||
|
|
087c26d494 | ||
|
|
4fb7001b00 | ||
|
|
f1cb3af345 | ||
|
|
427dc093cc | ||
|
|
87f8b91a96 | ||
|
|
fdcae01d21 | ||
|
|
f95d5f36bb | ||
|
|
1938280e27 | ||
|
|
dc5d5f8436 | ||
|
|
efc161dacd | ||
|
|
2ee5a92ef0 | ||
|
|
4a991f7187 | ||
|
|
91358476a1 | ||
|
|
ec5e0f09ea | ||
|
|
a1da42ff00 | ||
|
|
5b34a66cb6 | ||
|
|
996b1791d5 | ||
|
|
3b0fcad39b | ||
|
|
484b80965c | ||
|
|
c6d8e34779 | ||
|
|
7bacd957bd | ||
|
|
e830ced554 | ||
|
|
2041b54a07 | ||
|
|
3473f1c20c | ||
|
|
10adea1691 | ||
|
|
b3a4b8a1cf | ||
|
|
78f8521145 | ||
|
|
ba627718be | ||
|
|
698f511676 | ||
|
|
f4004656a3 | ||
|
|
66e1a98869 | ||
|
|
cc38c2641b | ||
|
|
d1c634abc2 | ||
|
|
4230aa1ff1 | ||
|
|
ae4dafb06d | ||
|
|
4c95921b06 | ||
|
|
8068d4e810 | ||
|
|
e8779eeb18 | ||
|
|
a1624138fe | ||
|
|
451d16a664 | ||
|
|
b6a665e007 | ||
|
|
e4154c055e | ||
|
|
28e41f6c7b | ||
|
|
3f15d5495d | ||
|
|
60e7482df1 | ||
|
|
7f057377d0 | ||
|
|
933a41492e | ||
|
|
1729f7e17e | ||
|
|
3c96c91ca8 | ||
|
|
cb8f57b3e3 | ||
|
|
5ba666de2e | ||
|
|
66f7ddd6b2 | ||
|
|
79b7df28c1 | ||
|
|
f0a4c08f26 | ||
|
|
36065b935c | ||
|
|
27eecff826 | ||
|
|
8bb5994715 | ||
|
|
91c3c9ca83 | ||
|
|
4b8eef0f3e | ||
|
|
4776605dec | ||
|
|
7b0be8e953 | ||
|
|
5af92474c3 | ||
|
|
f749bed1dd | ||
|
|
e308025143 | ||
|
|
63dd6df217 | ||
|
|
bb5d178220 | ||
|
|
edb89a65d5 | ||
|
|
e2058edfdd | ||
|
|
6cf96c5d72 | ||
|
|
abe3ef199f | ||
|
|
50b4f33207 | ||
|
|
0663efe8cb | ||
|
|
501c97b27c | ||
|
|
d4a5b3cf2e | ||
|
|
37bb4b82ad | ||
|
|
b035bfc9aa | ||
|
|
6b968b1d14 | ||
|
|
f06f4cd1ba | ||
|
|
b5ecdc8dee | ||
|
|
7e90d73003 | ||
|
|
71b63cd0b3 | ||
|
|
8f94ac8b09 | ||
|
|
871c661ba9 | ||
|
|
3f3046893e | ||
|
|
0f6b6ae960 | ||
|
|
cd6264d0df | ||
|
|
285096cc99 | ||
|
|
84a453597c | ||
|
|
7dc8102dee | ||
|
|
ed2d7e4282 | ||
|
|
afaa96b737 | ||
|
|
d8ebea0d8b | ||
|
|
a0fef34a1f | ||
|
|
9d3aef2efa | ||
|
|
6646b2821a | ||
|
|
613e18952b | ||
|
|
6afcfb2598 | ||
|
|
4efad3d3da | ||
|
|
73571e7a18 | ||
|
|
f629ec17fa | ||
|
|
a8909028c8 | ||
|
|
5b9b45c91b | ||
|
|
1d0b4d0ecb | ||
|
|
1d4177faeb | ||
|
|
9b5edde621 | ||
|
|
c6cc570f1d | ||
|
|
c438676eae | ||
|
|
5213583af2 | ||
|
|
7da0fd6794 | ||
|
|
71e290a8ad | ||
|
|
03f828ba9a | ||
|
|
092149711b | ||
|
|
728e7ce70b | ||
|
|
99da618811 | ||
|
|
c2f6c9f7bc | ||
|
|
2004b6ef18 | ||
|
|
1ac99309e7 | ||
|
|
de7cc0b52b | ||
|
|
bc6b48cce9 | ||
|
|
982c8cbfac | ||
|
|
3fc819d12e | ||
|
|
96824e60ab | ||
|
|
46074dbd8c | ||
|
|
a4192b58db | ||
|
|
c5bf2f86ca | ||
|
|
13846b022c | ||
|
|
7475b3a66a | ||
|
|
3239813ce7 | ||
|
|
d97a8c63f9 | ||
|
|
2e26c212a2 | ||
|
|
dd608080b3 | ||
|
|
f5189d5cdc | ||
|
|
ab8e9ed87e | ||
|
|
78ef2a9266 | ||
|
|
071c01e644 | ||
|
|
19e4955392 | ||
|
|
3c21b09fa4 | ||
|
|
480d6f8c59 | ||
|
|
a0092b78ca | ||
|
|
ee1c221e6d | ||
|
|
1b5a3f08d2 | ||
|
|
feffcd18de | ||
|
|
41fd416338 | ||
|
|
4a062e5f5c | ||
|
|
1dab88e06a | ||
|
|
a6277b810e | ||
|
|
5c9f4ba65b | ||
|
|
aa5c2c11ad | ||
|
|
1057c6dd0d | ||
|
|
32e6a9a5d1 | ||
|
|
4a115ee215 | ||
|
|
6c7dac23ec | ||
|
|
6a489de167 | ||
|
|
f2a310f6c3 | ||
|
|
6861dc967f | ||
|
|
0896930f65 |
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.bundle.js -text -diff
|
||||
3
.gitignore
vendored
@@ -1 +1,4 @@
|
||||
node_modules
|
||||
*.swp
|
||||
.idea/
|
||||
*.iml
|
||||
24
Makefile
Normal file
@@ -0,0 +1,24 @@
|
||||
NPM = npm
|
||||
BROWSERIFY = browserify
|
||||
GLOBAL_FLAGS = -x jquery -e
|
||||
OUTPUT_DIR = .
|
||||
DEPLOY_DIR = libs
|
||||
|
||||
all: compile deploy clean
|
||||
|
||||
compile:FLAGS = $(GLOBAL_FLAGS)
|
||||
compile: app
|
||||
|
||||
debug: compile-debug deploy clean
|
||||
|
||||
compile-debug:FLAGS = -d $(GLOBAL_FLAGS)
|
||||
compile-debug: app
|
||||
|
||||
app:
|
||||
$(NPM) update && $(BROWSERIFY) $(FLAGS) app.js -s APP -o $(OUTPUT_DIR)/app.bundle.js
|
||||
|
||||
clean:
|
||||
@rm -f $(OUTPUT_DIR)/*.bundle.js
|
||||
|
||||
deploy:
|
||||
@mkdir -p $(DEPLOY_DIR) && cp $(OUTPUT_DIR)/*.bundle.js $(DEPLOY_DIR)
|
||||
25
README.md
@@ -1,18 +1,33 @@
|
||||
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.
|
||||
Jitsi Meet is an open-source (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
|
||||
## Installation
|
||||
|
||||
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.
|
||||
Installing Jitsi Meet is quite a simple experience. For Debian-based systems, we recommend following the [quick-install](https://github.com/jitsi/jitsi-meet/blob/master/doc/quick-install.md) document, which uses the package system.
|
||||
|
||||
You can find information on how to deploy Jitsi Meet in the [installation instructions](https://jitsi.org/meet/deploy)
|
||||
For other systems, or if you wish to install all components manually, see the [detailed manual installation instructions](https://github.com/jitsi/jitsi-meet/blob/master/doc/manual-install.md).
|
||||
|
||||
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/)
|
||||
## Building the sources
|
||||
|
||||
Jitsi Meet uses [Browserify](http://browserify.org). If you want to make changes in the code you need to [install Browserify](http://browserify.org/#install). Browserify requires [nodejs](http://nodejs.org).
|
||||
|
||||
On Debian/Ubuntu systems, the required packages can be installed with:
|
||||
```
|
||||
sudo apt-get install npm
|
||||
sudo npm install -g browserify
|
||||
cd jitsi-meet
|
||||
npm install
|
||||
```
|
||||
|
||||
To build the Jitsi Meet application, just type
|
||||
```
|
||||
make
|
||||
```
|
||||
|
||||
## Discuss
|
||||
Please use the [Jitsi dev mailing list](http://lists.jitsi.org/pipermail/dev/) to discuss feature requests before opening an issue on github.
|
||||
|
||||
202
api_connector.js
@@ -1,202 +0,0 @@
|
||||
/**
|
||||
* 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 {{
|
||||
* incommingMessage: boolean,
|
||||
* outgoingMessage: boolean,
|
||||
* displayNameChange: boolean,
|
||||
* participantJoined: boolean,
|
||||
* participantLeft: boolean
|
||||
* }}
|
||||
*/
|
||||
var events =
|
||||
{
|
||||
incommingMessage: 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;
|
||||
})();
|
||||
@@ -1,42 +0,0 @@
|
||||
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 || {}));
|
||||
452
chat.js
@@ -1,452 +0,0 @@
|
||||
/* global $, Util, connection, nickname:true, getVideoSize, getVideoPosition, showToolbar, processReplacements */
|
||||
/**
|
||||
* Chat related user interface.
|
||||
*/
|
||||
var Chat = (function (my) {
|
||||
var notificationInterval = false;
|
||||
var unreadMessages = 0;
|
||||
|
||||
/**
|
||||
* Initializes chat related interface.
|
||||
*/
|
||||
my.init = function () {
|
||||
var storedDisplayName = window.localStorage.displayname;
|
||||
if (storedDisplayName) {
|
||||
nickname = storedDisplayName;
|
||||
|
||||
Chat.setChatConversationMode(true);
|
||||
}
|
||||
|
||||
$('#nickinput').keydown(function (event) {
|
||||
if (event.keyCode === 13) {
|
||||
event.preventDefault();
|
||||
var val = Util.escapeHtml(this.value);
|
||||
this.value = '';
|
||||
if (!nickname) {
|
||||
nickname = val;
|
||||
window.localStorage.displayname = nickname;
|
||||
|
||||
connection.emuc.addDisplayNameToPresence(nickname);
|
||||
connection.emuc.sendPresence();
|
||||
|
||||
Chat.setChatConversationMode(true);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$('#usermsg').keydown(function (event) {
|
||||
if (event.keyCode === 13) {
|
||||
event.preventDefault();
|
||||
var value = this.value;
|
||||
$('#usermsg').val('').trigger('autosize.resize');
|
||||
this.focus();
|
||||
var command = new CommandsProcessor(value);
|
||||
if(command.isCommand())
|
||||
{
|
||||
command.processCommand();
|
||||
}
|
||||
else
|
||||
{
|
||||
var message = Util.escapeHtml(value);
|
||||
connection.emuc.sendMessage(message, nickname);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var onTextAreaResize = function () {
|
||||
resizeChatConversation();
|
||||
scrollChatToBottom();
|
||||
};
|
||||
$('#usermsg').autosize({callback: onTextAreaResize});
|
||||
|
||||
$("#chatspace").bind("shown",
|
||||
function () {
|
||||
unreadMessages = 0;
|
||||
setVisualNotification(false);
|
||||
});
|
||||
|
||||
addSmileys();
|
||||
};
|
||||
|
||||
/**
|
||||
* Appends the given message to the chat conversation.
|
||||
*/
|
||||
my.updateChatConversation = function (from, displayName, message) {
|
||||
var divClassName = '';
|
||||
|
||||
if (connection.emuc.myroomjid === from) {
|
||||
divClassName = "localuser";
|
||||
}
|
||||
else {
|
||||
divClassName = "remoteuser";
|
||||
|
||||
if (!Chat.isVisible()) {
|
||||
unreadMessages++;
|
||||
Util.playSoundNotification('chatNotification');
|
||||
setVisualNotification(true);
|
||||
}
|
||||
}
|
||||
|
||||
//replace links and smileys
|
||||
var escMessage = Util.escapeHtml(message);
|
||||
var escDisplayName = Util.escapeHtml(displayName);
|
||||
message = processReplacements(escMessage);
|
||||
|
||||
var messageContainer =
|
||||
'<div class="chatmessage">'+
|
||||
'<img src="../images/chatArrow.svg" class="chatArrow">' +
|
||||
'<div class="username ' + divClassName +'">' + escDisplayName + '</div>' +
|
||||
'<div class="timestamp">' + getCurrentTime() + '</div>' +
|
||||
'<div class="usermessage">' + message + '</div>' +
|
||||
'</div>';
|
||||
|
||||
$('#chatconversation').append(messageContainer);
|
||||
$('#chatconversation').animate(
|
||||
{ scrollTop: $('#chatconversation')[0].scrollHeight}, 1000);
|
||||
};
|
||||
|
||||
/**
|
||||
* Appends error message to the conversation
|
||||
* @param errorMessage the received error message.
|
||||
* @param originalText the original message.
|
||||
*/
|
||||
my.chatAddError = function(errorMessage, originalText)
|
||||
{
|
||||
errorMessage = Util.escapeHtml(errorMessage);
|
||||
originalText = Util.escapeHtml(originalText);
|
||||
|
||||
$('#chatconversation').append('<div class="errorMessage"><b>Error: </b>'
|
||||
+ 'Your message' + (originalText? (' \"'+ originalText + '\"') : "")
|
||||
+ ' was not sent.' + (errorMessage? (' Reason: ' + errorMessage) : '')
|
||||
+ '</div>');
|
||||
$('#chatconversation').animate(
|
||||
{ scrollTop: $('#chatconversation')[0].scrollHeight}, 1000);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the subject to the UI
|
||||
* @param subject the subject
|
||||
*/
|
||||
my.chatSetSubject = function(subject)
|
||||
{
|
||||
if(subject)
|
||||
subject = subject.trim();
|
||||
$('#subject').html(linkify(Util.escapeHtml(subject)));
|
||||
if(subject == "")
|
||||
{
|
||||
$("#subject").css({display: "none"});
|
||||
}
|
||||
else
|
||||
{
|
||||
$("#subject").css({display: "block"});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Opens / closes the chat area.
|
||||
*/
|
||||
my.toggleChat = function () {
|
||||
var chatspace = $('#chatspace');
|
||||
var videospace = $('#videospace');
|
||||
|
||||
var chatSize = (Chat.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 = Chat.isVisible() ?
|
||||
function() {} : function () {
|
||||
scrollChatToBottom();
|
||||
chatspace.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 (Chat.isVisible()) {
|
||||
chatspace.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);
|
||||
}
|
||||
|
||||
chatspace.show("slide", { direction: "right",
|
||||
queue: false,
|
||||
duration: 500,
|
||||
complete: function () {
|
||||
// Request the focus in the nickname field or the chat input field.
|
||||
if ($('#nickname').css('visibility') === 'visible') {
|
||||
$('#nickinput').focus();
|
||||
} else {
|
||||
$('#usermsg').focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
Chat.resizeChat();
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the chat conversation mode.
|
||||
*/
|
||||
my.setChatConversationMode = function (isConversationMode) {
|
||||
if (isConversationMode) {
|
||||
$('#nickname').css({visibility: 'hidden'});
|
||||
$('#chatconversation').css({visibility: 'visible'});
|
||||
$('#usermsg').css({visibility: 'visible'});
|
||||
$('#smileysarea').css({visibility: 'visible'});
|
||||
$('#usermsg').focus();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Resizes the chat area.
|
||||
*/
|
||||
my.resizeChat = function () {
|
||||
var chatSize = Chat.getChatSize();
|
||||
|
||||
$('#chatspace').width(chatSize[0]);
|
||||
$('#chatspace').height(chatSize[1]);
|
||||
|
||||
resizeChatConversation();
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the size of the chat.
|
||||
*/
|
||||
my.getChatSize = function () {
|
||||
var availableHeight = window.innerHeight;
|
||||
var availableWidth = window.innerWidth;
|
||||
|
||||
var chatWidth = 200;
|
||||
if (availableWidth * 0.2 < 200)
|
||||
chatWidth = availableWidth * 0.2;
|
||||
|
||||
return [chatWidth, availableHeight];
|
||||
};
|
||||
|
||||
/**
|
||||
* Indicates if the chat is currently visible.
|
||||
*/
|
||||
my.isVisible = function () {
|
||||
return $('#chatspace').is(":visible");
|
||||
};
|
||||
/**
|
||||
* Shows and hides the window with the smileys
|
||||
*/
|
||||
my.toggleSmileys = function() {
|
||||
var smileys = $('#smileysContainer');
|
||||
if(!smileys.is(':visible')) {
|
||||
smileys.show("slide", { direction: "down", duration: 300});
|
||||
} else {
|
||||
smileys.hide("slide", { direction: "down", duration: 300});
|
||||
}
|
||||
$('#usermsg').focus();
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds the smileys container to the chat
|
||||
*/
|
||||
function addSmileys() {
|
||||
var smileysContainer = document.createElement('div');
|
||||
smileysContainer.id = 'smileysContainer';
|
||||
function addClickFunction(smiley, number) {
|
||||
smiley.onclick = function addSmileyToMessage() {
|
||||
var usermsg = $('#usermsg');
|
||||
var message = usermsg.val();
|
||||
message += smileys['smiley' + number];
|
||||
usermsg.val(message);
|
||||
usermsg.get(0).setSelectionRange(message.length, message.length);
|
||||
Chat.toggleSmileys();
|
||||
usermsg.focus();
|
||||
};
|
||||
}
|
||||
for(var i = 1; i <= 21; i++) {
|
||||
var smileyContainer = document.createElement('div');
|
||||
smileyContainer.id = 'smiley' + i;
|
||||
smileyContainer.className = 'smileyContainer';
|
||||
var smiley = document.createElement('img');
|
||||
smiley.src = 'images/smileys/smiley' + i + '.svg';
|
||||
smiley.className = 'smiley';
|
||||
addClickFunction(smiley, i);
|
||||
smileyContainer.appendChild(smiley);
|
||||
smileysContainer.appendChild(smileyContainer);
|
||||
}
|
||||
|
||||
$("#chatspace").append(smileysContainer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resizes the chat conversation.
|
||||
*/
|
||||
function resizeChatConversation() {
|
||||
var msgareaHeight = $('#usermsg').outerHeight();
|
||||
var chatspace = $('#chatspace');
|
||||
var width = chatspace.width();
|
||||
var chat = $('#chatconversation');
|
||||
var smileys = $('#smileysarea');
|
||||
|
||||
smileys.height(msgareaHeight);
|
||||
$("#smileys").css('bottom', (msgareaHeight - 26) / 2);
|
||||
$('#smileysContainer').css('bottom', msgareaHeight);
|
||||
chat.width(width - 10);
|
||||
chat.height(window.innerHeight - 15 - msgareaHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows/hides a visual notification, indicating that a message has arrived.
|
||||
*/
|
||||
function setVisualNotification(show) {
|
||||
var unreadMsgElement = document.getElementById('unreadMessages');
|
||||
var unreadMsgBottomElement = document.getElementById('bottomUnreadMessages');
|
||||
|
||||
var glower = $('#chatButton');
|
||||
var bottomGlower = $('#chatBottomButton');
|
||||
|
||||
if (unreadMessages) {
|
||||
unreadMsgElement.innerHTML = unreadMessages.toString();
|
||||
unreadMsgBottomElement.innerHTML = unreadMessages.toString();
|
||||
|
||||
ToolbarToggler.dockToolbar(true);
|
||||
|
||||
var chatButtonElement
|
||||
= document.getElementById('chatButton').parentNode;
|
||||
var leftIndent = (Util.getTextWidth(chatButtonElement) -
|
||||
Util.getTextWidth(unreadMsgElement)) / 2;
|
||||
var topIndent = (Util.getTextHeight(chatButtonElement) -
|
||||
Util.getTextHeight(unreadMsgElement)) / 2 - 3;
|
||||
|
||||
unreadMsgElement.setAttribute(
|
||||
'style',
|
||||
'top:' + topIndent +
|
||||
'; left:' + leftIndent + ';');
|
||||
|
||||
var chatBottomButtonElement
|
||||
= document.getElementById('chatBottomButton').parentNode;
|
||||
var bottomLeftIndent = (Util.getTextWidth(chatBottomButtonElement) -
|
||||
Util.getTextWidth(unreadMsgBottomElement)) / 2;
|
||||
var bottomTopIndent = (Util.getTextHeight(chatBottomButtonElement) -
|
||||
Util.getTextHeight(unreadMsgBottomElement)) / 2 - 2;
|
||||
|
||||
unreadMsgBottomElement.setAttribute(
|
||||
'style',
|
||||
'top:' + bottomTopIndent +
|
||||
'; left:' + bottomLeftIndent + ';');
|
||||
|
||||
|
||||
if (!glower.hasClass('icon-chat-simple')) {
|
||||
glower.removeClass('icon-chat');
|
||||
glower.addClass('icon-chat-simple');
|
||||
}
|
||||
}
|
||||
else {
|
||||
unreadMsgElement.innerHTML = '';
|
||||
unreadMsgBottomElement.innerHTML = '';
|
||||
glower.removeClass('icon-chat-simple');
|
||||
glower.addClass('icon-chat');
|
||||
}
|
||||
|
||||
if (show && !notificationInterval) {
|
||||
notificationInterval = window.setInterval(function () {
|
||||
glower.toggleClass('active');
|
||||
bottomGlower.toggleClass('active glowing');
|
||||
}, 800);
|
||||
}
|
||||
else if (!show && notificationInterval) {
|
||||
window.clearInterval(notificationInterval);
|
||||
notificationInterval = false;
|
||||
glower.removeClass('active');
|
||||
bottomGlower.removeClass('glowing');
|
||||
bottomGlower.addClass('active');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrolls chat to the bottom.
|
||||
*/
|
||||
function scrollChatToBottom() {
|
||||
setTimeout(function () {
|
||||
$('#chatconversation').scrollTop(
|
||||
$('#chatconversation')[0].scrollHeight);
|
||||
}, 5);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current time in the format it is shown to the user
|
||||
* @returns {string}
|
||||
*/
|
||||
function getCurrentTime() {
|
||||
var now = new Date();
|
||||
var hour = now.getHours();
|
||||
var minute = now.getMinutes();
|
||||
var second = now.getSeconds();
|
||||
if(hour.toString().length === 1) {
|
||||
hour = '0'+hour;
|
||||
}
|
||||
if(minute.toString().length === 1) {
|
||||
minute = '0'+minute;
|
||||
}
|
||||
if(second.toString().length === 1) {
|
||||
second = '0'+second;
|
||||
}
|
||||
return hour+':'+minute+':'+second;
|
||||
}
|
||||
|
||||
return my;
|
||||
}(Chat || {}));
|
||||
@@ -1,20 +0,0 @@
|
||||
<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>
|
||||
98
commands.js
@@ -1,98 +0,0 @@
|
||||
/**
|
||||
* 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;
|
||||
})();
|
||||
13
config.js
@@ -4,7 +4,9 @@ var config = {
|
||||
//anonymousdomain: 'guest.example.com',
|
||||
muc: 'conference.jitsi-meet.example.com', // FIXME: use XEP-0030
|
||||
bridge: 'jitsi-videobridge.jitsi-meet.example.com', // FIXME: use XEP-0030
|
||||
//call_control: 'callcontrol.jitsi-meet.example.com'
|
||||
//jirecon: 'jirecon.jitsi-meet.example.com',
|
||||
//call_control: 'callcontrol.jitsi-meet.example.com',
|
||||
//focus: 'focus.jitsi-meet.example.com' - defaults to 'focus.jitsi-meet.example.com'
|
||||
},
|
||||
// getroomnode: function (path) { return 'someprefixpossiblybasedonpath'; },
|
||||
// useStunTurn: true, // use XEP-0215 to fetch STUN and TURN server
|
||||
@@ -12,19 +14,24 @@ var config = {
|
||||
useNicks: false,
|
||||
bosh: '//jitsi-meet.example.com/http-bind', // FIXME: use xep-0156 for that
|
||||
clientNode: 'http://jitsi.org/jitsimeet', // The name of client node advertised in XEP-0115 'c' stanza
|
||||
//focusUserJid: 'focus@auth.jitsi-meet.example.com', // The real JID of focus participant - can be overridden here
|
||||
//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
|
||||
disableStats: false,
|
||||
disableAudioLevels: false,
|
||||
channelLastN: -1, // The default value of the channel attribute last-n.
|
||||
adaptiveLastN: false,
|
||||
adaptiveSimulcast: false,
|
||||
useRtcpMux: true,
|
||||
useBundle: true,
|
||||
enableRecording: false,
|
||||
enableWelcomePage: false,
|
||||
enableWelcomePage: true,
|
||||
enableSimulcast: false,
|
||||
enableFirefoxSupport: false, //firefox support is still experimental, only one-to-one conferences with chrome focus
|
||||
// will work when simulcast, bundle, mux, lastN and SCTP are disabled.
|
||||
logStats: false // Enable logging of PeerConnection stats via the focus
|
||||
};
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
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;
|
||||
})();
|
||||
275
contact_list.js
@@ -1,275 +0,0 @@
|
||||
/**
|
||||
* 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');
|
||||
newContact.id = resourceJid;
|
||||
newContact.className = "clickable";
|
||||
newContact.onclick = function(event) {
|
||||
if(event.currentTarget.className === "clickable") {
|
||||
var jid = event.currentTarget.id;
|
||||
var videoContainer = $("#participant_" + jid);
|
||||
if (videoContainer.length > 0) {
|
||||
videoContainer.click();
|
||||
} else if (jid == Strophe.getResourceFromJid(connection.emuc.myroomjid)) {
|
||||
$("#localVideoContainer").click();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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()) {
|
||||
$('#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);
|
||||
|
||||
$('#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.innerHTML = 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.html(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 || {}));
|
||||
16
css/chat.css
@@ -221,3 +221,19 @@
|
||||
#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;
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
margin-right: 10px;
|
||||
vertical-align: middle;
|
||||
font-size: 22pt;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
#contactlist .clickable {
|
||||
|
||||
@@ -42,6 +42,9 @@
|
||||
.icon-recEnable:before {
|
||||
content: "\e614";
|
||||
}
|
||||
.icon-authenticate:before {
|
||||
content: "\e1ae";
|
||||
}
|
||||
.icon-kick1:before {
|
||||
content: "\e60f";
|
||||
}
|
||||
@@ -112,4 +115,8 @@
|
||||
.icon-connection:before {
|
||||
line-height: normal;
|
||||
content: "\e61a";
|
||||
}
|
||||
|
||||
.icon-settings:before {
|
||||
content: "\e61b";
|
||||
}
|
||||
@@ -99,5 +99,7 @@
|
||||
width: 90px;
|
||||
height: 16px;
|
||||
padding-top: 4px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
margin: 15px auto 0px auto;
|
||||
}
|
||||
|
||||
61
css/login_menu.css
Normal file
@@ -0,0 +1,61 @@
|
||||
/*Initialize*/
|
||||
ul.loginmenu {
|
||||
display:none;
|
||||
position: absolute;
|
||||
margin: 0;
|
||||
padding: 5px;
|
||||
padding-bottom: 7px;
|
||||
top: 45px;
|
||||
left: -5px;
|
||||
background-color: rgba(0,0,0,0.9);
|
||||
border: 1px solid rgba(256, 256, 256, 0.2);
|
||||
border-radius:8px;
|
||||
}
|
||||
|
||||
ul.loginmenu li {
|
||||
list-style-type: none;
|
||||
padding: 7px;
|
||||
}
|
||||
|
||||
ul.loginmenu li.identity {
|
||||
color: #fff;
|
||||
font-size: 11pt;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
ul.loginmenu:after {
|
||||
content: url('../images/dropdownPointer.png');
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: -7px;
|
||||
left: 18px;
|
||||
}
|
||||
|
||||
li a.authButton{
|
||||
background-color: #06a5df;
|
||||
padding-top: 3px;
|
||||
padding-bottom: 3px;
|
||||
padding-left: 29px;
|
||||
padding-right: 29px;
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
font-size: 11pt;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
span.authentication:hover ul.loginmenu, ul.loginmenu:hover {
|
||||
display:block !important;
|
||||
}
|
||||
|
||||
a.disabled {
|
||||
color: gray !important;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.loginmenuPadding {
|
||||
width: 50px;
|
||||
height: 30px;
|
||||
position: absolute;
|
||||
top: -30px;
|
||||
left: 0px;
|
||||
}
|
||||
60
css/main.css
@@ -13,8 +13,7 @@ html, body{
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
#chatspace,
|
||||
#contactlist {
|
||||
.right-panel {
|
||||
display:none;
|
||||
position:absolute;
|
||||
float: right;
|
||||
@@ -38,10 +37,6 @@ html, body{
|
||||
display:none;
|
||||
}
|
||||
|
||||
#settingsButton {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.toolbar_span {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
@@ -311,3 +306,56 @@ form {
|
||||
text-decoration: none;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
#toast-container.notification-bottom-right {
|
||||
bottom: 120px;
|
||||
right: 5px;
|
||||
}
|
||||
|
||||
#toast-container.notification-bottom-right-center {
|
||||
right: 205px;
|
||||
}
|
||||
|
||||
#toast-container .toast-info {
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.toast-close-button {
|
||||
right: -7px;
|
||||
top: -19px;
|
||||
}
|
||||
|
||||
#toast-container .toast-info {
|
||||
background-color: black;
|
||||
border: 1px solid #3a3a3a;
|
||||
width: 220px;
|
||||
padding: 10px 10px 10px 50px;
|
||||
}
|
||||
|
||||
.connected {
|
||||
color: forestgreen;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.disconnected {
|
||||
color: darkred;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.lastN {
|
||||
color: #a3a3a3;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.toast-close-button:hover,
|
||||
.toast-close-button:focus {
|
||||
color: #ffffff;
|
||||
opacity: 1;
|
||||
-ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=100);
|
||||
filter: alpha(opacity=100);
|
||||
}
|
||||
|
||||
.toast-message .nickname {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
49
css/settingsmenu.css
Normal file
@@ -0,0 +1,49 @@
|
||||
#settingsmenu {
|
||||
background: black;
|
||||
color: #00ccff;
|
||||
}
|
||||
|
||||
#settingsmenu input, select {
|
||||
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: 45%;
|
||||
left: 26%;
|
||||
padding: 0;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#settingsmenu #avatar {
|
||||
width: 24%;
|
||||
left: 38%;
|
||||
border-radius: 25px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#settingsmenu .icon-settings {
|
||||
padding: 34px;
|
||||
}
|
||||
|
||||
#languages_selectbox{
|
||||
height: 40px;
|
||||
}
|
||||
180
css/toastr.css
Normal file
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* Toastr
|
||||
* Copyright 2012-2014 John Papa and Hans Fjällemark.
|
||||
* All Rights Reserved.
|
||||
* Use, reproduction, distribution, and modification of this code is subject to the terms and
|
||||
* conditions of the MIT license, available at http://www.opensource.org/licenses/mit-license.php
|
||||
*
|
||||
* Author: John Papa and Hans Fjällemark
|
||||
* Project: https://github.com/CodeSeven/toastr
|
||||
*/
|
||||
.toast-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
.toast-message {
|
||||
-ms-word-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.toast-message a,
|
||||
.toast-message label {
|
||||
color: #ffffff;
|
||||
}
|
||||
.toast-message a:hover {
|
||||
color: #cccccc;
|
||||
text-decoration: none;
|
||||
}
|
||||
.toast-close-button {
|
||||
position: relative;
|
||||
right: -0.3em;
|
||||
top: -0.3em;
|
||||
float: right;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #ffffff;
|
||||
-webkit-text-shadow: 0 1px 0 #ffffff;
|
||||
text-shadow: 0 1px 0 #ffffff;
|
||||
opacity: 0.8;
|
||||
-ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=80);
|
||||
filter: alpha(opacity=80);
|
||||
}
|
||||
.toast-close-button:hover,
|
||||
.toast-close-button:focus {
|
||||
color: #000000;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
opacity: 0.4;
|
||||
-ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=40);
|
||||
filter: alpha(opacity=40);
|
||||
}
|
||||
/*Additional properties for button version
|
||||
iOS requires the button element instead of an anchor tag.
|
||||
If you want the anchor version, it requires `href="#"`.*/
|
||||
button.toast-close-button {
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
.toast-top-full-width {
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.toast-bottom-full-width {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.toast-top-left {
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
}
|
||||
.toast-top-right {
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
}
|
||||
.toast-bottom-right {
|
||||
right: 12px;
|
||||
bottom: 12px;
|
||||
}
|
||||
.toast-bottom-left {
|
||||
bottom: 12px;
|
||||
left: 12px;
|
||||
}
|
||||
#toast-container {
|
||||
position: fixed;
|
||||
z-index: 999999;
|
||||
/*overrides*/
|
||||
|
||||
}
|
||||
#toast-container * {
|
||||
-moz-box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
#toast-container > div {
|
||||
margin: 0 0 6px;
|
||||
padding: 15px 15px 15px 50px;
|
||||
width: 300px;
|
||||
-moz-border-radius: 3px 3px 3px 3px;
|
||||
-webkit-border-radius: 3px 3px 3px 3px;
|
||||
border-radius: 3px 3px 3px 3px;
|
||||
background-position: 15px center;
|
||||
background-repeat: no-repeat;
|
||||
-moz-box-shadow: 0 0 12px #999999;
|
||||
-webkit-box-shadow: 0 0 12px #999999;
|
||||
box-shadow: 0 0 12px #999999;
|
||||
color: #ffffff;
|
||||
opacity: 0.8;
|
||||
-ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=80);
|
||||
filter: alpha(opacity=80);
|
||||
}
|
||||
#toast-container > :hover {
|
||||
-moz-box-shadow: 0 0 12px #000000;
|
||||
-webkit-box-shadow: 0 0 12px #000000;
|
||||
box-shadow: 0 0 12px #000000;
|
||||
opacity: 1;
|
||||
-ms-filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=100);
|
||||
filter: alpha(opacity=100);
|
||||
cursor: pointer;
|
||||
}
|
||||
#toast-container > .toast-info {
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGwSURBVEhLtZa9SgNBEMc9sUxxRcoUKSzSWIhXpFMhhYWFhaBg4yPYiWCXZxBLERsLRS3EQkEfwCKdjWJAwSKCgoKCcudv4O5YLrt7EzgXhiU3/4+b2ckmwVjJSpKkQ6wAi4gwhT+z3wRBcEz0yjSseUTrcRyfsHsXmD0AmbHOC9Ii8VImnuXBPglHpQ5wwSVM7sNnTG7Za4JwDdCjxyAiH3nyA2mtaTJufiDZ5dCaqlItILh1NHatfN5skvjx9Z38m69CgzuXmZgVrPIGE763Jx9qKsRozWYw6xOHdER+nn2KkO+Bb+UV5CBN6WC6QtBgbRVozrahAbmm6HtUsgtPC19tFdxXZYBOfkbmFJ1VaHA1VAHjd0pp70oTZzvR+EVrx2Ygfdsq6eu55BHYR8hlcki+n+kERUFG8BrA0BwjeAv2M8WLQBtcy+SD6fNsmnB3AlBLrgTtVW1c2QN4bVWLATaIS60J2Du5y1TiJgjSBvFVZgTmwCU+dAZFoPxGEEs8nyHC9Bwe2GvEJv2WXZb0vjdyFT4Cxk3e/kIqlOGoVLwwPevpYHT+00T+hWwXDf4AJAOUqWcDhbwAAAAASUVORK5CYII=") !important;
|
||||
}
|
||||
#toast-container > .toast-error {
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHOSURBVEhLrZa/SgNBEMZzh0WKCClSCKaIYOED+AAKeQQLG8HWztLCImBrYadgIdY+gIKNYkBFSwu7CAoqCgkkoGBI/E28PdbLZmeDLgzZzcx83/zZ2SSXC1j9fr+I1Hq93g2yxH4iwM1vkoBWAdxCmpzTxfkN2RcyZNaHFIkSo10+8kgxkXIURV5HGxTmFuc75B2RfQkpxHG8aAgaAFa0tAHqYFfQ7Iwe2yhODk8+J4C7yAoRTWI3w/4klGRgR4lO7Rpn9+gvMyWp+uxFh8+H+ARlgN1nJuJuQAYvNkEnwGFck18Er4q3egEc/oO+mhLdKgRyhdNFiacC0rlOCbhNVz4H9FnAYgDBvU3QIioZlJFLJtsoHYRDfiZoUyIxqCtRpVlANq0EU4dApjrtgezPFad5S19Wgjkc0hNVnuF4HjVA6C7QrSIbylB+oZe3aHgBsqlNqKYH48jXyJKMuAbiyVJ8KzaB3eRc0pg9VwQ4niFryI68qiOi3AbjwdsfnAtk0bCjTLJKr6mrD9g8iq/S/B81hguOMlQTnVyG40wAcjnmgsCNESDrjme7wfftP4P7SP4N3CJZdvzoNyGq2c/HWOXJGsvVg+RA/k2MC/wN6I2YA2Pt8GkAAAAASUVORK5CYII=") !important;
|
||||
}
|
||||
#toast-container > .toast-success {
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADsSURBVEhLY2AYBfQMgf///3P8+/evAIgvA/FsIF+BavYDDWMBGroaSMMBiE8VC7AZDrIFaMFnii3AZTjUgsUUWUDA8OdAH6iQbQEhw4HyGsPEcKBXBIC4ARhex4G4BsjmweU1soIFaGg/WtoFZRIZdEvIMhxkCCjXIVsATV6gFGACs4Rsw0EGgIIH3QJYJgHSARQZDrWAB+jawzgs+Q2UO49D7jnRSRGoEFRILcdmEMWGI0cm0JJ2QpYA1RDvcmzJEWhABhD/pqrL0S0CWuABKgnRki9lLseS7g2AlqwHWQSKH4oKLrILpRGhEQCw2LiRUIa4lwAAAABJRU5ErkJggg==") !important;
|
||||
}
|
||||
#toast-container > .toast-warning {
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGYSURBVEhL5ZSvTsNQFMbXZGICMYGYmJhAQIJAICYQPAACiSDB8AiICQQJT4CqQEwgJvYASAQCiZiYmJhAIBATCARJy+9rTsldd8sKu1M0+dLb057v6/lbq/2rK0mS/TRNj9cWNAKPYIJII7gIxCcQ51cvqID+GIEX8ASG4B1bK5gIZFeQfoJdEXOfgX4QAQg7kH2A65yQ87lyxb27sggkAzAuFhbbg1K2kgCkB1bVwyIR9m2L7PRPIhDUIXgGtyKw575yz3lTNs6X4JXnjV+LKM/m3MydnTbtOKIjtz6VhCBq4vSm3ncdrD2lk0VgUXSVKjVDJXJzijW1RQdsU7F77He8u68koNZTz8Oz5yGa6J3H3lZ0xYgXBK2QymlWWA+RWnYhskLBv2vmE+hBMCtbA7KX5drWyRT/2JsqZ2IvfB9Y4bWDNMFbJRFmC9E74SoS0CqulwjkC0+5bpcV1CZ8NMej4pjy0U+doDQsGyo1hzVJttIjhQ7GnBtRFN1UarUlH8F3xict+HY07rEzoUGPlWcjRFRr4/gChZgc3ZL2d8oAAAAASUVORK5CYII=") !important;
|
||||
}
|
||||
#toast-container.toast-top-full-width > div,
|
||||
#toast-container.toast-bottom-full-width > div {
|
||||
width: 96%;
|
||||
margin: auto;
|
||||
}
|
||||
.toast {
|
||||
background-color: #030303;
|
||||
}
|
||||
.toast-success {
|
||||
background-color: #51a351;
|
||||
}
|
||||
.toast-error {
|
||||
background-color: #bd362f;
|
||||
}
|
||||
.toast-info {
|
||||
background-color: #2f96b4;
|
||||
}
|
||||
.toast-warning {
|
||||
background-color: #f89406;
|
||||
}
|
||||
/*Responsive Design*/
|
||||
@media all and (max-width: 240px) {
|
||||
#toast-container > div {
|
||||
padding: 8px 8px 8px 50px;
|
||||
width: 11em;
|
||||
}
|
||||
#toast-container .toast-close-button {
|
||||
right: -0.2em;
|
||||
top: -0.2em;
|
||||
}
|
||||
}
|
||||
@media all and (min-width: 241px) and (max-width: 480px) {
|
||||
#toast-container > div {
|
||||
padding: 8px 8px 8px 50px;
|
||||
width: 18em;
|
||||
}
|
||||
#toast-container .toast-close-button {
|
||||
right: -0.2em;
|
||||
top: -0.2em;
|
||||
}
|
||||
}
|
||||
@media all and (min-width: 481px) and (max-width: 768px) {
|
||||
#toast-container > div {
|
||||
padding: 15px 15px 15px 50px;
|
||||
width: 25em;
|
||||
}
|
||||
}
|
||||
111
css/unsupported_browser.css
Normal file
@@ -0,0 +1,111 @@
|
||||
body {
|
||||
width:100%;
|
||||
height:100%;
|
||||
background-color: white;
|
||||
color: #424242;
|
||||
font-family:Helvetica,'YanoneKaffeesatzLight',Verdana,Tahoma,Arial;
|
||||
font-size: 28px;
|
||||
margin:0;
|
||||
padding:0;
|
||||
}
|
||||
#wrap{
|
||||
display: block;
|
||||
position: absolute;
|
||||
width:900px;
|
||||
height: 365px;
|
||||
overflow:hidden;
|
||||
text-align: center;
|
||||
margin: auto;
|
||||
top: 0; left: 0; bottom: 0; right: 0;
|
||||
}
|
||||
.firefox{
|
||||
font-size: 11pt;
|
||||
color: #c8c8c8;
|
||||
width: 468px;
|
||||
text-align: center;
|
||||
margin: 30px auto 0px auto;
|
||||
padding-left: 15px;
|
||||
}
|
||||
#text{
|
||||
display:inline-block;
|
||||
font-size: 28px;
|
||||
width: 568px;
|
||||
vertical-align:middle;
|
||||
padding-top: 25px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #087dba;
|
||||
text-decoration:none;
|
||||
}
|
||||
|
||||
.browser {
|
||||
width: 138px;
|
||||
height: 163px;
|
||||
margin-top: 5px;
|
||||
background-color: #e8e8e8;
|
||||
border: 1px solid #cfcfcf;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.browser_wrapper
|
||||
{
|
||||
width: 138px;
|
||||
height: 188px;
|
||||
vertical-align: middle;
|
||||
color: #929391;
|
||||
font-size: 20px;
|
||||
float: left;
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.supported_browsers
|
||||
{
|
||||
margin: 0px auto 0px auto;
|
||||
width: 460px;
|
||||
}
|
||||
|
||||
.clear
|
||||
{
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.button
|
||||
{
|
||||
background-color: #62c82a;
|
||||
border: 1px solid #3c8117;
|
||||
border-radius: 10px;
|
||||
color: #FFFFFF;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
width: 115px;
|
||||
height: 26px;
|
||||
padding-top: 13px;
|
||||
margin: 15px auto 0px auto;
|
||||
}
|
||||
|
||||
.logo
|
||||
{
|
||||
margin: 20px auto 0px auto;
|
||||
}
|
||||
|
||||
#chrome_logo
|
||||
{
|
||||
width: 78px;
|
||||
height: 78px;
|
||||
background-image: url('/images/chrome.png');
|
||||
}
|
||||
#chromium_logo
|
||||
{
|
||||
width: 77px;
|
||||
height: 79px;
|
||||
background-image: url('/images/chromium.png');
|
||||
}
|
||||
#opera_logo
|
||||
{
|
||||
width: 73px;
|
||||
height: 78px;
|
||||
background-image: url('/images/opera.png');
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
|
||||
#remoteVideos .videocontainer {
|
||||
display: inline-block;
|
||||
background-image:url(../images/avatar1.png);
|
||||
background-color: black;
|
||||
background-size: contain;
|
||||
border-radius:8px;
|
||||
border: 2px solid #212425;
|
||||
@@ -102,6 +102,11 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#largeVideo
|
||||
{
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#presentation,
|
||||
#etherpad,
|
||||
#localVideoWrapper>video,
|
||||
@@ -115,10 +120,6 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.dominantspeaker {
|
||||
background: #000 !important;
|
||||
}
|
||||
|
||||
#etherpad,
|
||||
#presentation {
|
||||
text-align: center;
|
||||
@@ -373,8 +374,44 @@
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
border-radius:10px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#activeSpeaker {
|
||||
visibility: hidden;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
margin: auto;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#activeSpeakerAudioLevel {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
z-index: 1;
|
||||
visibility: inherit;
|
||||
}
|
||||
|
||||
#mixedstream {
|
||||
display:none !important;
|
||||
}
|
||||
|
||||
#activeSpeakerAvatar {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
top: 25px;
|
||||
margin: auto;
|
||||
position: relative;
|
||||
border-radius: 50px;
|
||||
z-index: 2;
|
||||
visibility: inherit;
|
||||
}
|
||||
|
||||
.userAvatar {
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
left: 35px;
|
||||
border-radius: 200px;
|
||||
}
|
||||
|
||||
163
data_channels.js
@@ -1,163 +0,0 @@
|
||||
/* global connection, Strophe, updateLargeVideo, focusedVideoSrc*/
|
||||
|
||||
// cache datachannels to avoid garbage collection
|
||||
// https://code.google.com/p/chromium/issues/detail?id=405545
|
||||
var _dataChannels = [];
|
||||
|
||||
/**
|
||||
* 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 Videobridge!", 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));
|
||||
|
||||
// when the data channel becomes available, tell the bridge about video
|
||||
// selections so that it can do adaptive simulcast,
|
||||
var largeVideoSrc = $('#largeVideo').attr('src');
|
||||
var userJid = getJidFromVideoSrc(largeVideoSrc);
|
||||
// we want the notification to trigger even if userJid is undefined,
|
||||
// or null.
|
||||
onSelectedEndpointChanged(userJid);
|
||||
};
|
||||
|
||||
dataChannel.onerror = function (error)
|
||||
{
|
||||
console.error("Data Channel Error:", error, dataChannel);
|
||||
};
|
||||
|
||||
dataChannel.onmessage = function (event)
|
||||
{
|
||||
var data = event.data;
|
||||
// JSON
|
||||
var obj;
|
||||
|
||||
try
|
||||
{
|
||||
obj = JSON.parse(data);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
console.error(
|
||||
"Failed to parse data channel message as JSON: ",
|
||||
data,
|
||||
dataChannel);
|
||||
}
|
||||
if (('undefined' !== typeof(obj)) && (null !== obj))
|
||||
{
|
||||
var colibriClass = obj.colibriClass;
|
||||
|
||||
if ("DominantSpeakerEndpointChangeEvent" === colibriClass)
|
||||
{
|
||||
// Endpoint ID from the Videobridge.
|
||||
var dominantSpeakerEndpoint = obj.dominantSpeakerEndpoint;
|
||||
|
||||
console.info(
|
||||
"Data channel new dominant speaker event: ",
|
||||
dominantSpeakerEndpoint);
|
||||
$(document).trigger(
|
||||
'dominantspeakerchanged',
|
||||
[dominantSpeakerEndpoint]);
|
||||
}
|
||||
else if ("LastNEndpointsChangeEvent" === colibriClass)
|
||||
{
|
||||
// The new/latest list of last-n endpoint IDs.
|
||||
var lastNEndpoints = obj.lastNEndpoints;
|
||||
/*
|
||||
* The list of endpoint IDs which are entering the list of
|
||||
* last-n at this time i.e. were not in the old list of last-n
|
||||
* endpoint IDs.
|
||||
*/
|
||||
var endpointsEnteringLastN = obj.endpointsEnteringLastN;
|
||||
|
||||
var stream = obj.stream;
|
||||
|
||||
console.log(
|
||||
"Data channel new last-n event: ",
|
||||
lastNEndpoints, endpointsEnteringLastN, obj);
|
||||
|
||||
$(document).trigger(
|
||||
'lastnchanged',
|
||||
[lastNEndpoints, endpointsEnteringLastN, stream]);
|
||||
}
|
||||
else if ("SimulcastLayersChangedEvent" === colibriClass)
|
||||
{
|
||||
var endpointSimulcastLayers = obj.endpointSimulcastLayers;
|
||||
$(document).trigger('simulcastlayerschanged', [endpointSimulcastLayers]);
|
||||
}
|
||||
else if ("SimulcastLayersChangingEvent" === colibriClass)
|
||||
{
|
||||
var endpointSimulcastLayers = obj.endpointSimulcastLayers;
|
||||
$(document).trigger('simulcastlayerschanging', [endpointSimulcastLayers]);
|
||||
}
|
||||
else if ("StartSimulcastLayerEvent" === colibriClass)
|
||||
{
|
||||
var simulcastLayer = obj.simulcastLayer;
|
||||
$(document).trigger('startsimulcastlayer', simulcastLayer);
|
||||
}
|
||||
else if ("StopSimulcastLayerEvent" === colibriClass)
|
||||
{
|
||||
var simulcastLayer = obj.simulcastLayer;
|
||||
$(document).trigger('stopsimulcastlayer', simulcastLayer);
|
||||
}
|
||||
else
|
||||
{
|
||||
console.debug("Data channel JSON-formatted message: ", obj);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
dataChannel.onclose = function ()
|
||||
{
|
||||
console.info("The Data Channel closed", dataChannel);
|
||||
var idx = _dataChannels.indexOf(dataChannel);
|
||||
if (idx > -1)
|
||||
_dataChannels = _dataChannels.splice(idx, 1);
|
||||
};
|
||||
_dataChannels.push(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);
|
||||
};*/
|
||||
}
|
||||
|
||||
2
debian/control
vendored
@@ -23,7 +23,7 @@ Description: WebRTC JavaScript video conferences
|
||||
Package: jitsi-meet-prosody
|
||||
Architecture: all
|
||||
Pre-Depends: openssl, prosody | prosody-trunk, jitsi-videobridge
|
||||
Depends: ${misc:Depends}
|
||||
Depends: ${misc:Depends}, jicofo
|
||||
Description: Prosody configuration for Jitsi Meet
|
||||
Jitsi Meet is a WebRTC JavaScript application that uses Jitsi
|
||||
Videobridge to provide high quality, scalable video conferences.
|
||||
|
||||
4
debian/jitsi-meet-prosody.config
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh -e
|
||||
|
||||
# Source debconf library.
|
||||
. /usr/share/debconf/confmodule
|
||||
53
debian/jitsi-meet-prosody.postinst
vendored
@@ -23,18 +23,68 @@ case "$1" in
|
||||
|
||||
. /etc/jitsi/videobridge/config
|
||||
|
||||
. /etc/jitsi/jicofo/config
|
||||
|
||||
# loading debconf
|
||||
. /usr/share/debconf/confmodule
|
||||
|
||||
# stores the hostname so we will reuse it later, like in purge
|
||||
db_set jitsi-meet-prosody/jvb-hostname $JVB_HOSTNAME
|
||||
|
||||
# and we're done with debconf
|
||||
db_stop
|
||||
|
||||
PROSODY_CONFIG_PRESENT="true"
|
||||
PROSODY_CREATE_JICOFO_USER="false"
|
||||
PROSODY_HOST_CONFIG="/etc/prosody/conf.avail/$JVB_HOSTNAME.cfg.lua"
|
||||
PROSODY_CONFIG_OLD="/etc/prosody/prosody.cfg.lua"
|
||||
# if there is no prosody config extract our template
|
||||
# check for config in conf.avail or check whether it wasn't already configured in main config
|
||||
if [ ! -f $PROSODY_HOST_CONFIG ] && ! grep -q "VirtualHost \"$JVB_HOSTNAME\"" /etc/prosody/prosody.cfg.lua; then
|
||||
if [ ! -f $PROSODY_HOST_CONFIG ] && ! grep -q "VirtualHost \"$JVB_HOSTNAME\"" $PROSODY_CONFIG_OLD; then
|
||||
PROSODY_CONFIG_PRESENT="false"
|
||||
mkdir -p /etc/prosody/conf.avail/
|
||||
cp /usr/share/doc/jitsi-meet-prosody/prosody.cfg.lua-jvb.example $PROSODY_HOST_CONFIG
|
||||
sed -i "s/jitmeet.example.com/$JVB_HOSTNAME/g" $PROSODY_HOST_CONFIG
|
||||
sed -i "s/jitmeetSecret/$JVB_SECRET/g" $PROSODY_HOST_CONFIG
|
||||
sed -i "s/focusSecret/$JICOFO_SECRET/g" $PROSODY_HOST_CONFIG
|
||||
sed -i "s/focusUser/$JICOFO_AUTH_USER/g" $PROSODY_HOST_CONFIG
|
||||
if [ ! -f /etc/prosody/conf.d/$JVB_HOSTNAME.cfg.lua ]; then
|
||||
ln -s $PROSODY_HOST_CONFIG /etc/prosody/conf.d/$JVB_HOSTNAME.cfg.lua
|
||||
fi
|
||||
PROSODY_CREATE_JICOFO_USER="true"
|
||||
# on some distributions main prosody config doesn't include configs
|
||||
# from conf.d folder enable it as this where we put our config by default
|
||||
if ! grep -q "Include \"conf\.d\/\*\.cfg.lua\"" $PROSODY_CONFIG_OLD; then
|
||||
echo -e "\nInclude \"conf.d/*.cfg.lua\"" >> $PROSODY_CONFIG_OLD
|
||||
fi
|
||||
fi
|
||||
# UPGRADE to server side focus check if focus is configured
|
||||
if [ -f $PROSODY_HOST_CONFIG ] && ! grep -q "VirtualHost \"auth.$JVB_HOSTNAME\"" $PROSODY_HOST_CONFIG; then
|
||||
echo -e "\nVirtualHost \"auth.$JVB_HOSTNAME\"" >> $PROSODY_HOST_CONFIG
|
||||
echo -e " authentication = \"internal_plain\"\n" >> $PROSODY_HOST_CONFIG
|
||||
sed -i "s/Component \"conference.$JVB_HOSTNAME\" \"muc\"/Component \"conference.$JVB_HOSTNAME\" \"muc\"\nadmins = { \"$JICOFO_AUTH_USER@auth.$JVB_HOSTNAME\" }\n/g" $PROSODY_HOST_CONFIG
|
||||
echo -e "Component \"focus.$JVB_HOSTNAME\"" >> $PROSODY_HOST_CONFIG
|
||||
echo -e " component_secret=\"$JICOFO_SECRET\"\n" >> $PROSODY_HOST_CONFIG
|
||||
PROSODY_CREATE_JICOFO_USER="true"
|
||||
# UPGRADE to server side focus on old config(/etc/prosody/prosody.cfg.lua)
|
||||
elif [ ! -f $PROSODY_HOST_CONFIG ] && ! grep -q "VirtualHost \"auth.$JVB_HOSTNAME\"" $PROSODY_CONFIG_OLD; then
|
||||
echo -e "\nVirtualHost \"auth.$JVB_HOSTNAME\"" >> $PROSODY_CONFIG_OLD
|
||||
echo -e " authentication = \"internal_plain\"\n" >> $PROSODY_CONFIG_OLD
|
||||
if ! grep -q "admins = { }" $PROSODY_CONFIG_OLD; then
|
||||
echo -e "admins = { \"$JICOFO_AUTH_USER@auth.$JVB_HOSTNAME\" }\n" >> $PROSODY_CONFIG_OLD
|
||||
else
|
||||
sed -i "s/admins = { }/admins = { \"$JICOFO_AUTH_USER@auth.$JVB_HOSTNAME\" }\n/g" $PROSODY_CONFIG_OLD
|
||||
fi
|
||||
echo -e "Component \"focus.$JVB_HOSTNAME\"" >> $PROSODY_CONFIG_OLD
|
||||
echo -e " component_secret=\"$JICOFO_SECRET\"\n" >> $PROSODY_CONFIG_OLD
|
||||
PROSODY_CREATE_JICOFO_USER="true"
|
||||
fi
|
||||
|
||||
if [ "$PROSODY_CREATE_JICOFO_USER" = "true" ]; then
|
||||
# create 'focus@auth.domain' prosody user
|
||||
prosodyctl register $JICOFO_AUTH_USER $JICOFO_AUTH_DOMAIN $JICOFO_AUTH_PASSWORD
|
||||
# trigger a restart
|
||||
PROSODY_CONFIG_PRESENT="false"
|
||||
fi
|
||||
|
||||
if [ ! -f /var/lib/prosody/$JVB_HOSTNAME.crt ]; then
|
||||
@@ -51,6 +101,7 @@ case "$1" in
|
||||
if [ "$PROSODY_CONFIG_PRESENT" = "false" ]; then
|
||||
invoke-rc.d prosody restart
|
||||
invoke-rc.d jitsi-videobridge restart
|
||||
invoke-rc.d jicofo restart
|
||||
fi
|
||||
;;
|
||||
|
||||
|
||||
11
debian/jitsi-meet-prosody.postrm
vendored
@@ -29,7 +29,16 @@ case "$1" in
|
||||
fi
|
||||
;;
|
||||
|
||||
purge|upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)
|
||||
purge)
|
||||
db_get jitsi-meet-prosody/jvb-hostname
|
||||
JVB_HOSTNAME=$RET
|
||||
if [ -n "$RET" ]; then
|
||||
rm -f /etc/prosody/conf.avail/$JVB_HOSTNAME.cfg.lua
|
||||
rm -f /etc/prosody/conf.d/$JVB_HOSTNAME.cfg.lua
|
||||
fi
|
||||
;;
|
||||
|
||||
upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)
|
||||
;;
|
||||
|
||||
*)
|
||||
|
||||
5
debian/jitsi-meet-prosody.templates
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
Template: jitsi-meet-prosody/jvb-hostname
|
||||
Type: string
|
||||
Default: ${default-key}
|
||||
_Description: The hostname of the current installation:
|
||||
The value for the hostname that is set in Jitsi Videobridge installation.
|
||||
4
debian/jitsi-meet.install
vendored
@@ -3,7 +3,9 @@
|
||||
*.html /usr/share/jitsi-meet/
|
||||
*.ico /usr/share/jitsi-meet/
|
||||
libs /usr/share/jitsi-meet/
|
||||
service /usr/share/jitsi-meet/
|
||||
css /usr/share/jitsi-meet/
|
||||
sounds /usr/share/jitsi-meet/
|
||||
fonts /usr/share/jitsi-meet/
|
||||
images /usr/share/jitsi-meet/
|
||||
images /usr/share/jitsi-meet/
|
||||
lang /usr/share/jitsi-meet/
|
||||
52
debian/jitsi-meet.postinst
vendored
@@ -37,35 +37,29 @@ case "$1" in
|
||||
sed -i "s/jitsi-meet.example.com/$JVB_HOSTNAME/g" /etc/nginx/sites-available/$JVB_HOSTNAME.conf
|
||||
fi
|
||||
|
||||
if grep "# server_names_hash_bucket_size 64" /etc/nginx/nginx.conf > /dev/null; then
|
||||
sed -i "s/#\ server_names_hash_bucket_size\ 64/\ server_names_hash_bucket_size\ 64/" /etc/nginx/nginx.conf
|
||||
fi
|
||||
|
||||
if [ ! -f /etc/ssl/$JVB_HOSTNAME.key ] || [ ! -f /etc/ssl/$JVB_HOSTNAME.crt ]; then
|
||||
# SSL for nginx
|
||||
db_get jitsi-meet/cert-choice
|
||||
CERT_CHOICE="$RET"
|
||||
if [ "$CERT_CHOICE" = 'A certificate is available and the files are uploaded on the server' ]; then
|
||||
db_set jitsi-meet/cert-path-key "/etc/ssl/$JVB_HOSTNAME.key"
|
||||
db_input critical jitsi-meet/cert-path-key || true
|
||||
db_go
|
||||
db_get jitsi-meet/cert-path-key
|
||||
CERT_KEY="$RET"
|
||||
db_set jitsi-meet/cert-path-crt "/etc/ssl/$JVB_HOSTNAME.crt"
|
||||
db_input critical jitsi-meet/cert-path-crt || true
|
||||
db_go
|
||||
db_get jitsi-meet/cert-path-crt
|
||||
CERT_CRT="$RET"
|
||||
# replace self-signed certificate paths with user provided ones
|
||||
CERT_KEY_ESC=$(echo $CERT_KEY | sed 's/\./\\\./g')
|
||||
CERT_KEY_ESC=$(echo $CERT_KEY_ESC | sed 's/\//\\\//g')
|
||||
sed -i "s/ssl_certificate_key\ \/var\/lib\/prosody\/.*key/ssl_certificate_key\ $CERT_KEY_ESC/g" \
|
||||
/etc/nginx/sites-available/$JVB_HOSTNAME.conf
|
||||
CERT_CRT_ESC=$(echo $CERT_CRT | sed 's/\./\\\./g')
|
||||
CERT_CRT_ESC=$(echo $CERT_CRT_ESC | sed 's/\//\\\//g')
|
||||
sed -i "s/ssl_certificate\ \/var\/lib\/prosody\/.*crt/ssl_certificate\ $CERT_CRT_ESC/g" \
|
||||
/etc/nginx/sites-available/$JVB_HOSTNAME.conf
|
||||
fi
|
||||
# SSL for nginx
|
||||
db_get jitsi-meet/cert-choice
|
||||
CERT_CHOICE="$RET"
|
||||
if [ "$CERT_CHOICE" = 'A certificate is available and the files are uploaded on the server' ]; then
|
||||
db_set jitsi-meet/cert-path-key "/etc/ssl/$JVB_HOSTNAME.key"
|
||||
db_input critical jitsi-meet/cert-path-key || true
|
||||
db_go
|
||||
db_get jitsi-meet/cert-path-key
|
||||
CERT_KEY="$RET"
|
||||
db_set jitsi-meet/cert-path-crt "/etc/ssl/$JVB_HOSTNAME.crt"
|
||||
db_input critical jitsi-meet/cert-path-crt || true
|
||||
db_go
|
||||
db_get jitsi-meet/cert-path-crt
|
||||
CERT_CRT="$RET"
|
||||
# replace self-signed certificate paths with user provided ones
|
||||
CERT_KEY_ESC=$(echo $CERT_KEY | sed 's/\./\\\./g')
|
||||
CERT_KEY_ESC=$(echo $CERT_KEY_ESC | sed 's/\//\\\//g')
|
||||
sed -i "s/ssl_certificate_key\ \/var\/lib\/prosody\/.*key/ssl_certificate_key\ $CERT_KEY_ESC/g" \
|
||||
/etc/nginx/sites-available/$JVB_HOSTNAME.conf
|
||||
CERT_CRT_ESC=$(echo $CERT_CRT | sed 's/\./\\\./g')
|
||||
CERT_CRT_ESC=$(echo $CERT_CRT_ESC | sed 's/\//\\\//g')
|
||||
sed -i "s/ssl_certificate\ \/var\/lib\/prosody\/.*crt/ssl_certificate\ $CERT_CRT_ESC/g" \
|
||||
/etc/nginx/sites-available/$JVB_HOSTNAME.conf
|
||||
fi
|
||||
|
||||
# jitsi meet
|
||||
|
||||
15
debian/patches/jquery-package
vendored
@@ -2,21 +2,18 @@ Index: jitsi-meet/index.html
|
||||
===================================================================
|
||||
--- jitsi-meet.orig/index.html
|
||||
+++ jitsi-meet/index.html
|
||||
@@ -9,7 +9,7 @@
|
||||
@@ -9,12 +9,12 @@
|
||||
<meta itemprop="name" content="Jitsi Meet"/>
|
||||
<meta itemprop="description" content="Join a WebRTC video conference powered by the Jitsi Videobridge"/>
|
||||
<meta itemprop="image" content="/images/jitsilogo.png"/>
|
||||
- <script src="libs/jquery-2.1.1.min.js"></script>
|
||||
+ <script src="libs/jquery.min.js"></script>
|
||||
<script src="config.js?v=5"></script><!-- adapt to your needs, i.e. set hosts and bosh path -->
|
||||
<script src="simulcast.js?v=5"></script><!-- simulcast handling -->
|
||||
<script src="libs/strophe/strophe.jingle.adapter.js?v=2"></script><!-- strophe.jingle bundles -->
|
||||
@@ -24,7 +24,7 @@
|
||||
<script src="libs/strophe/strophe.util.js"></script>
|
||||
<script src="libs/colibri/colibri.focus.js?v=10"></script><!-- colibri focus implementation -->
|
||||
<script src="libs/colibri/colibri.session.js?v=1"></script>
|
||||
<script src="config.js?v=6"></script><!-- adapt to your needs, i.e. set hosts and bosh path -->
|
||||
<script src="libs/strophe/strophe.min.js?v=1"></script>
|
||||
<script src="libs/strophe/strophe.disco.min.js?v=1"></script>
|
||||
<script src="libs/strophe/strophe.caps.jsonly.min.js?v=1"></script>
|
||||
- <script src="libs/jquery-ui.js"></script>
|
||||
+ <script src="libs/jquery-ui.min.js"></script>
|
||||
<script src="libs/rayo.js?v=1"></script>
|
||||
<script src="libs/tooltip.js?v=1"></script><!-- bootstrap tooltip lib -->
|
||||
<script src="libs/popover.js?v=1"></script><!-- bootstrap tooltip lib -->
|
||||
<script src="libs/toastr.js?v=1"></script><!-- notifications lib -->
|
||||
|
||||
15
debian/rules
vendored
@@ -1,10 +1,4 @@
|
||||
#!/usr/bin/make -f
|
||||
# -*- makefile -*-
|
||||
# Sample debian/rules that uses debhelper.
|
||||
# This file was originally written by Joey Hess and Craig Small.
|
||||
# As a special exception, when this file is copied by dh-make into a
|
||||
# dh-make output file, you may use that output file without restriction.
|
||||
# This special exception was added by Craig Small in version 0.37 of dh-make.
|
||||
|
||||
# Uncomment this to turn on verbose mode.
|
||||
#export DH_VERBOSE=1
|
||||
@@ -12,11 +6,14 @@
|
||||
%:
|
||||
dh $@
|
||||
|
||||
# we skip making Makefile exists for updating browserify modules when developing
|
||||
override_dh_auto_build:
|
||||
|
||||
override_dh_install:
|
||||
dh_installdirs
|
||||
dh_install -X/config.js
|
||||
yui-compressor -o debian/jitsi-meet/usr/share/jitsi-meet/libs/strophe/strophe.caps.jsonly.min.js \
|
||||
debian/patches/missing-source/libs/strophe/strophe.caps.jsonly.js
|
||||
debian/missing-source/libs/strophe/strophe.caps.jsonly.js
|
||||
yui-compressor -o debian/jitsi-meet/usr/share/jitsi-meet/libs/strophe/strophe.disco.min.js \
|
||||
debian/patches/missing-source/libs/strophe/sha1.js \
|
||||
debian/patches/missing-source/libs/strophe/strophe.disco.js
|
||||
debian/missing-source/libs/strophe/sha1.js \
|
||||
debian/missing-source/libs/strophe/strophe.disco.js
|
||||
|
||||
2
debian/watch
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
version=3
|
||||
https://github.com/jitsi/jitsi-meet/releases/ /jitsi/jitsi-meet/archive/(\S+)\.tar\.gz
|
||||
10
doc/api.md
@@ -91,7 +91,7 @@ with data related to the event.
|
||||
|
||||
Currently we support the following events:
|
||||
|
||||
* **incommingMessage** - event notifications about incomming
|
||||
* **incomingMessage** - event notifications about incoming
|
||||
messages. The listener will receive object with the following structure:
|
||||
```
|
||||
{
|
||||
@@ -135,7 +135,7 @@ This method requires one argument of type Object. The object argument must
|
||||
have keys with the names of the events and values the listeners of the events.
|
||||
|
||||
```
|
||||
function incommingMessageListener(object)
|
||||
function incomingMessageListener(object)
|
||||
{
|
||||
...
|
||||
}
|
||||
@@ -146,19 +146,19 @@ function outgoingMessageListener(object)
|
||||
}
|
||||
|
||||
api.addEventListeners({
|
||||
incommingMessage: incommingMessageListener,
|
||||
incomingMessage: incomingMessageListener,
|
||||
outgoingMessage: outgoingMessageListener})
|
||||
```
|
||||
|
||||
If you want to remove a listener you can use ```removeEventListener``` method with argument the name of the event.
|
||||
```
|
||||
api.removeEventListener("incommingMessage");
|
||||
api.removeEventListener("incomingMessage");
|
||||
```
|
||||
|
||||
If you want to remove more than one event you can use ```removeEventListeners``` method with argument
|
||||
array with the names of the events.
|
||||
```
|
||||
api.removeEventListeners(["incommingMessage", "outgoingMessageListener"]);
|
||||
api.removeEventListeners(["incomingMessage", "outgoingMessageListener"]);
|
||||
```
|
||||
|
||||
You can remove the embedded Jitsi Meet Conference with the following code:
|
||||
|
||||
@@ -16,6 +16,13 @@ VirtualHost "jitmeet.example.com"
|
||||
}
|
||||
|
||||
Component "conference.jitmeet.example.com" "muc"
|
||||
admins = { "focusUser@auth.jitmeet.example.com" }
|
||||
|
||||
Component "jitsi-videobridge.jitmeet.example.com"
|
||||
component_secret = "jitmeetSecret"
|
||||
|
||||
VirtualHost "auth.jitmeet.example.com"
|
||||
authentication = "internal_plain"
|
||||
|
||||
Component "focus.jitmeet.example.com"
|
||||
component_secret = "focusSecret"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
server_names_hash_bucket_size 64;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name jitsi-meet.example.com;
|
||||
@@ -17,7 +19,7 @@ server {
|
||||
alias /etc/jitsi/meet/jitsi-meet.example.com-config.js;
|
||||
}
|
||||
|
||||
location ~ ^/([a-zA-Z0-9]+)$ {
|
||||
location ~ ^/([a-zA-Z0-9=\?]+)$ {
|
||||
rewrite ^/(.*)$ / break;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ server {
|
||||
root /srv/jitsi.example.com;
|
||||
index index.html;
|
||||
|
||||
location ~ ^/([a-zA-Z0-9]+)$ {
|
||||
location ~ ^/([a-zA-Z0-9=\?]+)$ {
|
||||
rewrite ^/(.*)$ / break;
|
||||
}
|
||||
|
||||
|
||||
26
doc/influxdb.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Overview
|
||||
Jitsi Meet supports logging to an [InfluxDB](http://influxdb.com/) database.
|
||||
|
||||
# Configuration
|
||||
The following needs to be done to enable this functionality.
|
||||
|
||||
## Install InfluxDB
|
||||
The details are outside the scope of the document, see http://influxdb.com/download/ .
|
||||
|
||||
## Enable logging for Jitsi Videobridge
|
||||
Add the following properties to <code>/usr/share/jitsi-videobridge/.sip-communicator/sip-communicator.properties</code>.
|
||||
|
||||
- org.jitsi.videobridge.log.INFLUX_DB_ENABLED=true
|
||||
- org.jitsi.videobridge.log.INFLUX_URL_BASE=http://influxdb.example.com:8086
|
||||
- org.jitsi.videobridge.log.INFLUX_DATABASE=jitsi_database
|
||||
- org.jitsi.videobridge.log.INFLUX_USER=user
|
||||
- org.jitsi.videobridge.log.INFLUX_PASS=pass
|
||||
|
||||
## Enable logging for Jicofo
|
||||
Add the same properties as above to <code>/usr/share/jitsi-videobridge/.sip-communicator/sip-communicator.properties</code>.
|
||||
|
||||
## Enable logging for Jitsi Meet itself
|
||||
Change "logStats" to "true" in <code>/etc/jitsi/meet/you-domain.config.js</code> or the <code>config.js</code> file used in your installation.
|
||||
|
||||
# User interface
|
||||
You can explore the database using the [Jiloin](https://github.com/jitsi/jiloin) web interface.
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
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`.
|
||||
`YOURSECRET1`, `YOURSECRET2`, `YOURSECRET3` and `YOURSECRET4`.
|
||||
|
||||
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.
|
||||
|
||||
@@ -36,17 +36,32 @@ 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";
|
||||
}
|
||||
authentication = "anonymous"
|
||||
ssl = {
|
||||
key = "/var/lib/prosody/jitsi.example.com.key";
|
||||
certificate = "/var/lib/prosody/jitsi.example.com.crt";
|
||||
}
|
||||
```
|
||||
- add domain with authentication for conference focus user:
|
||||
```
|
||||
VirtualHost "auth.jitsi.example.com"
|
||||
authentication = "internal_plain"
|
||||
ssl = {
|
||||
key = "/var/lib/prosody/jitsi.example.com.key";
|
||||
certificate = "/var/lib/prosody/jitsi.example.com.crt";
|
||||
}
|
||||
```
|
||||
- add focus user to server admins:
|
||||
```
|
||||
admins = { "focus@auth.jitsi.example.com" }
|
||||
```
|
||||
- and finally configure components:
|
||||
```
|
||||
Component "conference.jitsi.example.com" "muc"
|
||||
Component "jitsi-videobridge.jitsi.example.com"
|
||||
component_secret = "YOURSECRET1"
|
||||
Component "focus.jitsi.example.com"
|
||||
component_secret = "YOURSECRET2"
|
||||
```
|
||||
|
||||
Generate certs for the domain:
|
||||
@@ -54,6 +69,11 @@ Generate certs for the domain:
|
||||
prosodyctl cert generate jitsi.example.com
|
||||
```
|
||||
|
||||
Create conference focus user:
|
||||
```sh
|
||||
prosodyctl register focus auth.jitsi.example.com YOURSECRET3
|
||||
```
|
||||
|
||||
Restart prosody XMPP server with the new config
|
||||
```sh
|
||||
prosodyctl restart
|
||||
@@ -137,6 +157,22 @@ Or autostart it by adding the line in `/etc/rc.local`:
|
||||
/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
|
||||
```
|
||||
|
||||
## Install Jitsi Conference Focus(jicofo)
|
||||
Clone source from Github repo:
|
||||
```sh
|
||||
git clone https://github.com/jitsi/jicofo.git
|
||||
```
|
||||
Build distribution package. Replace {os-name} with one of: 'lin', 'lin64', 'macosx', 'win', 'win64'.
|
||||
```sh
|
||||
cd jicofo
|
||||
ant dist.{os-name}
|
||||
```
|
||||
Run jicofo:
|
||||
```sh
|
||||
cd dist/{os-name}'
|
||||
./jicofo.sh --domain=jitsi.exmaple.com --secret=YOURSECRET2 --user_domain=auth.jitsi.example.com --user_name=focus --user_password=YOURSECRET3
|
||||
```
|
||||
|
||||
## Deploy Jitsi Meet
|
||||
Checkout and configure Jitsi Meet:
|
||||
```sh
|
||||
@@ -192,7 +228,7 @@ Configure addresses and ports as desired, and the password to be configured in p
|
||||
```
|
||||
realm jitsi.example.com
|
||||
# share this with your prosody server
|
||||
auth_shared YOURSECRET2
|
||||
auth_shared YOURSECRET4
|
||||
|
||||
# modules
|
||||
module_path /usr/lib/restund/modules
|
||||
@@ -201,7 +237,7 @@ 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_secret = "YOURSECRET4";
|
||||
turncredentials = {
|
||||
{ type = "turn", host = "turn.address.ip.configured", port = 3478, transport = "tcp" }
|
||||
}
|
||||
@@ -7,22 +7,6 @@ N.B.: All commands are supposed to be run by root. If you are logged in as a reg
|
||||
## Basic Jitsi Meet install
|
||||
|
||||
### Add the repository
|
||||
|
||||
```sh
|
||||
add-apt-repository 'deb http://download.jitsi.org/nightly/deb unstable/'
|
||||
wget -qO - https://download.jitsi.org/nightly/deb/unstable/archive.key | apt-key add -
|
||||
```
|
||||
|
||||
add-apt-repository is in the default Ubuntu install and is available for both Ubuntu and Debian, but if it's not present, either install it with
|
||||
|
||||
```sh
|
||||
apt-get -y install software-properties-common
|
||||
add-apt-repository 'deb http://download.jitsi.org/nightly/deb unstable/'
|
||||
wget -qO - https://download.jitsi.org/nightly/deb/unstable/archive.key | apt-key add -
|
||||
```
|
||||
|
||||
or add the repository by hand with
|
||||
|
||||
```sh
|
||||
echo 'deb http://download.jitsi.org/nightly/deb unstable/' >> /etc/apt/sources.list
|
||||
wget -qO - https://download.jitsi.org/nightly/deb/unstable/archive.key | apt-key add -
|
||||
@@ -74,18 +58,10 @@ Launch again a browser with the Jitsi Meet URL and you'll see a telephone icon o
|
||||
|
||||
Enjoy!
|
||||
|
||||
## Troubleshoot
|
||||
|
||||
If the SIP gateway doesn't work on first try, restart it.
|
||||
|
||||
```sh
|
||||
/etc/init.d/jigasi restart
|
||||
```
|
||||
|
||||
## Deinstall
|
||||
|
||||
```sh
|
||||
apt-get purge jigasi jitsi-meet jitsi-videobridge
|
||||
apt-get purge jigasi jitsi-meet jicofo jitsi-videobridge
|
||||
```
|
||||
|
||||
Somethimes the following packages will fail to uninstall properly:
|
||||
|
||||
17
estos_log.js
@@ -1,17 +0,0 @@
|
||||
/* global Strophe */
|
||||
Strophe.addConnectionPlugin('logger', {
|
||||
// logs raw stanzas and makes them available for download as JSON
|
||||
connection: null,
|
||||
log: [],
|
||||
init: function (conn) {
|
||||
this.connection = conn;
|
||||
this.connection.rawInput = this.log_incoming.bind(this);
|
||||
this.connection.rawOutput = this.log_outgoing.bind(this);
|
||||
},
|
||||
log_incoming: function (stanza) {
|
||||
this.log.push([new Date().getTime(), 'incoming', stanza]);
|
||||
},
|
||||
log_outgoing: function (stanza) {
|
||||
this.log.push([new Date().getTime(), 'outgoing', stanza]);
|
||||
},
|
||||
});
|
||||
197
etherpad.js
@@ -1,197 +0,0 @@
|
||||
/* global $, config, Prezi, Util, connection, setLargeVideoVisible, dockToolbar */
|
||||
var Etherpad = (function (my) {
|
||||
var etherpadName = null;
|
||||
var etherpadIFrame = null;
|
||||
var domain = null;
|
||||
var options = "?showControls=true&showChat=false&showLineNumbers=true&useMonospaceFont=false";
|
||||
|
||||
/**
|
||||
* Initializes the etherpad.
|
||||
*/
|
||||
my.init = function (name) {
|
||||
|
||||
if (config.etherpad_base && !etherpadName) {
|
||||
|
||||
domain = config.etherpad_base;
|
||||
|
||||
if (!name) {
|
||||
// In case we're the focus we generate the name.
|
||||
etherpadName = Math.random().toString(36).substring(7) +
|
||||
'_' + (new Date().getTime()).toString();
|
||||
shareEtherpad();
|
||||
}
|
||||
else
|
||||
etherpadName = name;
|
||||
|
||||
enableEtherpadButton();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Opens/hides the Etherpad.
|
||||
*/
|
||||
my.toggleEtherpad = function (isPresentation) {
|
||||
if (!etherpadIFrame)
|
||||
createIFrame();
|
||||
|
||||
var largeVideo = null;
|
||||
if (Prezi.isPresentationVisible())
|
||||
largeVideo = $('#presentation>iframe');
|
||||
else
|
||||
largeVideo = $('#largeVideo');
|
||||
|
||||
if ($('#etherpad>iframe').css('visibility') === 'hidden') {
|
||||
largeVideo.fadeOut(300, function () {
|
||||
if (Prezi.isPresentationVisible()) {
|
||||
largeVideo.css({opacity: '0'});
|
||||
} else {
|
||||
VideoLayout.setLargeVideoVisible(false);
|
||||
}
|
||||
|
||||
$('#etherpad>iframe').fadeIn(300, function () {
|
||||
document.body.style.background = '#eeeeee';
|
||||
$('#etherpad>iframe').css({visibility: 'visible'});
|
||||
$('#etherpad').css({zIndex: 2});
|
||||
});
|
||||
});
|
||||
}
|
||||
else if ($('#etherpad>iframe')) {
|
||||
$('#etherpad>iframe').fadeOut(300, function () {
|
||||
$('#etherpad>iframe').css({visibility: 'hidden'});
|
||||
$('#etherpad').css({zIndex: 0});
|
||||
document.body.style.background = 'black';
|
||||
if (!isPresentation) {
|
||||
$('#largeVideo').fadeIn(300, function () {
|
||||
VideoLayout.setLargeVideoVisible(true);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
resize();
|
||||
};
|
||||
|
||||
/**
|
||||
* Resizes the etherpad.
|
||||
*/
|
||||
function resize() {
|
||||
if ($('#etherpad>iframe').length) {
|
||||
var remoteVideos = $('#remoteVideos');
|
||||
var availableHeight
|
||||
= window.innerHeight - remoteVideos.outerHeight();
|
||||
var availableWidth = Util.getAvailableVideoWidth();
|
||||
|
||||
$('#etherpad>iframe').width(availableWidth);
|
||||
$('#etherpad>iframe').height(availableHeight);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shares the Etherpad name with other participants.
|
||||
*/
|
||||
function shareEtherpad() {
|
||||
connection.emuc.addEtherpadToPresence(etherpadName);
|
||||
connection.emuc.sendPresence();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the Etherpad button and adds it to the toolbar.
|
||||
*/
|
||||
function enableEtherpadButton() {
|
||||
if (!$('#etherpadButton').is(":visible"))
|
||||
$('#etherpadButton').css({display: 'inline-block'});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the IFrame for the etherpad.
|
||||
*/
|
||||
function createIFrame() {
|
||||
etherpadIFrame = document.createElement('iframe');
|
||||
etherpadIFrame.src = domain + etherpadName + options;
|
||||
etherpadIFrame.frameBorder = 0;
|
||||
etherpadIFrame.scrolling = "no";
|
||||
etherpadIFrame.width = $('#largeVideoContainer').width() || 640;
|
||||
etherpadIFrame.height = $('#largeVideoContainer').height() || 480;
|
||||
etherpadIFrame.setAttribute('style', 'visibility: hidden;');
|
||||
|
||||
document.getElementById('etherpad').appendChild(etherpadIFrame);
|
||||
|
||||
etherpadIFrame.onload = function() {
|
||||
|
||||
document.domain = document.domain;
|
||||
bubbleIframeMouseMove(etherpadIFrame);
|
||||
setTimeout(function() {
|
||||
//the iframes inside of the etherpad are not yet loaded when the etherpad iframe is loaded
|
||||
var outer = etherpadIFrame.contentDocument.getElementsByName("ace_outer")[0];
|
||||
bubbleIframeMouseMove(outer);
|
||||
var inner = outer.contentDocument.getElementsByName("ace_inner")[0];
|
||||
bubbleIframeMouseMove(inner);
|
||||
}, 2000);
|
||||
};
|
||||
}
|
||||
|
||||
function bubbleIframeMouseMove(iframe){
|
||||
var existingOnMouseMove = iframe.contentWindow.onmousemove;
|
||||
iframe.contentWindow.onmousemove = function(e){
|
||||
if(existingOnMouseMove) existingOnMouseMove(e);
|
||||
var evt = document.createEvent("MouseEvents");
|
||||
var boundingClientRect = iframe.getBoundingClientRect();
|
||||
evt.initMouseEvent(
|
||||
"mousemove",
|
||||
true, // bubbles
|
||||
false, // not cancelable
|
||||
window,
|
||||
e.detail,
|
||||
e.screenX,
|
||||
e.screenY,
|
||||
e.clientX + boundingClientRect.left,
|
||||
e.clientY + boundingClientRect.top,
|
||||
e.ctrlKey,
|
||||
e.altKey,
|
||||
e.shiftKey,
|
||||
e.metaKey,
|
||||
e.button,
|
||||
null // no related element
|
||||
);
|
||||
iframe.dispatchEvent(evt);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* On Etherpad added to muc.
|
||||
*/
|
||||
$(document).bind('etherpadadded.muc', function (event, jid, etherpadName) {
|
||||
console.log("Etherpad added", etherpadName);
|
||||
if (config.etherpad_base && !focus) {
|
||||
Etherpad.init(etherpadName);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* On focus changed event.
|
||||
*/
|
||||
$(document).bind('focusechanged.muc', function (event, focus) {
|
||||
console.log("Focus changed");
|
||||
if (config.etherpad_base)
|
||||
shareEtherpad();
|
||||
});
|
||||
|
||||
/**
|
||||
* On video selected event.
|
||||
*/
|
||||
$(document).bind('video.selected', function (event, isPresentation) {
|
||||
if (!config.etherpad_base)
|
||||
return;
|
||||
|
||||
if (etherpadIFrame && etherpadIFrame.style.visibility !== 'hidden')
|
||||
Etherpad.toggleEtherpad(isPresentation);
|
||||
});
|
||||
|
||||
/**
|
||||
* Resizes the etherpad, when the window is resized.
|
||||
*/
|
||||
$(window).resize(function () {
|
||||
resize();
|
||||
});
|
||||
|
||||
return my;
|
||||
}(Etherpad || {}));
|
||||
@@ -48,7 +48,7 @@ var JitsiMeetExternalAPI = (function()
|
||||
this.iframeHolder.style.width = width + "px";
|
||||
this.iframeHolder.style.height = height + "px";
|
||||
this.frameName = "jitsiConferenceFrame" + JitsiMeetExternalAPI.id;
|
||||
this.url = "http://" + domain + "/";
|
||||
this.url = "//" + domain + "/";
|
||||
if(room_name)
|
||||
this.url += room_name;
|
||||
this.url += "#external";
|
||||
@@ -139,7 +139,7 @@ var JitsiMeetExternalAPI = (function()
|
||||
* event and value - the listener.
|
||||
* Currently we support the following
|
||||
* events:
|
||||
* incommingMessage - receives event notifications about incomming
|
||||
* incomingMessage - receives event notifications about incoming
|
||||
* messages. The listener will receive object with the following structure:
|
||||
* {{
|
||||
* "from": from,//JID of the user that sent the message
|
||||
@@ -185,7 +185,7 @@ var JitsiMeetExternalAPI = (function()
|
||||
/**
|
||||
* Adds event listeners to Meet Jitsi. Currently we support the following
|
||||
* events:
|
||||
* incommingMessage - receives event notifications about incomming
|
||||
* incomingMessage - receives event notifications about incoming
|
||||
* messages. The listener will receive object with the following structure:
|
||||
* {{
|
||||
* "from": from,//JID of the user that sent the message
|
||||
|
||||
BIN
fonts/jitsi.eot
@@ -34,4 +34,5 @@
|
||||
<glyph unicode="" d="M797.086 112.301c-0.059 0.163-0.119 0.328-0.16 0.485-71.399-45.638-151.782-69.931-234.023-69.931-0.013 0-0.021 0-0.028 0-122.52 0-237.501 52.772-315.469 144.741-99.778 117.698-134.252 329.954-73.022 427.789 4.004-1.662 7.875-3.233 11.68-4.773 13.585-5.511 26.413-10.716 42.305-19.096 6.063-3.202 12.338-4.812 18.673-4.812 11.714 0 22.6 5.648 29.848 15.486 7.815 10.617 10.313 24.778 6.538 36.951l-3.525 11.41c-10.687 34.59-21.723 70.354-34.211 105.078-9.983 27.765-22.399 62.327-59.226 62.327-12.057 0-26.037-3.656-46.73-12.204-44.294-18.319-71.058-29.961-114.534-49.81-15.102-6.887-25.234-22.698-25.203-39.343 0.028-15.842 8.992-29.337 23.975-36.115 18.208-8.257 30.536-13.716 43.468-19.447l10.687-4.753c-101.938-259.102 24.803-526.458 211.314-639.212 83.497-50.474 178.5-77.14 274.769-77.14h0.041c102.72 0 205.561 31.099 284.501 85.198-31.729 28.803-45.566 69.167-51.671 87.171zM1098.203 210.090c-18.113 8.577-30.356 14.258-43.221 20.244l-10.496 4.892c106.448 257.268-15.569 526.801-200.067 642.788-85.36 53.663-183.123 82.032-282.716 82.032-104.848 0-206.41-30.593-285.967-86.165l-5.385-3.764c31.597-27.564 45.86-66.788 52.917-86.41 72.926 47.94 155.675 73.409 239.895 73.409 125.407 0 242.142-54.785 320.294-150.316 97.683-119.447 128.439-332.255 65.498-429.015-3.989 1.736-7.815 3.385-11.624 4.998-13.471 5.759-26.204 11.18-41.954 19.821-6.203 3.424-12.645 5.155-19.212 5.155-11.585 0-22.399-5.558-29.69-15.267-7.813-10.434-10.478-24.432-6.966-36.515l3.279-11.301c10.096-34.845 20.531-70.857 32.412-105.842 9.588-28.238 21.514-63.382 59.179-63.382 11.843 0 25.577 3.424 45.881 11.399 44.351 17.439 71.319 28.601 115.409 47.777 15.19 6.623 25.601 22.252 25.859 38.894 0.281 15.822-8.445 29.499-23.325 36.569z" horiz-adv-x="1122" />
|
||||
<glyph unicode="" d="M46.993 961.7c461.234 0 553.793 0 1015.024 0 35.919 0 53.356-25.959 53.356-57.959-0.581-303.259-0.325-606.488-0.449-909.809 0-43.984-13.203-57.058-57.703-57.058-443.072-0.126-556.453-0.126-999.553 0-44.534 0-57.799 13.009-57.799 57.058-0.098 303.257 0.485 608.072-0.093 911.329-0.034 26.21 11.301 53.761 47.217 56.439zM311.405 450.298c0-119.045-0.072-172.168 0.057-291.249 0.036-50.043 11.208-61.050 62.12-61.050 233.352 0 137.075 0 370.522 0 47.075 0 59.249 11.982 59.249 58.095 0.126 239.111 0.126 346.338 0 585.389 0 48.138-10.687 58.991-57.768 58.991-235.323 0.101-140.844 0.101-376.157 0-47.044 0-57.93-11.043-57.966-58.89-0.129-119.109-0.057-172.209-0.057-291.287zM153.944 838.566c-74.929-0.062-66.687 5.958-66.845-66.685-0.201-63.95-7.054-63.534 62.528-63.372 72.999 0.194 67.201-3.764 67.302 67.554 0 67.722 4.087 62.595-62.985 62.502zM963.644 838.566c-71.159-0.034-65.56 6.185-65.751-65.364-0.129-67.302-4.508-64.693 64.528-64.693 73.089 0 65.299-2.031 65.299 66.238-0.003 68.646 6.956 63.911-64.076 63.818zM216.828 122.408c0.359 73.094 4.639 66.914-67.358 67.17-68.104 0.191-62.569 2.763-62.407-63.31 0.129-73.476-6.954-66.52 67.074-66.649 66.042-0.065 63.142-6.056 62.691 62.789zM1027.718 124.4c0.134 68.334 6.443 65.304-63.297 65.178-70.132-0.132-66.656 5.793-66.527-65.304 0.129-70.645-4.384-64.721 63.756-64.657 71.995 0.132 66.202-6.698 66.068 64.783zM1027.718 342.077c0 70.55 7.219 66.842-67.485 66.522-0.898 0-1.873 0-2.838 0-59.375 0-59.375 0-59.375-58.023 0-77.922-6.443-69.936 69.293-70.196 66.076-0.387 60.539-3.091 60.405 61.697zM151.307 489.873c68.295-0.163 65.815-5.568 65.624 62.982-0.194 71.128 4.895 64.917-66.014 65.010-69.905 0.101-63.813 4.704-63.885-63.978-0.062-67.431-5.7-64.463 64.275-64.014zM961.263 489.873c72.511-0.258 66.589-4.603 66.455 64.494 0 68.558 6.185 63.537-64.267 63.498-70.196-0.028-65.686 6.053-65.498-65.493 0.132-62.5 0.067-62.5 63.31-62.5zM150.399 280.38c71.004 0 66.659-6.567 66.466 64.528-0.163 63.694-0.036 63.501-65.013 63.756-70.805 0.258-64.822 2.673-64.822-63.756 0.036-69.167-5.919-64.788 63.369-64.528z" horiz-adv-x="1115" />
|
||||
<glyph unicode="" d="M3.881 146.835h220.26v-210.835h-220.26v210.835zM308.817 350.143h220.27v-414.143h-220.27v414.143zM613.764 553.412h220.268v-617.412h-220.268v617.412zM918.685 756.715h220.265v-820.715h-220.265v820.715zM1223.629 960h220.263v-1024h-220.263v1024z" horiz-adv-x="1444" />
|
||||
<glyph unicode="" d="M526.071 234.749c-28.637-30.869-56.465-60.861-84.282-90.859-51.578-55.636-103.047-111.376-154.842-166.832-7.606-8.135-15.958-16.1-25.317-22.012-28.075-17.708-58.31-18.090-88.472-6.492-59.84 23.028-80.004 90.727-59.734 139.234 5.413 12.95 13.721 23.601 23.709 33.173 70.256 67.351 140.506 134.717 210.76 202.077 15.638 14.993 31.264 29.995 47.364 45.45-9.302 9.529-18.386 18.833-27.451 28.137-12.122 12.442-13.234 20.28-5.067 35.498 4.735 8.816 4.789 8.878-2.627 16.198-20.012 19.72-40.168 39.198-63.498 55.188-27.167 18.624-57.161 24.233-89.083 19.849-53.402-7.328-91.609-38.372-121.413-81.046-12.774-18.299-15.365-40.313-17.517-61.875-3.23-32.245-2.415-64.479 2.209-96.597 1.654-11.515-3.863-16.539-13.835-11.175-8.306 4.448-16.095 11.048-22.115 18.353-15.574 18.89-22.223 42.042-27.474 65.395-12.955 57.652-8.86 114.49 12.191 169.495 32.345 84.537 79.743 159.571 145.953 221.932 13.659 12.857 176.841 180.564 202.944 207.021 7.493 7.599 14.895 7.635 22.393 0.028 43.009-43.641 85.985-87.316 128.927-131.029 8.117-8.267 8.019-15.097-0.222-23.49-26.339-26.834-52.726-53.627-79.106-80.419-6.244-6.334-97.34-82.437-73.027-128.816 22.693-25.090 46.196-49.449 69.575-73.904 1.189-1.238 4.686-1.386 6.523-0.632 3.63 1.499 6.848 3.997 10.248 6.066 9.745 5.94 19.545 4.918 27.812-3.083 11.755-11.381 23.405-22.858 35.392-34.59 4.807 4.575 9.939 9.41 15.027 14.294 27.128 26.039 54.272 52.071 81.351 78.146 16.413 15.778 18.652 28.418 11.038 49.658-10.473 29.221-14.356 59.677-13.85 90.624 1.017 61.045 20.438 115.334 61.003 161.416 32.825 37.286 72.054 64.311 121.643 74.325 35.227 7.101 69.139 4.513 100.663-14.026 6.365-3.752 11.908-9.007 17.455-14.005 3.491-3.125 3.153-6.236-0.565-9.98-42.503-42.885-84.772-86.013-127.154-129.035-12.442-12.638-12.356-23.167 0.196-35.914 40.344-40.978 80.597-82.050 120.936-123.052 10.076-10.233 19.537-10.021 29.504 0.134 43.195 44.077 86.449 88.090 129.706 132.118 1.21 1.233 2.572 2.322 5.135 4.624 5.491-5.893 11.895-10.924 15.961-17.406 19.452-30.944 22.608-64.83 17.073-100.25-14.253-91.080-97.188-175.638-197.712-190.123-39.977-5.764-79.372-2.562-118.067 9.031-5.898 1.775-11.541 4.629-17.538 5.829-12.47 2.474-23.872-0.366-32.74-9.877-30.921-33.168-61.674-66.484-92.474-99.758-0.73-0.805-1.349-1.718-0.181-1.099 8.992-10.006 17.354-20.662 27.061-29.94 81.064-77.54 164.91-151.986 250.882-224.063 9.936-8.347 10.274-15.695 1.040-25.1-42.338-43.068-84.689-86.111-127.059-129.154-9.413-9.575-16.846-9.152-25.291 1.295-76.686 94.78-156.8 186.609-239.707 276.002-1.334 1.453-2.562 3.029-4.257 5.042z" horiz-adv-x="1105" />
|
||||
</font></defs></svg>
|
||||
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 33 KiB |
BIN
fonts/jitsi.ttf
BIN
fonts/jitsi.woff
@@ -1,12 +1,46 @@
|
||||
{
|
||||
"IcoMoonType": "selection",
|
||||
"icons": [
|
||||
{
|
||||
"icon": {
|
||||
"paths": [
|
||||
"M526.071 725.251c-28.637 30.869-56.465 60.861-84.282 90.859-51.578 55.636-103.047 111.376-154.842 166.832-7.606 8.135-15.958 16.1-25.317 22.012-28.075 17.708-58.31 18.090-88.472 6.492-59.84-23.028-80.004-90.727-59.734-139.234 5.413-12.95 13.721-23.601 23.709-33.173 70.256-67.351 140.506-134.717 210.76-202.077 15.638-14.993 31.264-29.995 47.364-45.45-9.302-9.529-18.386-18.833-27.451-28.137-12.122-12.442-13.234-20.28-5.067-35.498 4.735-8.816 4.789-8.878-2.627-16.198-20.012-19.72-40.168-39.198-63.498-55.188-27.167-18.624-57.161-24.233-89.083-19.849-53.402 7.328-91.609 38.372-121.413 81.046-12.774 18.299-15.365 40.313-17.517 61.875-3.23 32.245-2.415 64.479 2.209 96.597 1.654 11.515-3.863 16.539-13.835 11.175-8.306-4.448-16.095-11.048-22.115-18.353-15.574-18.89-22.223-42.042-27.474-65.395-12.955-57.652-8.86-114.49 12.191-169.495 32.345-84.537 79.743-159.571 145.953-221.932 13.659-12.857 176.841-180.564 202.944-207.021 7.493-7.599 14.895-7.635 22.393-0.028 43.009 43.641 85.985 87.316 128.927 131.029 8.117 8.267 8.019 15.097-0.222 23.49-26.339 26.834-52.726 53.627-79.106 80.419-6.244 6.334-97.34 82.437-73.027 128.816 22.693 25.090 46.196 49.449 69.575 73.904 1.189 1.238 4.686 1.386 6.523 0.632 3.63-1.499 6.848-3.997 10.248-6.066 9.745-5.94 19.545-4.918 27.812 3.083 11.755 11.381 23.405 22.858 35.392 34.59 4.807-4.575 9.939-9.41 15.027-14.294 27.128-26.039 54.272-52.071 81.351-78.146 16.413-15.778 18.652-28.418 11.038-49.658-10.473-29.221-14.356-59.677-13.85-90.624 1.017-61.045 20.438-115.334 61.003-161.416 32.825-37.286 72.054-64.311 121.643-74.325 35.227-7.101 69.139-4.513 100.663 14.026 6.365 3.752 11.908 9.007 17.455 14.005 3.491 3.125 3.153 6.236-0.565 9.98-42.503 42.885-84.772 86.013-127.154 129.035-12.442 12.638-12.356 23.167 0.196 35.914 40.344 40.978 80.597 82.050 120.936 123.052 10.076 10.233 19.537 10.021 29.504-0.134 43.195-44.077 86.449-88.090 129.706-132.118 1.21-1.233 2.572-2.322 5.135-4.624 5.491 5.893 11.895 10.924 15.961 17.406 19.452 30.944 22.608 64.83 17.073 100.25-14.253 91.080-97.188 175.638-197.712 190.123-39.977 5.764-79.372 2.562-118.067-9.031-5.898-1.775-11.541-4.629-17.538-5.829-12.47-2.474-23.872 0.366-32.74 9.877-30.921 33.168-61.674 66.484-92.474 99.758-0.73 0.805-1.349 1.718-0.181 1.099 8.992 10.006 17.354 20.662 27.061 29.94 81.064 77.54 164.91 151.986 250.882 224.063 9.936 8.347 10.274 15.695 1.040 25.1-42.338 43.068-84.689 86.111-127.059 129.154-9.413 9.575-16.846 9.152-25.291-1.295-76.686-94.78-156.8-186.609-239.707-276.002-1.334-1.453-2.562-3.029-4.257-5.042z"
|
||||
],
|
||||
"attrs": [
|
||||
{
|
||||
"opacity": 1,
|
||||
"visibility": false
|
||||
}
|
||||
],
|
||||
"width": 1105,
|
||||
"grid": 0,
|
||||
"tags": [
|
||||
"settings"
|
||||
]
|
||||
},
|
||||
"attrs": [
|
||||
{
|
||||
"opacity": 1,
|
||||
"visibility": false
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"order": 1,
|
||||
"id": 33,
|
||||
"prevSize": 32,
|
||||
"code": 58907,
|
||||
"name": "settings"
|
||||
},
|
||||
"setIdx": 0,
|
||||
"iconIdx": 0
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
"paths": [
|
||||
"M1223.129 242.783l-180.128 175.796v-217.716c0-74.673-59.512-135.496-132.599-135.496h-634.716c-73.084 0-132.596 60.823-132.596 135.496v609.237c0 74.673 59.512 135.496 132.596 135.496h634.716c73.084 0 132.599-60.82 132.599-135.496v-172.679l193.45 153.712c48.784 35.558 96.695-5.178 96.695-40.424v-483.533c-0.003-35.248-55.897-71.306-110.017-24.393zM601.169 760.065c-141.111 0-255.524-114.411-255.524-255.521s114.411-255.521 255.524-255.521c141.108 0 255.519 114.411 255.519 255.521-0 141.113-114.408 255.521-255.519 255.521z",
|
||||
"M599.045 359.751c-80.474 0-145.727 65.253-145.727 145.729 0 80.471 65.25 145.727 145.727 145.727s145.729-65.256 145.729-145.727c0-80.474-65.253-145.729-145.729-145.729z"
|
||||
],
|
||||
"width": 1334,
|
||||
"attrs": [
|
||||
{
|
||||
"opacity": 1,
|
||||
@@ -17,11 +51,10 @@
|
||||
"visibility": false
|
||||
}
|
||||
],
|
||||
"width": 1334,
|
||||
"grid": 0,
|
||||
"tags": [
|
||||
"webCam"
|
||||
]
|
||||
],
|
||||
"grid": 0
|
||||
},
|
||||
"attrs": [
|
||||
{
|
||||
@@ -714,7 +747,7 @@
|
||||
"ligatures": ""
|
||||
},
|
||||
"setIdx": 0,
|
||||
"iconIdx": 26
|
||||
"iconIdx": 25
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -740,7 +773,7 @@
|
||||
"ligatures": ""
|
||||
},
|
||||
"setIdx": 0,
|
||||
"iconIdx": 27
|
||||
"iconIdx": 26
|
||||
},
|
||||
{
|
||||
"icon": {
|
||||
@@ -765,7 +798,7 @@
|
||||
"ligatures": ""
|
||||
},
|
||||
"setIdx": 0,
|
||||
"iconIdx": 28
|
||||
"iconIdx": 27
|
||||
}
|
||||
],
|
||||
"height": 1024,
|
||||
@@ -775,6 +808,7 @@
|
||||
"preferences": {
|
||||
"showGlyphs": true,
|
||||
"showQuickUse": true,
|
||||
"showQuickUse2": true,
|
||||
"showSVGs": true,
|
||||
"fontPref": {
|
||||
"prefix": "icon-",
|
||||
@@ -791,7 +825,8 @@
|
||||
},
|
||||
"imagePref": {
|
||||
"prefix": "icon-",
|
||||
"png": true
|
||||
"png": true,
|
||||
"useClassSelector": true
|
||||
},
|
||||
"historySize": 100,
|
||||
"showCodes": true,
|
||||
|
||||
BIN
images/chrome.png
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
BIN
images/chromium.png
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
images/dropdownPointer.png
Normal file
|
After Width: | Height: | Size: 234 B |
BIN
images/firefox.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
images/opera.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
206
index.html
@@ -10,75 +10,38 @@
|
||||
<meta itemprop="description" content="Join a WebRTC video conference powered by the Jitsi Videobridge"/>
|
||||
<meta itemprop="image" content="/images/jitsilogo.png"/>
|
||||
<script src="libs/jquery-2.1.1.min.js"></script>
|
||||
<script src="config.js?v=5"></script><!-- adapt to your needs, i.e. set hosts and bosh path -->
|
||||
<script src="simulcast.js?v=6"></script><!-- simulcast handling -->
|
||||
<script src="libs/strophe/strophe.jingle.adapter.js?v=2"></script><!-- strophe.jingle bundles -->
|
||||
<script src="config.js?v=6"></script><!-- adapt to your needs, i.e. set hosts and bosh path -->
|
||||
<script src="libs/strophe/strophe.min.js?v=1"></script>
|
||||
<script src="libs/strophe/strophe.disco.min.js?v=1"></script>
|
||||
<script src="libs/strophe/strophe.caps.jsonly.min.js?v=1"></script>
|
||||
<script src="libs/strophe/strophe.jingle.js?v=2"></script>
|
||||
<script src="libs/strophe/strophe.jingle.sdp.js?v=2"></script>
|
||||
<script src="libs/strophe/strophe.jingle.sdp.util.js?v=1"></script>
|
||||
<script src="libs/strophe/strophe.jingle.sessionbase.js?v=1"></script>
|
||||
<script src="libs/strophe/strophe.jingle.session.js?v=2"></script>
|
||||
<script src="libs/strophe/strophe.util.js"></script>
|
||||
<script src="libs/colibri/colibri.focus.js?v=11"></script><!-- colibri focus implementation -->
|
||||
<script src="libs/colibri/colibri.session.js?v=1"></script>
|
||||
<script src="libs/jquery-ui.js"></script>
|
||||
<script src="libs/rayo.js?v=1"></script>
|
||||
<script src="libs/tooltip.js?v=1"></script><!-- bootstrap tooltip lib -->
|
||||
<script src="libs/popover.js?v=1"></script><!-- bootstrap tooltip lib -->
|
||||
<script src="interface_config.js?v=3"></script>
|
||||
<script src="muc.js?v=16"></script><!-- simple MUC library -->
|
||||
<script src="estos_log.js?v=2"></script><!-- simple stanza logger -->
|
||||
<script src="desktopsharing.js?v=3"></script><!-- desktop sharing -->
|
||||
<script src="data_channels.js?v=3"></script><!-- data channels -->
|
||||
<script src="app.js?v=20"></script><!-- application logic -->
|
||||
<script src="commands.js?v=1"></script><!-- application logic -->
|
||||
<script src="chat.js?v=13"></script><!-- chat logic -->
|
||||
<script src="contact_list.js?v=5"></script><!-- contact list logic -->
|
||||
<script src="util.js?v=6"></script><!-- utility functions -->
|
||||
<script src="etherpad.js?v=9"></script><!-- etherpad plugin -->
|
||||
<script src="prezi.js?v=6"></script><!-- prezi plugin -->
|
||||
<script src="smileys.js?v=3"></script><!-- smiley images -->
|
||||
<script src="replacement.js?v=7"></script><!-- link and smiley replacement -->
|
||||
<script src="moderatemuc.js?v=4"></script><!-- moderator plugin -->
|
||||
<script src="libs/toastr.js?v=1"></script><!-- notifications lib -->
|
||||
<script src="interface_config.js?v=5"></script>
|
||||
<script src="libs/app.bundle.js?v=34"></script>
|
||||
<script src="analytics.js?v=1"></script><!-- google analytics plugin -->
|
||||
<script src="rtp_sts.js?v=5"></script><!-- RTP stats processing -->
|
||||
<script src="local_sts.js?v=2"></script><!-- Local stats processing -->
|
||||
<script src="videolayout.js?v=24"></script><!-- video ui -->
|
||||
<script src="connectionquality.js?v=1"></script>
|
||||
<script src="toolbar.js?v=6"></script><!-- toolbar ui -->
|
||||
<script src="toolbar_toggler.js?v=2"></script>
|
||||
<script src="canvas_util.js?v=1"></script><!-- canvas drawing utils -->
|
||||
<script src="audio_levels.js?v=2"></script><!-- audio levels plugin -->
|
||||
<script src="media_stream.js?v=1"></script><!-- media stream -->
|
||||
<script src="bottom_toolbar.js?v=5"></script><!-- media stream -->
|
||||
<script src="roomname_generator.js?v=1"></script><!-- generator for random room names -->
|
||||
<script src="keyboard_shortcut.js?v=3"></script>
|
||||
<script src="tracking.js?v=1"></script><!-- tracking -->
|
||||
<script src="jitsipopover.js?v=3"></script>
|
||||
<script src="message_handler.js?v=1"></script>
|
||||
<script src="api_connector.js?v=2"></script>
|
||||
<link rel="stylesheet" href="css/font.css?v=6"/>
|
||||
<link rel="stylesheet" href="css/toastr.css?v=1">
|
||||
<link rel="stylesheet" type="text/css" media="screen" href="css/main.css?v=30"/>
|
||||
<link rel="stylesheet" type="text/css" media="screen" href="css/videolayout_default.css?v=16" id="videolayout_default"/>
|
||||
<link href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="css/font.css?v=5"/>
|
||||
<link rel="stylesheet" type="text/css" media="screen" href="css/main.css?v=29"/>
|
||||
<link rel="stylesheet" type="text/css" media="screen" href="css/videolayout_default.css?v=13" id="videolayout_default"/>
|
||||
<link rel="stylesheet" href="css/jquery-impromptu.css?v=4">
|
||||
<link rel="stylesheet" href="css/modaldialog.css?v=3">
|
||||
<link rel="stylesheet" href="css/popup_menu.css?v=4">
|
||||
<link rel="stylesheet" href="css/login_menu.css?v=1">
|
||||
<link rel="stylesheet" href="css/popover.css?v=2">
|
||||
<link rel="stylesheet" href="css/jitsi_popover.css?v=2">
|
||||
<link rel="stylesheet" href="css/contact_list.css?v=3">
|
||||
<link rel="stylesheet" href="css/jitsi_popover.css?v=2">
|
||||
<link rel="stylesheet" href="css/contact_list.css?v=4">
|
||||
<link rel="stylesheet" href="css/chat.css?v=5">
|
||||
<link rel="stylesheet" href="css/welcome_page.css?v=2">
|
||||
<link rel="stylesheet" href="css/settingsmenu.css?v=1">
|
||||
<!--
|
||||
Link used for inline installation of chrome desktop streaming extension,
|
||||
is updated automatically from the code with the value defined in config.js -->
|
||||
<link rel="chrome-webstore-item" href="https://chrome.google.com/webstore/detail/diibjkoicjeejcmhdnailmkgecihlobk">
|
||||
<script src="libs/jquery-impromptu.js"></script>
|
||||
<script src="libs/jquery.autosize.js"></script>
|
||||
<script src="libs/prezi_player.js?v=2"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="welcome_page">
|
||||
@@ -89,76 +52,66 @@
|
||||
<a target="_new">
|
||||
<div class="watermark rightwatermark"></div>
|
||||
</a>
|
||||
<a class="poweredby" href="http://jitsi.org" target="_new" >powered by jitsi.org</a>
|
||||
<a class="poweredby" href="http://jitsi.org" target="_new" ><span data-i18n="poweredby"></span> jitsi.org</a>
|
||||
|
||||
<div id="enter_room_container">
|
||||
<div id="enter_room_form" >
|
||||
<div id="domain_name"></div>
|
||||
<div id="enter_room">
|
||||
<input id="enter_room_field" type="text" autofocus placeholder="Enter room name" />
|
||||
<input id="enter_room_field" type="text" autofocus data-i18n="[placeholder]welcomepage.roomname" placeholder="Enter room name" />
|
||||
<div class="icon-reload" id="reload_roomname"></div>
|
||||
<input id="enter_room_button" type="button" value="GO" />
|
||||
<input id="enter_room_button" type="button" data-i18n="[value]welcomepage.go" value="GO" />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="brand_header"></div>
|
||||
<input type='checkbox' name='checkbox' id="disable_welcome"/>
|
||||
<label for="disable_welcome" class="disable_welcome_position">Don't show this page next time I enter</label>
|
||||
<label for="disable_welcome" class="disable_welcome_position" data-i18n="welcomepage.disable"></label>
|
||||
<div id="header_text"></div>
|
||||
</div>
|
||||
<div id="welcome_page_main">
|
||||
<div id="features">
|
||||
<div class="feature_row">
|
||||
<div class="feature_holder">
|
||||
<div class="feature_icon">Simple to use</div>
|
||||
<div class="feature_description">
|
||||
No downloads required. <span name="appName"></span> works directly within your browser. Simply share your conference URL with others to get started.
|
||||
<div class="feature_icon" data-i18n="welcomepage.feature1.title" ></div>
|
||||
<div class="feature_description" data-i18n="welcomepage.feature1.content" data-i18n-options='{ "postProcess": "resolveAppName" }'>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feature_holder">
|
||||
<div class="feature_icon">Low bandwidth</div>
|
||||
<div class="feature_description">
|
||||
Multi-party video conferences work with as little as 128Kbps. Screen-sharing and audio-only conferences are possible with far less.
|
||||
<div class="feature_icon" data-i18n="welcomepage.feature2.title" ></div>
|
||||
<div class="feature_description" data-i18n="welcomepage.feature2.content">
|
||||
</div>
|
||||
</div>
|
||||
<div class="feature_holder">
|
||||
<div class="feature_icon">Open source</div>
|
||||
<div class="feature_description">
|
||||
<span name="appName"></span> is licensed under MIT. You are free to download, use, modify, and share them as per these licenses.
|
||||
<div class="feature_icon" data-i18n="welcomepage.feature3.title" ></div>
|
||||
<div class="feature_description" data-i18n="welcomepage.feature3.content" data-i18n-options='{ "postProcess": "resolveAppName" }'>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feature_holder">
|
||||
<div class="feature_icon">Unlimited users</div>
|
||||
<div class="feature_description">
|
||||
There are no artificial restrictions on the number of users or conference participants. Server power and bandwidth are the only limiting factors.
|
||||
<div class="feature_icon" data-i18n="welcomepage.feature4.title" ></div>
|
||||
<div class="feature_description" data-i18n="welcomepage.feature4.content">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feature_row">
|
||||
<div class="feature_holder">
|
||||
<div class="feature_icon">Screen sharing</div>
|
||||
<div class="feature_description">
|
||||
It's easy to share your screen with others. <span name="appName"></span> is ideal for on-line presentations, lectures, and tech support sessions.
|
||||
</div>
|
||||
<div class="feature_icon" data-i18n="welcomepage.feature5.title" ></div>
|
||||
<div class="feature_description" data-i18n="welcomepage.feature5.content" data-i18n-options='{ "postProcess": "resolveAppName" }'>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feature_holder">
|
||||
<div class="feature_icon">Secure rooms</div>
|
||||
<div class="feature_description">
|
||||
Need some privacy? <span name="appName"></span> conference rooms can be secured with a password in order to exclude unwanted guests and prevent interruptions.
|
||||
</div>
|
||||
<div class="feature_icon" data-i18n="welcomepage.feature6.title" ></div>
|
||||
<div class="feature_description" data-i18n="welcomepage.feature6.content" data-i18n-options='{ "postProcess": "resolveAppName" }'>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feature_holder">
|
||||
<div class="feature_icon">Shared notes</div>
|
||||
<div class="feature_description">
|
||||
<span name="appName"></span> features Etherpad, a real-time collaborative text editor that's great for meeting minutes, writing articles, and more.
|
||||
</div>
|
||||
<div class="feature_icon" data-i18n="welcomepage.feature7.title" ></div>
|
||||
<div class="feature_description" data-i18n="welcomepage.feature7.content" data-i18n-options='{ "postProcess": "resolveAppName" }'></div>
|
||||
</div>
|
||||
<div class="feature_holder">
|
||||
<div class="feature_icon">Usage statistics</div>
|
||||
<div class="feature_description">
|
||||
Learn about your users through easy integration with Piwik, Google Analytics, and other usage monitoring and statistics systems.
|
||||
</div>
|
||||
<div class="feature_icon" data-i18n="welcomepage.feature8.title" ></div>
|
||||
<div class="feature_description" data-i18n="welcomepage.feature8.content"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -168,30 +121,46 @@
|
||||
<div style="position: relative;" id="header_container">
|
||||
<div id="header">
|
||||
<span id="toolbar">
|
||||
<a class="button" data-container="body" data-toggle="popover" data-placement="bottom" shortcut="mutePopover" content="Mute / Unmute" onclick='toggleAudio();'>
|
||||
<span id="authentication" class="authentication" style="display: none">
|
||||
<a class="button" id="toolbar_button_authentication" >
|
||||
<i id="authButton" class="icon-avatar"></i>
|
||||
</a>
|
||||
<ul class="loginmenu">
|
||||
<span class="loginmenuPadding"></span>
|
||||
<li id="toolbar_auth_identity" class="identity"></li>
|
||||
<li id="toolbar_button_login">
|
||||
<a class="authButton" data-i18n="toolbar.login"></a>
|
||||
</li>
|
||||
<li id="toolbar_button_logout">
|
||||
<a class="authButton" data-i18n="toolbar.logout"></a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="header_button_separator"></div>
|
||||
</span>
|
||||
<a class="button" id="toolbar_button_mute" data-container="body" data-toggle="popover" data-placement="bottom" shortcut="mutePopover" data-i18n="[content]toolbar.mute" content="Mute / Unmute">
|
||||
<i id="mute" class="icon-microphone"></i>
|
||||
</a>
|
||||
<div class="header_button_separator"></div>
|
||||
<a class="button" data-container="body" data-toggle="popover" data-placement="bottom" shortcut="toggleVideoPopover" content="Start / stop camera" onclick='toggleVideo();'>
|
||||
<a class="button" id="toolbar_button_camera" data-container="body" data-toggle="popover" data-placement="bottom" shortcut="toggleVideoPopover" data-i18n="[content]toolbar.videomute" content="Start / stop camera">
|
||||
<i id="video" class="icon-camera"></i>
|
||||
</a>
|
||||
<span id="recording" style="display: none">
|
||||
<div class="header_button_separator"></div>
|
||||
<a class="button" data-container="body" data-toggle="popover" data-placement="bottom" content="Record" onclick='toggleRecording();'>
|
||||
<a class="button" id="toolbar_button_record" data-container="body" data-toggle="popover" data-placement="bottom" data-i18n="[content]toolbar.record" content="Record">
|
||||
<i id="recordButton" class="icon-recEnable"></i>
|
||||
</a>
|
||||
</span>
|
||||
<div class="header_button_separator"></div>
|
||||
<a class="button" data-container="body" data-toggle="popover" data-placement="bottom" content="Lock / unlock room" onclick="Toolbar.openLockDialog();">
|
||||
<a class="button" id="toolbar_button_security" data-container="body" data-toggle="popover" data-placement="bottom" data-i18n="[content]toolbar.lock" content="Lock / unlock room">
|
||||
<i id="lockIcon" class="icon-security"></i>
|
||||
</a>
|
||||
<div class="header_button_separator"></div>
|
||||
<a class="button" data-container="body" data-toggle="popover" data-placement="bottom" content="Invite others" onclick="Toolbar.openLinkDialog();">
|
||||
<a class="button" id="toolbar_button_link" data-container="body" data-toggle="popover" data-placement="bottom" data-i18n="[content]toolbar.invite" content="Invite others">
|
||||
<i class="icon-link"></i>
|
||||
</a>
|
||||
<div class="header_button_separator"></div>
|
||||
<span class="toolbar_span">
|
||||
<a class="button" data-container="body" data-toggle="popover" shortcut="toggleChatPopover" data-placement="bottom" content="Open / close chat" onclick='BottomToolbar.toggleChat();'>
|
||||
<a class="button" id="toolbar_button_chat" data-container="body" data-toggle="popover" shortcut="toggleChatPopover" data-placement="bottom" data-i18n="[content]toolbar.chat" content="Open / close chat">
|
||||
<i id="chatButton" class="icon-chat">
|
||||
<span id="unreadMessages"></span>
|
||||
</i>
|
||||
@@ -199,34 +168,38 @@
|
||||
</span>
|
||||
<span id="prezi_button">
|
||||
<div class="header_button_separator"></div>
|
||||
<a class="button" data-container="body" data-toggle="popover" data-placement="bottom" content="Share Prezi" onclick='Prezi.openPreziDialog();'>
|
||||
<a class="button" id="toolbar_button_prezi" data-container="body" data-toggle="popover" data-placement="bottom" data-i18n="[content]toolbar.prezi" content="Share Prezi">
|
||||
<i class="icon-prezi"></i>
|
||||
</a>
|
||||
</span>
|
||||
<span id="etherpadButton">
|
||||
<div class="header_button_separator"></div>
|
||||
<a class="button" data-container="body" data-toggle="popover" data-placement="bottom" content="Shared document" onclick='Etherpad.toggleEtherpad(0);'>
|
||||
<a class="button" id="toolbar_button_etherpad" data-container="body" data-toggle="popover" data-placement="bottom" content="Shared document" data-i18n="[content]toolbar.etherpad">
|
||||
<i class="icon-share-doc"></i>
|
||||
</a>
|
||||
</span>
|
||||
<div class="header_button_separator"></div>
|
||||
<span id="desktopsharing" style="display: none">
|
||||
<a class="button" data-container="body" data-toggle="popover" data-placement="bottom" content="Share screen" onclick="toggleScreenSharing();">
|
||||
<a class="button" id="toolbar_button_desktopsharing" data-container="body" data-toggle="popover" data-placement="bottom" content="Share screen" data-i18n="[content]toolbar.sharescreen">
|
||||
<i class="icon-share-desktop"></i>
|
||||
</a>
|
||||
</span>
|
||||
<div class="header_button_separator"></div>
|
||||
<a class="button" data-container="body" data-toggle="popover" data-placement="bottom" content="Enter / Exit Full Screen" onclick='buttonClick("#fullScreen", "icon-full-screen icon-exit-full-screen");Toolbar.toggleFullScreen();'>
|
||||
<a class="button" id="toolbar_button_fullScreen" data-container="body" data-toggle="popover" data-placement="bottom" content="Enter / Exit Full Screen" data-i18n="[content]toolbar.fullscreen">
|
||||
<i id="fullScreen" class="icon-full-screen"></i>
|
||||
</a>
|
||||
<span id="sipCallButton" style="display: none">
|
||||
<div class="header_button_separator"></div>
|
||||
<a class="button" data-container="body" data-toggle="popover" data-placement="bottom" content="Call SIP number" onclick='callSipButtonClicked();'>
|
||||
<a class="button" id="toolbar_button_sip" data-container="body" data-toggle="popover" data-placement="bottom" content="Call SIP number" data-i18n="[content]toolbar.sip">
|
||||
<i class="icon-telephone"></i></a>
|
||||
</span>
|
||||
<div class="header_button_separator"></div>
|
||||
<a class="button" id="toolbar_button_settings" data-container="body" data-toggle="popover" data-placement="bottom" content="Settings" data-i18n="[content]toolbar.Settings">
|
||||
<i id="settingsButton" class="icon-settings"></i>
|
||||
</a>
|
||||
<div class="header_button_separator"></div>
|
||||
<span id="hangup">
|
||||
<a class="button" data-container="body" data-toggle="popover" data-placement="bottom" content="Hang Up" onclick='hangup();'>
|
||||
<a class="button" id="toolbar_button_hangup" data-container="body" data-toggle="popover" data-placement="bottom" content="Hang Up" data-i18n="[content]toolbar.hangup">
|
||||
<i class="icon-hangup" style="color:#ff0000;font-size: 1.4em;"></i>
|
||||
</a>
|
||||
</span>
|
||||
@@ -235,7 +208,7 @@
|
||||
<div id="subject"></div>
|
||||
</div>
|
||||
<div id="settings">
|
||||
<h1>Connection Settings</h1>
|
||||
<h1 data-i18n="connectionsettings"></h1>
|
||||
<form id="loginInfo">
|
||||
<label>JID: <input id="jid" type="text" name="jid" placeholder="me@example.com"/></label>
|
||||
<label>Password: <input id="password" type="password" name="password" placeholder="secret"/></label>
|
||||
@@ -243,14 +216,18 @@
|
||||
<input id="connect" type="submit" value="Connect" />
|
||||
</form>
|
||||
</div>
|
||||
<div id="reloadPresentation"><a onclick='Prezi.reloadPresentation();'><i title="Reload Prezi" class="fa fa-repeat fa-lg"></i></a></div>
|
||||
<div id="videospace" onmousemove="ToolbarToggler.showToolbar();">
|
||||
<div id="reloadPresentation"><a id="reloadPresentationLink"><i title="Reload Prezi" class="fa fa-repeat fa-lg"></i></a></div>
|
||||
<div id="videospace">
|
||||
<div id="largeVideoContainer" class="videocontainer">
|
||||
<div id="presentation"></div>
|
||||
<div id="etherpad"></div>
|
||||
<a target="_new"><div class="watermark leftwatermark"></div></a>
|
||||
<a target="_new"><div class="watermark rightwatermark"></div></a>
|
||||
<a class="poweredby" href="http://jitsi.org" target="_new" >powered by jitsi.org</a>
|
||||
<a class="poweredby" href="http://jitsi.org" target="_new" ><span data-i18n="poweredby"></span> jitsi.org</a>
|
||||
<div id="activeSpeaker">
|
||||
<img id="activeSpeakerAvatar" src=""/>
|
||||
<canvas id="activeSpeakerAudioLevel"></canvas>
|
||||
</div>
|
||||
<video id="largeVideo" autoplay oncontextmenu="return false;"></video>
|
||||
</div>
|
||||
<div id="remoteVideos">
|
||||
@@ -261,18 +238,13 @@
|
||||
</span>
|
||||
<audio id="localAudio" autoplay oncontextmenu="return false;" muted></audio>
|
||||
<span class="focusindicator"></span>
|
||||
<!--<div class="connectionindicator">
|
||||
<span class="connection connection_empty"><i class="icon-connection"></i></span>
|
||||
<span class="connection connection_full"><i class="icon-connection"></i></span>
|
||||
</div>-->
|
||||
|
||||
</span>
|
||||
<audio id="userJoined" src="sounds/joined.wav" preload="auto"></audio>
|
||||
<audio id="userLeft" src="sounds/left.wav" preload="auto"></audio>
|
||||
</div>
|
||||
<span id="bottomToolbar">
|
||||
<span class="bottomToolbar_span">
|
||||
<a class="bottomToolbarButton" data-container="body" data-toggle="popover" shortcut="toggleChatPopover" data-placement="top" content="Open / close chat" onclick='BottomToolbar.toggleChat();'>
|
||||
<a class="bottomToolbarButton" id="bottom_toolbar_chat" data-container="body" data-toggle="popover" shortcut="toggleChatPopover" data-placement="top" data-i18n="[content]bottomtoolbar.chat" content="Open / close chat">
|
||||
<i id="chatBottomButton" class="icon-chat-simple">
|
||||
<span id="bottomUnreadMessages"></span>
|
||||
</i>
|
||||
@@ -280,7 +252,7 @@
|
||||
</span>
|
||||
<div class="bottom_button_separator"></div>
|
||||
<span class="bottomToolbar_span">
|
||||
<a class="bottomToolbarButton" data-container="body" data-toggle="popover" data-placement="top" id="contactlistpopover" content="Open / close contact list" onclick='BottomToolbar.toggleContactList();'>
|
||||
<a class="bottomToolbarButton" id="bottom_toolbar_contact_list" data-container="body" data-toggle="popover" data-placement="top" id="contactlistpopover" data-i18n="[content]bottomtoolbar.contactlist" content="Open / close contact list">
|
||||
<i id="contactListButton" class="icon-contactList">
|
||||
<span id="numberOfParticipants"></span>
|
||||
</i>
|
||||
@@ -288,36 +260,44 @@
|
||||
</span>
|
||||
<div class="bottom_button_separator"></div>
|
||||
<span class="bottomToolbar_span">
|
||||
<a class="bottomToolbarButton" data-container="body" data-toggle="popover" shortcut="filmstripPopover" data-placement="top" content="Show / hide film strip" onclick='BottomToolbar.toggleFilmStrip()'>
|
||||
<a class="bottomToolbarButton" id="bottom_toolbar_film_strip" data-container="body" data-toggle="popover" shortcut="filmstripPopover" data-placement="top" data-i18n="[content]bottomtoolbar.filmstrip" content="Show / hide film strip">
|
||||
<i id="filmStripButton" class="icon-filmstrip"></i>
|
||||
</a>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div id="chatspace">
|
||||
<div id="chatspace" class="right-panel">
|
||||
<div id="nickname">
|
||||
Enter a nickname in the box below
|
||||
<span data-i18n="chat.nickname.title"></span>
|
||||
<form>
|
||||
<input type='text' id="nickinput" placeholder='Choose a nickname' autofocus>
|
||||
<input type='text' id="nickinput" data-i18n="[placeholder]chat.nickname.popover" autofocus>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!--div><i class="fa fa-comments"> </i><span class='nick'></span>: <span class='chattext'></span></div-->
|
||||
<div id="chatconversation"></div>
|
||||
<audio id="chatNotification" src="sounds/incomingMessage.wav" preload="auto"></audio>
|
||||
<textarea id="usermsg" placeholder='Enter text...' autofocus></textarea>
|
||||
<textarea id="usermsg" data-i18n="[placeholder]chat.messagebox" autofocus></textarea>
|
||||
<div id="smileysarea">
|
||||
<div id="smileys" onclick="Chat.toggleSmileys()">
|
||||
<div id="smileys" id="toggle_smileys">
|
||||
<img src="images/smile.svg"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="contactlist">
|
||||
<div id="contactlist" class="right-panel">
|
||||
<ul>
|
||||
<li class="title"><i class="icon-contact-list"></i> CONTACT LIST</li>
|
||||
<li class="title"><i class="icon-contact-list"></i><span data-i18n="contactlist"></span></li>
|
||||
</ul>
|
||||
</div>
|
||||
<a id="downloadlog" onclick='dump(event.target);' data-container="body" data-toggle="popover" data-placement="right" data-content="Download logs" ><i class="fa fa-cloud-download"></i></a>
|
||||
<div id="settingsmenu" class="right-panel">
|
||||
<div class="icon-settings" data-i18n="settings.title"></div>
|
||||
<img id="avatar" src="https://www.gravatar.com/avatar/87291c37c25be69a072a4514931b1749?d=wavatar&size=30"/>
|
||||
<div class="arrow-up"></div>
|
||||
<input type="text" id="setDisplayName" data-i18n="[placeholder]settings.name" placeholder="Name">
|
||||
<input type="text" id="setEmail" placeholder="E-Mail">
|
||||
<button id="updateSettings" data-i18n="settings.update"></button>
|
||||
</div>
|
||||
<a id="downloadlog" onclick='dump(event.target);' data-container="body" data-toggle="popover" data-placement="right" data-i18n="[data-content]downloadlogs" ><i class="fa fa-cloud-download"></i></a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -5,12 +5,15 @@ var interfaceConfig = {
|
||||
INITIAL_TOOLBAR_TIMEOUT: 20000,
|
||||
TOOLBAR_TIMEOUT: 4000,
|
||||
DEFAULT_REMOTE_DISPLAY_NAME: "Fellow Jitster",
|
||||
DEFAULT_DOMINANT_SPEAKER_DISPLAY_NAME: "Speaker",
|
||||
DEFAULT_DOMINANT_SPEAKER_DISPLAY_NAME: "speaker",
|
||||
DEFAULT_LOCAL_DISPLAY_NAME: "me",
|
||||
SHOW_JITSI_WATERMARK: true,
|
||||
JITSI_WATERMARK_LINK: "http://jitsi.org",
|
||||
SHOW_BRAND_WATERMARK: false,
|
||||
BRAND_WATERMARK_LINK: "",
|
||||
SHOW_POWERED_BY: false,
|
||||
GENERATE_ROOMNAMES_ON_WELCOME_PAGE: true,
|
||||
APP_NAME: "Jitsi Meet"
|
||||
APP_NAME: "Jitsi Meet",
|
||||
INVITATION_POWERED_BY: true,
|
||||
ACTIVE_SPEAKER_AVATAR_SIZE: 100
|
||||
};
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
var KeyboardShortcut = (function(my) {
|
||||
//maps keycode to character, id of popover for given function and function
|
||||
var shortcuts = {
|
||||
67: {
|
||||
character: "C",
|
||||
id: "toggleChatPopover",
|
||||
function: BottomToolbar.toggleChat
|
||||
},
|
||||
70: {
|
||||
character: "F",
|
||||
id: "filmstripPopover",
|
||||
function: BottomToolbar.toggleFilmStrip
|
||||
},
|
||||
77: {
|
||||
character: "M",
|
||||
id: "mutePopover",
|
||||
function: toggleAudio
|
||||
},
|
||||
84: {
|
||||
character: "T",
|
||||
function: function() {
|
||||
if(!isAudioMuted()) {
|
||||
toggleAudio();
|
||||
}
|
||||
}
|
||||
},
|
||||
86: {
|
||||
character: "V",
|
||||
id: "toggleVideoPopover",
|
||||
function: toggleVideo
|
||||
}
|
||||
};
|
||||
|
||||
window.onkeyup = function(e) {
|
||||
var keycode = e.which;
|
||||
if(!($(":focus").is("input[type=text]") || $(":focus").is("input[type=password]") || $(":focus").is("textarea"))) {
|
||||
if (typeof shortcuts[keycode] === "object") {
|
||||
shortcuts[keycode].function();
|
||||
} else if (keycode >= "0".charCodeAt(0) && keycode <= "9".charCodeAt(0)) {
|
||||
var remoteVideos = $(".videocontainer:not(#mixedstream)"),
|
||||
videoWanted = keycode - "0".charCodeAt(0) + 1;
|
||||
if (remoteVideos.length > videoWanted) {
|
||||
remoteVideos[videoWanted].click();
|
||||
}
|
||||
}
|
||||
//esc while the smileys are visible hides them
|
||||
} else if (keycode === 27 && $('#smileysContainer').is(':visible')) {
|
||||
Chat.toggleSmileys();
|
||||
}
|
||||
};
|
||||
|
||||
window.onkeydown = function(e) {
|
||||
if(!($(":focus").is("input[type=text]") || $(":focus").is("input[type=password]") || $(":focus").is("textarea"))) {
|
||||
if(e.which === "T".charCodeAt(0)) {
|
||||
if(isAudioMuted()) {
|
||||
toggleAudio();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param id indicates the popover associated with the shortcut
|
||||
* @returns {string} the keyboard shortcut used for the id given
|
||||
*/
|
||||
my.getShortcut = function(id) {
|
||||
for(var keycode in shortcuts) {
|
||||
if(shortcuts.hasOwnProperty(keycode)) {
|
||||
if (shortcuts[keycode].id === id) {
|
||||
return " (" + shortcuts[keycode].character + ")";
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
};
|
||||
return my;
|
||||
}(KeyboardShortcut || {}))
|
||||
|
||||
6
lang/languages-bg.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"en": "Английски",
|
||||
"bg": "Български",
|
||||
"de": "Немски",
|
||||
"tr": "Турски"
|
||||
}
|
||||
6
lang/languages-de.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"en": "Englisch",
|
||||
"bg": "Bulgarisch",
|
||||
"de": "Deutsch",
|
||||
"tr": "Türkisch"
|
||||
}
|
||||
5
lang/languages-tr.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"en": "İngilizce",
|
||||
"bg": "Bulgarca",
|
||||
"de": "Almanca"
|
||||
}
|
||||
6
lang/languages.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"en": "English",
|
||||
"bg": "Bulgarian",
|
||||
"de": "German",
|
||||
"tr": "Turkish"
|
||||
}
|
||||
204
lang/main-bg.json
Normal file
@@ -0,0 +1,204 @@
|
||||
{
|
||||
"contactlist": "СПИСЪК С КОНТАКТИ",
|
||||
"connectionsettings": "Настройки на връзката",
|
||||
"poweredby": "",
|
||||
"downloadlogs": "",
|
||||
"roomUrlDefaultMsg": "",
|
||||
"participant": "Участник",
|
||||
"me": "аз",
|
||||
"speaker": "",
|
||||
"defaultNickname": "",
|
||||
"defaultPreziLink": "",
|
||||
"welcomepage": {
|
||||
"go": "",
|
||||
"roomname": "Въведете име на стаята",
|
||||
"disable": "",
|
||||
"feature1": {
|
||||
"title": "",
|
||||
"content": ""
|
||||
},
|
||||
"feature2": {
|
||||
"title": "",
|
||||
"content": ""
|
||||
},
|
||||
"feature3": {
|
||||
"title": "",
|
||||
"content": ""
|
||||
},
|
||||
"feature4": {
|
||||
"title": "",
|
||||
"content": ""
|
||||
},
|
||||
"feature5": {
|
||||
"title": "",
|
||||
"content": ""
|
||||
},
|
||||
"feature6": {
|
||||
"title": "",
|
||||
"content": ""
|
||||
},
|
||||
"feature7": {
|
||||
"title": "",
|
||||
"content": ""
|
||||
},
|
||||
"feature8": {
|
||||
"title": "",
|
||||
"content": ""
|
||||
}
|
||||
},
|
||||
"toolbar": {
|
||||
"mute": "",
|
||||
"videomute": "",
|
||||
"authenticate": "",
|
||||
"record": "",
|
||||
"lock": "",
|
||||
"invite": "",
|
||||
"chat": "",
|
||||
"prezi": "",
|
||||
"etherpad": "",
|
||||
"sharescreen": "",
|
||||
"fullscreen": "",
|
||||
"sip": "",
|
||||
"Settings": "",
|
||||
"hangup": "",
|
||||
"login": "",
|
||||
"logout": ""
|
||||
},
|
||||
"bottomtoolbar": {
|
||||
"chat": "",
|
||||
"filmstrip": "",
|
||||
"contactlist": ""
|
||||
},
|
||||
"chat": {
|
||||
"nickname": {
|
||||
"title": "",
|
||||
"popover": ""
|
||||
},
|
||||
"messagebox": ""
|
||||
},
|
||||
"settings": {
|
||||
"title": "НАСТРОЙКИ",
|
||||
"update": "",
|
||||
"name": ""
|
||||
},
|
||||
"videothumbnail": {
|
||||
"editnickname": "",
|
||||
"moderator": "",
|
||||
"videomute": "",
|
||||
"mute": "",
|
||||
"kick": "",
|
||||
"muted": "",
|
||||
"domute": ""
|
||||
},
|
||||
"connectionindicator": {
|
||||
"bitrate": "",
|
||||
"packetloss": "",
|
||||
"resolution": "",
|
||||
"less": "",
|
||||
"more": "",
|
||||
"address": "",
|
||||
"remoteport": "",
|
||||
"remoteport_plural": "",
|
||||
"localport": "",
|
||||
"localport_plural": "",
|
||||
"localaddress": "",
|
||||
"localaddress_plural": "",
|
||||
"remoteaddress": "",
|
||||
"remoteaddress_plural": "",
|
||||
"transport": "",
|
||||
"bandwidth": "",
|
||||
"na": ""
|
||||
},
|
||||
"notify": {
|
||||
"disconnected": "",
|
||||
"moderator": "",
|
||||
"connected": "",
|
||||
"somebody": "",
|
||||
"me": "",
|
||||
"focus": "",
|
||||
"focusFail": "",
|
||||
"grantedTo": "",
|
||||
"grantedToUnknown": ""
|
||||
},
|
||||
"dialog": {
|
||||
"kickMessage": "",
|
||||
"popupError": "",
|
||||
"passwordError": "",
|
||||
"passwordError2": "",
|
||||
"joinError": "",
|
||||
"connectError": "",
|
||||
"error": "",
|
||||
"detectext": "",
|
||||
"failtoinstall": "",
|
||||
"failedpermissions": "",
|
||||
"bridgeUnavailable": "",
|
||||
"lockTitle": "",
|
||||
"lockMessage": "",
|
||||
"warning": "",
|
||||
"passwordNotSupported": "",
|
||||
"sorry": "",
|
||||
"internalError": "",
|
||||
"unableToSwitch": "",
|
||||
"SLDFailure": "",
|
||||
"SRDFailure": "",
|
||||
"oops": "",
|
||||
"defaultError": "",
|
||||
"passwordRequired": "",
|
||||
"Ok": "",
|
||||
"removePreziTitle": "",
|
||||
"removePreziMsg": "",
|
||||
"sharePreziTitle": "",
|
||||
"sharePreziMsg": "",
|
||||
"Remove": "",
|
||||
"Stop": "",
|
||||
"AuthMsg": "",
|
||||
"Authenticate": "",
|
||||
"Cancel": "",
|
||||
"logoutTitle": "",
|
||||
"logoutQuestion": "",
|
||||
"sessTerminated": "",
|
||||
"hungUp": "",
|
||||
"joinAgain": "",
|
||||
"Share": "",
|
||||
"preziLinkError": "",
|
||||
"Save": "",
|
||||
"recordingToken": "",
|
||||
"Dial": "",
|
||||
"sipMsg": "",
|
||||
"passwordCheck": "",
|
||||
"passwordMsg": "",
|
||||
"Invite": "",
|
||||
"shareLink": "",
|
||||
"settings1": "",
|
||||
"settings2": "",
|
||||
"settings3": "",
|
||||
"yourPassword": "",
|
||||
"Back": "",
|
||||
"serviceUnavailable": "",
|
||||
"gracefulShutdown": "",
|
||||
"Yes": "",
|
||||
"reservationError": "",
|
||||
"reservationErrorMsg": "",
|
||||
"password": "",
|
||||
"userPassword": "",
|
||||
"token": ""
|
||||
},
|
||||
"email": {
|
||||
"sharedKeyThis conference is password protected. Please use the following pin when joining:\n\n\n__sharedKey__\n\n": "",
|
||||
"subject": "",
|
||||
"bodyHey there, I%27d like to invite you to a __appName__ conference I%27ve just set up.\n\n\nPlease click on the following link in order to join the conference.\n\n\n__roomUrl__\n\n\n__sharedKeyText__\n Note that __appName__ is currently only supported by __supportedBrowsers__, so you need to be using one of these browsers.\n\n\nTalk to you in a sec!": [
|
||||
"Здравей, Бих искал да те поканя в една __appName__ конференция, която създадох.",
|
||||
"",
|
||||
"",
|
||||
"Кликни на следния линк за да се присъединиш в конференцията.",
|
||||
"",
|
||||
"",
|
||||
"__sharedKeyText__",
|
||||
"__appName__ поддържа __supportedBrowsers__, така че трябва да използваш един от тези браузъри.",
|
||||
"",
|
||||
"",
|
||||
"Ще се видим след секунда!"
|
||||
],
|
||||
"and": ""
|
||||
}
|
||||
}
|
||||
207
lang/main-de.json
Normal file
@@ -0,0 +1,207 @@
|
||||
{
|
||||
"contactlist": "Kontaktliste",
|
||||
"connectionsettings": "Verbindungseinstellungen",
|
||||
"poweredby": "Betrieben von",
|
||||
"downloadlogs": "Log herunterladen",
|
||||
"roomUrlDefaultMsg": "Die Konferenz wird erstellt...",
|
||||
"participant": "Teilnehmer",
|
||||
"me": "ich",
|
||||
"speaker": "Sprecher",
|
||||
"defaultNickname": "Bsp.: __name__",
|
||||
"defaultPreziLink": "Bsp.: __url__",
|
||||
"welcomepage": {
|
||||
"go": "Los",
|
||||
"roomname": "Raumnamen eingeben",
|
||||
"disable": "Diese Seite beim nächsten Betreten nicht mehr anzeigen",
|
||||
"feature1": {
|
||||
"title": "Einfach zu benutzen",
|
||||
"content": "Kein Download nötig. __app__ läuft direkt im Browser. Einfach die Konferenzadresse teilen und los geht's."
|
||||
},
|
||||
"feature2": {
|
||||
"title": "Niedrige Bandbreite",
|
||||
"content": "Videokonferenzen mit mehreren Teilnehmen mit weniger als 128Kpbs. Bildschirmfreigaben und Telefonkonferenzen kommen sogar mit noch weniger Bandbreite aus."
|
||||
},
|
||||
"feature3": {
|
||||
"title": "Open Source",
|
||||
"content": "__app__ steht unter der MIT Lizenz. __app__ kann gemäss der Lizenz heruntergeladen, verwendet, verändert und weitergegeben werden."
|
||||
},
|
||||
"feature4": {
|
||||
"title": "Unbegrenzte Anzahl Benutzer",
|
||||
"content": "Es gibt keine künstliche Beschränkung der Anzahl der Benutzer oder Konferenzteilnehmer. Die Leistung des Servers und die Bandbreite sind die einzigen limitierenden Faktoren."
|
||||
},
|
||||
"feature5": {
|
||||
"title": "Bildschirmfreigabe",
|
||||
"content": "Es ist ganz einfach den Bildschirm zu teilen. __app__ ist ideal für Online-Präsentationen, Vorlesungen und Fernwartungsanfragen."
|
||||
},
|
||||
"feature6": {
|
||||
"title": "Sichere Konferenzen",
|
||||
"content": "Privatsphäre gewünscht? __app__ Konferenzen können mit einem Passwort geschützt werden um ungebetene Gäste fernzuhalten und Unterbrechungen zu vermeiden."
|
||||
},
|
||||
"feature7": {
|
||||
"title": "Freigegebene Notizen",
|
||||
"content": "__app__ verwendent Etherpad, ein Editor zur kollaborativen Bearbeitung von Texten."
|
||||
},
|
||||
"feature8": {
|
||||
"title": "Benutzungsstatistiken",
|
||||
"content": "Die Verwendung kann durch die Integration mit Piwik, Google Analytics und anderen Überwachungs- und Statistikprogrammen protokolliert werden."
|
||||
}
|
||||
},
|
||||
"toolbar": {
|
||||
"mute": "Stummschaltung aktivieren / deaktivieren",
|
||||
"videomute": "Kamera starten / stoppen",
|
||||
"authenticate": "Anmelden",
|
||||
"record": "Aufnehmen",
|
||||
"lock": "Raum schützen / Schutz aufheben",
|
||||
"invite": "Andere einladen",
|
||||
"chat": "Chat öffnen / schliessen",
|
||||
"prezi": "Prezi freigeben",
|
||||
"etherpad": "Geteiltes Dokument",
|
||||
"sharescreen": "Bildschirm freigeben",
|
||||
"fullscreen": "Vollbildmodus aktivieren / deaktivieren",
|
||||
"sip": "SIP Nummer anrufen",
|
||||
"Settings": "Einstellungen",
|
||||
"hangup": "Auflegen",
|
||||
"login": "Anmelden",
|
||||
"logout": "Abmelden"
|
||||
},
|
||||
"bottomtoolbar": {
|
||||
"chat": "Chat öffnen / schliessen",
|
||||
"filmstrip": "Videovorschauen anzeigen / verstecken",
|
||||
"contactlist": "Kontaktliste öffnen / schliessen"
|
||||
},
|
||||
"chat": {
|
||||
"nickname": {
|
||||
"title": "Nickname im Eingabefeld eingeben",
|
||||
"popover": "Einen Namen auswählen"
|
||||
},
|
||||
"messagebox": "Text eingeben..."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Einstellungen",
|
||||
"update": "Aktualisieren",
|
||||
"name": "Name"
|
||||
},
|
||||
"videothumbnail": {
|
||||
"editnickname": "Klicken um den Anzeigenamen zu bearbeiten",
|
||||
"moderator": "Besitzer dieser Konferenz",
|
||||
"videomute": "Teilnehmer hat die Kamera pausiert.",
|
||||
"mute": "Teilnehmer ist stumm geschaltet",
|
||||
"kick": "Hinauswerfen",
|
||||
"muted": "Stummgeschaltet",
|
||||
"domute": "Stummschalten"
|
||||
},
|
||||
"connectionindicator": {
|
||||
"bitrate": "Bitrate:",
|
||||
"packetloss": "Paketverlust:",
|
||||
"resolution": "Auflösung:",
|
||||
"less": "Weniger anzeigen",
|
||||
"more": "Mehr anzeigen",
|
||||
"address": "Adresse:",
|
||||
"remoteport": "Entfernter Port:",
|
||||
"remoteport_plural": "Entfernte Ports:",
|
||||
"localport": "Lokaler Port:",
|
||||
"localport_plural": "Lokale Ports:",
|
||||
"localaddress": "Lokale Adresse:",
|
||||
"localaddress_plural": "Lokale Adressen:",
|
||||
"remoteaddress": "Entfernte Adresse:",
|
||||
"remoteaddress_plural": "Entfernte Adressen:",
|
||||
"transport": "Protokoll:",
|
||||
"bandwidth": "Geschätzte Bandbreite:",
|
||||
"na": "Verbindungsdaten erneut anzeigen wenn die Konferenz begonnen hat"
|
||||
},
|
||||
"notify": {
|
||||
"disconnected": "getrennt",
|
||||
"moderator": "Moderatorenrechte vergeben",
|
||||
"connected": "verbunden",
|
||||
"somebody": "Jemand",
|
||||
"me": "Ich",
|
||||
"focus": "Konferenz-Organisator",
|
||||
"focusFail": "__component__ ist im Moment nicht verfügbar - wiederholen in __ms__ Sekunden",
|
||||
"grantedTo": "Moderatorenrechte an __to__ vergeben.",
|
||||
"grantedToUnknown": "Moderatorenrechte an $t(somebody) vergeben."
|
||||
},
|
||||
"dialog": {
|
||||
"kickMessage": "Oh! Sie wurden aus der Konferenz ausgeschlossen.",
|
||||
"popupError": "Ihr Browser blockiert Popups von dieser Webseite. Bitte erlauben Sie Popups in den Sicherheitseinstellungen und versuchen Sie es erneut.",
|
||||
"passwordError": "Diese Konferenz ist mit einem Paswort geschützt. Nur der Besitzer der Konferenz kann ein Passwort vergeben.",
|
||||
"passwordError2": "Diese Konferenzt ist nicht mit einem Passwort geschützt. Nur der Besitzer der Konferenz kann ein Passwort vergeben.",
|
||||
"joinError": "Oh! Der Konferenz konnte nicht beigetreten werden. Diese könnte ein Problem mit den Sicherheitseinstellungen sein. Bitte kontaktieren Sie den Administrator des Dienstes.",
|
||||
"connectError": "Oh! Es hat etwas nicht geklappt und der Konferenz konnte nicht beigetreten werden.",
|
||||
"error": "Fehler",
|
||||
"detectext": "Fehler bei der Erkennung der Bildschirmfreigabeerweiterung.",
|
||||
"failtoinstall": "Die Bildschirmfreigabeerweiterung konnte nicht installiert werden.",
|
||||
"failedpermissions": "Die Zugriffsberechtigungen auf das Mikrofon und/oder die Kamera konnte nicht eingeholt werden.",
|
||||
"bridgeUnavailable": "Die Jitsi Videobridge ist momentan nicht erreichbar. Bitte versuchen Sie es später noch einmal.",
|
||||
"lockTitle": "Sperren fehlgeschlagen",
|
||||
"lockMessage": "Die Konferenz konnte nicht gesperrt werden.",
|
||||
"warning": "Warnung",
|
||||
"passwordNotSupported": "Passwörter für Räume werden nicht unterstützt.",
|
||||
"sorry": "Entschuldigung",
|
||||
"internalError": "Interner Anwendungsfehler [setRemoteDescription]",
|
||||
"unableToSwitch": "Der Videodatenstrom kann nicht gewechselt werden.",
|
||||
"SLDFailure": "Oh! Die Stummschaltung konnte nicht aktiviert werden. (SLD Fehler)",
|
||||
"SRDFailure": "Oh! Das Video konnte nicht gestoppt werden. (SRD Fehler)",
|
||||
"oops": "Oh!",
|
||||
"defaultError": "Es ist ein Fehler aufgetreten",
|
||||
"passwordRequired": "Passwort erforderlich",
|
||||
"Ok": "OK",
|
||||
"removePreziTitle": "Prezi entfernen",
|
||||
"removePreziMsg": "Sind Sie sich sicher dass sie Prezi entfernen möchten?",
|
||||
"sharePreziTitle": "Ein Prezi teilen",
|
||||
"sharePreziMsg": "Ein anderer Teilnehmer teilt bereits ein Prezi. Diese Konferenz kann nur eine Prezi auf einmal anzeigen.",
|
||||
"Remove": "Entfernen",
|
||||
"Stop": "Stopp",
|
||||
"AuthMsg": "Für die Erstellung des Raums ist eine Authentifizierung erforderlich. <br/><b>__room__</b><br/>Sie können sich entweder anmelden oder warten bis jemand anderes die Authentifizierung vornimmt.",
|
||||
"Authenticate": "Anmelden",
|
||||
"Cancel": "Abbrechen",
|
||||
"logoutTitle": "Abmelden",
|
||||
"logoutQuestion": "Sind Sie sicher dass Sie sich abmelden und die Konferenz verlassen möchten?",
|
||||
"sessTerminated": "Sitzung beendet",
|
||||
"hungUp": "Anruf beendet",
|
||||
"joinAgain": "Erneut beitreten",
|
||||
"Share": "Teilen",
|
||||
"preziLinkError": "Bitte einen gültigen Prezi-Link angeben.",
|
||||
"Save": "Speichern",
|
||||
"recordingToken": "Aufnahme-Token eingeben",
|
||||
"Dial": "Wählen",
|
||||
"sipMsg": "Geben Sie eine SIP Nummer ein",
|
||||
"passwordCheck": "Sind Sie sicher dass Sie das Passwort entfernen möchten?",
|
||||
"passwordMsg": "Passwort setzen um den Raum zu schützen",
|
||||
"Invite": "Einladen",
|
||||
"shareLink": "Teilen Sie diesen Link mit jedem den Sie einladen möchten",
|
||||
"settings1": "Konferenz einrichten",
|
||||
"settings2": "Teilnehmer treten stummgeschaltet bei",
|
||||
"settings3": "Nickname erforderlich<br/><br/>Setzen Sie ein Passwort um den Raum zu schützen:",
|
||||
"yourPassword": "Ihr Passwort",
|
||||
"Back": "Zurück",
|
||||
"serviceUnavailable": "Dienst nicht verfügbar",
|
||||
"gracefulShutdown": "Der Dienst steht momentan wegen Wartungsarbeiten nicht zur Verfügung. Bitte versuchen Sie es später noch einmal.",
|
||||
"Yes": "Ja",
|
||||
"reservationError": "Fehler im Reservationssystem",
|
||||
"reservationErrorMsg": "Fehler, Nummer: __code__, Nachricht: __msg__",
|
||||
"password": "Passwort",
|
||||
"userPassword": "Benutzerpasswort",
|
||||
"token": "Token"
|
||||
},
|
||||
"email": {
|
||||
"sharedKeyThis conference is password protected. Please use the following pin when joining:\n\n __sharedKey__ \n\n": [
|
||||
"Diese Konferenz ist Passwortgeschützt. Bitte verwenden Sie diesen PIN zum Beitreten:",
|
||||
"",
|
||||
" __sharedKey__",
|
||||
"",
|
||||
"",
|
||||
""
|
||||
],
|
||||
"subject": "Einladung zu einer __appName__ (__conferenceName__)",
|
||||
"bodyHey there, I%27d like to invite you to a __appName__ conference I%27ve just set up.\n\nPlease click on the following link in order to join the conference.\n\n __roomUrl__\n\n__sharedKeyText__ Note that __appName__ is currently only supported by Chromium, Google Chrome and Opera, so you need to be using one of these browsers.\n\nTalk to you in a sec!": [
|
||||
"Hallo!",
|
||||
"Ich möchte dich zu einer eben erstellten __appName__-Konferenz einladen.",
|
||||
"",
|
||||
"Bitte klicke auf den folgenden Link um der Konferenz ebenfalls beizutreten:\\",
|
||||
" __roomUrl__",
|
||||
"__sharedKeyText__ Bitte beachte, dass __appName__ momentan nur mit einem der Browser Chromium, Google und Opera verwendet werden kann.",
|
||||
"",
|
||||
"Bis gleich!"
|
||||
]
|
||||
}
|
||||
}
|
||||
173
lang/main-tr.json
Normal file
@@ -0,0 +1,173 @@
|
||||
{
|
||||
"contactlist": "KİŞİ LİSTESİ",
|
||||
"connectionsettings": "Bağlantı Ayarları",
|
||||
"poweredby": "Gücünün kaynağı",
|
||||
"downloadlogs": "Günlükleri indir",
|
||||
"welcomepage": {
|
||||
"go": "GİT",
|
||||
"roomname": "Oda adı girin",
|
||||
"disable": "Sonraki girişimde bu sayfayı gösterme",
|
||||
"feature1": {
|
||||
"title": "Kullanımı kolay",
|
||||
"content": "İndirmeye gerek yok. __app__ tarayıcınızda doğrudan çalışır. Başlamak için görüşme bağlantısını URL diğerleri ile paylaşın."
|
||||
},
|
||||
"feature2": {
|
||||
"title": "Düşük bant genişliği ihtiyacı",
|
||||
"content": "Ekran paylaşımı ve sadece ses ile çok katılımcılı video görüşmeleri, 128Kbps bağlantı ile mümkündür."
|
||||
},
|
||||
"feature3": {
|
||||
"title": "Açık kaynak kodlu",
|
||||
"content": "__app__ MIT ile lisanslanmıştır. Bu lisansa uygun olarak indirmek, kullanmak, değiştirmek ve paylaşmakta özgürsün."
|
||||
},
|
||||
"feature4": {
|
||||
"title": "Sınırsız sayıda kullanıcı",
|
||||
"content": "Kullanıcılar veya konferans katılımcılarının sayısında hiçbir yapay kısıtlama yoktur. Sadece sunucun güç ve bant genişliği, sınırlayıcı unsurdur."
|
||||
},
|
||||
"feature5": {
|
||||
"title": "Ekran paylaşımı",
|
||||
"content": "Diğerlerinle ekranınızı kolayca paylaşın. __app__ çevrimiçi sunumlar, dersler ve teknik destek oturumları için idealdir."
|
||||
},
|
||||
"feature6": {
|
||||
"title": "Güvenli odalar",
|
||||
"content": "Biraz gizliliğe ihtiyacınız var? __app__ görüşme odaları, istemeyen misafirleri uzak tutmak ve kesinleri önlemek için bir parola ile güvence altına alınabilir."
|
||||
},
|
||||
"feature7": {
|
||||
"title": "Paylaşımlı notlar",
|
||||
"content": "__app__ Etherpad içerir, gerçek zamanlı bir ortak çalışma metin düzenleyicisidir. Görüşme tutanakları, makale yazımı ve daha fazlası için biçilmiş kaftandır."
|
||||
},
|
||||
"feature8": {
|
||||
"title": "Kullanım istatistikleri",
|
||||
"content": "Piwik, Google Analytics ve diğer kullanım izleme ve istatistik sistemleri ile kolay tümleştirmeyle kullanıcılar hakkında bilgi edinin."
|
||||
}
|
||||
},
|
||||
"toolbar": {
|
||||
"mute": "Sessiz / Sesli",
|
||||
"videomute": "Kamera başlat / durdur",
|
||||
"authenticate": "",
|
||||
"record": "Kaydet",
|
||||
"lock": "Odayı kilitle / kilit aç",
|
||||
"invite": "Arkadaşlarını davet et",
|
||||
"chat": "",
|
||||
"prezi": "Prezi paylaş",
|
||||
"etherpad": "Paylaşımlı belge",
|
||||
"sharescreen": "Ekran paylaş",
|
||||
"fullscreen": "Tam Ekrana Gir / Çık",
|
||||
"sip": "SIP numara ara",
|
||||
"Settings": "Ayarlar",
|
||||
"hangup": "Kapat",
|
||||
"login": "Oturum aç",
|
||||
"logout": ""
|
||||
},
|
||||
"bottomtoolbar": {
|
||||
"chat": "Sohbeti aç / kapa",
|
||||
"filmstrip": "Kişi listesi aç / kapa",
|
||||
"contactlist": "Film şeridini göster / gizle"
|
||||
},
|
||||
"chat": {
|
||||
"nickname": {
|
||||
"title": "Aşağıdaki kutuya bir takma ad girin",
|
||||
"popover": "Bir takma ad seçin"
|
||||
},
|
||||
"messagebox": "Metin girin..."
|
||||
},
|
||||
"settings": {
|
||||
"title": "AYARLAR",
|
||||
"update": "Güncelle",
|
||||
"name": "Ad"
|
||||
},
|
||||
"videothumbnail": {
|
||||
"editnickname": "Görünür adınızı değiştirmek<br/>için tıkla",
|
||||
"moderator": "Bu görüşmenin<br/>sahibi",
|
||||
"videomute": "Katılımcı<br/>kamera durdurdu.",
|
||||
"mute": "Katılımcı sessiz",
|
||||
"kick": "Kovuldu",
|
||||
"muted": "Sessiz",
|
||||
"domute": "Sustur"
|
||||
},
|
||||
"connectionindicator": {
|
||||
"bitrate": "Bit hızı:",
|
||||
"packetloss": "Paket kaybı:",
|
||||
"resolution": "Çözünürlük:",
|
||||
"less": "Daha az göster",
|
||||
"more": "Daha fazla göster",
|
||||
"address": "Adres:",
|
||||
"remoteport": "Uzak port:Uzak portlar:",
|
||||
"localport": "Yerel port:Yerel portlar:",
|
||||
"localaddress": "Yerel adres:Yerel adresler:",
|
||||
"remoteaddress": "Uzak adres:Uzak adresler:",
|
||||
"transport": "Transport:",
|
||||
"bandwidth": "Tahmini bant genişliği:",
|
||||
"na": "Görüşme başladıktan sonra bağlantı bilgileri için buraya gel"
|
||||
},
|
||||
"notify": {
|
||||
"disconnected": "bağlantı kesildi",
|
||||
"moderator": "Görüşme yöneticisi hakları verildi!",
|
||||
"connected": "bağlandı",
|
||||
"somebody": "Birisi",
|
||||
"me": "Bana",
|
||||
"focus": "Görüşme odağı",
|
||||
"focusFail": "__component__ uygun değil - __ms__ saniye içinde tekrar deneyin",
|
||||
"grantedTo": "__to__, görüşme yöneticisi hakları verildi!",
|
||||
"grantedToUnknown": "$t(somebody), görüşme yöneticisi hakları verildi!"
|
||||
},
|
||||
"dialog": {
|
||||
"kickMessage": "Ahhh! Görüşmeden, kavuldun!",
|
||||
"popupError": "Tarayıcınız bu siteden açılır pencereleri engelliyor. Lütfen, tarayıcınızın güvenlik ayarlarında pop-up etkinleştirin ve tekrar deneyin.",
|
||||
"passwordError": "Bu görüşme şu anda bir parola ile korunmaktadır. Sadece görüşmenin sahibi bir parola ayarlayabilir.",
|
||||
"passwordError2": "Bu görüşme şu anda bir parola ile korunmamaktadır. Sadece görüşmenin sahibi bir parola ayarlayabilir.",
|
||||
"joinError": "Amanin boo! Görüşmeye katılamadık. Güvenlik yapılandırması ile ilgili bir sorun olabilir. Hizmet yöneticisi ile bağlantı kurun.",
|
||||
"connectError": "Amanin boo! Birşeyler ters gitti ve görüşmeye bağlanamadık.",
|
||||
"error": "Hata",
|
||||
"detectext": "Ekran paylaşımı eklentisi tespit edilirken hata.",
|
||||
"failtoinstall": "Masaüstü paylaşım eklentisi yüklenemedi",
|
||||
"failedpermissions": "Yerel mikrofon ve/veya kamerayı kullanmak için izinler alınamadı.",
|
||||
"bridgeUnavailable": "Jitsi Videobridge şu anda kullanılamıyor. Daha sonra tekrar deneyiniz!",
|
||||
"lockTitle": "Kilitlenemedi",
|
||||
"lockMessage": "Görüşme kilitlenemedi.",
|
||||
"warning": "Uyarı",
|
||||
"passwordNotSupported": "Oda parolaları şu anda desteklenmemekte.",
|
||||
"sorry": "Üzgünüz",
|
||||
"internalError": "İç uygulama hatası [setRemoteDescription]",
|
||||
"unableToSwitch": "Video akışı açılamıyor.",
|
||||
"SLDFailure": "Amanin boo! Birşeyler ters gitti ve sessize alamadık! (SLD Başarısız)",
|
||||
"SRDFailure": "Amanin boo! Birşeyler ters gitti ve videoyu durduramadık! (SRD Başarısız)",
|
||||
"oops": "Amanin boo!",
|
||||
"defaultError": "Bir tür hata var",
|
||||
"passwordRequired": "Parola gerekli",
|
||||
"Ok": "Tamam",
|
||||
"removePreziTitle": "Prezi kaldır",
|
||||
"removePreziMsg": "Prezi kaldırmak istediğinizden emin misiniz?",
|
||||
"sharePreziTitle": "Bir Prezi paylaşın",
|
||||
"sharePreziMsg": "Diğer katılımcı hala bir Prezi paylaşıyor.Bu görüşme aynı zamanda sadece bir Prezi izin verir.",
|
||||
"Remove": "Kaldır",
|
||||
"Stop": "Durdur",
|
||||
"AuthMsg": "Oda oluşturmak için kimlik doğrulama gerekli:<br/><b>__room__ </b></br> Oda oluşturmak için ya kimlik doğrulamalı ya da bunu yapması için bir başkasını beklemelisiniz.",
|
||||
"Authenticate": "Kimlik doğrula",
|
||||
"Cancel": "İptal",
|
||||
"logoutTitle": "Oturum kapat",
|
||||
"logoutQuestion": "Oturumu ve görüşmeyi sonlandırmak istediğinizden emin misiniz?",
|
||||
"sessTerminated": "Oturum sonlandırıldı",
|
||||
"hungUp": "Görüşmeyi bitirdiniz",
|
||||
"joinAgain": "Yeniden katıl",
|
||||
"Share": "Paylaş",
|
||||
"preziLinkError": "Lütfen doğru prezi bağlantısı verin.",
|
||||
"Save": "Kaydet",
|
||||
"recordingToken": "Kayıt jetonu girin",
|
||||
"Dial": "Ara",
|
||||
"sipMsg": "SIP numarası gir",
|
||||
"passwordCheck": "Parolanızı kaldırmak istediğinizden emin misiniz?",
|
||||
"passwordMsg": "Odanızı kilitlemek için bir parola koyun",
|
||||
"Invite": "Davet et",
|
||||
"shareLink": "Davet etmek istediğiniz herkesle bu bağlantıyı paylaşın",
|
||||
"settings1": "Görüşmenizi yapılandır",
|
||||
"settings2": "Katılımcılar sessiz katılsın",
|
||||
"settings3": "Takma adlar gerekli<br/><br/>Odanızı kitlemek için bir parola ayarlayın:",
|
||||
"yourPassword": "parolanız",
|
||||
"Back": "Geri",
|
||||
"serviceUnavailable": "Hizmet kullanım dışı",
|
||||
"gracefulShutdown": "Hizmetimiz bakıp için durduruldu. Daha sonra tekrar deneyiniz.",
|
||||
"Yes": "Evet",
|
||||
"reservationError": "Rezervasyon sistemi hatası",
|
||||
"reservationErrorMsg": "Hata kodu: __code__, mesaj: __msg__"
|
||||
}
|
||||
}
|
||||
220
lang/main.json
Normal file
@@ -0,0 +1,220 @@
|
||||
{
|
||||
"contactlist": "CONTACT LIST",
|
||||
"connectionsettings": "Connection Settings",
|
||||
"poweredby": "powered by",
|
||||
"downloadlogs": "Download logs",
|
||||
"roomUrlDefaultMsg": "Your conference is currently being created...",
|
||||
"participant": "Participant",
|
||||
"me": "me",
|
||||
"speaker": "Speaker",
|
||||
"defaultNickname": "ex. __name__",
|
||||
"defaultPreziLink": "e.g. __url__",
|
||||
"welcomepage":{
|
||||
"go": "GO",
|
||||
"roomname": "Enter room name",
|
||||
"disable": "Don't show this page next time I enter",
|
||||
"feature1": {
|
||||
"title": "Simple to use",
|
||||
"content": "No downloads required. __app__ works directly within your browser. Simply share your conference URL with others to get started."
|
||||
},
|
||||
"feature2": {
|
||||
"title": "Low bandwidth",
|
||||
"content": "Multi-party video conferences work with as little as 128Kbps. Screen-sharing and audio-only conferences are possible with far less."
|
||||
},
|
||||
"feature3": {
|
||||
"title": "Open source",
|
||||
"content": "__app__ is licensed under MIT. You are free to download, use, modify, and share them as per these licenses."
|
||||
},
|
||||
"feature4": {
|
||||
"title": "Unlimited users",
|
||||
"content": "There are no artificial restrictions on the number of users or conference participants. Server power and bandwidth are the only limiting factors."
|
||||
},
|
||||
"feature5": {
|
||||
"title": "Screen sharing",
|
||||
"content": "It's easy to share your screen with others. __app__ is ideal for on-line presentations, lectures, and tech support sessions."
|
||||
},
|
||||
"feature6": {
|
||||
"title": "Secure rooms",
|
||||
"content": "Need some privacy? __app__ conference rooms can be secured with a password in order to exclude unwanted guests and prevent interruptions."
|
||||
},
|
||||
"feature7": {
|
||||
"title": "Shared notes",
|
||||
"content": "__app__ features Etherpad, a real-time collaborative text editor that's great for meeting minutes, writing articles, and more."
|
||||
},
|
||||
"feature8": {
|
||||
"title": "Usage statistics",
|
||||
"content": "Learn about your users through easy integration with Piwik, Google Analytics, and other usage monitoring and statistics systems."
|
||||
}
|
||||
},
|
||||
"toolbar": {
|
||||
"mute": "Mute / Unmute",
|
||||
"videomute": "Start / stop camera",
|
||||
"authenticate": "Authenticate",
|
||||
"record": "Record",
|
||||
"lock": "Lock / unlock room",
|
||||
"invite": "Invite others",
|
||||
"chat": "Open / close chat",
|
||||
"prezi": "Share Prezi",
|
||||
"etherpad": "Shared document",
|
||||
"sharescreen": "Share screen",
|
||||
"fullscreen": "Enter / Exit Full Screen",
|
||||
"sip": "Call SIP number",
|
||||
"Settings": "Settings",
|
||||
"hangup": "Hang Up",
|
||||
"login": "Login",
|
||||
"logout": "Logout"
|
||||
},
|
||||
"bottomtoolbar": {
|
||||
"chat": "Open / close chat",
|
||||
"filmstrip": "Show / hide film strip",
|
||||
"contactlist": "Open / close contact list"
|
||||
},
|
||||
"chat":{
|
||||
"nickname": {
|
||||
"title": "Enter a nickname in the box below",
|
||||
"popover": "Choose a nickname"
|
||||
},
|
||||
"messagebox": "Enter text..."
|
||||
},
|
||||
"settings":
|
||||
{
|
||||
"title": "SETTINGS",
|
||||
"update": "Update",
|
||||
"name": "Name"
|
||||
},
|
||||
"videothumbnail":
|
||||
{
|
||||
"editnickname": "Click to edit your<br/>display name",
|
||||
"moderator": "The owner of<br/>this conference",
|
||||
"videomute": "Participant has<br/>stopped the camera.",
|
||||
"mute": "Participant is muted",
|
||||
"kick": "Kick out",
|
||||
"muted": "Muted",
|
||||
"domute": "Mute"
|
||||
|
||||
},
|
||||
"connectionindicator":
|
||||
{
|
||||
"bitrate": "Bitrate:",
|
||||
"packetloss": "Packet loss:",
|
||||
"resolution": "Resolution:",
|
||||
"less": "Show less",
|
||||
"more": "Show more",
|
||||
"address": "Address:",
|
||||
"remoteport_plural": "Remote ports:",
|
||||
"localport_plural": "Local ports:",
|
||||
"remoteport": "Remote port:",
|
||||
"localport": "Local port:",
|
||||
"localaddress": "Local address:",
|
||||
"localaddress_plural": "Local addresses:",
|
||||
"remoteaddress": "Remote address:",
|
||||
"remoteaddress_plural": "Remote addresses:",
|
||||
"transport": "Transport:",
|
||||
"bandwidth": "Estimated bandwidth:",
|
||||
"na": "Come back here for connection information once the conference starts"
|
||||
},
|
||||
"notify": {
|
||||
"disconnected": "disconnected",
|
||||
"moderator": "Moderator rights granted!",
|
||||
"connected": "connected",
|
||||
"somebody": "Somebody",
|
||||
"me": "Me",
|
||||
"focus": "Conference focus",
|
||||
"focusFail": "__component__ not available - retry in __ms__ sec",
|
||||
"grantedTo": "Moderator rights granted to __to__!",
|
||||
"grantedToUnknown": "Moderator rights granted to $t(somebody)!"
|
||||
|
||||
},
|
||||
"dialog": {
|
||||
"kickMessage": "Ouch! You have been kicked out of the meet!",
|
||||
"popupError": "Your browser is blocking popup windows from this site. Please enable popups in your browser security settings and try again.",
|
||||
"passwordError": "This conversation is currently protected by a password. Only the owner of the conference could set a password.",
|
||||
"passwordError2": "This conversation isn't currently protected by a password. Only the owner of the conference could set a password.",
|
||||
"joinError": "Oops! We couldn't join the conference. There might be some problem with security configuration. Please contact service administrator.",
|
||||
"connectError": "Oops! Something went wrong and we couldn't connect to the conference.",
|
||||
"error": "Error",
|
||||
"detectext": "Error when trying to detect desktopsharing extension.",
|
||||
"failtoinstall": "Failed to install desktop sharing extension",
|
||||
"failedpermissions": "Failed to obtain permissions to use the local microphone and/or camera.",
|
||||
"bridgeUnavailable": "Jitsi Videobridge is currently unavailable. Please try again later!",
|
||||
"lockTitle": "Lock failed",
|
||||
"lockMessage": "Failed to lock conference.",
|
||||
"warning": "Warning",
|
||||
"passwordNotSupported": "Room passwords are currently not supported.",
|
||||
"sorry": "Sorry",
|
||||
"internalError": "Internal application error [setRemoteDescription]",
|
||||
"unableToSwitch": "Unable to switch video stream.",
|
||||
"SLDFailure": "Oops! Something went wrong and we failed to mute! (SLD Failure)",
|
||||
"SRDFailure": "Oops! Something went wrong and we failed to stop video! (SRD Failure)",
|
||||
"oops": "Oops!",
|
||||
"defaultError": "There was some kind of error",
|
||||
"passwordRequired": "Password required",
|
||||
"Ok": "Ok",
|
||||
"removePreziTitle": "Remove Prezi",
|
||||
"removePreziMsg": "Are you sure you would like to remove your Prezi?",
|
||||
"sharePreziTitle": "Share a Prezi",
|
||||
"sharePreziMsg": "Another participant is already sharing a Prezi.This conference allows only one Prezi at a time.",
|
||||
"Remove": "Remove",
|
||||
"Stop": "Stop",
|
||||
"AuthMsg": "Authentication is required to create room:<br/><b>__room__ </b></br> You can either authenticate to create the room or just wait for someone else to do so.",
|
||||
"Authenticate": "Authenticate",
|
||||
"Cancel": "Cancel",
|
||||
"logoutTitle" : "Logout",
|
||||
"logoutQuestion" : "Are you sure you want to logout and stop the conference?",
|
||||
"sessTerminated": "Session Terminated",
|
||||
"hungUp": "You hung up the call",
|
||||
"joinAgain": "Join again",
|
||||
"Share": "Share",
|
||||
"preziLinkError": "Please provide a correct prezi link.",
|
||||
"Save": "Save",
|
||||
"recordingToken": "Enter recording token",
|
||||
"Dial": "Dial",
|
||||
"sipMsg": "Enter SIP number",
|
||||
"passwordCheck": "Are you sure you would like to remove your password?",
|
||||
"Remove": "Remove",
|
||||
"passwordMsg": "Set a password to lock your room",
|
||||
"Invite": "Invite",
|
||||
"shareLink": "Share this link with everyone you want to invite",
|
||||
"settings1": "Configure your conference",
|
||||
"settings2": "Participants join muted",
|
||||
"settings3": "Require nicknames<br/><br/>Set a password to lock your room:",
|
||||
"yourPassword": "your password",
|
||||
"Back": "Back",
|
||||
"serviceUnavailable": "Service unavailable",
|
||||
"gracefulShutdown": "Our service is currently down for maintenance. Please try again later.",
|
||||
"Yes": "Yes",
|
||||
"reservationError": "Reservation system error",
|
||||
"reservationErrorMsg": "Error code: __code__, message: __msg__",
|
||||
"password": "password",
|
||||
"userPassword": "user password",
|
||||
"token": "token"
|
||||
},
|
||||
"email":
|
||||
{
|
||||
"sharedKey": [
|
||||
"This conference is password protected. Please use the following pin when joining:",
|
||||
"",
|
||||
"",
|
||||
"__sharedKey__",
|
||||
"",
|
||||
""],
|
||||
"subject": "Invitation to a __appName__ (__conferenceName__)",
|
||||
"body": [
|
||||
"Hey there, I%27d like to invite you to a __appName__ conference I%27ve just set up.",
|
||||
"",
|
||||
"",
|
||||
"Please click on the following link in order to join the conference.",
|
||||
"",
|
||||
"",
|
||||
"__roomUrl__",
|
||||
"",
|
||||
"",
|
||||
"__sharedKeyText__",
|
||||
" Note that __appName__ is currently only supported by __supportedBrowsers__, so you need to be using one of these browsers.",
|
||||
"",
|
||||
"",
|
||||
"Talk to you in a sec!"
|
||||
],
|
||||
"and": "and"
|
||||
}
|
||||
}
|
||||
26842
libs/app.bundle.js
Normal file
@@ -1,94 +0,0 @@
|
||||
/* colibri.js -- a COLIBRI focus
|
||||
* The colibri spec has been submitted to the XMPP Standards Foundation
|
||||
* for publications as a XMPP extensions:
|
||||
* http://xmpp.org/extensions/inbox/colibri.html
|
||||
*
|
||||
* colibri.js is a participating focus, i.e. the focus participates
|
||||
* in the conference. The conference itself can be ad-hoc, through a
|
||||
* MUC, through PubSub, etc.
|
||||
*
|
||||
* colibri.js relies heavily on the strophe.jingle library available
|
||||
* from https://github.com/ESTOS/strophe.jingle
|
||||
* and interoperates with the Jitsi videobridge available from
|
||||
* https://jitsi.org/Projects/JitsiVideobridge
|
||||
*/
|
||||
/*
|
||||
Copyright (c) 2013 ESTOS GmbH
|
||||
|
||||
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.
|
||||
*/
|
||||
// A colibri session is similar to a jingle session, it just implements some things differently
|
||||
// FIXME: inherit jinglesession, see https://github.com/legastero/Jingle-RTCPeerConnection/blob/master/index.js
|
||||
function ColibriSession(me, sid, connection) {
|
||||
this.me = me;
|
||||
this.sid = sid;
|
||||
this.connection = connection;
|
||||
//this.peerconnection = null;
|
||||
//this.mychannel = null;
|
||||
//this.channels = null;
|
||||
this.peerjid = null;
|
||||
|
||||
this.colibri = null;
|
||||
}
|
||||
|
||||
// implementation of JingleSession interface
|
||||
ColibriSession.prototype.initiate = function (peerjid, isInitiator) {
|
||||
this.peerjid = peerjid;
|
||||
};
|
||||
|
||||
ColibriSession.prototype.sendOffer = function (offer) {
|
||||
console.log('ColibriSession.sendOffer');
|
||||
};
|
||||
|
||||
|
||||
ColibriSession.prototype.accept = function () {
|
||||
console.log('ColibriSession.accept');
|
||||
};
|
||||
|
||||
ColibriSession.prototype.addSource = function (elem, fromJid) {
|
||||
this.colibri.addSource(elem, fromJid);
|
||||
};
|
||||
|
||||
ColibriSession.prototype.removeSource = function (elem, fromJid) {
|
||||
this.colibri.removeSource(elem, fromJid);
|
||||
};
|
||||
|
||||
ColibriSession.prototype.terminate = function (reason) {
|
||||
this.colibri.terminate(this, reason);
|
||||
};
|
||||
|
||||
ColibriSession.prototype.active = function () {
|
||||
console.log('ColibriSession.active');
|
||||
};
|
||||
|
||||
ColibriSession.prototype.setRemoteDescription = function (elem, desctype) {
|
||||
this.colibri.setRemoteDescription(this, elem, desctype);
|
||||
};
|
||||
|
||||
ColibriSession.prototype.addIceCandidate = function (elem) {
|
||||
this.colibri.addIceCandidate(this, elem);
|
||||
};
|
||||
|
||||
ColibriSession.prototype.sendAnswer = function (sdp, provisional) {
|
||||
console.log('ColibriSession.sendAnswer');
|
||||
};
|
||||
|
||||
ColibriSession.prototype.sendTerminate = function (reason, text) {
|
||||
this.colibri.sendTerminate(this, reason, text);
|
||||
};
|
||||
94
libs/rayo.js
@@ -1,94 +0,0 @@
|
||||
/* jshint -W117 */
|
||||
Strophe.addConnectionPlugin('rayo',
|
||||
{
|
||||
RAYO_XMLNS: 'urn:xmpp:rayo:1',
|
||||
connection: null,
|
||||
init: function (conn)
|
||||
{
|
||||
this.connection = conn;
|
||||
if (this.connection.disco)
|
||||
{
|
||||
this.connection.disco.addFeature('urn:xmpp:rayo:client:1');
|
||||
}
|
||||
|
||||
this.connection.addHandler(
|
||||
this.onRayo.bind(this), this.RAYO_XMLNS, 'iq', 'set', null, null);
|
||||
},
|
||||
onRayo: function (iq)
|
||||
{
|
||||
console.info("Rayo IQ", iq);
|
||||
},
|
||||
dial: function (to, from, roomName)
|
||||
{
|
||||
var self = this;
|
||||
var req = $iq(
|
||||
{
|
||||
type: 'set',
|
||||
to: config.hosts.call_control
|
||||
}
|
||||
);
|
||||
req.c('dial',
|
||||
{
|
||||
xmlns: this.RAYO_XMLNS,
|
||||
to: to,
|
||||
from: from
|
||||
});
|
||||
req.c('header',
|
||||
{
|
||||
name: 'JvbRoomName',
|
||||
value: roomName
|
||||
});
|
||||
|
||||
this.connection.sendIQ(
|
||||
req,
|
||||
function (result)
|
||||
{
|
||||
console.info('Dial result ', result);
|
||||
|
||||
var resource = $(result).find('ref').attr('uri');
|
||||
this.call_resource = resource.substr('xmpp:'.length);
|
||||
console.info(
|
||||
"Received call resource: " + this.call_resource);
|
||||
},
|
||||
function (error)
|
||||
{
|
||||
console.info('Dial error ', error);
|
||||
}
|
||||
);
|
||||
},
|
||||
hang_up: function ()
|
||||
{
|
||||
if (!this.call_resource)
|
||||
{
|
||||
console.warn("No call in progress");
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
var req = $iq(
|
||||
{
|
||||
type: 'set',
|
||||
to: this.call_resource
|
||||
}
|
||||
);
|
||||
req.c('hangup',
|
||||
{
|
||||
xmlns: this.RAYO_XMLNS
|
||||
});
|
||||
|
||||
this.connection.sendIQ(
|
||||
req,
|
||||
function (result)
|
||||
{
|
||||
console.info('Hangup result ', result);
|
||||
self.call_resource = null;
|
||||
},
|
||||
function (error)
|
||||
{
|
||||
console.info('Hangup error ', error);
|
||||
self.call_resource = null;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -1,702 +0,0 @@
|
||||
function TraceablePeerConnection(ice_config, constraints) {
|
||||
var self = this;
|
||||
var RTCPeerconnection = navigator.mozGetUserMedia ? mozRTCPeerConnection : webkitRTCPeerConnection;
|
||||
this.peerconnection = new RTCPeerconnection(ice_config, constraints);
|
||||
this.updateLog = [];
|
||||
this.stats = {};
|
||||
this.statsinterval = null;
|
||||
this.maxstats = 0; // limit to 300 values, i.e. 5 minutes; set to 0 to disable
|
||||
|
||||
/**
|
||||
* Array of ssrcs that will be added on next modifySources call.
|
||||
* @type {Array}
|
||||
*/
|
||||
this.addssrc = [];
|
||||
/**
|
||||
* Array of ssrcs that will be added on next modifySources call.
|
||||
* @type {Array}
|
||||
*/
|
||||
this.removessrc = [];
|
||||
/**
|
||||
* Pending operation that will be done during modifySources call.
|
||||
* Currently 'mute'/'unmute' operations are supported.
|
||||
*
|
||||
* @type {String}
|
||||
*/
|
||||
this.pendingop = null;
|
||||
|
||||
/**
|
||||
* Flag indicates that peer connection stream have changed and modifySources should proceed.
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.switchstreams = false;
|
||||
|
||||
// override as desired
|
||||
this.trace = function (what, info) {
|
||||
//console.warn('WTRACE', what, info);
|
||||
self.updateLog.push({
|
||||
time: new Date(),
|
||||
type: what,
|
||||
value: info || ""
|
||||
});
|
||||
};
|
||||
this.onicecandidate = null;
|
||||
this.peerconnection.onicecandidate = function (event) {
|
||||
self.trace('onicecandidate', JSON.stringify(event.candidate, null, ' '));
|
||||
if (self.onicecandidate !== null) {
|
||||
self.onicecandidate(event);
|
||||
}
|
||||
};
|
||||
this.onaddstream = null;
|
||||
this.peerconnection.onaddstream = function (event) {
|
||||
self.trace('onaddstream', event.stream.id);
|
||||
if (self.onaddstream !== null) {
|
||||
self.onaddstream(event);
|
||||
}
|
||||
};
|
||||
this.onremovestream = null;
|
||||
this.peerconnection.onremovestream = function (event) {
|
||||
self.trace('onremovestream', event.stream.id);
|
||||
if (self.onremovestream !== null) {
|
||||
self.onremovestream(event);
|
||||
}
|
||||
};
|
||||
this.onsignalingstatechange = null;
|
||||
this.peerconnection.onsignalingstatechange = function (event) {
|
||||
self.trace('onsignalingstatechange', self.signalingState);
|
||||
if (self.onsignalingstatechange !== null) {
|
||||
self.onsignalingstatechange(event);
|
||||
}
|
||||
};
|
||||
this.oniceconnectionstatechange = null;
|
||||
this.peerconnection.oniceconnectionstatechange = function (event) {
|
||||
self.trace('oniceconnectionstatechange', self.iceConnectionState);
|
||||
if (self.oniceconnectionstatechange !== null) {
|
||||
self.oniceconnectionstatechange(event);
|
||||
}
|
||||
};
|
||||
this.onnegotiationneeded = null;
|
||||
this.peerconnection.onnegotiationneeded = function (event) {
|
||||
self.trace('onnegotiationneeded');
|
||||
if (self.onnegotiationneeded !== null) {
|
||||
self.onnegotiationneeded(event);
|
||||
}
|
||||
};
|
||||
self.ondatachannel = null;
|
||||
this.peerconnection.ondatachannel = function (event) {
|
||||
self.trace('ondatachannel', event);
|
||||
if (self.ondatachannel !== null) {
|
||||
self.ondatachannel(event);
|
||||
}
|
||||
};
|
||||
if (!navigator.mozGetUserMedia && this.maxstats) {
|
||||
this.statsinterval = window.setInterval(function() {
|
||||
self.peerconnection.getStats(function(stats) {
|
||||
var results = stats.result();
|
||||
for (var i = 0; i < results.length; ++i) {
|
||||
//console.log(results[i].type, results[i].id, results[i].names())
|
||||
var now = new Date();
|
||||
results[i].names().forEach(function (name) {
|
||||
var id = results[i].id + '-' + name;
|
||||
if (!self.stats[id]) {
|
||||
self.stats[id] = {
|
||||
startTime: now,
|
||||
endTime: now,
|
||||
values: [],
|
||||
times: []
|
||||
};
|
||||
}
|
||||
self.stats[id].values.push(results[i].stat(name));
|
||||
self.stats[id].times.push(now.getTime());
|
||||
if (self.stats[id].values.length > self.maxstats) {
|
||||
self.stats[id].values.shift();
|
||||
self.stats[id].times.shift();
|
||||
}
|
||||
self.stats[id].endTime = now;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
}, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
dumpSDP = function(description) {
|
||||
return 'type: ' + description.type + '\r\n' + description.sdp;
|
||||
}
|
||||
|
||||
if (TraceablePeerConnection.prototype.__defineGetter__ !== undefined) {
|
||||
TraceablePeerConnection.prototype.__defineGetter__('signalingState', function() { return this.peerconnection.signalingState; });
|
||||
TraceablePeerConnection.prototype.__defineGetter__('iceConnectionState', function() { return this.peerconnection.iceConnectionState; });
|
||||
TraceablePeerConnection.prototype.__defineGetter__('localDescription', function() {
|
||||
var publicLocalDescription = simulcast.reverseTransformLocalDescription(this.peerconnection.localDescription);
|
||||
return publicLocalDescription;
|
||||
});
|
||||
TraceablePeerConnection.prototype.__defineGetter__('remoteDescription', function() {
|
||||
var publicRemoteDescription = simulcast.reverseTransformRemoteDescription(this.peerconnection.remoteDescription);
|
||||
return publicRemoteDescription;
|
||||
});
|
||||
}
|
||||
|
||||
TraceablePeerConnection.prototype.addStream = function (stream) {
|
||||
this.trace('addStream', stream.id);
|
||||
this.peerconnection.addStream(stream);
|
||||
};
|
||||
|
||||
TraceablePeerConnection.prototype.removeStream = function (stream) {
|
||||
this.trace('removeStream', stream.id);
|
||||
this.peerconnection.removeStream(stream);
|
||||
};
|
||||
|
||||
TraceablePeerConnection.prototype.createDataChannel = function (label, opts) {
|
||||
this.trace('createDataChannel', label, opts);
|
||||
return this.peerconnection.createDataChannel(label, opts);
|
||||
};
|
||||
|
||||
TraceablePeerConnection.prototype.setLocalDescription = function (description, successCallback, failureCallback) {
|
||||
var self = this;
|
||||
description = simulcast.transformLocalDescription(description);
|
||||
this.trace('setLocalDescription', dumpSDP(description));
|
||||
this.peerconnection.setLocalDescription(description,
|
||||
function () {
|
||||
self.trace('setLocalDescriptionOnSuccess');
|
||||
successCallback();
|
||||
},
|
||||
function (err) {
|
||||
self.trace('setLocalDescriptionOnFailure', err);
|
||||
failureCallback(err);
|
||||
}
|
||||
);
|
||||
/*
|
||||
if (this.statsinterval === null && this.maxstats > 0) {
|
||||
// start gathering stats
|
||||
}
|
||||
*/
|
||||
};
|
||||
|
||||
TraceablePeerConnection.prototype.setRemoteDescription = function (description, successCallback, failureCallback) {
|
||||
var self = this;
|
||||
description = simulcast.transformRemoteDescription(description);
|
||||
this.trace('setRemoteDescription', dumpSDP(description));
|
||||
this.peerconnection.setRemoteDescription(description,
|
||||
function () {
|
||||
self.trace('setRemoteDescriptionOnSuccess');
|
||||
successCallback();
|
||||
},
|
||||
function (err) {
|
||||
self.trace('setRemoteDescriptionOnFailure', err);
|
||||
failureCallback(err);
|
||||
}
|
||||
);
|
||||
/*
|
||||
if (this.statsinterval === null && this.maxstats > 0) {
|
||||
// start gathering stats
|
||||
}
|
||||
*/
|
||||
};
|
||||
|
||||
TraceablePeerConnection.prototype.hardMuteVideo = function (muted) {
|
||||
this.pendingop = muted ? 'mute' : 'unmute';
|
||||
};
|
||||
|
||||
TraceablePeerConnection.prototype.enqueueAddSsrc = function(channel, ssrcLines) {
|
||||
if (!this.addssrc[channel]) {
|
||||
this.addssrc[channel] = '';
|
||||
}
|
||||
this.addssrc[channel] += ssrcLines;
|
||||
}
|
||||
|
||||
TraceablePeerConnection.prototype.addSource = function (elem) {
|
||||
console.log('addssrc', new Date().getTime());
|
||||
console.log('ice', this.iceConnectionState);
|
||||
var sdp = new SDP(this.remoteDescription.sdp);
|
||||
var mySdp = new SDP(this.peerconnection.localDescription.sdp);
|
||||
|
||||
var self = this;
|
||||
$(elem).each(function (idx, content) {
|
||||
var name = $(content).attr('name');
|
||||
var lines = '';
|
||||
tmp = $(content).find('ssrc-group[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]').each(function() {
|
||||
var semantics = this.getAttribute('semantics');
|
||||
var ssrcs = $(this).find('>source').map(function () {
|
||||
return this.getAttribute('ssrc');
|
||||
}).get();
|
||||
|
||||
if (ssrcs.length != 0) {
|
||||
lines += 'a=ssrc-group:' + semantics + ' ' + ssrcs.join(' ') + '\r\n';
|
||||
}
|
||||
});
|
||||
tmp = $(content).find('source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]'); // can handle both >source and >description>source
|
||||
tmp.each(function () {
|
||||
var ssrc = $(this).attr('ssrc');
|
||||
if(mySdp.containsSSRC(ssrc)){
|
||||
/**
|
||||
* This happens when multiple participants change their streams at the same time and
|
||||
* ColibriFocus.modifySources have to wait for stable state. In the meantime multiple
|
||||
* addssrc are scheduled for update IQ. See
|
||||
*/
|
||||
console.warn("Got add stream request for my own ssrc: "+ssrc);
|
||||
return;
|
||||
}
|
||||
$(this).find('>parameter').each(function () {
|
||||
lines += 'a=ssrc:' + ssrc + ' ' + $(this).attr('name');
|
||||
if ($(this).attr('value') && $(this).attr('value').length)
|
||||
lines += ':' + $(this).attr('value');
|
||||
lines += '\r\n';
|
||||
});
|
||||
});
|
||||
sdp.media.forEach(function(media, idx) {
|
||||
if (!SDPUtil.find_line(media, 'a=mid:' + name))
|
||||
return;
|
||||
sdp.media[idx] += lines;
|
||||
self.enqueueAddSsrc(idx, lines);
|
||||
});
|
||||
sdp.raw = sdp.session + sdp.media.join('');
|
||||
});
|
||||
};
|
||||
|
||||
TraceablePeerConnection.prototype.enqueueRemoveSsrc = function(channel, ssrcLines) {
|
||||
if (!this.removessrc[channel]){
|
||||
this.removessrc[channel] = '';
|
||||
}
|
||||
this.removessrc[channel] += ssrcLines;
|
||||
}
|
||||
|
||||
TraceablePeerConnection.prototype.removeSource = function (elem) {
|
||||
console.log('removessrc', new Date().getTime());
|
||||
console.log('ice', this.iceConnectionState);
|
||||
var sdp = new SDP(this.remoteDescription.sdp);
|
||||
var mySdp = new SDP(this.peerconnection.localDescription.sdp);
|
||||
|
||||
var self = this;
|
||||
$(elem).each(function (idx, content) {
|
||||
var name = $(content).attr('name');
|
||||
var lines = '';
|
||||
tmp = $(content).find('ssrc-group[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]').each(function() {
|
||||
var semantics = this.getAttribute('semantics');
|
||||
var ssrcs = $(this).find('>source').map(function () {
|
||||
return this.getAttribute('ssrc');
|
||||
}).get();
|
||||
|
||||
if (ssrcs.length != 0) {
|
||||
lines += 'a=ssrc-group:' + semantics + ' ' + ssrcs.join(' ') + '\r\n';
|
||||
}
|
||||
});
|
||||
tmp = $(content).find('source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]'); // can handle both >source and >description>source
|
||||
tmp.each(function () {
|
||||
var ssrc = $(this).attr('ssrc');
|
||||
// This should never happen, but can be useful for bug detection
|
||||
if(mySdp.containsSSRC(ssrc)){
|
||||
console.error("Got remove stream request for my own ssrc: "+ssrc);
|
||||
return;
|
||||
}
|
||||
$(this).find('>parameter').each(function () {
|
||||
lines += 'a=ssrc:' + ssrc + ' ' + $(this).attr('name');
|
||||
if ($(this).attr('value') && $(this).attr('value').length)
|
||||
lines += ':' + $(this).attr('value');
|
||||
lines += '\r\n';
|
||||
});
|
||||
});
|
||||
sdp.media.forEach(function(media, idx) {
|
||||
if (!SDPUtil.find_line(media, 'a=mid:' + name))
|
||||
return;
|
||||
sdp.media[idx] += lines;
|
||||
self.enqueueRemoveSsrc(idx, lines);
|
||||
});
|
||||
sdp.raw = sdp.session + sdp.media.join('');
|
||||
});
|
||||
};
|
||||
|
||||
TraceablePeerConnection.prototype.modifySources = function(successCallback) {
|
||||
var self = this;
|
||||
if (this.signalingState == 'closed') return;
|
||||
if (!(this.addssrc.length || this.removessrc.length || this.pendingop !== null || this.switchstreams)){
|
||||
// There is nothing to do since scheduled job might have been executed by another succeeding call
|
||||
if(successCallback){
|
||||
successCallback();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// FIXME: this is a big hack
|
||||
// https://code.google.com/p/webrtc/issues/detail?id=2688
|
||||
if (!(this.signalingState == 'stable' && this.iceConnectionState == 'connected')) {
|
||||
console.warn('modifySources not yet', this.signalingState, this.iceConnectionState);
|
||||
this.wait = true;
|
||||
window.setTimeout(function() { self.modifySources(successCallback); }, 250);
|
||||
return;
|
||||
}
|
||||
if (this.wait) {
|
||||
window.setTimeout(function() { self.modifySources(successCallback); }, 2500);
|
||||
this.wait = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset switch streams flag
|
||||
this.switchstreams = false;
|
||||
|
||||
var sdp = new SDP(this.remoteDescription.sdp);
|
||||
|
||||
// add sources
|
||||
this.addssrc.forEach(function(lines, idx) {
|
||||
sdp.media[idx] += lines;
|
||||
});
|
||||
this.addssrc = [];
|
||||
|
||||
// remove sources
|
||||
this.removessrc.forEach(function(lines, idx) {
|
||||
lines = lines.split('\r\n');
|
||||
lines.pop(); // remove empty last element;
|
||||
lines.forEach(function(line) {
|
||||
sdp.media[idx] = sdp.media[idx].replace(line + '\r\n', '');
|
||||
});
|
||||
});
|
||||
this.removessrc = [];
|
||||
|
||||
sdp.raw = sdp.session + sdp.media.join('');
|
||||
this.setRemoteDescription(new RTCSessionDescription({type: 'offer', sdp: sdp.raw}),
|
||||
function() {
|
||||
|
||||
if(self.signalingState == 'closed') {
|
||||
console.error("createAnswer attempt on closed state");
|
||||
return;
|
||||
}
|
||||
|
||||
self.createAnswer(
|
||||
function(modifiedAnswer) {
|
||||
// change video direction, see https://github.com/jitsi/jitmeet/issues/41
|
||||
if (self.pendingop !== null) {
|
||||
var sdp = new SDP(modifiedAnswer.sdp);
|
||||
if (sdp.media.length > 1) {
|
||||
switch(self.pendingop) {
|
||||
case 'mute':
|
||||
sdp.media[1] = sdp.media[1].replace('a=sendrecv', 'a=recvonly');
|
||||
break;
|
||||
case 'unmute':
|
||||
sdp.media[1] = sdp.media[1].replace('a=recvonly', 'a=sendrecv');
|
||||
break;
|
||||
}
|
||||
sdp.raw = sdp.session + sdp.media.join('');
|
||||
modifiedAnswer.sdp = sdp.raw;
|
||||
}
|
||||
self.pendingop = null;
|
||||
}
|
||||
|
||||
// FIXME: pushing down an answer while ice connection state
|
||||
// is still checking is bad...
|
||||
//console.log(self.peerconnection.iceConnectionState);
|
||||
|
||||
// trying to work around another chrome bug
|
||||
//modifiedAnswer.sdp = modifiedAnswer.sdp.replace(/a=setup:active/g, 'a=setup:actpass');
|
||||
self.setLocalDescription(modifiedAnswer,
|
||||
function() {
|
||||
//console.log('modified setLocalDescription ok');
|
||||
if(successCallback){
|
||||
successCallback();
|
||||
}
|
||||
},
|
||||
function(error) {
|
||||
console.error('modified setLocalDescription failed', error);
|
||||
}
|
||||
);
|
||||
},
|
||||
function(error) {
|
||||
console.error('modified answer failed', error);
|
||||
}
|
||||
);
|
||||
},
|
||||
function(error) {
|
||||
console.error('modify failed', error);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
TraceablePeerConnection.prototype.close = function () {
|
||||
this.trace('stop');
|
||||
if (this.statsinterval !== null) {
|
||||
window.clearInterval(this.statsinterval);
|
||||
this.statsinterval = null;
|
||||
}
|
||||
this.peerconnection.close();
|
||||
};
|
||||
|
||||
TraceablePeerConnection.prototype.createOffer = function (successCallback, failureCallback, constraints) {
|
||||
var self = this;
|
||||
this.trace('createOffer', JSON.stringify(constraints, null, ' '));
|
||||
this.peerconnection.createOffer(
|
||||
function (offer) {
|
||||
self.trace('createOfferOnSuccess', dumpSDP(offer));
|
||||
successCallback(offer);
|
||||
},
|
||||
function(err) {
|
||||
self.trace('createOfferOnFailure', err);
|
||||
failureCallback(err);
|
||||
},
|
||||
constraints
|
||||
);
|
||||
};
|
||||
|
||||
TraceablePeerConnection.prototype.createAnswer = function (successCallback, failureCallback, constraints) {
|
||||
var self = this;
|
||||
this.trace('createAnswer', JSON.stringify(constraints, null, ' '));
|
||||
this.peerconnection.createAnswer(
|
||||
function (answer) {
|
||||
answer = simulcast.transformAnswer(answer);
|
||||
self.trace('createAnswerOnSuccess', dumpSDP(answer));
|
||||
successCallback(answer);
|
||||
},
|
||||
function(err) {
|
||||
self.trace('createAnswerOnFailure', err);
|
||||
failureCallback(err);
|
||||
},
|
||||
constraints
|
||||
);
|
||||
};
|
||||
|
||||
TraceablePeerConnection.prototype.addIceCandidate = function (candidate, successCallback, failureCallback) {
|
||||
var self = this;
|
||||
this.trace('addIceCandidate', JSON.stringify(candidate, null, ' '));
|
||||
this.peerconnection.addIceCandidate(candidate);
|
||||
/* maybe later
|
||||
this.peerconnection.addIceCandidate(candidate,
|
||||
function () {
|
||||
self.trace('addIceCandidateOnSuccess');
|
||||
successCallback();
|
||||
},
|
||||
function (err) {
|
||||
self.trace('addIceCandidateOnFailure', err);
|
||||
failureCallback(err);
|
||||
}
|
||||
);
|
||||
*/
|
||||
};
|
||||
|
||||
TraceablePeerConnection.prototype.getStats = function(callback, errback) {
|
||||
if (navigator.mozGetUserMedia) {
|
||||
// ignore for now...
|
||||
} else {
|
||||
this.peerconnection.getStats(callback);
|
||||
}
|
||||
};
|
||||
|
||||
// mozilla chrome compat layer -- very similar to adapter.js
|
||||
function setupRTC() {
|
||||
var RTC = null;
|
||||
if (navigator.mozGetUserMedia) {
|
||||
console.log('This appears to be Firefox');
|
||||
var version = parseInt(navigator.userAgent.match(/Firefox\/([0-9]+)\./)[1], 10);
|
||||
if (version >= 22) {
|
||||
RTC = {
|
||||
peerconnection: mozRTCPeerConnection,
|
||||
browser: 'firefox',
|
||||
getUserMedia: navigator.mozGetUserMedia.bind(navigator),
|
||||
attachMediaStream: function (element, stream) {
|
||||
element[0].mozSrcObject = stream;
|
||||
element[0].play();
|
||||
},
|
||||
pc_constraints: {}
|
||||
};
|
||||
if (!MediaStream.prototype.getVideoTracks)
|
||||
MediaStream.prototype.getVideoTracks = function () { return []; };
|
||||
if (!MediaStream.prototype.getAudioTracks)
|
||||
MediaStream.prototype.getAudioTracks = function () { return []; };
|
||||
RTCSessionDescription = mozRTCSessionDescription;
|
||||
RTCIceCandidate = mozRTCIceCandidate;
|
||||
}
|
||||
} else if (navigator.webkitGetUserMedia) {
|
||||
console.log('This appears to be Chrome');
|
||||
RTC = {
|
||||
peerconnection: webkitRTCPeerConnection,
|
||||
browser: 'chrome',
|
||||
getUserMedia: navigator.webkitGetUserMedia.bind(navigator),
|
||||
attachMediaStream: function (element, stream) {
|
||||
element.attr('src', webkitURL.createObjectURL(stream));
|
||||
},
|
||||
// DTLS should now be enabled by default but..
|
||||
pc_constraints: {'optional': [{'DtlsSrtpKeyAgreement': 'true'}]}
|
||||
};
|
||||
if (navigator.userAgent.indexOf('Android') != -1) {
|
||||
RTC.pc_constraints = {}; // disable DTLS on Android
|
||||
}
|
||||
if (!webkitMediaStream.prototype.getVideoTracks) {
|
||||
webkitMediaStream.prototype.getVideoTracks = function () {
|
||||
return this.videoTracks;
|
||||
};
|
||||
}
|
||||
if (!webkitMediaStream.prototype.getAudioTracks) {
|
||||
webkitMediaStream.prototype.getAudioTracks = function () {
|
||||
return this.audioTracks;
|
||||
};
|
||||
}
|
||||
}
|
||||
if (RTC === null) {
|
||||
try { console.log('Browser does not appear to be WebRTC-capable'); } catch (e) { }
|
||||
}
|
||||
return RTC;
|
||||
}
|
||||
|
||||
function getUserMediaWithConstraints(um, success_callback, failure_callback, resolution, bandwidth, fps, desktopStream) {
|
||||
var constraints = {audio: false, video: false};
|
||||
|
||||
if (um.indexOf('video') >= 0) {
|
||||
constraints.video = { mandatory: {}, optional: [] };// same behaviour as true
|
||||
}
|
||||
if (um.indexOf('audio') >= 0) {
|
||||
constraints.audio = { mandatory: {}, optional: []};// same behaviour as true
|
||||
}
|
||||
if (um.indexOf('screen') >= 0) {
|
||||
constraints.video = {
|
||||
mandatory: {
|
||||
chromeMediaSource: "screen",
|
||||
googLeakyBucket: true,
|
||||
maxWidth: window.screen.width,
|
||||
maxHeight: window.screen.height,
|
||||
maxFrameRate: 3
|
||||
},
|
||||
optional: []
|
||||
};
|
||||
}
|
||||
if (um.indexOf('desktop') >= 0) {
|
||||
constraints.video = {
|
||||
mandatory: {
|
||||
chromeMediaSource: "desktop",
|
||||
chromeMediaSourceId: desktopStream,
|
||||
googLeakyBucket: true,
|
||||
maxWidth: window.screen.width,
|
||||
maxHeight: window.screen.height,
|
||||
maxFrameRate: 3
|
||||
},
|
||||
optional: []
|
||||
}
|
||||
}
|
||||
|
||||
if (constraints.audio) {
|
||||
// if it is good enough for hangouts...
|
||||
constraints.audio.optional.push(
|
||||
{googEchoCancellation: true},
|
||||
{googAutoGainControl: true},
|
||||
{googNoiseSupression: true},
|
||||
{googHighpassFilter: true},
|
||||
{googNoisesuppression2: true},
|
||||
{googEchoCancellation2: true},
|
||||
{googAutoGainControl2: true}
|
||||
);
|
||||
}
|
||||
if (constraints.video) {
|
||||
constraints.video.optional.push(
|
||||
{googNoiseReduction: false} // chrome 37 workaround for issue 3807, reenable in M38
|
||||
);
|
||||
if (um.indexOf('video') >= 0) {
|
||||
constraints.video.optional.push(
|
||||
{googLeakyBucket: true}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we are running on Android device
|
||||
var isAndroid = navigator.userAgent.indexOf('Android') != -1;
|
||||
|
||||
if (resolution && !constraints.video || isAndroid) {
|
||||
constraints.video = { mandatory: {}, optional: [] };// same behaviour as true
|
||||
}
|
||||
// see https://code.google.com/p/chromium/issues/detail?id=143631#c9 for list of supported resolutions
|
||||
switch (resolution) {
|
||||
// 16:9 first
|
||||
case '1080':
|
||||
case 'fullhd':
|
||||
constraints.video.mandatory.minWidth = 1920;
|
||||
constraints.video.mandatory.minHeight = 1080;
|
||||
break;
|
||||
case '720':
|
||||
case 'hd':
|
||||
constraints.video.mandatory.minWidth = 1280;
|
||||
constraints.video.mandatory.minHeight = 720;
|
||||
break;
|
||||
case '360':
|
||||
constraints.video.mandatory.minWidth = 640;
|
||||
constraints.video.mandatory.minHeight = 360;
|
||||
break;
|
||||
case '180':
|
||||
constraints.video.mandatory.minWidth = 320;
|
||||
constraints.video.mandatory.minHeight = 180;
|
||||
break;
|
||||
// 4:3
|
||||
case '960':
|
||||
constraints.video.mandatory.minWidth = 960;
|
||||
constraints.video.mandatory.minHeight = 720;
|
||||
break;
|
||||
case '640':
|
||||
case 'vga':
|
||||
constraints.video.mandatory.minWidth = 640;
|
||||
constraints.video.mandatory.minHeight = 480;
|
||||
break;
|
||||
case '320':
|
||||
constraints.video.mandatory.minWidth = 320;
|
||||
constraints.video.mandatory.minHeight = 240;
|
||||
break;
|
||||
default:
|
||||
if (isAndroid) {
|
||||
constraints.video.mandatory.minWidth = 320;
|
||||
constraints.video.mandatory.minHeight = 240;
|
||||
constraints.video.mandatory.maxFrameRate = 15;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (constraints.video.mandatory.minWidth)
|
||||
constraints.video.mandatory.maxWidth = constraints.video.mandatory.minWidth;
|
||||
if (constraints.video.mandatory.minHeight)
|
||||
constraints.video.mandatory.maxHeight = constraints.video.mandatory.minHeight;
|
||||
|
||||
if (bandwidth) { // doesn't work currently, see webrtc issue 1846
|
||||
if (!constraints.video) constraints.video = {mandatory: {}, optional: []};//same behaviour as true
|
||||
constraints.video.optional.push({bandwidth: bandwidth});
|
||||
}
|
||||
if (fps) { // for some cameras it might be necessary to request 30fps
|
||||
// so they choose 30fps mjpg over 10fps yuy2
|
||||
if (!constraints.video) constraints.video = {mandatory: {}, optional: []};// same behaviour as true;
|
||||
constraints.video.mandatory.minFrameRate = fps;
|
||||
}
|
||||
|
||||
var isFF = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
|
||||
|
||||
try {
|
||||
if (config.enableSimulcast
|
||||
&& constraints.video
|
||||
&& constraints.video.chromeMediaSource !== 'screen'
|
||||
&& constraints.video.chromeMediaSource !== 'desktop'
|
||||
&& !isAndroid
|
||||
|
||||
// We currently do not support FF, as it doesn't have multistream support.
|
||||
&& !isFF) {
|
||||
simulcast.getUserMedia(constraints, function (stream) {
|
||||
console.log('onUserMediaSuccess');
|
||||
success_callback(stream);
|
||||
},
|
||||
function (error) {
|
||||
console.warn('Failed to get access to local media. Error ', error);
|
||||
if (failure_callback) {
|
||||
failure_callback(error);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
||||
RTC.getUserMedia(constraints,
|
||||
function (stream) {
|
||||
console.log('onUserMediaSuccess');
|
||||
success_callback(stream);
|
||||
},
|
||||
function (error) {
|
||||
console.warn('Failed to get access to local media. Error ', error);
|
||||
if (failure_callback) {
|
||||
failure_callback(error);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('GUM failed: ', e);
|
||||
if(failure_callback) {
|
||||
failure_callback(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,298 +0,0 @@
|
||||
/* jshint -W117 */
|
||||
Strophe.addConnectionPlugin('jingle', {
|
||||
connection: null,
|
||||
sessions: {},
|
||||
jid2session: {},
|
||||
ice_config: {iceServers: []},
|
||||
pc_constraints: {},
|
||||
media_constraints: {
|
||||
mandatory: {
|
||||
'OfferToReceiveAudio': true,
|
||||
'OfferToReceiveVideo': true
|
||||
}
|
||||
// MozDontOfferDataChannel: true when this is firefox
|
||||
},
|
||||
localAudio: null,
|
||||
localVideo: null,
|
||||
|
||||
init: function (conn) {
|
||||
this.connection = conn;
|
||||
if (this.connection.disco) {
|
||||
// http://xmpp.org/extensions/xep-0167.html#support
|
||||
// http://xmpp.org/extensions/xep-0176.html#support
|
||||
this.connection.disco.addFeature('urn:xmpp:jingle:1');
|
||||
this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:1');
|
||||
this.connection.disco.addFeature('urn:xmpp:jingle:transports:ice-udp:1');
|
||||
this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:audio');
|
||||
this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:video');
|
||||
|
||||
|
||||
// this is dealt with by SDP O/A so we don't need to annouce this
|
||||
//this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:rtcp-fb:0'); // XEP-0293
|
||||
//this.connection.disco.addFeature('urn:xmpp:jingle:apps:rtp:rtp-hdrext:0'); // XEP-0294
|
||||
this.connection.disco.addFeature('urn:ietf:rfc:5761'); // rtcp-mux
|
||||
//this.connection.disco.addFeature('urn:ietf:rfc:5888'); // a=group, e.g. bundle
|
||||
//this.connection.disco.addFeature('urn:ietf:rfc:5576'); // a=ssrc
|
||||
}
|
||||
this.connection.addHandler(this.onJingle.bind(this), 'urn:xmpp:jingle:1', 'iq', 'set', null, null);
|
||||
},
|
||||
onJingle: function (iq) {
|
||||
var sid = $(iq).find('jingle').attr('sid');
|
||||
var action = $(iq).find('jingle').attr('action');
|
||||
var fromJid = iq.getAttribute('from');
|
||||
// send ack first
|
||||
var ack = $iq({type: 'result',
|
||||
to: fromJid,
|
||||
id: iq.getAttribute('id')
|
||||
});
|
||||
console.log('on jingle ' + action + ' from ' + fromJid, iq);
|
||||
var sess = this.sessions[sid];
|
||||
if ('session-initiate' != action) {
|
||||
if (sess === null) {
|
||||
ack.type = 'error';
|
||||
ack.c('error', {type: 'cancel'})
|
||||
.c('item-not-found', {xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'}).up()
|
||||
.c('unknown-session', {xmlns: 'urn:xmpp:jingle:errors:1'});
|
||||
this.connection.send(ack);
|
||||
return true;
|
||||
}
|
||||
// compare from to sess.peerjid (bare jid comparison for later compat with message-mode)
|
||||
// local jid is not checked
|
||||
if (Strophe.getBareJidFromJid(fromJid) != Strophe.getBareJidFromJid(sess.peerjid)) {
|
||||
console.warn('jid mismatch for session id', sid, fromJid, sess.peerjid);
|
||||
ack.type = 'error';
|
||||
ack.c('error', {type: 'cancel'})
|
||||
.c('item-not-found', {xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'}).up()
|
||||
.c('unknown-session', {xmlns: 'urn:xmpp:jingle:errors:1'});
|
||||
this.connection.send(ack);
|
||||
return true;
|
||||
}
|
||||
} else if (sess !== undefined) {
|
||||
// existing session with same session id
|
||||
// this might be out-of-order if the sess.peerjid is the same as from
|
||||
ack.type = 'error';
|
||||
ack.c('error', {type: 'cancel'})
|
||||
.c('service-unavailable', {xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'}).up();
|
||||
console.warn('duplicate session id', sid);
|
||||
this.connection.send(ack);
|
||||
return true;
|
||||
}
|
||||
// FIXME: check for a defined action
|
||||
this.connection.send(ack);
|
||||
// see http://xmpp.org/extensions/xep-0166.html#concepts-session
|
||||
switch (action) {
|
||||
case 'session-initiate':
|
||||
sess = new JingleSession($(iq).attr('to'), $(iq).find('jingle').attr('sid'), this.connection);
|
||||
// configure session
|
||||
if (this.localAudio) {
|
||||
sess.localStreams.push(this.localAudio);
|
||||
}
|
||||
if (this.localVideo) {
|
||||
sess.localStreams.push(this.localVideo);
|
||||
}
|
||||
sess.media_constraints = this.media_constraints;
|
||||
sess.pc_constraints = this.pc_constraints;
|
||||
sess.ice_config = this.ice_config;
|
||||
|
||||
sess.initiate(fromJid, false);
|
||||
// FIXME: setRemoteDescription should only be done when this call is to be accepted
|
||||
sess.setRemoteDescription($(iq).find('>jingle'), 'offer');
|
||||
|
||||
this.sessions[sess.sid] = sess;
|
||||
this.jid2session[sess.peerjid] = sess;
|
||||
|
||||
// the callback should either
|
||||
// .sendAnswer and .accept
|
||||
// or .sendTerminate -- not necessarily synchronus
|
||||
$(document).trigger('callincoming.jingle', [sess.sid]);
|
||||
break;
|
||||
case 'session-accept':
|
||||
sess.setRemoteDescription($(iq).find('>jingle'), 'answer');
|
||||
sess.accept();
|
||||
$(document).trigger('callaccepted.jingle', [sess.sid]);
|
||||
break;
|
||||
case 'session-terminate':
|
||||
// If this is not the focus sending the terminate, we have
|
||||
// nothing more to do here.
|
||||
if (Object.keys(this.sessions).length < 1
|
||||
|| !(this.sessions[Object.keys(this.sessions)[0]]
|
||||
instanceof JingleSession))
|
||||
{
|
||||
break;
|
||||
}
|
||||
console.log('terminating...', sess.sid);
|
||||
sess.terminate();
|
||||
this.terminate(sess.sid);
|
||||
if ($(iq).find('>jingle>reason').length) {
|
||||
$(document).trigger('callterminated.jingle', [
|
||||
sess.sid,
|
||||
sess.peerjid,
|
||||
$(iq).find('>jingle>reason>:first')[0].tagName,
|
||||
$(iq).find('>jingle>reason>text').text()
|
||||
]);
|
||||
} else {
|
||||
$(document).trigger('callterminated.jingle',
|
||||
[sess.sid, sess.peerjid]);
|
||||
}
|
||||
break;
|
||||
case 'transport-info':
|
||||
sess.addIceCandidate($(iq).find('>jingle>content'));
|
||||
break;
|
||||
case 'session-info':
|
||||
var affected;
|
||||
if ($(iq).find('>jingle>ringing[xmlns="urn:xmpp:jingle:apps:rtp:info:1"]').length) {
|
||||
$(document).trigger('ringing.jingle', [sess.sid]);
|
||||
} else if ($(iq).find('>jingle>mute[xmlns="urn:xmpp:jingle:apps:rtp:info:1"]').length) {
|
||||
affected = $(iq).find('>jingle>mute[xmlns="urn:xmpp:jingle:apps:rtp:info:1"]').attr('name');
|
||||
$(document).trigger('mute.jingle', [sess.sid, affected]);
|
||||
} else if ($(iq).find('>jingle>unmute[xmlns="urn:xmpp:jingle:apps:rtp:info:1"]').length) {
|
||||
affected = $(iq).find('>jingle>unmute[xmlns="urn:xmpp:jingle:apps:rtp:info:1"]').attr('name');
|
||||
$(document).trigger('unmute.jingle', [sess.sid, affected]);
|
||||
}
|
||||
break;
|
||||
case 'addsource': // FIXME: proprietary, un-jingleish
|
||||
case 'source-add': // FIXME: proprietary
|
||||
sess.addSource($(iq).find('>jingle>content'), fromJid);
|
||||
break;
|
||||
case 'removesource': // FIXME: proprietary, un-jingleish
|
||||
case 'source-remove': // FIXME: proprietary
|
||||
sess.removeSource($(iq).find('>jingle>content'), fromJid);
|
||||
break;
|
||||
default:
|
||||
console.warn('jingle action not implemented', action);
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
initiate: function (peerjid, myjid) { // initiate a new jinglesession to peerjid
|
||||
var sess = new JingleSession(myjid || this.connection.jid,
|
||||
Math.random().toString(36).substr(2, 12), // random string
|
||||
this.connection);
|
||||
// configure session
|
||||
if (this.localAudio) {
|
||||
sess.localStreams.push(this.localAudio);
|
||||
}
|
||||
if (this.localVideo) {
|
||||
sess.localStreams.push(this.localVideo);
|
||||
}
|
||||
sess.media_constraints = this.media_constraints;
|
||||
sess.pc_constraints = this.pc_constraints;
|
||||
sess.ice_config = this.ice_config;
|
||||
|
||||
sess.initiate(peerjid, true);
|
||||
this.sessions[sess.sid] = sess;
|
||||
this.jid2session[sess.peerjid] = sess;
|
||||
sess.sendOffer();
|
||||
return sess;
|
||||
},
|
||||
terminate: function (sid, reason, text) { // terminate by sessionid (or all sessions)
|
||||
if (sid === null || sid === undefined) {
|
||||
for (sid in this.sessions) {
|
||||
if (this.sessions[sid].state != 'ended') {
|
||||
this.sessions[sid].sendTerminate(reason || (!this.sessions[sid].active()) ? 'cancel' : null, text);
|
||||
this.sessions[sid].terminate();
|
||||
}
|
||||
delete this.jid2session[this.sessions[sid].peerjid];
|
||||
delete this.sessions[sid];
|
||||
}
|
||||
} else if (this.sessions.hasOwnProperty(sid)) {
|
||||
if (this.sessions[sid].state != 'ended') {
|
||||
this.sessions[sid].sendTerminate(reason || (!this.sessions[sid].active()) ? 'cancel' : null, text);
|
||||
this.sessions[sid].terminate();
|
||||
}
|
||||
delete this.jid2session[this.sessions[sid].peerjid];
|
||||
delete this.sessions[sid];
|
||||
}
|
||||
},
|
||||
// Used to terminate a session when an unavailable presence is received.
|
||||
terminateByJid: function (jid) {
|
||||
if (this.jid2session.hasOwnProperty(jid)) {
|
||||
var sess = this.jid2session[jid];
|
||||
if (sess) {
|
||||
sess.terminate();
|
||||
console.log('peer went away silently', jid);
|
||||
delete this.sessions[sess.sid];
|
||||
delete this.jid2session[jid];
|
||||
$(document).trigger('callterminated.jingle',
|
||||
[sess.sid, jid], 'gone');
|
||||
}
|
||||
}
|
||||
},
|
||||
terminateRemoteByJid: function (jid, reason) {
|
||||
if (this.jid2session.hasOwnProperty(jid)) {
|
||||
var sess = this.jid2session[jid];
|
||||
if (sess) {
|
||||
sess.sendTerminate(reason || (!sess.active()) ? 'kick' : null);
|
||||
sess.terminate();
|
||||
console.log('terminate peer with jid', sess.sid, jid);
|
||||
delete this.sessions[sess.sid];
|
||||
delete this.jid2session[jid];
|
||||
$(document).trigger('callterminated.jingle',
|
||||
[sess.sid, jid, 'kicked']);
|
||||
}
|
||||
}
|
||||
},
|
||||
getStunAndTurnCredentials: function () {
|
||||
// get stun and turn configuration from server via xep-0215
|
||||
// uses time-limited credentials as described in
|
||||
// http://tools.ietf.org/html/draft-uberti-behave-turn-rest-00
|
||||
//
|
||||
// see https://code.google.com/p/prosody-modules/source/browse/mod_turncredentials/mod_turncredentials.lua
|
||||
// for a prosody module which implements this
|
||||
//
|
||||
// currently, this doesn't work with updateIce and therefore credentials with a long
|
||||
// validity have to be fetched before creating the peerconnection
|
||||
// TODO: implement refresh via updateIce as described in
|
||||
// https://code.google.com/p/webrtc/issues/detail?id=1650
|
||||
var self = this;
|
||||
this.connection.sendIQ(
|
||||
$iq({type: 'get', to: this.connection.domain})
|
||||
.c('services', {xmlns: 'urn:xmpp:extdisco:1'}).c('service', {host: 'turn.' + this.connection.domain}),
|
||||
function (res) {
|
||||
var iceservers = [];
|
||||
$(res).find('>services>service').each(function (idx, el) {
|
||||
el = $(el);
|
||||
var dict = {};
|
||||
var type = el.attr('type');
|
||||
switch (type) {
|
||||
case 'stun':
|
||||
dict.url = 'stun:' + el.attr('host');
|
||||
if (el.attr('port')) {
|
||||
dict.url += ':' + el.attr('port');
|
||||
}
|
||||
iceservers.push(dict);
|
||||
break;
|
||||
case 'turn':
|
||||
case 'turns':
|
||||
dict.url = type + ':';
|
||||
if (el.attr('username')) { // https://code.google.com/p/webrtc/issues/detail?id=1508
|
||||
if (navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./) && parseInt(navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)[2], 10) < 28) {
|
||||
dict.url += el.attr('username') + '@';
|
||||
} else {
|
||||
dict.username = el.attr('username'); // only works in M28
|
||||
}
|
||||
}
|
||||
dict.url += el.attr('host');
|
||||
if (el.attr('port') && el.attr('port') != '3478') {
|
||||
dict.url += ':' + el.attr('port');
|
||||
}
|
||||
if (el.attr('transport') && el.attr('transport') != 'udp') {
|
||||
dict.url += '?transport=' + el.attr('transport');
|
||||
}
|
||||
if (el.attr('password')) {
|
||||
dict.credential = el.attr('password');
|
||||
}
|
||||
iceservers.push(dict);
|
||||
break;
|
||||
}
|
||||
});
|
||||
self.ice_config.iceServers = iceservers;
|
||||
},
|
||||
function (err) {
|
||||
console.warn('getting turn credentials failed', err);
|
||||
console.warn('is mod_turncredentials or similar installed?');
|
||||
}
|
||||
);
|
||||
// implement push?
|
||||
}
|
||||
});
|
||||
@@ -1,724 +0,0 @@
|
||||
/* jshint -W117 */
|
||||
// Jingle stuff
|
||||
JingleSession.prototype = Object.create(SessionBase.prototype);
|
||||
function JingleSession(me, sid, connection) {
|
||||
|
||||
SessionBase.call(this, connection, sid);
|
||||
|
||||
this.me = me;
|
||||
this.initiator = null;
|
||||
this.responder = null;
|
||||
this.isInitiator = null;
|
||||
this.peerjid = null;
|
||||
this.state = null;
|
||||
this.localSDP = null;
|
||||
this.remoteSDP = null;
|
||||
this.localStreams = [];
|
||||
this.relayedStreams = [];
|
||||
this.remoteStreams = [];
|
||||
this.startTime = null;
|
||||
this.stopTime = null;
|
||||
this.media_constraints = null;
|
||||
this.pc_constraints = null;
|
||||
this.ice_config = {};
|
||||
this.drip_container = [];
|
||||
|
||||
this.usetrickle = true;
|
||||
this.usepranswer = false; // early transport warmup -- mind you, this might fail. depends on webrtc issue 1718
|
||||
this.usedrip = false; // dripping is sending trickle candidates not one-by-one
|
||||
|
||||
this.hadstuncandidate = false;
|
||||
this.hadturncandidate = false;
|
||||
this.lasticecandidate = false;
|
||||
|
||||
this.statsinterval = null;
|
||||
|
||||
this.reason = null;
|
||||
|
||||
this.wait = true;
|
||||
}
|
||||
|
||||
JingleSession.prototype.initiate = function (peerjid, isInitiator) {
|
||||
var self = this;
|
||||
if (this.state !== null) {
|
||||
console.error('attempt to initiate on session ' + this.sid +
|
||||
'in state ' + this.state);
|
||||
return;
|
||||
}
|
||||
this.isInitiator = isInitiator;
|
||||
this.state = 'pending';
|
||||
this.initiator = isInitiator ? this.me : peerjid;
|
||||
this.responder = !isInitiator ? this.me : peerjid;
|
||||
this.peerjid = peerjid;
|
||||
this.hadstuncandidate = false;
|
||||
this.hadturncandidate = false;
|
||||
this.lasticecandidate = false;
|
||||
|
||||
this.peerconnection
|
||||
= new TraceablePeerConnection(
|
||||
this.connection.jingle.ice_config,
|
||||
this.connection.jingle.pc_constraints );
|
||||
|
||||
this.peerconnection.onicecandidate = function (event) {
|
||||
self.sendIceCandidate(event.candidate);
|
||||
};
|
||||
this.peerconnection.onaddstream = function (event) {
|
||||
self.remoteStreams.push(event.stream);
|
||||
$(document).trigger('remotestreamadded.jingle', [event, self.sid]);
|
||||
};
|
||||
this.peerconnection.onremovestream = function (event) {
|
||||
// Remove the stream from remoteStreams
|
||||
var streamIdx = self.remoteStreams.indexOf(event.stream);
|
||||
if(streamIdx !== -1){
|
||||
self.remoteStreams.splice(streamIdx, 1);
|
||||
}
|
||||
// FIXME: remotestreamremoved.jingle not defined anywhere(unused)
|
||||
$(document).trigger('remotestreamremoved.jingle', [event, self.sid]);
|
||||
};
|
||||
this.peerconnection.onsignalingstatechange = function (event) {
|
||||
if (!(self && self.peerconnection)) return;
|
||||
};
|
||||
this.peerconnection.oniceconnectionstatechange = function (event) {
|
||||
if (!(self && self.peerconnection)) return;
|
||||
switch (self.peerconnection.iceConnectionState) {
|
||||
case 'connected':
|
||||
this.startTime = new Date();
|
||||
break;
|
||||
case 'disconnected':
|
||||
this.stopTime = new Date();
|
||||
break;
|
||||
}
|
||||
$(document).trigger('iceconnectionstatechange.jingle', [self.sid, self]);
|
||||
};
|
||||
// add any local and relayed stream
|
||||
this.localStreams.forEach(function(stream) {
|
||||
self.peerconnection.addStream(stream);
|
||||
});
|
||||
this.relayedStreams.forEach(function(stream) {
|
||||
self.peerconnection.addStream(stream);
|
||||
});
|
||||
};
|
||||
|
||||
JingleSession.prototype.accept = function () {
|
||||
var self = this;
|
||||
this.state = 'active';
|
||||
|
||||
var pranswer = this.peerconnection.localDescription;
|
||||
if (!pranswer || pranswer.type != 'pranswer') {
|
||||
return;
|
||||
}
|
||||
console.log('going from pranswer to answer');
|
||||
if (this.usetrickle) {
|
||||
// remove candidates already sent from session-accept
|
||||
var lines = SDPUtil.find_lines(pranswer.sdp, 'a=candidate:');
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
pranswer.sdp = pranswer.sdp.replace(lines[i] + '\r\n', '');
|
||||
}
|
||||
}
|
||||
while (SDPUtil.find_line(pranswer.sdp, 'a=inactive')) {
|
||||
// FIXME: change any inactive to sendrecv or whatever they were originally
|
||||
pranswer.sdp = pranswer.sdp.replace('a=inactive', 'a=sendrecv');
|
||||
}
|
||||
pranswer = simulcast.reverseTransformLocalDescription(pranswer);
|
||||
var prsdp = new SDP(pranswer.sdp);
|
||||
var accept = $iq({to: this.peerjid,
|
||||
type: 'set'})
|
||||
.c('jingle', {xmlns: 'urn:xmpp:jingle:1',
|
||||
action: 'session-accept',
|
||||
initiator: this.initiator,
|
||||
responder: this.responder,
|
||||
sid: this.sid });
|
||||
prsdp.toJingle(accept, this.initiator == this.me ? 'initiator' : 'responder');
|
||||
this.connection.sendIQ(accept,
|
||||
function () {
|
||||
var ack = {};
|
||||
ack.source = 'answer';
|
||||
$(document).trigger('ack.jingle', [self.sid, ack]);
|
||||
},
|
||||
function (stanza) {
|
||||
var error = ($(stanza).find('error').length) ? {
|
||||
code: $(stanza).find('error').attr('code'),
|
||||
reason: $(stanza).find('error :first')[0].tagName,
|
||||
}:{};
|
||||
error.source = 'answer';
|
||||
$(document).trigger('error.jingle', [self.sid, error]);
|
||||
},
|
||||
10000);
|
||||
|
||||
var sdp = this.peerconnection.localDescription.sdp;
|
||||
while (SDPUtil.find_line(sdp, 'a=inactive')) {
|
||||
// FIXME: change any inactive to sendrecv or whatever they were originally
|
||||
sdp = sdp.replace('a=inactive', 'a=sendrecv');
|
||||
}
|
||||
this.peerconnection.setLocalDescription(new RTCSessionDescription({type: 'answer', sdp: sdp}),
|
||||
function () {
|
||||
//console.log('setLocalDescription success');
|
||||
$(document).trigger('setLocalDescription.jingle', [self.sid]);
|
||||
},
|
||||
function (e) {
|
||||
console.error('setLocalDescription failed', e);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements SessionBase.sendSSRCUpdate.
|
||||
*/
|
||||
JingleSession.prototype.sendSSRCUpdate = function(sdpMediaSsrcs, fromJid, isadd) {
|
||||
|
||||
var self = this;
|
||||
console.log('tell', self.peerjid, 'about ' + (isadd ? 'new' : 'removed') + ' ssrcs from' + self.me);
|
||||
|
||||
if (!(this.peerconnection.signalingState == 'stable' && this.peerconnection.iceConnectionState == 'connected')){
|
||||
console.log("Too early to send updates");
|
||||
return;
|
||||
}
|
||||
|
||||
this.sendSSRCUpdateIq(sdpMediaSsrcs, self.sid, self.initiator, self.peerjid, isadd);
|
||||
};
|
||||
|
||||
JingleSession.prototype.terminate = function (reason) {
|
||||
this.state = 'ended';
|
||||
this.reason = reason;
|
||||
this.peerconnection.close();
|
||||
if (this.statsinterval !== null) {
|
||||
window.clearInterval(this.statsinterval);
|
||||
this.statsinterval = null;
|
||||
}
|
||||
};
|
||||
|
||||
JingleSession.prototype.active = function () {
|
||||
return this.state == 'active';
|
||||
};
|
||||
|
||||
JingleSession.prototype.sendIceCandidate = function (candidate) {
|
||||
var self = this;
|
||||
if (candidate && !this.lasticecandidate) {
|
||||
var ice = SDPUtil.iceparams(this.localSDP.media[candidate.sdpMLineIndex], this.localSDP.session);
|
||||
var jcand = SDPUtil.candidateToJingle(candidate.candidate);
|
||||
if (!(ice && jcand)) {
|
||||
console.error('failed to get ice && jcand');
|
||||
return;
|
||||
}
|
||||
ice.xmlns = 'urn:xmpp:jingle:transports:ice-udp:1';
|
||||
|
||||
if (jcand.type === 'srflx') {
|
||||
this.hadstuncandidate = true;
|
||||
} else if (jcand.type === 'relay') {
|
||||
this.hadturncandidate = true;
|
||||
}
|
||||
|
||||
if (this.usetrickle) {
|
||||
if (this.usedrip) {
|
||||
if (this.drip_container.length === 0) {
|
||||
// start 20ms callout
|
||||
window.setTimeout(function () {
|
||||
if (self.drip_container.length === 0) return;
|
||||
self.sendIceCandidates(self.drip_container);
|
||||
self.drip_container = [];
|
||||
}, 20);
|
||||
|
||||
}
|
||||
this.drip_container.push(event.candidate);
|
||||
return;
|
||||
} else {
|
||||
self.sendIceCandidate([event.candidate]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
//console.log('sendIceCandidate: last candidate.');
|
||||
if (!this.usetrickle) {
|
||||
//console.log('should send full offer now...');
|
||||
var init = $iq({to: this.peerjid,
|
||||
type: 'set'})
|
||||
.c('jingle', {xmlns: 'urn:xmpp:jingle:1',
|
||||
action: this.peerconnection.localDescription.type == 'offer' ? 'session-initiate' : 'session-accept',
|
||||
initiator: this.initiator,
|
||||
sid: this.sid});
|
||||
this.localSDP = new SDP(this.peerconnection.localDescription.sdp);
|
||||
this.localSDP.toJingle(init, this.initiator == this.me ? 'initiator' : 'responder');
|
||||
this.connection.sendIQ(init,
|
||||
function () {
|
||||
//console.log('session initiate ack');
|
||||
var ack = {};
|
||||
ack.source = 'offer';
|
||||
$(document).trigger('ack.jingle', [self.sid, ack]);
|
||||
},
|
||||
function (stanza) {
|
||||
self.state = 'error';
|
||||
self.peerconnection.close();
|
||||
var error = ($(stanza).find('error').length) ? {
|
||||
code: $(stanza).find('error').attr('code'),
|
||||
reason: $(stanza).find('error :first')[0].tagName,
|
||||
}:{};
|
||||
error.source = 'offer';
|
||||
$(document).trigger('error.jingle', [self.sid, error]);
|
||||
},
|
||||
10000);
|
||||
}
|
||||
this.lasticecandidate = true;
|
||||
console.log('Have we encountered any srflx candidates? ' + this.hadstuncandidate);
|
||||
console.log('Have we encountered any relay candidates? ' + this.hadturncandidate);
|
||||
|
||||
if (!(this.hadstuncandidate || this.hadturncandidate) && this.peerconnection.signalingState != 'closed') {
|
||||
$(document).trigger('nostuncandidates.jingle', [this.sid]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
JingleSession.prototype.sendIceCandidates = function (candidates) {
|
||||
console.log('sendIceCandidates', candidates);
|
||||
var cand = $iq({to: this.peerjid, type: 'set'})
|
||||
.c('jingle', {xmlns: 'urn:xmpp:jingle:1',
|
||||
action: 'transport-info',
|
||||
initiator: this.initiator,
|
||||
sid: this.sid});
|
||||
for (var mid = 0; mid < this.localSDP.media.length; mid++) {
|
||||
var cands = candidates.filter(function (el) { return el.sdpMLineIndex == mid; });
|
||||
if (cands.length > 0) {
|
||||
var ice = SDPUtil.iceparams(this.localSDP.media[mid], this.localSDP.session);
|
||||
ice.xmlns = 'urn:xmpp:jingle:transports:ice-udp:1';
|
||||
cand.c('content', {creator: this.initiator == this.me ? 'initiator' : 'responder',
|
||||
name: cands[0].sdpMid
|
||||
}).c('transport', ice);
|
||||
for (var i = 0; i < cands.length; i++) {
|
||||
cand.c('candidate', SDPUtil.candidateToJingle(cands[i].candidate)).up();
|
||||
}
|
||||
// add fingerprint
|
||||
if (SDPUtil.find_line(this.localSDP.media[mid], 'a=fingerprint:', this.localSDP.session)) {
|
||||
var tmp = SDPUtil.parse_fingerprint(SDPUtil.find_line(this.localSDP.media[mid], 'a=fingerprint:', this.localSDP.session));
|
||||
tmp.required = true;
|
||||
cand.c(
|
||||
'fingerprint',
|
||||
{xmlns: 'urn:xmpp:jingle:apps:dtls:0'})
|
||||
.t(tmp.fingerprint);
|
||||
delete tmp.fingerprint;
|
||||
cand.attrs(tmp);
|
||||
cand.up();
|
||||
}
|
||||
cand.up(); // transport
|
||||
cand.up(); // content
|
||||
}
|
||||
}
|
||||
// might merge last-candidate notification into this, but it is called alot later. See webrtc issue #2340
|
||||
//console.log('was this the last candidate', this.lasticecandidate);
|
||||
this.connection.sendIQ(cand,
|
||||
function () {
|
||||
var ack = {};
|
||||
ack.source = 'transportinfo';
|
||||
$(document).trigger('ack.jingle', [this.sid, ack]);
|
||||
},
|
||||
function (stanza) {
|
||||
var error = ($(stanza).find('error').length) ? {
|
||||
code: $(stanza).find('error').attr('code'),
|
||||
reason: $(stanza).find('error :first')[0].tagName,
|
||||
}:{};
|
||||
error.source = 'transportinfo';
|
||||
$(document).trigger('error.jingle', [this.sid, error]);
|
||||
},
|
||||
10000);
|
||||
};
|
||||
|
||||
|
||||
JingleSession.prototype.sendOffer = function () {
|
||||
//console.log('sendOffer...');
|
||||
var self = this;
|
||||
this.peerconnection.createOffer(function (sdp) {
|
||||
self.createdOffer(sdp);
|
||||
},
|
||||
function (e) {
|
||||
console.error('createOffer failed', e);
|
||||
},
|
||||
this.media_constraints
|
||||
);
|
||||
};
|
||||
|
||||
JingleSession.prototype.createdOffer = function (sdp) {
|
||||
//console.log('createdOffer', sdp);
|
||||
var self = this;
|
||||
this.localSDP = new SDP(sdp.sdp);
|
||||
//this.localSDP.mangle();
|
||||
if (this.usetrickle) {
|
||||
var init = $iq({to: this.peerjid,
|
||||
type: 'set'})
|
||||
.c('jingle', {xmlns: 'urn:xmpp:jingle:1',
|
||||
action: 'session-initiate',
|
||||
initiator: this.initiator,
|
||||
sid: this.sid});
|
||||
this.localSDP.toJingle(init, this.initiator == this.me ? 'initiator' : 'responder');
|
||||
this.connection.sendIQ(init,
|
||||
function () {
|
||||
var ack = {};
|
||||
ack.source = 'offer';
|
||||
$(document).trigger('ack.jingle', [self.sid, ack]);
|
||||
},
|
||||
function (stanza) {
|
||||
self.state = 'error';
|
||||
self.peerconnection.close();
|
||||
var error = ($(stanza).find('error').length) ? {
|
||||
code: $(stanza).find('error').attr('code'),
|
||||
reason: $(stanza).find('error :first')[0].tagName,
|
||||
}:{};
|
||||
error.source = 'offer';
|
||||
$(document).trigger('error.jingle', [self.sid, error]);
|
||||
},
|
||||
10000);
|
||||
}
|
||||
sdp.sdp = this.localSDP.raw;
|
||||
this.peerconnection.setLocalDescription(sdp,
|
||||
function () {
|
||||
$(document).trigger('setLocalDescription.jingle', [self.sid]);
|
||||
//console.log('setLocalDescription success');
|
||||
},
|
||||
function (e) {
|
||||
console.error('setLocalDescription failed', e);
|
||||
}
|
||||
);
|
||||
var cands = SDPUtil.find_lines(this.localSDP.raw, 'a=candidate:');
|
||||
for (var i = 0; i < cands.length; i++) {
|
||||
var cand = SDPUtil.parse_icecandidate(cands[i]);
|
||||
if (cand.type == 'srflx') {
|
||||
this.hadstuncandidate = true;
|
||||
} else if (cand.type == 'relay') {
|
||||
this.hadturncandidate = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
JingleSession.prototype.setRemoteDescription = function (elem, desctype) {
|
||||
//console.log('setting remote description... ', desctype);
|
||||
this.remoteSDP = new SDP('');
|
||||
this.remoteSDP.fromJingle(elem);
|
||||
if (this.peerconnection.remoteDescription !== null) {
|
||||
console.log('setRemoteDescription when remote description is not null, should be pranswer', this.peerconnection.remoteDescription);
|
||||
if (this.peerconnection.remoteDescription.type == 'pranswer') {
|
||||
var pranswer = new SDP(this.peerconnection.remoteDescription.sdp);
|
||||
for (var i = 0; i < pranswer.media.length; i++) {
|
||||
// make sure we have ice ufrag and pwd
|
||||
if (!SDPUtil.find_line(this.remoteSDP.media[i], 'a=ice-ufrag:', this.remoteSDP.session)) {
|
||||
if (SDPUtil.find_line(pranswer.media[i], 'a=ice-ufrag:', pranswer.session)) {
|
||||
this.remoteSDP.media[i] += SDPUtil.find_line(pranswer.media[i], 'a=ice-ufrag:', pranswer.session) + '\r\n';
|
||||
} else {
|
||||
console.warn('no ice ufrag?');
|
||||
}
|
||||
if (SDPUtil.find_line(pranswer.media[i], 'a=ice-pwd:', pranswer.session)) {
|
||||
this.remoteSDP.media[i] += SDPUtil.find_line(pranswer.media[i], 'a=ice-pwd:', pranswer.session) + '\r\n';
|
||||
} else {
|
||||
console.warn('no ice pwd?');
|
||||
}
|
||||
}
|
||||
// copy over candidates
|
||||
var lines = SDPUtil.find_lines(pranswer.media[i], 'a=candidate:');
|
||||
for (var j = 0; j < lines.length; j++) {
|
||||
this.remoteSDP.media[i] += lines[j] + '\r\n';
|
||||
}
|
||||
}
|
||||
this.remoteSDP.raw = this.remoteSDP.session + this.remoteSDP.media.join('');
|
||||
}
|
||||
}
|
||||
var remotedesc = new RTCSessionDescription({type: desctype, sdp: this.remoteSDP.raw});
|
||||
|
||||
this.peerconnection.setRemoteDescription(remotedesc,
|
||||
function () {
|
||||
//console.log('setRemoteDescription success');
|
||||
},
|
||||
function (e) {
|
||||
console.error('setRemoteDescription error', e);
|
||||
$(document).trigger('fatalError.jingle', [self, e]);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
JingleSession.prototype.addIceCandidate = function (elem) {
|
||||
var self = this;
|
||||
if (this.peerconnection.signalingState == 'closed') {
|
||||
return;
|
||||
}
|
||||
if (!this.peerconnection.remoteDescription && this.peerconnection.signalingState == 'have-local-offer') {
|
||||
console.log('trickle ice candidate arriving before session accept...');
|
||||
// create a PRANSWER for setRemoteDescription
|
||||
if (!this.remoteSDP) {
|
||||
var cobbled = 'v=0\r\n' +
|
||||
'o=- ' + '1923518516' + ' 2 IN IP4 0.0.0.0\r\n' +// FIXME
|
||||
's=-\r\n' +
|
||||
't=0 0\r\n';
|
||||
// first, take some things from the local description
|
||||
for (var i = 0; i < this.localSDP.media.length; i++) {
|
||||
cobbled += SDPUtil.find_line(this.localSDP.media[i], 'm=') + '\r\n';
|
||||
cobbled += SDPUtil.find_lines(this.localSDP.media[i], 'a=rtpmap:').join('\r\n') + '\r\n';
|
||||
if (SDPUtil.find_line(this.localSDP.media[i], 'a=mid:')) {
|
||||
cobbled += SDPUtil.find_line(this.localSDP.media[i], 'a=mid:') + '\r\n';
|
||||
}
|
||||
cobbled += 'a=inactive\r\n';
|
||||
}
|
||||
this.remoteSDP = new SDP(cobbled);
|
||||
}
|
||||
// then add things like ice and dtls from remote candidate
|
||||
elem.each(function () {
|
||||
for (var i = 0; i < self.remoteSDP.media.length; i++) {
|
||||
if (SDPUtil.find_line(self.remoteSDP.media[i], 'a=mid:' + $(this).attr('name')) ||
|
||||
self.remoteSDP.media[i].indexOf('m=' + $(this).attr('name')) === 0) {
|
||||
if (!SDPUtil.find_line(self.remoteSDP.media[i], 'a=ice-ufrag:')) {
|
||||
var tmp = $(this).find('transport');
|
||||
self.remoteSDP.media[i] += 'a=ice-ufrag:' + tmp.attr('ufrag') + '\r\n';
|
||||
self.remoteSDP.media[i] += 'a=ice-pwd:' + tmp.attr('pwd') + '\r\n';
|
||||
tmp = $(this).find('transport>fingerprint');
|
||||
if (tmp.length) {
|
||||
self.remoteSDP.media[i] += 'a=fingerprint:' + tmp.attr('hash') + ' ' + tmp.text() + '\r\n';
|
||||
} else {
|
||||
console.log('no dtls fingerprint (webrtc issue #1718?)');
|
||||
self.remoteSDP.media[i] += 'a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:BAADBAADBAADBAADBAADBAADBAADBAADBAADBAAD\r\n';
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
this.remoteSDP.raw = this.remoteSDP.session + this.remoteSDP.media.join('');
|
||||
|
||||
// we need a complete SDP with ice-ufrag/ice-pwd in all parts
|
||||
// this makes the assumption that the PRANSWER is constructed such that the ice-ufrag is in all mediaparts
|
||||
// but it could be in the session part as well. since the code above constructs this sdp this can't happen however
|
||||
var iscomplete = this.remoteSDP.media.filter(function (mediapart) {
|
||||
return SDPUtil.find_line(mediapart, 'a=ice-ufrag:');
|
||||
}).length == this.remoteSDP.media.length;
|
||||
|
||||
if (iscomplete) {
|
||||
console.log('setting pranswer');
|
||||
try {
|
||||
this.peerconnection.setRemoteDescription(new RTCSessionDescription({type: 'pranswer', sdp: this.remoteSDP.raw }),
|
||||
function() {
|
||||
},
|
||||
function(e) {
|
||||
console.log('setRemoteDescription pranswer failed', e.toString());
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('setting pranswer failed', e);
|
||||
}
|
||||
} else {
|
||||
//console.log('not yet setting pranswer');
|
||||
}
|
||||
}
|
||||
// operate on each content element
|
||||
elem.each(function () {
|
||||
// would love to deactivate this, but firefox still requires it
|
||||
var idx = -1;
|
||||
var i;
|
||||
for (i = 0; i < self.remoteSDP.media.length; i++) {
|
||||
if (SDPUtil.find_line(self.remoteSDP.media[i], 'a=mid:' + $(this).attr('name')) ||
|
||||
self.remoteSDP.media[i].indexOf('m=' + $(this).attr('name')) === 0) {
|
||||
idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (idx == -1) { // fall back to localdescription
|
||||
for (i = 0; i < self.localSDP.media.length; i++) {
|
||||
if (SDPUtil.find_line(self.localSDP.media[i], 'a=mid:' + $(this).attr('name')) ||
|
||||
self.localSDP.media[i].indexOf('m=' + $(this).attr('name')) === 0) {
|
||||
idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
var name = $(this).attr('name');
|
||||
// TODO: check ice-pwd and ice-ufrag?
|
||||
$(this).find('transport>candidate').each(function () {
|
||||
var line, candidate;
|
||||
line = SDPUtil.candidateFromJingle(this);
|
||||
candidate = new RTCIceCandidate({sdpMLineIndex: idx,
|
||||
sdpMid: name,
|
||||
candidate: line});
|
||||
try {
|
||||
self.peerconnection.addIceCandidate(candidate);
|
||||
} catch (e) {
|
||||
console.error('addIceCandidate failed', e.toString(), line);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
JingleSession.prototype.sendAnswer = function (provisional) {
|
||||
//console.log('createAnswer', provisional);
|
||||
var self = this;
|
||||
this.peerconnection.createAnswer(
|
||||
function (sdp) {
|
||||
self.createdAnswer(sdp, provisional);
|
||||
},
|
||||
function (e) {
|
||||
console.error('createAnswer failed', e);
|
||||
},
|
||||
this.media_constraints
|
||||
);
|
||||
};
|
||||
|
||||
JingleSession.prototype.createdAnswer = function (sdp, provisional) {
|
||||
//console.log('createAnswer callback');
|
||||
var self = this;
|
||||
this.localSDP = new SDP(sdp.sdp);
|
||||
//this.localSDP.mangle();
|
||||
this.usepranswer = provisional === true;
|
||||
if (this.usetrickle) {
|
||||
if (!this.usepranswer) {
|
||||
var accept = $iq({to: this.peerjid,
|
||||
type: 'set'})
|
||||
.c('jingle', {xmlns: 'urn:xmpp:jingle:1',
|
||||
action: 'session-accept',
|
||||
initiator: this.initiator,
|
||||
responder: this.responder,
|
||||
sid: this.sid });
|
||||
var publicLocalDesc = simulcast.reverseTransformLocalDescription(sdp);
|
||||
var publicLocalSDP = new SDP(publicLocalDesc.sdp);
|
||||
publicLocalSDP.toJingle(accept, this.initiator == this.me ? 'initiator' : 'responder');
|
||||
this.connection.sendIQ(accept,
|
||||
function () {
|
||||
var ack = {};
|
||||
ack.source = 'answer';
|
||||
$(document).trigger('ack.jingle', [self.sid, ack]);
|
||||
},
|
||||
function (stanza) {
|
||||
var error = ($(stanza).find('error').length) ? {
|
||||
code: $(stanza).find('error').attr('code'),
|
||||
reason: $(stanza).find('error :first')[0].tagName,
|
||||
}:{};
|
||||
error.source = 'answer';
|
||||
$(document).trigger('error.jingle', [self.sid, error]);
|
||||
},
|
||||
10000);
|
||||
} else {
|
||||
sdp.type = 'pranswer';
|
||||
for (var i = 0; i < this.localSDP.media.length; i++) {
|
||||
this.localSDP.media[i] = this.localSDP.media[i].replace('a=sendrecv\r\n', 'a=inactive\r\n');
|
||||
}
|
||||
this.localSDP.raw = this.localSDP.session + '\r\n' + this.localSDP.media.join('');
|
||||
}
|
||||
}
|
||||
sdp.sdp = this.localSDP.raw;
|
||||
this.peerconnection.setLocalDescription(sdp,
|
||||
function () {
|
||||
$(document).trigger('setLocalDescription.jingle', [self.sid]);
|
||||
//console.log('setLocalDescription success');
|
||||
},
|
||||
function (e) {
|
||||
console.error('setLocalDescription failed', e);
|
||||
}
|
||||
);
|
||||
var cands = SDPUtil.find_lines(this.localSDP.raw, 'a=candidate:');
|
||||
for (var j = 0; j < cands.length; j++) {
|
||||
var cand = SDPUtil.parse_icecandidate(cands[j]);
|
||||
if (cand.type == 'srflx') {
|
||||
this.hadstuncandidate = true;
|
||||
} else if (cand.type == 'relay') {
|
||||
this.hadturncandidate = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
JingleSession.prototype.sendTerminate = function (reason, text) {
|
||||
var self = this,
|
||||
term = $iq({to: this.peerjid,
|
||||
type: 'set'})
|
||||
.c('jingle', {xmlns: 'urn:xmpp:jingle:1',
|
||||
action: 'session-terminate',
|
||||
initiator: this.initiator,
|
||||
sid: this.sid})
|
||||
.c('reason')
|
||||
.c(reason || 'success');
|
||||
|
||||
if (text) {
|
||||
term.up().c('text').t(text);
|
||||
}
|
||||
|
||||
this.connection.sendIQ(term,
|
||||
function () {
|
||||
self.peerconnection.close();
|
||||
self.peerconnection = null;
|
||||
self.terminate();
|
||||
var ack = {};
|
||||
ack.source = 'terminate';
|
||||
$(document).trigger('ack.jingle', [self.sid, ack]);
|
||||
},
|
||||
function (stanza) {
|
||||
var error = ($(stanza).find('error').length) ? {
|
||||
code: $(stanza).find('error').attr('code'),
|
||||
reason: $(stanza).find('error :first')[0].tagName,
|
||||
}:{};
|
||||
$(document).trigger('ack.jingle', [self.sid, error]);
|
||||
},
|
||||
10000);
|
||||
if (this.statsinterval !== null) {
|
||||
window.clearInterval(this.statsinterval);
|
||||
this.statsinterval = null;
|
||||
}
|
||||
};
|
||||
|
||||
JingleSession.prototype.sendMute = function (muted, content) {
|
||||
var info = $iq({to: this.peerjid,
|
||||
type: 'set'})
|
||||
.c('jingle', {xmlns: 'urn:xmpp:jingle:1',
|
||||
action: 'session-info',
|
||||
initiator: this.initiator,
|
||||
sid: this.sid });
|
||||
info.c(muted ? 'mute' : 'unmute', {xmlns: 'urn:xmpp:jingle:apps:rtp:info:1'});
|
||||
info.attrs({'creator': this.me == this.initiator ? 'creator' : 'responder'});
|
||||
if (content) {
|
||||
info.attrs({'name': content});
|
||||
}
|
||||
this.connection.send(info);
|
||||
};
|
||||
|
||||
JingleSession.prototype.sendRinging = function () {
|
||||
var info = $iq({to: this.peerjid,
|
||||
type: 'set'})
|
||||
.c('jingle', {xmlns: 'urn:xmpp:jingle:1',
|
||||
action: 'session-info',
|
||||
initiator: this.initiator,
|
||||
sid: this.sid });
|
||||
info.c('ringing', {xmlns: 'urn:xmpp:jingle:apps:rtp:info:1'});
|
||||
this.connection.send(info);
|
||||
};
|
||||
|
||||
JingleSession.prototype.getStats = function (interval) {
|
||||
var self = this;
|
||||
var recv = {audio: 0, video: 0};
|
||||
var lost = {audio: 0, video: 0};
|
||||
var lastrecv = {audio: 0, video: 0};
|
||||
var lastlost = {audio: 0, video: 0};
|
||||
var loss = {audio: 0, video: 0};
|
||||
var delta = {audio: 0, video: 0};
|
||||
this.statsinterval = window.setInterval(function () {
|
||||
if (self && self.peerconnection && self.peerconnection.getStats) {
|
||||
self.peerconnection.getStats(function (stats) {
|
||||
var results = stats.result();
|
||||
// TODO: there are so much statistics you can get from this..
|
||||
for (var i = 0; i < results.length; ++i) {
|
||||
if (results[i].type == 'ssrc') {
|
||||
var packetsrecv = results[i].stat('packetsReceived');
|
||||
var packetslost = results[i].stat('packetsLost');
|
||||
if (packetsrecv && packetslost) {
|
||||
packetsrecv = parseInt(packetsrecv, 10);
|
||||
packetslost = parseInt(packetslost, 10);
|
||||
|
||||
if (results[i].stat('googFrameRateReceived')) {
|
||||
lastlost.video = lost.video;
|
||||
lastrecv.video = recv.video;
|
||||
recv.video = packetsrecv;
|
||||
lost.video = packetslost;
|
||||
} else {
|
||||
lastlost.audio = lost.audio;
|
||||
lastrecv.audio = recv.audio;
|
||||
recv.audio = packetsrecv;
|
||||
lost.audio = packetslost;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
delta.audio = recv.audio - lastrecv.audio;
|
||||
delta.video = recv.video - lastrecv.video;
|
||||
loss.audio = (delta.audio > 0) ? Math.ceil(100 * (lost.audio - lastlost.audio) / delta.audio) : 0;
|
||||
loss.video = (delta.video > 0) ? Math.ceil(100 * (lost.video - lastlost.video) / delta.video) : 0;
|
||||
$(document).trigger('packetloss.jingle', [self.sid, loss]);
|
||||
});
|
||||
}
|
||||
}, interval || 3000);
|
||||
return this.statsinterval;
|
||||
};
|
||||
@@ -1,246 +0,0 @@
|
||||
/**
|
||||
* Base class for ColibriFocus and JingleSession.
|
||||
* @param connection Strophe connection object
|
||||
* @param sid my session identifier(resource)
|
||||
* @constructor
|
||||
*/
|
||||
function SessionBase(connection, sid){
|
||||
|
||||
this.connection = connection;
|
||||
this.sid = sid;
|
||||
}
|
||||
|
||||
|
||||
SessionBase.prototype.modifySources = function (successCallback) {
|
||||
var self = this;
|
||||
if(this.peerconnection)
|
||||
this.peerconnection.modifySources(function(){
|
||||
$(document).trigger('setLocalDescription.jingle', [self.sid]);
|
||||
if(successCallback) {
|
||||
successCallback();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
SessionBase.prototype.addSource = function (elem, fromJid) {
|
||||
|
||||
var self = this;
|
||||
// FIXME: dirty waiting
|
||||
if (!this.peerconnection.localDescription)
|
||||
{
|
||||
console.warn("addSource - localDescription not ready yet")
|
||||
setTimeout(function()
|
||||
{
|
||||
self.addSource(elem, fromJid);
|
||||
},
|
||||
200
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.peerconnection.addSource(elem);
|
||||
|
||||
this.modifySources();
|
||||
};
|
||||
|
||||
SessionBase.prototype.removeSource = function (elem, fromJid) {
|
||||
|
||||
var self = this;
|
||||
// FIXME: dirty waiting
|
||||
if (!this.peerconnection.localDescription)
|
||||
{
|
||||
console.warn("removeSource - localDescription not ready yet")
|
||||
setTimeout(function()
|
||||
{
|
||||
self.removeSource(elem, fromJid);
|
||||
},
|
||||
200
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.peerconnection.removeSource(elem);
|
||||
|
||||
this.modifySources();
|
||||
};
|
||||
/**
|
||||
* Switches video streams.
|
||||
* @param new_stream new stream that will be used as video of this session.
|
||||
* @param oldStream old video stream of this session.
|
||||
* @param success_callback callback executed after successful stream switch.
|
||||
*/
|
||||
SessionBase.prototype.switchStreams = function (new_stream, oldStream, success_callback) {
|
||||
|
||||
var self = this;
|
||||
|
||||
// Stop the stream to trigger onended event for old stream
|
||||
oldStream.stop();
|
||||
|
||||
// Remember SDP to figure out added/removed SSRCs
|
||||
var oldSdp = null;
|
||||
if(self.peerconnection) {
|
||||
if(self.peerconnection.localDescription) {
|
||||
oldSdp = new SDP(self.peerconnection.localDescription.sdp);
|
||||
}
|
||||
self.peerconnection.removeStream(oldStream);
|
||||
self.peerconnection.addStream(new_stream);
|
||||
}
|
||||
|
||||
self.connection.jingle.localVideo = new_stream;
|
||||
|
||||
self.connection.jingle.localStreams = [];
|
||||
self.connection.jingle.localStreams.push(self.connection.jingle.localAudio);
|
||||
self.connection.jingle.localStreams.push(self.connection.jingle.localVideo);
|
||||
|
||||
// Conference is not active
|
||||
if(!oldSdp || !self.peerconnection) {
|
||||
success_callback();
|
||||
return;
|
||||
}
|
||||
|
||||
self.peerconnection.switchstreams = true;
|
||||
self.modifySources(function() {
|
||||
console.log('modify sources done');
|
||||
|
||||
var newSdp = new SDP(self.peerconnection.localDescription.sdp);
|
||||
console.log("SDPs", oldSdp, newSdp);
|
||||
self.notifyMySSRCUpdate(oldSdp, newSdp);
|
||||
|
||||
success_callback();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Figures out added/removed ssrcs and send update IQs.
|
||||
* @param old_sdp SDP object for old description.
|
||||
* @param new_sdp SDP object for new description.
|
||||
*/
|
||||
SessionBase.prototype.notifyMySSRCUpdate = function (old_sdp, new_sdp) {
|
||||
|
||||
var old_media = old_sdp.getMediaSsrcMap();
|
||||
var new_media = new_sdp.getMediaSsrcMap();
|
||||
//console.log("old/new medias: ", old_media, new_media);
|
||||
|
||||
var toAdd = old_sdp.getNewMedia(new_sdp);
|
||||
var toRemove = new_sdp.getNewMedia(old_sdp);
|
||||
//console.log("to add", toAdd);
|
||||
//console.log("to remove", toRemove);
|
||||
if(Object.keys(toRemove).length > 0){
|
||||
this.sendSSRCUpdate(toRemove, null, false);
|
||||
}
|
||||
if(Object.keys(toAdd).length > 0){
|
||||
this.sendSSRCUpdate(toAdd, null, true);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Empty method that does nothing by default. It should send SSRC update IQs to session participants.
|
||||
* @param sdpMediaSsrcs array of
|
||||
* @param fromJid
|
||||
* @param isAdd
|
||||
*/
|
||||
SessionBase.prototype.sendSSRCUpdate = function(sdpMediaSsrcs, fromJid, isAdd) {
|
||||
//FIXME: put default implementation here(maybe from JingleSession?)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends SSRC update IQ.
|
||||
* @param sdpMediaSsrcs SSRCs map obtained from SDP.getNewMedia. Cntains SSRCs to add/remove.
|
||||
* @param sid session identifier that will be put into the IQ.
|
||||
* @param initiator initiator identifier.
|
||||
* @param toJid destination Jid
|
||||
* @param isAdd indicates if this is remove or add operation.
|
||||
*/
|
||||
SessionBase.prototype.sendSSRCUpdateIq = function(sdpMediaSsrcs, sid, initiator, toJid, isAdd) {
|
||||
|
||||
var self = this;
|
||||
var modify = $iq({to: toJid, type: 'set'})
|
||||
.c('jingle', {
|
||||
xmlns: 'urn:xmpp:jingle:1',
|
||||
action: isAdd ? 'source-add' : 'source-remove',
|
||||
initiator: initiator,
|
||||
sid: sid
|
||||
}
|
||||
);
|
||||
// FIXME: only announce video ssrcs since we mix audio and dont need
|
||||
// the audio ssrcs therefore
|
||||
var modified = false;
|
||||
Object.keys(sdpMediaSsrcs).forEach(function(channelNum){
|
||||
modified = true;
|
||||
var channel = sdpMediaSsrcs[channelNum];
|
||||
modify.c('content', {name: channel.mediaType});
|
||||
|
||||
modify.c('description', {xmlns:'urn:xmpp:jingle:apps:rtp:1', media: channel.mediaType});
|
||||
// FIXME: not completly sure this operates on blocks and / or handles different ssrcs correctly
|
||||
// generate sources from lines
|
||||
Object.keys(channel.ssrcs).forEach(function(ssrcNum) {
|
||||
var mediaSsrc = channel.ssrcs[ssrcNum];
|
||||
modify.c('source', { xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
|
||||
modify.attrs({ssrc: mediaSsrc.ssrc});
|
||||
// iterate over ssrc lines
|
||||
mediaSsrc.lines.forEach(function (line) {
|
||||
var idx = line.indexOf(' ');
|
||||
var kv = line.substr(idx + 1);
|
||||
modify.c('parameter');
|
||||
if (kv.indexOf(':') == -1) {
|
||||
modify.attrs({ name: kv });
|
||||
} else {
|
||||
modify.attrs({ name: kv.split(':', 2)[0] });
|
||||
modify.attrs({ value: kv.split(':', 2)[1] });
|
||||
}
|
||||
modify.up(); // end of parameter
|
||||
});
|
||||
modify.up(); // end of source
|
||||
});
|
||||
|
||||
// generate source groups from lines
|
||||
channel.ssrcGroups.forEach(function(ssrcGroup) {
|
||||
if (ssrcGroup.ssrcs.length != 0) {
|
||||
|
||||
modify.c('ssrc-group', {
|
||||
semantics: ssrcGroup.semantics,
|
||||
xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0'
|
||||
});
|
||||
|
||||
ssrcGroup.ssrcs.forEach(function (ssrc) {
|
||||
modify.c('source', { ssrc: ssrc })
|
||||
.up(); // end of source
|
||||
});
|
||||
modify.up(); // end of ssrc-group
|
||||
}
|
||||
});
|
||||
|
||||
modify.up(); // end of description
|
||||
modify.up(); // end of content
|
||||
});
|
||||
if (modified) {
|
||||
self.connection.sendIQ(modify,
|
||||
function (res) {
|
||||
console.info('got modify result', res);
|
||||
},
|
||||
function (err) {
|
||||
console.error('got modify error', err);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
console.log('modification not necessary');
|
||||
}
|
||||
};
|
||||
|
||||
// SDP-based mute by going recvonly/sendrecv
|
||||
// FIXME: should probably black out the screen as well
|
||||
SessionBase.prototype.toggleVideoMute = function (callback) {
|
||||
|
||||
var ismuted = false;
|
||||
var localVideo = connection.jingle.localVideo;
|
||||
for (var idx = 0; idx < localVideo.getVideoTracks().length; idx++) {
|
||||
ismuted = !localVideo.getVideoTracks()[idx].enabled;
|
||||
}
|
||||
for (var idx = 0; idx < localVideo.getVideoTracks().length; idx++) {
|
||||
localVideo.getVideoTracks()[idx].enabled = !localVideo.getVideoTracks()[idx].enabled;
|
||||
}
|
||||
|
||||
if(this.peerconnection)
|
||||
this.peerconnection.hardMuteVideo(!ismuted);
|
||||
this.modifySources(callback(!ismuted));
|
||||
};
|
||||
2
libs/strophe/strophe.min.js
vendored
@@ -1,41 +0,0 @@
|
||||
/**
|
||||
* Strophe logger implementation. Logs from level WARN and above.
|
||||
*/
|
||||
Strophe.log = function (level, msg) {
|
||||
switch(level) {
|
||||
case Strophe.LogLevel.WARN:
|
||||
console.warn("Strophe: "+msg);
|
||||
break;
|
||||
case Strophe.LogLevel.ERROR:
|
||||
case Strophe.LogLevel.FATAL:
|
||||
console.error("Strophe: "+msg);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
Strophe.getStatusString = function(status)
|
||||
{
|
||||
switch (status)
|
||||
{
|
||||
case Strophe.Status.ERROR:
|
||||
return "ERROR";
|
||||
case Strophe.Status.CONNECTING:
|
||||
return "CONNECTING";
|
||||
case Strophe.Status.CONNFAIL:
|
||||
return "CONNFAIL";
|
||||
case Strophe.Status.AUTHENTICATING:
|
||||
return "AUTHENTICATING";
|
||||
case Strophe.Status.AUTHFAIL:
|
||||
return "AUTHFAIL";
|
||||
case Strophe.Status.CONNECTED:
|
||||
return "CONNECTED";
|
||||
case Strophe.Status.DISCONNECTED:
|
||||
return "DISCONNECTED";
|
||||
case Strophe.Status.DISCONNECTING:
|
||||
return "DISCONNECTING";
|
||||
case Strophe.Status.ATTACHED:
|
||||
return "ATTACHED";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
};
|
||||
342
libs/toastr.js
Normal file
@@ -0,0 +1,342 @@
|
||||
/*
|
||||
* Toastr
|
||||
* Copyright 2012-2014 John Papa and Hans Fjällemark.
|
||||
* All Rights Reserved.
|
||||
* Use, reproduction, distribution, and modification of this code is subject to the terms and
|
||||
* conditions of the MIT license, available at http://www.opensource.org/licenses/mit-license.php
|
||||
*
|
||||
* Author: John Papa and Hans Fjällemark
|
||||
* ARIA Support: Greta Krafsig
|
||||
* Project: https://github.com/CodeSeven/toastr
|
||||
*/
|
||||
; (function (define) {
|
||||
define(['jquery'], function ($) {
|
||||
return (function () {
|
||||
var $container;
|
||||
var listener;
|
||||
var toastId = 0;
|
||||
var toastType = {
|
||||
error: 'error',
|
||||
info: 'info',
|
||||
success: 'success',
|
||||
warning: 'warning'
|
||||
};
|
||||
|
||||
var toastr = {
|
||||
clear: clear,
|
||||
remove: remove,
|
||||
error: error,
|
||||
getContainer: getContainer,
|
||||
info: info,
|
||||
options: {},
|
||||
subscribe: subscribe,
|
||||
success: success,
|
||||
version: '2.0.3',
|
||||
warning: warning
|
||||
};
|
||||
|
||||
return toastr;
|
||||
|
||||
//#region Accessible Methods
|
||||
function error(message, title, optionsOverride) {
|
||||
return notify({
|
||||
type: toastType.error,
|
||||
iconClass: getOptions().iconClasses.error,
|
||||
message: message,
|
||||
optionsOverride: optionsOverride,
|
||||
title: title
|
||||
});
|
||||
}
|
||||
|
||||
function getContainer(options, create) {
|
||||
if (!options) { options = getOptions(); }
|
||||
$container = $('#' + options.containerId);
|
||||
if ($container.length) {
|
||||
return $container;
|
||||
}
|
||||
if(create) {
|
||||
$container = createContainer(options);
|
||||
}
|
||||
return $container;
|
||||
}
|
||||
|
||||
function info(message, title, optionsOverride) {
|
||||
return notify({
|
||||
type: toastType.info,
|
||||
iconClass: getOptions().iconClasses.info,
|
||||
message: message,
|
||||
optionsOverride: optionsOverride,
|
||||
title: title
|
||||
});
|
||||
}
|
||||
|
||||
function subscribe(callback) {
|
||||
listener = callback;
|
||||
}
|
||||
|
||||
function success(message, title, optionsOverride) {
|
||||
return notify({
|
||||
type: toastType.success,
|
||||
iconClass: getOptions().iconClasses.success,
|
||||
message: message,
|
||||
optionsOverride: optionsOverride,
|
||||
title: title
|
||||
});
|
||||
}
|
||||
|
||||
function warning(message, title, optionsOverride) {
|
||||
return notify({
|
||||
type: toastType.warning,
|
||||
iconClass: getOptions().iconClasses.warning,
|
||||
message: message,
|
||||
optionsOverride: optionsOverride,
|
||||
title: title
|
||||
});
|
||||
}
|
||||
|
||||
function clear($toastElement) {
|
||||
var options = getOptions();
|
||||
if (!$container) { getContainer(options); }
|
||||
if (!clearToast($toastElement, options)) {
|
||||
clearContainer(options);
|
||||
}
|
||||
}
|
||||
|
||||
function remove($toastElement) {
|
||||
var options = getOptions();
|
||||
if (!$container) { getContainer(options); }
|
||||
if ($toastElement && $(':focus', $toastElement).length === 0) {
|
||||
removeToast($toastElement);
|
||||
return;
|
||||
}
|
||||
if ($container.children().length) {
|
||||
$container.remove();
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region Internal Methods
|
||||
|
||||
function clearContainer(options){
|
||||
var toastsToClear = $container.children();
|
||||
for (var i = toastsToClear.length - 1; i >= 0; i--) {
|
||||
clearToast($(toastsToClear[i]), options);
|
||||
};
|
||||
}
|
||||
|
||||
function clearToast($toastElement, options){
|
||||
if ($toastElement && $(':focus', $toastElement).length === 0) {
|
||||
$toastElement[options.hideMethod]({
|
||||
duration: options.hideDuration,
|
||||
easing: options.hideEasing,
|
||||
complete: function () { removeToast($toastElement); }
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function createContainer(options) {
|
||||
$container = $('<div/>')
|
||||
.attr('id', options.containerId)
|
||||
.addClass(options.positionClass)
|
||||
.attr('aria-live', 'polite')
|
||||
.attr('role', 'alert');
|
||||
|
||||
$container.appendTo($(options.target));
|
||||
return $container;
|
||||
}
|
||||
|
||||
function getDefaults() {
|
||||
return {
|
||||
tapToDismiss: true,
|
||||
toastClass: 'toast',
|
||||
containerId: 'toast-container',
|
||||
debug: false,
|
||||
|
||||
showMethod: 'fadeIn', //fadeIn, slideDown, and show are built into jQuery
|
||||
showDuration: 300,
|
||||
showEasing: 'swing', //swing and linear are built into jQuery
|
||||
onShown: undefined,
|
||||
hideMethod: 'fadeOut',
|
||||
hideDuration: 1000,
|
||||
hideEasing: 'swing',
|
||||
onHidden: undefined,
|
||||
|
||||
extendedTimeOut: 1000,
|
||||
iconClasses: {
|
||||
error: 'toast-error',
|
||||
info: 'toast-info',
|
||||
success: 'toast-success',
|
||||
warning: 'toast-warning'
|
||||
},
|
||||
iconClass: 'toast-info',
|
||||
positionClass: 'toast-top-right',
|
||||
timeOut: 5000, // Set timeOut and extendedTimeout to 0 to make it sticky
|
||||
titleClass: 'toast-title',
|
||||
messageClass: 'toast-message',
|
||||
target: 'body',
|
||||
closeHtml: '<button>×</button>',
|
||||
newestOnTop: true
|
||||
};
|
||||
}
|
||||
|
||||
function publish(args) {
|
||||
if (!listener) { return; }
|
||||
listener(args);
|
||||
}
|
||||
|
||||
function notify(map) {
|
||||
var options = getOptions(),
|
||||
iconClass = map.iconClass || options.iconClass;
|
||||
|
||||
if (typeof (map.optionsOverride) !== 'undefined') {
|
||||
options = $.extend(options, map.optionsOverride);
|
||||
iconClass = map.optionsOverride.iconClass || iconClass;
|
||||
}
|
||||
|
||||
toastId++;
|
||||
|
||||
$container = getContainer(options, true);
|
||||
var intervalId = null,
|
||||
$toastElement = $('<div/>'),
|
||||
$titleElement = $('<div/>'),
|
||||
$messageElement = $('<div/>'),
|
||||
$closeElement = $(options.closeHtml),
|
||||
response = {
|
||||
toastId: toastId,
|
||||
state: 'visible',
|
||||
startTime: new Date(),
|
||||
options: options,
|
||||
map: map
|
||||
};
|
||||
|
||||
if (map.iconClass) {
|
||||
$toastElement.addClass(options.toastClass).addClass(iconClass);
|
||||
}
|
||||
|
||||
if (map.title) {
|
||||
$titleElement.append(map.title).addClass(options.titleClass);
|
||||
$toastElement.append($titleElement);
|
||||
}
|
||||
|
||||
if (map.message) {
|
||||
$messageElement.append(map.message).addClass(options.messageClass);
|
||||
$toastElement.append($messageElement);
|
||||
}
|
||||
|
||||
if (options.closeButton) {
|
||||
$closeElement.addClass('toast-close-button').attr("role", "button");
|
||||
$toastElement.prepend($closeElement);
|
||||
}
|
||||
|
||||
if (options.reposition) {
|
||||
options.reposition();
|
||||
}
|
||||
|
||||
$toastElement.hide();
|
||||
if (options.newestOnTop) {
|
||||
$container.prepend($toastElement);
|
||||
} else {
|
||||
$container.append($toastElement);
|
||||
}
|
||||
|
||||
|
||||
$toastElement[options.showMethod](
|
||||
{ duration: options.showDuration, easing: options.showEasing, complete: options.onShown }
|
||||
);
|
||||
|
||||
if (options.timeOut > 0) {
|
||||
intervalId = setTimeout(hideToast, options.timeOut);
|
||||
}
|
||||
|
||||
$toastElement.hover(stickAround, delayedHideToast);
|
||||
if (!options.onclick && options.tapToDismiss) {
|
||||
$toastElement.click(hideToast);
|
||||
}
|
||||
|
||||
if (options.closeButton && $closeElement) {
|
||||
$closeElement.click(function (event) {
|
||||
if( event.stopPropagation ) {
|
||||
event.stopPropagation();
|
||||
} else if( event.cancelBubble !== undefined && event.cancelBubble !== true ) {
|
||||
event.cancelBubble = true;
|
||||
}
|
||||
hideToast(true);
|
||||
});
|
||||
}
|
||||
|
||||
if (options.onclick) {
|
||||
$toastElement.click(function () {
|
||||
options.onclick();
|
||||
hideToast();
|
||||
});
|
||||
}
|
||||
|
||||
publish(response);
|
||||
|
||||
if (options.debug && console) {
|
||||
console.log(response);
|
||||
}
|
||||
|
||||
return $toastElement;
|
||||
|
||||
function hideToast(override) {
|
||||
if ($(':focus', $toastElement).length && !override) {
|
||||
return;
|
||||
}
|
||||
return $toastElement[options.hideMethod]({
|
||||
duration: options.hideDuration,
|
||||
easing: options.hideEasing,
|
||||
complete: function () {
|
||||
removeToast($toastElement);
|
||||
if (options.onHidden && response.state !== 'hidden') {
|
||||
options.onHidden();
|
||||
}
|
||||
response.state = 'hidden';
|
||||
response.endTime = new Date();
|
||||
publish(response);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function delayedHideToast() {
|
||||
if (options.timeOut > 0 || options.extendedTimeOut > 0) {
|
||||
intervalId = setTimeout(hideToast, options.extendedTimeOut);
|
||||
}
|
||||
}
|
||||
|
||||
function stickAround() {
|
||||
clearTimeout(intervalId);
|
||||
$toastElement.stop(true, true)[options.showMethod](
|
||||
{ duration: options.showDuration, easing: options.showEasing }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getOptions() {
|
||||
return $.extend({}, getDefaults(), toastr.options);
|
||||
}
|
||||
|
||||
function removeToast($toastElement) {
|
||||
if (!$container) { $container = getContainer(); }
|
||||
if ($toastElement.is(':visible')) {
|
||||
return;
|
||||
}
|
||||
$toastElement.remove();
|
||||
$toastElement = null;
|
||||
if ($container.children().length === 0) {
|
||||
$container.remove();
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
|
||||
})();
|
||||
});
|
||||
}(typeof define === 'function' && define.amd ? define : function (deps, factory) {
|
||||
if (typeof module !== 'undefined' && module.exports) { //Node
|
||||
module.exports = factory(require('jquery'));
|
||||
} else {
|
||||
window['toastr'] = factory(window['jQuery']);
|
||||
}
|
||||
}));
|
||||
131
local_sts.js
@@ -1,131 +0,0 @@
|
||||
/**
|
||||
* Provides statistics for the local stream.
|
||||
*/
|
||||
var LocalStatsCollector = (function() {
|
||||
/**
|
||||
* Size of the webaudio analizer buffer.
|
||||
* @type {number}
|
||||
*/
|
||||
var WEBAUDIO_ANALIZER_FFT_SIZE = 2048;
|
||||
|
||||
/**
|
||||
* Value of the webaudio analizer smoothing time parameter.
|
||||
* @type {number}
|
||||
*/
|
||||
var WEBAUDIO_ANALIZER_SMOOTING_TIME = 0.8;
|
||||
|
||||
/**
|
||||
* <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.audioLevelsUpdateCallback = 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.getByteTimeDomainData(array);
|
||||
var audioLevel = TimeDomainDataToAudioLevel(array);
|
||||
if(audioLevel != self.audioLevel) {
|
||||
self.audioLevel = animateLevel(audioLevel, self.audioLevel);
|
||||
self.audioLevelsUpdateCallback(LocalStatsCollectorProto.LOCAL_JID, self.audioLevel);
|
||||
}
|
||||
},
|
||||
this.intervalMilis
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Stops collecting the statistics.
|
||||
*/
|
||||
LocalStatsCollectorProto.prototype.stop = function () {
|
||||
if (this.intervalId) {
|
||||
clearInterval(this.intervalId);
|
||||
this.intervalId = null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts time domain data array to audio level.
|
||||
* @param array the time domain data array.
|
||||
* @returns {number} the audio level
|
||||
*/
|
||||
var TimeDomainDataToAudioLevel = function (samples) {
|
||||
|
||||
var maxVolume = 0;
|
||||
|
||||
var length = samples.length;
|
||||
|
||||
for (var i = 0; i < length; i++) {
|
||||
if (maxVolume < samples[i])
|
||||
maxVolume = samples[i];
|
||||
}
|
||||
|
||||
return parseFloat(((maxVolume - 127) / 128).toFixed(3));
|
||||
};
|
||||
|
||||
/**
|
||||
* Animates audio level change
|
||||
* @param newLevel the new audio level
|
||||
* @param lastLevel the last audio level
|
||||
* @returns {Number} the audio level to be set
|
||||
*/
|
||||
function animateLevel(newLevel, lastLevel)
|
||||
{
|
||||
var value = 0;
|
||||
var diff = lastLevel - newLevel;
|
||||
if(diff > 0.2)
|
||||
{
|
||||
value = lastLevel - 0.2;
|
||||
}
|
||||
else if(diff < -0.4)
|
||||
{
|
||||
value = lastLevel + 0.4;
|
||||
}
|
||||
else
|
||||
{
|
||||
value = newLevel;
|
||||
}
|
||||
|
||||
return parseFloat(value.toFixed(3));
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that this audio level is for local jid.
|
||||
* @type {string}
|
||||
*/
|
||||
LocalStatsCollectorProto.LOCAL_JID = 'local';
|
||||
|
||||
return LocalStatsCollectorProto;
|
||||
})();
|
||||
@@ -1,30 +0,0 @@
|
||||
/**
|
||||
* Provides a wrapper class for the MediaStream.
|
||||
*
|
||||
* TODO : Add here the src from the video element and other related properties
|
||||
* and get rid of some of the mappings that we use throughout the UI.
|
||||
*/
|
||||
var MediaStream = (function() {
|
||||
/**
|
||||
* Creates a MediaStream object for the given data, session id and ssrc.
|
||||
*
|
||||
* @param data the data object from which we obtain the stream,
|
||||
* the peerjid, etc.
|
||||
* @param sid the session id
|
||||
* @param ssrc the ssrc corresponding to this MediaStream
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
function MediaStreamProto(data, sid, ssrc) {
|
||||
this.VIDEO_TYPE = "Video";
|
||||
this.AUDIO_TYPE = "Audio";
|
||||
this.stream = data.stream;
|
||||
this.peerjid = data.peerjid;
|
||||
this.ssrc = ssrc;
|
||||
this.session = connection.jingle.sessions[sid];
|
||||
this.type = (this.stream.getVideoTracks().length > 0)
|
||||
? this.VIDEO_TYPE : this.AUDIO_TYPE;
|
||||
}
|
||||
|
||||
return MediaStreamProto;
|
||||
})();
|
||||
@@ -1,112 +0,0 @@
|
||||
var messageHandler = (function(my) {
|
||||
|
||||
/**
|
||||
* Shows a message to the user.
|
||||
*
|
||||
* @param titleString the title of the message
|
||||
* @param messageString the text of the message
|
||||
*/
|
||||
my.openMessageDialog = function(titleString, messageString) {
|
||||
$.prompt(messageString,
|
||||
{
|
||||
title: titleString,
|
||||
persistent: false
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows a message to the user with two buttons: first is given as a parameter and the second is Cancel.
|
||||
*
|
||||
* @param titleString the title of the message
|
||||
* @param msgString the text of the message
|
||||
* @param persistent boolean value which determines whether the message is persistent or not
|
||||
* @param leftButton the fist button's text
|
||||
* @param submitFunction function to be called on submit
|
||||
* @param loadedFunction function to be called after the prompt is fully loaded
|
||||
* @param closeFunction function to be called after the prompt is closed
|
||||
*/
|
||||
my.openTwoButtonDialog = function(titleString, msgString, persistent, leftButton,
|
||||
submitFunction, loadedFunction, closeFunction) {
|
||||
var buttons = {};
|
||||
buttons[leftButton] = true;
|
||||
buttons.Cancel = false;
|
||||
$.prompt(msgString, {
|
||||
title: titleString,
|
||||
persistent: false,
|
||||
buttons: buttons,
|
||||
defaultButton: 1,
|
||||
loaded: loadedFunction,
|
||||
submit: submitFunction,
|
||||
close: closeFunction
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows a message to the user with two buttons: first is given as a parameter and the second is Cancel.
|
||||
*
|
||||
* @param titleString the title of the message
|
||||
* @param msgString the text of the message
|
||||
* @param persistent boolean value which determines whether the message is persistent or not
|
||||
* @param buttons object with the buttons. The keys must be the name of the button and value is the value
|
||||
* that will be passed to submitFunction
|
||||
* @param submitFunction function to be called on submit
|
||||
* @param loadedFunction function to be called after the prompt is fully loaded
|
||||
*/
|
||||
my.openDialog = function(titleString, msgString, persistent, buttons, submitFunction, loadedFunction) {
|
||||
$.prompt(msgString, {
|
||||
title: titleString,
|
||||
persistent: false,
|
||||
buttons: buttons,
|
||||
defaultButton: 1,
|
||||
loaded: loadedFunction,
|
||||
submit: submitFunction
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows a dialog with different states to the user.
|
||||
*
|
||||
* @param statesObject object containing all the states of the dialog
|
||||
* @param loadedFunction function to be called after the prompt is fully loaded
|
||||
* @param stateChangedFunction function to be called when the state of the dialog is changed
|
||||
*/
|
||||
my.openDialogWithStates = function(statesObject, loadedFunction, stateChangedFunction) {
|
||||
|
||||
|
||||
var myPrompt = $.prompt(statesObject);
|
||||
|
||||
myPrompt.on('impromptu:loaded', loadedFunction);
|
||||
myPrompt.on('impromptu:statechanged', stateChangedFunction);
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows a dialog prompting the user to send an error report.
|
||||
*
|
||||
* @param titleString the title of the message
|
||||
* @param msgString the text of the message
|
||||
* @param error the error that is being reported
|
||||
*/
|
||||
my.openReportDialog = function(titleString, msgString, error) {
|
||||
my.openMessageDialog(titleString, msgString);
|
||||
console.log(error);
|
||||
//FIXME send the error to the server
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows an error dialog to the user.
|
||||
* @param title the title of the message
|
||||
* @param message the text of the messafe
|
||||
*/
|
||||
my.showError = function(title, message) {
|
||||
if(!(title || message)) {
|
||||
title = title || "Oops!";
|
||||
message = message || "There was some kind of error";
|
||||
}
|
||||
messageHandler.openMessageDialog(title, message);
|
||||
};
|
||||
|
||||
return my;
|
||||
}(messageHandler || {}));
|
||||
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
/**
|
||||
* Moderate connection plugin.
|
||||
*/
|
||||
Strophe.addConnectionPlugin('moderate', {
|
||||
connection: null,
|
||||
roomjid: null,
|
||||
myroomjid: null,
|
||||
members: {},
|
||||
list_members: [], // so we can elect a new focus
|
||||
presMap: {},
|
||||
preziMap: {},
|
||||
joined: false,
|
||||
isOwner: false,
|
||||
init: function (conn) {
|
||||
this.connection = conn;
|
||||
|
||||
this.connection.addHandler( this.onMute.bind(this),
|
||||
'http://jitsi.org/jitmeet/audio',
|
||||
'iq',
|
||||
'set',
|
||||
null,
|
||||
null);
|
||||
},
|
||||
setMute: function(jid, mute) {
|
||||
var iq = $iq({to: jid, type: 'set'})
|
||||
.c('mute', {xmlns: 'http://jitsi.org/jitmeet/audio'})
|
||||
.t(mute.toString())
|
||||
.up();
|
||||
|
||||
this.connection.sendIQ(
|
||||
iq,
|
||||
function (result) {
|
||||
console.log('set mute', result);
|
||||
},
|
||||
function (error) {
|
||||
console.log('set mute error', error);
|
||||
messageHandler.openReportDialog(null, 'Failed to mute ' +
|
||||
$("#participant_" + jid).find(".displayname").text() ||
|
||||
"participant" + '.', error);
|
||||
});
|
||||
},
|
||||
onMute: function(iq) {
|
||||
var mute = $(iq).find('mute');
|
||||
if (mute.length) {
|
||||
toggleAudio();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
eject: function(jid) {
|
||||
connection.jingle.terminateRemoteByJid(jid, 'kick');
|
||||
connection.emuc.kick(jid);
|
||||
}
|
||||
});
|
||||
231
modules/API/API.js
Normal file
@@ -0,0 +1,231 @@
|
||||
/**
|
||||
* 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 XMPPEvents = require("../../service/xmpp/XMPPEvents");
|
||||
|
||||
/**
|
||||
* List of the available commands.
|
||||
* @type {{
|
||||
* displayName: inputDisplayNameHandler,
|
||||
* muteAudio: toggleAudio,
|
||||
* muteVideo: toggleVideo,
|
||||
* filmStrip: toggleFilmStrip
|
||||
* }}
|
||||
*/
|
||||
var commands =
|
||||
{
|
||||
displayName: APP.UI.inputDisplayNameHandler,
|
||||
muteAudio: APP.UI.toggleAudio,
|
||||
muteVideo: APP.UI.toggleVideo,
|
||||
toggleFilmStrip: APP.UI.toggleFilmStrip,
|
||||
toggleChat: APP.UI.toggleChat,
|
||||
toggleContactList: APP.UI.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
|
||||
};
|
||||
|
||||
var displayName = {};
|
||||
|
||||
/**
|
||||
* Processes commands from external applicaiton.
|
||||
* @param message the object with the command
|
||||
*/
|
||||
function processCommand(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
|
||||
*/
|
||||
function processEvent(event) {
|
||||
if(!event.action)
|
||||
{
|
||||
console.error("Event with no action is received.");
|
||||
return;
|
||||
}
|
||||
|
||||
var i = 0;
|
||||
switch(event.action)
|
||||
{
|
||||
case "add":
|
||||
for(; i < event.events.length; i++)
|
||||
{
|
||||
events[event.events[i]] = true;
|
||||
}
|
||||
break;
|
||||
case "remove":
|
||||
for(; i < event.events.length; i++)
|
||||
{
|
||||
events[event.events[i]] = false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.error("Unknown action for event.");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends message to the external application.
|
||||
* @param object
|
||||
*/
|
||||
function sendMessage(object) {
|
||||
window.parent.postMessage(JSON.stringify(object), "*");
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a message event from the external application
|
||||
* @param event the message event
|
||||
*/
|
||||
function processMessage(event)
|
||||
{
|
||||
var message;
|
||||
try {
|
||||
message = JSON.parse(event.data);
|
||||
} catch (e) {}
|
||||
|
||||
if(!message.type)
|
||||
return;
|
||||
switch (message.type)
|
||||
{
|
||||
case "command":
|
||||
processCommand(message);
|
||||
break;
|
||||
case "event":
|
||||
processEvent(message);
|
||||
break;
|
||||
default:
|
||||
console.error("Unknown type of the message");
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function setupListeners() {
|
||||
APP.xmpp.addListener(XMPPEvents.MUC_ENTER, function (from) {
|
||||
API.triggerEvent("participantJoined", {jid: from});
|
||||
});
|
||||
APP.xmpp.addListener(XMPPEvents.MESSAGE_RECEIVED, function (from, nick, txt, myjid) {
|
||||
if (from != myjid)
|
||||
API.triggerEvent("incomingMessage",
|
||||
{"from": from, "nick": nick, "message": txt});
|
||||
});
|
||||
APP.xmpp.addListener(XMPPEvents.MUC_LEFT, function (jid) {
|
||||
API.triggerEvent("participantLeft", {jid: jid});
|
||||
});
|
||||
APP.xmpp.addListener(XMPPEvents.DISPLAY_NAME_CHANGED, function (jid, newDisplayName) {
|
||||
name = displayName[jid];
|
||||
if(!name || name != newDisplayName) {
|
||||
API.triggerEvent("displayNameChange", {jid: jid, displayname: newDisplayName});
|
||||
displayName[jid] = newDisplayName;
|
||||
}
|
||||
});
|
||||
APP.xmpp.addListener(XMPPEvents.SENDING_CHAT_MESSAGE, function (body) {
|
||||
APP.API.triggerEvent("outgoingMessage", {"message": body});
|
||||
});
|
||||
}
|
||||
|
||||
var API = {
|
||||
/**
|
||||
* Check whether the API should be enabled or not.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
init: function () {
|
||||
if (window.addEventListener)
|
||||
{
|
||||
window.addEventListener('message',
|
||||
processMessage, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
window.attachEvent('onmessage', processMessage);
|
||||
}
|
||||
sendMessage({type: "system", loaded: true});
|
||||
setupListeners();
|
||||
},
|
||||
/**
|
||||
* Checks whether the event is enabled ot not.
|
||||
* @param name the name of the event.
|
||||
* @returns {*}
|
||||
*/
|
||||
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
|
||||
*/
|
||||
triggerEvent: function (name, object) {
|
||||
if(this.isEnabled() && this.isEventEnabled(name))
|
||||
sendMessage({
|
||||
type: "event", action: "result", event: name, result: object});
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes the listeners.
|
||||
*/
|
||||
dispose: function () {
|
||||
if(window.removeEventListener)
|
||||
{
|
||||
window.removeEventListener("message",
|
||||
processMessage, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
window.detachEvent('onmessage', processMessage);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
module.exports = API;
|
||||
234
modules/RTC/DataChannels.js
Normal file
@@ -0,0 +1,234 @@
|
||||
/* global Strophe, updateLargeVideo, focusedVideoSrc*/
|
||||
|
||||
// cache datachannels to avoid garbage collection
|
||||
// https://code.google.com/p/chromium/issues/detail?id=405545
|
||||
var RTCEvents = require("../../service/RTC/RTCEvents");
|
||||
|
||||
var _dataChannels = [];
|
||||
var eventEmitter = null;
|
||||
|
||||
|
||||
|
||||
|
||||
var DataChannels =
|
||||
{
|
||||
|
||||
/**
|
||||
* Callback triggered by PeerConnection when new data channel is opened
|
||||
* on the bridge.
|
||||
* @param event the event info object.
|
||||
*/
|
||||
|
||||
onDataChannel: function (event)
|
||||
{
|
||||
var dataChannel = event.channel;
|
||||
|
||||
dataChannel.onopen = function () {
|
||||
console.info("Data channel opened by the Videobridge!", 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));
|
||||
|
||||
// when the data channel becomes available, tell the bridge about video
|
||||
// selections so that it can do adaptive simulcast,
|
||||
// we want the notification to trigger even if userJid is undefined,
|
||||
// or null.
|
||||
var userJid = APP.UI.getLargeVideoState().userResourceJid;
|
||||
// we want the notification to trigger even if userJid is undefined,
|
||||
// or null.
|
||||
onSelectedEndpointChanged(userJid);
|
||||
};
|
||||
|
||||
dataChannel.onerror = function (error) {
|
||||
console.error("Data Channel Error:", error, dataChannel);
|
||||
};
|
||||
|
||||
dataChannel.onmessage = function (event) {
|
||||
var data = event.data;
|
||||
// JSON
|
||||
var obj;
|
||||
|
||||
try {
|
||||
obj = JSON.parse(data);
|
||||
}
|
||||
catch (e) {
|
||||
console.error(
|
||||
"Failed to parse data channel message as JSON: ",
|
||||
data,
|
||||
dataChannel);
|
||||
}
|
||||
if (('undefined' !== typeof(obj)) && (null !== obj)) {
|
||||
var colibriClass = obj.colibriClass;
|
||||
|
||||
if ("DominantSpeakerEndpointChangeEvent" === colibriClass) {
|
||||
// Endpoint ID from the Videobridge.
|
||||
var dominantSpeakerEndpoint = obj.dominantSpeakerEndpoint;
|
||||
|
||||
console.info(
|
||||
"Data channel new dominant speaker event: ",
|
||||
dominantSpeakerEndpoint);
|
||||
eventEmitter.emit(RTCEvents.DOMINANTSPEAKER_CHANGED, dominantSpeakerEndpoint);
|
||||
}
|
||||
else if ("InLastNChangeEvent" === colibriClass)
|
||||
{
|
||||
var oldValue = obj.oldValue;
|
||||
var newValue = obj.newValue;
|
||||
// Make sure that oldValue and newValue are of type boolean.
|
||||
var type;
|
||||
|
||||
if ((type = typeof oldValue) !== 'boolean') {
|
||||
if (type === 'string') {
|
||||
oldValue = (oldValue == "true");
|
||||
} else {
|
||||
oldValue = new Boolean(oldValue).valueOf();
|
||||
}
|
||||
}
|
||||
if ((type = typeof newValue) !== 'boolean') {
|
||||
if (type === 'string') {
|
||||
newValue = (newValue == "true");
|
||||
} else {
|
||||
newValue = new Boolean(newValue).valueOf();
|
||||
}
|
||||
}
|
||||
|
||||
eventEmitter.emit(RTCEvents.LASTN_CHANGED, oldValue, newValue);
|
||||
}
|
||||
else if ("LastNEndpointsChangeEvent" === colibriClass)
|
||||
{
|
||||
// The new/latest list of last-n endpoint IDs.
|
||||
var lastNEndpoints = obj.lastNEndpoints;
|
||||
// The list of endpoint IDs which are entering the list of
|
||||
// last-n at this time i.e. were not in the old list of last-n
|
||||
// endpoint IDs.
|
||||
var endpointsEnteringLastN = obj.endpointsEnteringLastN;
|
||||
var stream = obj.stream;
|
||||
|
||||
console.log(
|
||||
"Data channel new last-n event: ",
|
||||
lastNEndpoints, endpointsEnteringLastN, obj);
|
||||
eventEmitter.emit(RTCEvents.LASTN_ENDPOINT_CHANGED,
|
||||
lastNEndpoints, endpointsEnteringLastN, obj);
|
||||
}
|
||||
else if ("SimulcastLayersChangedEvent" === colibriClass)
|
||||
{
|
||||
eventEmitter.emit(RTCEvents.SIMULCAST_LAYER_CHANGED,
|
||||
obj.endpointSimulcastLayers);
|
||||
}
|
||||
else if ("SimulcastLayersChangingEvent" === colibriClass)
|
||||
{
|
||||
eventEmitter.emit(RTCEvents.SIMULCAST_LAYER_CHANGING,
|
||||
obj.endpointSimulcastLayers);
|
||||
}
|
||||
else if ("StartSimulcastLayerEvent" === colibriClass)
|
||||
{
|
||||
eventEmitter.emit(RTCEvents.SIMULCAST_START, obj.simulcastLayer);
|
||||
}
|
||||
else if ("StopSimulcastLayerEvent" === colibriClass)
|
||||
{
|
||||
eventEmitter.emit(RTCEvents.SIMULCAST_STOP, obj.simulcastLayer);
|
||||
}
|
||||
else
|
||||
{
|
||||
console.debug("Data channel JSON-formatted message: ", obj);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
dataChannel.onclose = function ()
|
||||
{
|
||||
console.info("The Data Channel closed", dataChannel);
|
||||
var idx = _dataChannels.indexOf(dataChannel);
|
||||
if (idx > -1)
|
||||
_dataChannels = _dataChannels.splice(idx, 1);
|
||||
};
|
||||
_dataChannels.push(dataChannel);
|
||||
},
|
||||
|
||||
/**
|
||||
* Binds "ondatachannel" event listener to given PeerConnection instance.
|
||||
* @param peerConnection WebRTC peer connection instance.
|
||||
*/
|
||||
init: function (peerConnection, emitter) {
|
||||
if(!config.openSctp)
|
||||
return;
|
||||
|
||||
peerConnection.ondatachannel = this.onDataChannel;
|
||||
eventEmitter = emitter;
|
||||
|
||||
// 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);
|
||||
};*/
|
||||
},
|
||||
handleSelectedEndpointEvent: onSelectedEndpointChanged,
|
||||
handlePinnedEndpointEvent: onPinnedEndpointChanged
|
||||
|
||||
};
|
||||
|
||||
function onSelectedEndpointChanged(userResource)
|
||||
{
|
||||
console.log('selected endpoint changed: ', userResource);
|
||||
if (_dataChannels && _dataChannels.length != 0)
|
||||
{
|
||||
_dataChannels.some(function (dataChannel) {
|
||||
if (dataChannel.readyState == 'open')
|
||||
{
|
||||
console.log('sending selected endpoint changed '
|
||||
+ 'notification to the bridge: ', userResource);
|
||||
dataChannel.send(JSON.stringify({
|
||||
'colibriClass': 'SelectedEndpointChangedEvent',
|
||||
'selectedEndpoint':
|
||||
(!userResource || userResource === null)?
|
||||
null : userResource
|
||||
}));
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function onPinnedEndpointChanged(userResource)
|
||||
{
|
||||
console.log('pinned endpoint changed: ', userResource);
|
||||
if (_dataChannels && _dataChannels.length != 0)
|
||||
{
|
||||
_dataChannels.some(function (dataChannel) {
|
||||
if (dataChannel.readyState == 'open')
|
||||
{
|
||||
dataChannel.send(JSON.stringify({
|
||||
'colibriClass': 'PinnedEndpointChangedEvent',
|
||||
'pinnedEndpoint':
|
||||
(!userResource || userResource == null)?
|
||||
null : userResource
|
||||
}));
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DataChannels;
|
||||
|
||||
86
modules/RTC/LocalStream.js
Normal file
@@ -0,0 +1,86 @@
|
||||
var StreamEventTypes = require("../../service/RTC/StreamEventTypes.js");
|
||||
|
||||
function LocalStream(stream, type, eventEmitter, videoType)
|
||||
{
|
||||
this.stream = stream;
|
||||
this.eventEmitter = eventEmitter;
|
||||
this.type = type;
|
||||
this.videoType = videoType;
|
||||
var self = this;
|
||||
if(type == "audio")
|
||||
{
|
||||
this.getTracks = function () {
|
||||
return self.stream.getAudioTracks();
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
this.getTracks = function () {
|
||||
return self.stream.getVideoTracks();
|
||||
};
|
||||
}
|
||||
|
||||
this.stream.onended = function()
|
||||
{
|
||||
self.streamEnded();
|
||||
};
|
||||
}
|
||||
|
||||
LocalStream.prototype.streamEnded = function () {
|
||||
this.eventEmitter.emit(StreamEventTypes.EVENT_TYPE_LOCAL_ENDED, this);
|
||||
}
|
||||
|
||||
LocalStream.prototype.getOriginalStream = function()
|
||||
{
|
||||
return this.stream;
|
||||
}
|
||||
|
||||
LocalStream.prototype.isAudioStream = function () {
|
||||
return (this.stream.getAudioTracks() && this.stream.getAudioTracks().length > 0);
|
||||
};
|
||||
|
||||
LocalStream.prototype.mute = function()
|
||||
{
|
||||
var ismuted = false;
|
||||
var tracks = this.getTracks();
|
||||
|
||||
for (var idx = 0; idx < tracks.length; idx++) {
|
||||
ismuted = !tracks[idx].enabled;
|
||||
tracks[idx].enabled = ismuted;
|
||||
}
|
||||
return ismuted;
|
||||
};
|
||||
|
||||
LocalStream.prototype.setMute = function(mute)
|
||||
{
|
||||
var tracks = this.getTracks();
|
||||
|
||||
for (var idx = 0; idx < tracks.length; idx++) {
|
||||
tracks[idx].enabled = mute;
|
||||
}
|
||||
};
|
||||
|
||||
LocalStream.prototype.isMuted = function () {
|
||||
var tracks = [];
|
||||
if(this.type == "audio")
|
||||
{
|
||||
tracks = this.stream.getAudioTracks();
|
||||
}
|
||||
else
|
||||
{
|
||||
tracks = this.stream.getVideoTracks();
|
||||
}
|
||||
for (var idx = 0; idx < tracks.length; idx++) {
|
||||
if(tracks[idx].enabled)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
LocalStream.prototype.getId = function () {
|
||||
return this.stream.getTracks()[0].id;
|
||||
}
|
||||
|
||||
|
||||
|
||||
module.exports = LocalStream;
|
||||
51
modules/RTC/MediaStream.js
Normal file
@@ -0,0 +1,51 @@
|
||||
////These lines should be uncommented when require works in app.js
|
||||
var RTCBrowserType = require("../../service/RTC/RTCBrowserType.js");
|
||||
var MediaStreamType = require("../../service/RTC/MediaStreamTypes");
|
||||
|
||||
/**
|
||||
* Creates a MediaStream object for the given data, session id and ssrc.
|
||||
* It is a wrapper class for the MediaStream.
|
||||
*
|
||||
* @param data the data object from which we obtain the stream,
|
||||
* the peerjid, etc.
|
||||
* @param sid the session id
|
||||
* @param ssrc the ssrc corresponding to this MediaStream
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
function MediaStream(data, sid, ssrc, browser) {
|
||||
|
||||
// XXX(gp) to minimize headaches in the future, we should build our
|
||||
// abstractions around tracks and not streams. ORTC is track based API.
|
||||
// Mozilla expects m-lines to represent media tracks.
|
||||
//
|
||||
// Practically, what I'm saying is that we should have a MediaTrack class
|
||||
// and not a MediaStream class.
|
||||
//
|
||||
// Also, we should be able to associate multiple SSRCs with a MediaTrack as
|
||||
// a track might have an associated RTX and FEC sources.
|
||||
|
||||
this.sid = sid;
|
||||
this.stream = data.stream;
|
||||
this.peerjid = data.peerjid;
|
||||
this.ssrc = ssrc;
|
||||
this.type = (this.stream.getVideoTracks().length > 0)?
|
||||
MediaStreamType.VIDEO_TYPE : MediaStreamType.AUDIO_TYPE;
|
||||
this.videoType = null;
|
||||
this.muted = false;
|
||||
}
|
||||
|
||||
|
||||
MediaStream.prototype.getOriginalStream = function()
|
||||
{
|
||||
return this.stream;
|
||||
}
|
||||
|
||||
MediaStream.prototype.setMute = function (value)
|
||||
{
|
||||
this.stream.muted = value;
|
||||
this.muted = value;
|
||||
}
|
||||
|
||||
|
||||
module.exports = MediaStream;
|
||||
204
modules/RTC/RTC.js
Normal file
@@ -0,0 +1,204 @@
|
||||
var EventEmitter = require("events");
|
||||
var RTCUtils = require("./RTCUtils.js");
|
||||
var LocalStream = require("./LocalStream.js");
|
||||
var DataChannels = require("./DataChannels");
|
||||
var MediaStream = require("./MediaStream.js");
|
||||
var DesktopSharingEventTypes
|
||||
= require("../../service/desktopsharing/DesktopSharingEventTypes");
|
||||
var MediaStreamType = require("../../service/RTC/MediaStreamTypes");
|
||||
var StreamEventTypes = require("../../service/RTC/StreamEventTypes.js");
|
||||
var XMPPEvents = require("../../service/xmpp/XMPPEvents");
|
||||
var UIEvents = require("../../service/UI/UIEvents");
|
||||
|
||||
var eventEmitter = new EventEmitter();
|
||||
|
||||
var RTC = {
|
||||
rtcUtils: null,
|
||||
localStreams: [],
|
||||
remoteStreams: {},
|
||||
localAudio: null,
|
||||
localVideo: null,
|
||||
addStreamListener: function (listener, eventType) {
|
||||
eventEmitter.on(eventType, listener);
|
||||
},
|
||||
addListener: function (type, listener) {
|
||||
eventEmitter.on(type, listener);
|
||||
},
|
||||
removeStreamListener: function (listener, eventType) {
|
||||
if(!(eventType instanceof StreamEventTypes))
|
||||
throw "Illegal argument";
|
||||
|
||||
eventEmitter.removeListener(eventType, listener);
|
||||
},
|
||||
createLocalStream: function (stream, type, change) {
|
||||
|
||||
var localStream = new LocalStream(stream, type, eventEmitter);
|
||||
//in firefox we have only one stream object
|
||||
if(this.localStreams.length == 0 ||
|
||||
this.localStreams[0].getOriginalStream() != stream)
|
||||
this.localStreams.push(localStream);
|
||||
if(type == "audio")
|
||||
{
|
||||
this.localAudio = localStream;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.localVideo = localStream;
|
||||
}
|
||||
var eventType = StreamEventTypes.EVENT_TYPE_LOCAL_CREATED;
|
||||
if(change)
|
||||
eventType = StreamEventTypes.EVENT_TYPE_LOCAL_CHANGED;
|
||||
|
||||
eventEmitter.emit(eventType, localStream);
|
||||
return localStream;
|
||||
},
|
||||
removeLocalStream: function (stream) {
|
||||
for(var i = 0; i < this.localStreams.length; i++)
|
||||
{
|
||||
if(this.localStreams[i].getOriginalStream() === stream) {
|
||||
delete this.localStreams[i];
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
createRemoteStream: function (data, sid, thessrc) {
|
||||
var remoteStream = new MediaStream(data, sid, thessrc,
|
||||
this.getBrowserType());
|
||||
var jid = data.peerjid || APP.xmpp.myJid();
|
||||
if(!this.remoteStreams[jid]) {
|
||||
this.remoteStreams[jid] = {};
|
||||
}
|
||||
this.remoteStreams[jid][remoteStream.type]= remoteStream;
|
||||
eventEmitter.emit(StreamEventTypes.EVENT_TYPE_REMOTE_CREATED, remoteStream);
|
||||
console.debug("ADD remote stream ", remoteStream.type, " ", jid, " ", thessrc);
|
||||
return remoteStream;
|
||||
},
|
||||
getBrowserType: function () {
|
||||
return this.rtcUtils.browser;
|
||||
},
|
||||
getPCConstraints: function () {
|
||||
return this.rtcUtils.pc_constraints;
|
||||
},
|
||||
getUserMediaWithConstraints:function(um, success_callback,
|
||||
failure_callback, resolution,
|
||||
bandwidth, fps, desktopStream)
|
||||
{
|
||||
return this.rtcUtils.getUserMediaWithConstraints(um, success_callback,
|
||||
failure_callback, resolution, bandwidth, fps, desktopStream);
|
||||
},
|
||||
attachMediaStream: function (element, stream) {
|
||||
this.rtcUtils.attachMediaStream(element, stream);
|
||||
},
|
||||
getStreamID: function (stream) {
|
||||
return this.rtcUtils.getStreamID(stream);
|
||||
},
|
||||
getVideoSrc: function (element) {
|
||||
return this.rtcUtils.getVideoSrc(element);
|
||||
},
|
||||
setVideoSrc: function (element, src) {
|
||||
this.rtcUtils.setVideoSrc(element, src);
|
||||
},
|
||||
dispose: function() {
|
||||
if (this.rtcUtils) {
|
||||
this.rtcUtils = null;
|
||||
}
|
||||
},
|
||||
stop: function () {
|
||||
this.dispose();
|
||||
},
|
||||
start: function () {
|
||||
var self = this;
|
||||
APP.desktopsharing.addListener(
|
||||
function (stream, isUsingScreenStream, callback) {
|
||||
self.changeLocalVideo(stream, isUsingScreenStream, callback);
|
||||
}, DesktopSharingEventTypes.NEW_STREAM_CREATED);
|
||||
APP.xmpp.addListener(XMPPEvents.CHANGED_STREAMS, function (jid, changedStreams) {
|
||||
for(var i = 0; i < changedStreams.length; i++) {
|
||||
var type = changedStreams[i].type;
|
||||
if (type != "audio") {
|
||||
var peerStreams = self.remoteStreams[jid];
|
||||
if(!peerStreams)
|
||||
continue;
|
||||
var videoStream = peerStreams[MediaStreamType.VIDEO_TYPE];
|
||||
if(!videoStream)
|
||||
continue;
|
||||
videoStream.videoType = changedStreams[i].type;
|
||||
}
|
||||
}
|
||||
});
|
||||
APP.xmpp.addListener(XMPPEvents.CALL_INCOMING, function(event) {
|
||||
DataChannels.init(event.peerconnection, eventEmitter);
|
||||
});
|
||||
APP.UI.addListener(UIEvents.SELECTED_ENDPOINT,
|
||||
DataChannels.handleSelectedEndpointEvent);
|
||||
APP.UI.addListener(UIEvents.PINNED_ENDPOINT,
|
||||
DataChannels.handlePinnedEndpointEvent);
|
||||
this.rtcUtils = new RTCUtils(this);
|
||||
this.rtcUtils.obtainAudioAndVideoPermissions();
|
||||
},
|
||||
muteRemoteVideoStream: function (jid, value) {
|
||||
var stream;
|
||||
|
||||
if(this.remoteStreams[jid] &&
|
||||
this.remoteStreams[jid][MediaStreamType.VIDEO_TYPE])
|
||||
{
|
||||
stream = this.remoteStreams[jid][MediaStreamType.VIDEO_TYPE];
|
||||
}
|
||||
|
||||
if(!stream)
|
||||
return false;
|
||||
|
||||
if (value != stream.muted) {
|
||||
stream.setMute(value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
switchVideoStreams: function (new_stream) {
|
||||
this.localVideo.stream = new_stream;
|
||||
|
||||
this.localStreams = [];
|
||||
|
||||
//in firefox we have only one stream object
|
||||
if (this.localAudio.getOriginalStream() != new_stream)
|
||||
this.localStreams.push(this.localAudio);
|
||||
this.localStreams.push(this.localVideo);
|
||||
},
|
||||
changeLocalVideo: function (stream, isUsingScreenStream, callback) {
|
||||
var oldStream = this.localVideo.getOriginalStream();
|
||||
var type = (isUsingScreenStream? "screen" : "video");
|
||||
this.localVideo = this.createLocalStream(stream, "video", true, type);
|
||||
// Stop the stream to trigger onended event for old stream
|
||||
oldStream.stop();
|
||||
APP.xmpp.switchStreams(stream, oldStream,callback);
|
||||
},
|
||||
/**
|
||||
* Checks if video identified by given src is desktop stream.
|
||||
* @param videoSrc eg.
|
||||
* blob:https%3A//pawel.jitsi.net/9a46e0bd-131e-4d18-9c14-a9264e8db395
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isVideoSrcDesktop: function (jid) {
|
||||
if(!jid)
|
||||
return false;
|
||||
var isDesktop = false;
|
||||
var stream = null;
|
||||
if (APP.xmpp.myJid() &&
|
||||
APP.xmpp.myResource() === jid) {
|
||||
// local video
|
||||
stream = this.localVideo;
|
||||
} else {
|
||||
var peerStreams = this.remoteStreams[jid];
|
||||
if(!peerStreams)
|
||||
return false;
|
||||
stream = peerStreams[MediaStreamType.VIDEO_TYPE];
|
||||
}
|
||||
|
||||
if(stream)
|
||||
isDesktop = (stream.videoType === "screen");
|
||||
|
||||
return isDesktop;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = RTC;
|
||||
365
modules/RTC/RTCUtils.js
Normal file
@@ -0,0 +1,365 @@
|
||||
var RTCBrowserType = require("../../service/RTC/RTCBrowserType.js");
|
||||
var Resolutions = require("../../service/RTC/Resolutions");
|
||||
|
||||
var currentResolution = null;
|
||||
|
||||
function getPreviousResolution(resolution) {
|
||||
if(!Resolutions[resolution])
|
||||
return null;
|
||||
var order = Resolutions[resolution].order;
|
||||
var res = null;
|
||||
var resName = null;
|
||||
for(var i in Resolutions)
|
||||
{
|
||||
var tmp = Resolutions[i];
|
||||
if(res == null || (res.order < tmp.order && tmp.order < order))
|
||||
{
|
||||
resName = i;
|
||||
res = tmp;
|
||||
}
|
||||
}
|
||||
return resName;
|
||||
}
|
||||
|
||||
function setResolutionConstraints(constraints, resolution, isAndroid)
|
||||
{
|
||||
if (resolution && !constraints.video || isAndroid) {
|
||||
constraints.video = { mandatory: {}, optional: [] };// same behaviour as true
|
||||
}
|
||||
|
||||
if(Resolutions[resolution])
|
||||
{
|
||||
constraints.video.mandatory.minWidth = Resolutions[resolution].width;
|
||||
constraints.video.mandatory.minHeight = Resolutions[resolution].height;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (isAndroid) {
|
||||
constraints.video.mandatory.minWidth = 320;
|
||||
constraints.video.mandatory.minHeight = 240;
|
||||
constraints.video.mandatory.maxFrameRate = 15;
|
||||
}
|
||||
}
|
||||
|
||||
if (constraints.video.mandatory.minWidth)
|
||||
constraints.video.mandatory.maxWidth = constraints.video.mandatory.minWidth;
|
||||
if (constraints.video.mandatory.minHeight)
|
||||
constraints.video.mandatory.maxHeight = constraints.video.mandatory.minHeight;
|
||||
}
|
||||
|
||||
function getConstraints(um, resolution, bandwidth, fps, desktopStream, isAndroid)
|
||||
{
|
||||
var constraints = {audio: false, video: false};
|
||||
|
||||
if (um.indexOf('video') >= 0) {
|
||||
constraints.video = { mandatory: {}, optional: [] };// same behaviour as true
|
||||
}
|
||||
if (um.indexOf('audio') >= 0) {
|
||||
constraints.audio = { mandatory: {}, optional: []};// same behaviour as true
|
||||
}
|
||||
if (um.indexOf('screen') >= 0) {
|
||||
constraints.video = {
|
||||
mandatory: {
|
||||
chromeMediaSource: "screen",
|
||||
googLeakyBucket: true,
|
||||
maxWidth: window.screen.width,
|
||||
maxHeight: window.screen.height,
|
||||
maxFrameRate: 3
|
||||
},
|
||||
optional: []
|
||||
};
|
||||
}
|
||||
if (um.indexOf('desktop') >= 0) {
|
||||
constraints.video = {
|
||||
mandatory: {
|
||||
chromeMediaSource: "desktop",
|
||||
chromeMediaSourceId: desktopStream,
|
||||
googLeakyBucket: true,
|
||||
maxWidth: window.screen.width,
|
||||
maxHeight: window.screen.height,
|
||||
maxFrameRate: 3
|
||||
},
|
||||
optional: []
|
||||
};
|
||||
}
|
||||
|
||||
if (constraints.audio) {
|
||||
// if it is good enough for hangouts...
|
||||
constraints.audio.optional.push(
|
||||
{googEchoCancellation: true},
|
||||
{googAutoGainControl: true},
|
||||
{googNoiseSupression: true},
|
||||
{googHighpassFilter: true},
|
||||
{googNoisesuppression2: true},
|
||||
{googEchoCancellation2: true},
|
||||
{googAutoGainControl2: true}
|
||||
);
|
||||
}
|
||||
if (constraints.video) {
|
||||
constraints.video.optional.push(
|
||||
{googNoiseReduction: false} // chrome 37 workaround for issue 3807, reenable in M38
|
||||
);
|
||||
if (um.indexOf('video') >= 0) {
|
||||
constraints.video.optional.push(
|
||||
{googLeakyBucket: true}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (um.indexOf('video') >= 0) {
|
||||
setResolutionConstraints(constraints, resolution, isAndroid);
|
||||
}
|
||||
|
||||
if (bandwidth) { // doesn't work currently, see webrtc issue 1846
|
||||
if (!constraints.video) constraints.video = {mandatory: {}, optional: []};//same behaviour as true
|
||||
constraints.video.optional.push({bandwidth: bandwidth});
|
||||
}
|
||||
if (fps) { // for some cameras it might be necessary to request 30fps
|
||||
// so they choose 30fps mjpg over 10fps yuy2
|
||||
if (!constraints.video) constraints.video = {mandatory: {}, optional: []};// same behaviour as true;
|
||||
constraints.video.mandatory.minFrameRate = fps;
|
||||
}
|
||||
|
||||
return constraints;
|
||||
}
|
||||
|
||||
|
||||
function RTCUtils(RTCService)
|
||||
{
|
||||
this.service = RTCService;
|
||||
if (navigator.mozGetUserMedia) {
|
||||
console.log('This appears to be Firefox');
|
||||
var version = parseInt(navigator.userAgent.match(/Firefox\/([0-9]+)\./)[1], 10);
|
||||
if (version >= 22) {
|
||||
this.peerconnection = mozRTCPeerConnection;
|
||||
this.browser = RTCBrowserType.RTC_BROWSER_FIREFOX;
|
||||
this.getUserMedia = navigator.mozGetUserMedia.bind(navigator);
|
||||
this.pc_constraints = {};
|
||||
this.attachMediaStream = function (element, stream) {
|
||||
// srcObject is being standardized and FF will eventually
|
||||
// support that unprefixed. FF also supports the
|
||||
// "element.src = URL.createObjectURL(...)" combo, but that
|
||||
// will be deprecated in favour of srcObject.
|
||||
//
|
||||
// https://groups.google.com/forum/#!topic/mozilla.dev.media/pKOiioXonJg
|
||||
// https://github.com/webrtc/samples/issues/302
|
||||
element[0].mozSrcObject = stream;
|
||||
element[0].play();
|
||||
};
|
||||
this.getStreamID = function (stream) {
|
||||
var tracks = stream.getVideoTracks();
|
||||
if(!tracks || tracks.length == 0)
|
||||
{
|
||||
tracks = stream.getAudioTracks();
|
||||
}
|
||||
return tracks[0].id.replace(/[\{,\}]/g,"");
|
||||
};
|
||||
this.getVideoSrc = function (element) {
|
||||
return element.mozSrcObject;
|
||||
};
|
||||
this.setVideoSrc = function (element, src) {
|
||||
element.mozSrcObject = src;
|
||||
};
|
||||
RTCSessionDescription = mozRTCSessionDescription;
|
||||
RTCIceCandidate = mozRTCIceCandidate;
|
||||
}
|
||||
} else if (navigator.webkitGetUserMedia) {
|
||||
console.log('This appears to be Chrome');
|
||||
this.peerconnection = webkitRTCPeerConnection;
|
||||
this.browser = RTCBrowserType.RTC_BROWSER_CHROME;
|
||||
this.getUserMedia = navigator.webkitGetUserMedia.bind(navigator);
|
||||
this.attachMediaStream = function (element, stream) {
|
||||
element.attr('src', webkitURL.createObjectURL(stream));
|
||||
};
|
||||
this.getStreamID = function (stream) {
|
||||
// streams from FF endpoints have the characters '{' and '}'
|
||||
// that make jQuery choke.
|
||||
return stream.id.replace(/[\{,\}]/g,"");
|
||||
};
|
||||
this.getVideoSrc = function (element) {
|
||||
return element.getAttribute("src");
|
||||
};
|
||||
this.setVideoSrc = function (element, src) {
|
||||
element.setAttribute("src", src);
|
||||
};
|
||||
// DTLS should now be enabled by default but..
|
||||
this.pc_constraints = {'optional': [{'DtlsSrtpKeyAgreement': 'true'}]};
|
||||
if (navigator.userAgent.indexOf('Android') != -1) {
|
||||
this.pc_constraints = {}; // disable DTLS on Android
|
||||
}
|
||||
if (!webkitMediaStream.prototype.getVideoTracks) {
|
||||
webkitMediaStream.prototype.getVideoTracks = function () {
|
||||
return this.videoTracks;
|
||||
};
|
||||
}
|
||||
if (!webkitMediaStream.prototype.getAudioTracks) {
|
||||
webkitMediaStream.prototype.getAudioTracks = function () {
|
||||
return this.audioTracks;
|
||||
};
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
try { console.log('Browser does not appear to be WebRTC-capable'); } catch (e) { }
|
||||
|
||||
window.location.href = 'unsupported_browser.html';
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.browser !== RTCBrowserType.RTC_BROWSER_CHROME &&
|
||||
config.enableFirefoxSupport !== true) {
|
||||
window.location.href = 'unsupported_browser.html';
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
RTCUtils.prototype.getUserMediaWithConstraints = function(
|
||||
um, success_callback, failure_callback, resolution,bandwidth, fps,
|
||||
desktopStream)
|
||||
{
|
||||
currentResolution = resolution;
|
||||
// Check if we are running on Android device
|
||||
var isAndroid = navigator.userAgent.indexOf('Android') != -1;
|
||||
|
||||
var constraints = getConstraints(
|
||||
um, resolution, bandwidth, fps, desktopStream, isAndroid);
|
||||
|
||||
var isFF = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
|
||||
|
||||
try {
|
||||
if (config.enableSimulcast
|
||||
&& constraints.video
|
||||
&& constraints.video.chromeMediaSource !== 'screen'
|
||||
&& constraints.video.chromeMediaSource !== 'desktop'
|
||||
&& !isAndroid
|
||||
|
||||
// We currently do not support FF, as it doesn't have multistream support.
|
||||
&& !isFF) {
|
||||
APP.simulcast.getUserMedia(constraints, function (stream) {
|
||||
console.log('onUserMediaSuccess');
|
||||
success_callback(stream);
|
||||
},
|
||||
function (error) {
|
||||
console.warn('Failed to get access to local media. Error ', error);
|
||||
if (failure_callback) {
|
||||
failure_callback(error);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
||||
this.getUserMedia(constraints,
|
||||
function (stream) {
|
||||
console.log('onUserMediaSuccess');
|
||||
success_callback(stream);
|
||||
},
|
||||
function (error) {
|
||||
console.warn('Failed to get access to local media. Error ',
|
||||
error, constraints);
|
||||
if (failure_callback) {
|
||||
failure_callback(error);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('GUM failed: ', e);
|
||||
if(failure_callback) {
|
||||
failure_callback(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* We ask for audio and video combined stream in order to get permissions and
|
||||
* not to ask twice.
|
||||
*/
|
||||
RTCUtils.prototype.obtainAudioAndVideoPermissions = function() {
|
||||
var self = this;
|
||||
// Get AV
|
||||
|
||||
this.getUserMediaWithConstraints(
|
||||
['audio', 'video'],
|
||||
function (stream) {
|
||||
self.successCallback(stream);
|
||||
},
|
||||
function (error) {
|
||||
self.errorCallback(error);
|
||||
},
|
||||
config.resolution || '360');
|
||||
}
|
||||
|
||||
RTCUtils.prototype.successCallback = function (stream) {
|
||||
console.log('got', stream, stream.getAudioTracks().length,
|
||||
stream.getVideoTracks().length);
|
||||
this.handleLocalStream(stream);
|
||||
};
|
||||
|
||||
RTCUtils.prototype.errorCallback = function (error) {
|
||||
var self = this;
|
||||
console.error('failed to obtain audio/video stream - trying audio only', error);
|
||||
var resolution = getPreviousResolution(currentResolution);
|
||||
if(typeof error == "object" && error.constraintName && error.name
|
||||
&& (error.name == "ConstraintNotSatisfiedError" ||
|
||||
error.name == "OverconstrainedError") &&
|
||||
(error.constraintName == "minWidth" || error.constraintName == "maxWidth" ||
|
||||
error.constraintName == "minHeight" || error.constraintName == "maxHeight")
|
||||
&& resolution != null)
|
||||
{
|
||||
self.getUserMediaWithConstraints(['audio', 'video'],
|
||||
function (stream) {
|
||||
return self.successCallback(stream);
|
||||
}, function (error) {
|
||||
return self.errorCallback(error);
|
||||
}, resolution);
|
||||
}
|
||||
else
|
||||
{
|
||||
self.getUserMediaWithConstraints(
|
||||
['audio'],
|
||||
function (stream) {
|
||||
return self.successCallback(stream);
|
||||
},
|
||||
function (error) {
|
||||
console.error('failed to obtain audio/video stream - stop',
|
||||
error);
|
||||
APP.UI.messageHandler.showError("dialog.error",
|
||||
"dialog.failedpermissions");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
RTCUtils.prototype.handleLocalStream = function(stream)
|
||||
{
|
||||
if(window.webkitMediaStream)
|
||||
{
|
||||
var audioStream = new webkitMediaStream();
|
||||
var videoStream = new webkitMediaStream();
|
||||
var audioTracks = stream.getAudioTracks();
|
||||
var videoTracks = stream.getVideoTracks();
|
||||
for (var i = 0; i < audioTracks.length; i++) {
|
||||
audioStream.addTrack(audioTracks[i]);
|
||||
}
|
||||
|
||||
this.service.createLocalStream(audioStream, "audio");
|
||||
|
||||
for (i = 0; i < videoTracks.length; i++) {
|
||||
videoStream.addTrack(videoTracks[i]);
|
||||
}
|
||||
|
||||
|
||||
this.service.createLocalStream(videoStream, "video");
|
||||
}
|
||||
else
|
||||
{//firefox
|
||||
this.service.createLocalStream(stream, "stream");
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
module.exports = RTCUtils;
|
||||
761
modules/UI/UI.js
Normal file
@@ -0,0 +1,761 @@
|
||||
var UI = {};
|
||||
|
||||
var VideoLayout = require("./videolayout/VideoLayout.js");
|
||||
var AudioLevels = require("./audio_levels/AudioLevels.js");
|
||||
var Prezi = require("./prezi/Prezi.js");
|
||||
var Etherpad = require("./etherpad/Etherpad.js");
|
||||
var Chat = require("./side_pannels/chat/Chat.js");
|
||||
var Toolbar = require("./toolbars/Toolbar");
|
||||
var ToolbarToggler = require("./toolbars/ToolbarToggler");
|
||||
var BottomToolbar = require("./toolbars/BottomToolbar");
|
||||
var ContactList = require("./side_pannels/contactlist/ContactList");
|
||||
var Avatar = require("./avatar/Avatar");
|
||||
var EventEmitter = require("events");
|
||||
var SettingsMenu = require("./side_pannels/settings/SettingsMenu");
|
||||
var Settings = require("./../settings/Settings");
|
||||
var PanelToggler = require("./side_pannels/SidePanelToggler");
|
||||
var RoomNameGenerator = require("./welcome_page/RoomnameGenerator");
|
||||
UI.messageHandler = require("./util/MessageHandler");
|
||||
var messageHandler = UI.messageHandler;
|
||||
var Authentication = require("./authentication/Authentication");
|
||||
var UIUtil = require("./util/UIUtil");
|
||||
var NicknameHandler = require("./util/NicknameHandler");
|
||||
var CQEvents = require("../../service/connectionquality/CQEvents");
|
||||
var DesktopSharingEventTypes
|
||||
= require("../../service/desktopsharing/DesktopSharingEventTypes");
|
||||
var RTCEvents = require("../../service/RTC/RTCEvents");
|
||||
var StreamEventTypes = require("../../service/RTC/StreamEventTypes");
|
||||
var XMPPEvents = require("../../service/xmpp/XMPPEvents");
|
||||
|
||||
var eventEmitter = new EventEmitter();
|
||||
var roomName = null;
|
||||
|
||||
|
||||
function setupPrezi()
|
||||
{
|
||||
$("#reloadPresentationLink").click(function()
|
||||
{
|
||||
Prezi.reloadPresentation();
|
||||
});
|
||||
}
|
||||
|
||||
function setupChat()
|
||||
{
|
||||
Chat.init();
|
||||
$("#toggle_smileys").click(function() {
|
||||
Chat.toggleSmileys();
|
||||
});
|
||||
}
|
||||
|
||||
function setupToolbars() {
|
||||
Toolbar.init(UI);
|
||||
Toolbar.setupButtonsFromConfig();
|
||||
BottomToolbar.init();
|
||||
}
|
||||
|
||||
function streamHandler(stream) {
|
||||
switch (stream.type)
|
||||
{
|
||||
case "audio":
|
||||
VideoLayout.changeLocalAudio(stream);
|
||||
break;
|
||||
case "video":
|
||||
VideoLayout.changeLocalVideo(stream);
|
||||
break;
|
||||
case "stream":
|
||||
VideoLayout.changeLocalStream(stream);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function onDisposeConference(unload) {
|
||||
Toolbar.showAuthenticateButton(false);
|
||||
};
|
||||
|
||||
function onDisplayNameChanged(jid, displayName) {
|
||||
ContactList.onDisplayNameChange(jid, displayName);
|
||||
SettingsMenu.onDisplayNameChange(jid, displayName);
|
||||
VideoLayout.onDisplayNameChanged(jid, displayName);
|
||||
}
|
||||
|
||||
function registerListeners() {
|
||||
APP.RTC.addStreamListener(streamHandler, StreamEventTypes.EVENT_TYPE_LOCAL_CREATED);
|
||||
|
||||
APP.RTC.addStreamListener(streamHandler, StreamEventTypes.EVENT_TYPE_LOCAL_CHANGED);
|
||||
APP.RTC.addStreamListener(function (stream) {
|
||||
VideoLayout.onRemoteStreamAdded(stream);
|
||||
}, StreamEventTypes.EVENT_TYPE_REMOTE_CREATED);
|
||||
APP.RTC.addListener(RTCEvents.LASTN_CHANGED, onLastNChanged);
|
||||
APP.RTC.addListener(RTCEvents.DOMINANTSPEAKER_CHANGED, function (resourceJid) {
|
||||
VideoLayout.onDominantSpeakerChanged(resourceJid);
|
||||
});
|
||||
APP.RTC.addListener(RTCEvents.LASTN_ENDPOINT_CHANGED,
|
||||
function (lastNEndpoints, endpointsEnteringLastN, stream) {
|
||||
VideoLayout.onLastNEndpointsChanged(lastNEndpoints,
|
||||
endpointsEnteringLastN, stream);
|
||||
});
|
||||
APP.RTC.addListener(RTCEvents.SIMULCAST_LAYER_CHANGED,
|
||||
function (endpointSimulcastLayers) {
|
||||
VideoLayout.onSimulcastLayersChanged(endpointSimulcastLayers);
|
||||
});
|
||||
APP.RTC.addListener(RTCEvents.SIMULCAST_LAYER_CHANGING,
|
||||
function (endpointSimulcastLayers) {
|
||||
VideoLayout.onSimulcastLayersChanging(endpointSimulcastLayers);
|
||||
});
|
||||
|
||||
APP.statistics.addAudioLevelListener(function(jid, audioLevel)
|
||||
{
|
||||
var resourceJid;
|
||||
if(jid === APP.statistics.LOCAL_JID)
|
||||
{
|
||||
resourceJid = AudioLevels.LOCAL_LEVEL;
|
||||
if(APP.RTC.localAudio.isMuted())
|
||||
{
|
||||
audioLevel = 0;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
resourceJid = Strophe.getResourceFromJid(jid);
|
||||
}
|
||||
|
||||
AudioLevels.updateAudioLevel(resourceJid, audioLevel,
|
||||
UI.getLargeVideoState().userResourceJid);
|
||||
});
|
||||
APP.desktopsharing.addListener(function () {
|
||||
ToolbarToggler.showDesktopSharingButton();
|
||||
}, DesktopSharingEventTypes.INIT);
|
||||
APP.desktopsharing.addListener(
|
||||
Toolbar.changeDesktopSharingButtonState,
|
||||
DesktopSharingEventTypes.SWITCHING_DONE);
|
||||
APP.connectionquality.addListener(CQEvents.LOCALSTATS_UPDATED,
|
||||
VideoLayout.updateLocalConnectionStats);
|
||||
APP.connectionquality.addListener(CQEvents.REMOTESTATS_UPDATED,
|
||||
VideoLayout.updateConnectionStats);
|
||||
APP.connectionquality.addListener(CQEvents.STOP,
|
||||
VideoLayout.onStatsStop);
|
||||
APP.xmpp.addListener(XMPPEvents.DISPOSE_CONFERENCE, onDisposeConference);
|
||||
APP.xmpp.addListener(XMPPEvents.GRACEFUL_SHUTDOWN, function () {
|
||||
messageHandler.openMessageDialog(
|
||||
'dialog.serviceUnavailable',
|
||||
'dialog.gracefulShutdown'
|
||||
);
|
||||
});
|
||||
APP.xmpp.addListener(XMPPEvents.RESERVATION_ERROR, function (code, msg) {
|
||||
var title = APP.translation.generateTranslatonHTML(
|
||||
"dialog.reservationError");
|
||||
var message = APP.translation.generateTranslatonHTML(
|
||||
"dialog.reservationErrorMsg", {code: code, msg: msg});
|
||||
messageHandler.openDialog(
|
||||
title,
|
||||
message,
|
||||
true, {},
|
||||
function (event, value, message, formVals)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
);
|
||||
});
|
||||
APP.xmpp.addListener(XMPPEvents.KICKED, function () {
|
||||
messageHandler.openMessageDialog("dialog.sessTerminated",
|
||||
"dialog.kickMessage");
|
||||
});
|
||||
APP.xmpp.addListener(XMPPEvents.MUC_DESTROYED, function (reason) {
|
||||
//FIXME: use Session Terminated from translation, but
|
||||
// 'reason' text comes from XMPP packet and is not translated
|
||||
var title = APP.translation.generateTranslatonHTML("dialog.sessTerminated");
|
||||
messageHandler.openDialog(
|
||||
title, reason, true, {},
|
||||
function (event, value, message, formVals)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
);
|
||||
});
|
||||
APP.xmpp.addListener(XMPPEvents.BRIDGE_DOWN, function () {
|
||||
messageHandler.showError("dialog.error",
|
||||
"dialog.bridgeUnavailable");
|
||||
});
|
||||
APP.xmpp.addListener(XMPPEvents.USER_ID_CHANGED, function (from, id) {
|
||||
Avatar.setUserAvatar(from, id);
|
||||
});
|
||||
APP.xmpp.addListener(XMPPEvents.CHANGED_STREAMS, function (jid, changedStreams) {
|
||||
for(stream in changedStreams)
|
||||
{
|
||||
// might need to update the direction if participant just went from sendrecv to recvonly
|
||||
if (stream.type === 'video' || stream.type === 'screen') {
|
||||
var el = $('#participant_' + Strophe.getResourceFromJid(jid) + '>video');
|
||||
switch (stream.direction) {
|
||||
case 'sendrecv':
|
||||
el.show();
|
||||
break;
|
||||
case 'recvonly':
|
||||
el.hide();
|
||||
// FIXME: Check if we have to change large video
|
||||
//VideoLayout.updateLargeVideo(el);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
APP.xmpp.addListener(XMPPEvents.DISPLAY_NAME_CHANGED, onDisplayNameChanged);
|
||||
APP.xmpp.addListener(XMPPEvents.MUC_JOINED, onMucJoined);
|
||||
APP.xmpp.addListener(XMPPEvents.LOCALROLE_CHANGED, onLocalRoleChange);
|
||||
APP.xmpp.addListener(XMPPEvents.MUC_ENTER, onMucEntered);
|
||||
APP.xmpp.addListener(XMPPEvents.MUC_ROLE_CHANGED, onMucRoleChanged);
|
||||
APP.xmpp.addListener(XMPPEvents.PRESENCE_STATUS, onMucPresenceStatus);
|
||||
APP.xmpp.addListener(XMPPEvents.SUBJECT_CHANGED, chatSetSubject);
|
||||
APP.xmpp.addListener(XMPPEvents.MESSAGE_RECEIVED, updateChatConversation);
|
||||
APP.xmpp.addListener(XMPPEvents.MUC_LEFT, onMucLeft);
|
||||
APP.xmpp.addListener(XMPPEvents.PASSWORD_REQUIRED, onPasswordReqiured);
|
||||
APP.xmpp.addListener(XMPPEvents.CHAT_ERROR_RECEIVED, chatAddError);
|
||||
APP.xmpp.addListener(XMPPEvents.ETHERPAD, initEtherpad);
|
||||
APP.xmpp.addListener(XMPPEvents.AUTHENTICATION_REQUIRED, onAuthenticationRequired);
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Mutes/unmutes the local video.
|
||||
*
|
||||
* @param mute <tt>true</tt> to mute the local video; otherwise, <tt>false</tt>
|
||||
* @param options an object which specifies optional arguments such as the
|
||||
* <tt>boolean</tt> key <tt>byUser</tt> with default value <tt>true</tt> which
|
||||
* specifies whether the method was initiated in response to a user command (in
|
||||
* contrast to an automatic decision taken by the application logic)
|
||||
*/
|
||||
function setVideoMute(mute, options) {
|
||||
APP.xmpp.setVideoMute(
|
||||
mute,
|
||||
function (mute) {
|
||||
var video = $('#video');
|
||||
var communicativeClass = "icon-camera";
|
||||
var muteClass = "icon-camera icon-camera-disabled";
|
||||
|
||||
if (mute) {
|
||||
video.removeClass(communicativeClass);
|
||||
video.addClass(muteClass);
|
||||
} else {
|
||||
video.removeClass(muteClass);
|
||||
video.addClass(communicativeClass);
|
||||
}
|
||||
},
|
||||
options);
|
||||
}
|
||||
|
||||
|
||||
function bindEvents()
|
||||
{
|
||||
/**
|
||||
* Resizes and repositions videos in full screen mode.
|
||||
*/
|
||||
$(document).on('webkitfullscreenchange mozfullscreenchange fullscreenchange',
|
||||
function () {
|
||||
VideoLayout.resizeLargeVideoContainer();
|
||||
VideoLayout.positionLarge();
|
||||
}
|
||||
);
|
||||
|
||||
$(window).resize(function () {
|
||||
VideoLayout.resizeLargeVideoContainer();
|
||||
VideoLayout.positionLarge();
|
||||
});
|
||||
}
|
||||
|
||||
UI.start = function (init) {
|
||||
document.title = interfaceConfig.APP_NAME;
|
||||
if(config.enableWelcomePage && window.location.pathname == "/" &&
|
||||
(!window.localStorage.welcomePageDisabled || window.localStorage.welcomePageDisabled == "false"))
|
||||
{
|
||||
$("#videoconference_page").hide();
|
||||
var setupWelcomePage = require("./welcome_page/WelcomePage");
|
||||
setupWelcomePage();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (interfaceConfig.SHOW_JITSI_WATERMARK) {
|
||||
var leftWatermarkDiv
|
||||
= $("#largeVideoContainer div[class='watermark leftwatermark']");
|
||||
|
||||
leftWatermarkDiv.css({display: 'block'});
|
||||
leftWatermarkDiv.parent().get(0).href
|
||||
= interfaceConfig.JITSI_WATERMARK_LINK;
|
||||
}
|
||||
|
||||
if (interfaceConfig.SHOW_BRAND_WATERMARK) {
|
||||
var rightWatermarkDiv
|
||||
= $("#largeVideoContainer div[class='watermark rightwatermark']");
|
||||
|
||||
rightWatermarkDiv.css({display: 'block'});
|
||||
rightWatermarkDiv.parent().get(0).href
|
||||
= interfaceConfig.BRAND_WATERMARK_LINK;
|
||||
rightWatermarkDiv.get(0).style.backgroundImage
|
||||
= "url(images/rightwatermark.png)";
|
||||
}
|
||||
|
||||
if (interfaceConfig.SHOW_POWERED_BY) {
|
||||
$("#largeVideoContainer>a[class='poweredby']").css({display: 'block'});
|
||||
}
|
||||
|
||||
$("#welcome_page").hide();
|
||||
|
||||
VideoLayout.resizeLargeVideoContainer();
|
||||
$("#videospace").mousemove(function () {
|
||||
return ToolbarToggler.showToolbar();
|
||||
});
|
||||
// Set the defaults for prompt dialogs.
|
||||
jQuery.prompt.setDefaults({persistent: false});
|
||||
|
||||
VideoLayout.init(eventEmitter);
|
||||
AudioLevels.init();
|
||||
NicknameHandler.init(eventEmitter);
|
||||
registerListeners();
|
||||
bindEvents();
|
||||
setupPrezi();
|
||||
setupToolbars();
|
||||
setupChat();
|
||||
|
||||
|
||||
document.title = interfaceConfig.APP_NAME;
|
||||
|
||||
$("#downloadlog").click(function (event) {
|
||||
dump(event.target);
|
||||
});
|
||||
|
||||
if(config.enableWelcomePage && window.location.pathname == "/" &&
|
||||
(!window.localStorage.welcomePageDisabled || window.localStorage.welcomePageDisabled == "false"))
|
||||
{
|
||||
$("#videoconference_page").hide();
|
||||
var setupWelcomePage = require("./welcome_page/WelcomePage");
|
||||
setupWelcomePage();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$("#welcome_page").hide();
|
||||
|
||||
document.getElementById('largeVideo').volume = 0;
|
||||
|
||||
if (!$('#settings').is(':visible')) {
|
||||
console.log('init');
|
||||
init();
|
||||
} else {
|
||||
loginInfo.onsubmit = function (e) {
|
||||
if (e.preventDefault) e.preventDefault();
|
||||
$('#settings').hide();
|
||||
init();
|
||||
};
|
||||
}
|
||||
|
||||
toastr.options = {
|
||||
"closeButton": true,
|
||||
"debug": false,
|
||||
"positionClass": "notification-bottom-right",
|
||||
"onclick": null,
|
||||
"showDuration": "300",
|
||||
"hideDuration": "1000",
|
||||
"timeOut": "2000",
|
||||
"extendedTimeOut": "1000",
|
||||
"showEasing": "swing",
|
||||
"hideEasing": "linear",
|
||||
"showMethod": "fadeIn",
|
||||
"hideMethod": "fadeOut",
|
||||
"reposition": function() {
|
||||
if(PanelToggler.isVisible()) {
|
||||
$("#toast-container").addClass("notification-bottom-right-center");
|
||||
} else {
|
||||
$("#toast-container").removeClass("notification-bottom-right-center");
|
||||
}
|
||||
},
|
||||
"newestOnTop": false
|
||||
};
|
||||
|
||||
SettingsMenu.init();
|
||||
|
||||
};
|
||||
|
||||
function chatAddError(errorMessage, originalText)
|
||||
{
|
||||
return Chat.chatAddError(errorMessage, originalText);
|
||||
};
|
||||
|
||||
function chatSetSubject(text)
|
||||
{
|
||||
return Chat.chatSetSubject(text);
|
||||
};
|
||||
|
||||
function updateChatConversation(from, displayName, message) {
|
||||
return Chat.updateChatConversation(from, displayName, message);
|
||||
};
|
||||
|
||||
function onMucJoined(jid, info) {
|
||||
Toolbar.updateRoomUrl(window.location.href);
|
||||
var meHTML = APP.translation.generateTranslatonHTML("me");
|
||||
$("#localNick").html(Strophe.getResourceFromJid(jid) + " (" + meHTML + ")");
|
||||
|
||||
var settings = Settings.getSettings();
|
||||
// Add myself to the contact list.
|
||||
ContactList.addContact(jid, settings.email || settings.uid);
|
||||
|
||||
// Once we've joined the muc show the toolbar
|
||||
ToolbarToggler.showToolbar();
|
||||
|
||||
var displayName = !config.displayJids
|
||||
? info.displayName : Strophe.getResourceFromJid(jid);
|
||||
|
||||
if (displayName)
|
||||
onDisplayNameChanged('localVideoContainer', displayName);
|
||||
}
|
||||
|
||||
function initEtherpad(name) {
|
||||
Etherpad.init(name);
|
||||
};
|
||||
|
||||
function onMucLeft(jid) {
|
||||
console.log('left.muc', jid);
|
||||
var displayName = $('#participant_' + Strophe.getResourceFromJid(jid) +
|
||||
'>.displayname').html();
|
||||
messageHandler.notify(displayName,'notify.somebody',
|
||||
'disconnected',
|
||||
'notify.disconnected');
|
||||
// Need to call this with a slight delay, otherwise the element couldn't be
|
||||
// found for some reason.
|
||||
// XXX(gp) it works fine without the timeout for me (with Chrome 38).
|
||||
window.setTimeout(function () {
|
||||
var container = document.getElementById(
|
||||
'participant_' + Strophe.getResourceFromJid(jid));
|
||||
if (container) {
|
||||
ContactList.removeContact(jid);
|
||||
VideoLayout.removeConnectionIndicator(jid);
|
||||
// hide here, wait for video to close before removing
|
||||
$(container).hide();
|
||||
VideoLayout.resizeThumbnails();
|
||||
}
|
||||
}, 10);
|
||||
|
||||
VideoLayout.participantLeft(jid);
|
||||
|
||||
};
|
||||
|
||||
|
||||
function onLocalRoleChange(jid, info, pres, isModerator)
|
||||
{
|
||||
|
||||
console.info("My role changed, new role: " + info.role);
|
||||
onModeratorStatusChanged(isModerator);
|
||||
VideoLayout.showModeratorIndicator();
|
||||
|
||||
if (isModerator) {
|
||||
Authentication.closeAuthenticationWindow();
|
||||
messageHandler.notify(null, "notify.me",
|
||||
'connected', "notify.moderator");
|
||||
}
|
||||
}
|
||||
|
||||
function onModeratorStatusChanged(isModerator) {
|
||||
|
||||
Toolbar.showSipCallButton(isModerator);
|
||||
Toolbar.showRecordingButton(
|
||||
isModerator); //&&
|
||||
// FIXME:
|
||||
// Recording visible if
|
||||
// there are at least 2(+ 1 focus) participants
|
||||
//Object.keys(connection.emuc.members).length >= 3);
|
||||
|
||||
if (isModerator && config.etherpad_base) {
|
||||
Etherpad.init();
|
||||
}
|
||||
};
|
||||
|
||||
function onPasswordReqiured(callback) {
|
||||
// password is required
|
||||
Toolbar.lockLockButton();
|
||||
var message = '<h2 data-i18n="dialog.passwordRequired">';
|
||||
message += APP.translation.translateString(
|
||||
"dialog.passwordRequired");
|
||||
message += '</h2>' +
|
||||
'<input id="lockKey" type="text" data-i18n=' +
|
||||
'"[placeholder]dialog.password" placeholder="' +
|
||||
APP.translation.translateString("dialog.password") +
|
||||
'" autofocus>';
|
||||
|
||||
messageHandler.openTwoButtonDialog(null, null, null, message,
|
||||
true,
|
||||
"dialog.Ok",
|
||||
function (e, v, m, f) {},
|
||||
function (event) {
|
||||
document.getElementById('lockKey').focus();
|
||||
},
|
||||
function (e, v, m, f) {
|
||||
if (v) {
|
||||
var lockKey = document.getElementById('lockKey');
|
||||
if (lockKey.value !== null) {
|
||||
Toolbar.setSharedKey(lockKey.value);
|
||||
callback(lockKey.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
function onMucEntered(jid, id, displayName) {
|
||||
messageHandler.notify(displayName,'notify.somebody',
|
||||
'connected',
|
||||
'notify.connected');
|
||||
|
||||
// Add Peer's container
|
||||
VideoLayout.ensurePeerContainerExists(jid,id);
|
||||
}
|
||||
|
||||
function onMucPresenceStatus( jid, info) {
|
||||
VideoLayout.setPresenceStatus(
|
||||
'participant_' + Strophe.getResourceFromJid(jid), info.status);
|
||||
}
|
||||
|
||||
function onMucRoleChanged(role, displayName) {
|
||||
VideoLayout.showModeratorIndicator();
|
||||
|
||||
if (role === 'moderator') {
|
||||
var messageKey, messageOptions = {};
|
||||
if (!displayName) {
|
||||
messageKey = "notify.grantedToUnknown";
|
||||
}
|
||||
else
|
||||
{
|
||||
messageKey = "notify.grantedTo";
|
||||
messageOptions = {to: displayName};
|
||||
}
|
||||
messageHandler.notify(
|
||||
displayName,'notify.somebody',
|
||||
'connected', messageKey,
|
||||
messageOptions);
|
||||
}
|
||||
}
|
||||
|
||||
function onAuthenticationRequired(intervalCallback) {
|
||||
Authentication.openAuthenticationDialog(
|
||||
roomName, intervalCallback, function () {
|
||||
Toolbar.authenticateClicked();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
function onLastNChanged(oldValue, newValue) {
|
||||
if (config.muteLocalVideoIfNotInLastN) {
|
||||
setVideoMute(!newValue, { 'byUser': false });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
UI.toggleSmileys = function () {
|
||||
Chat.toggleSmileys();
|
||||
};
|
||||
|
||||
UI.getSettings = function () {
|
||||
return Settings.getSettings();
|
||||
};
|
||||
|
||||
UI.toggleFilmStrip = function () {
|
||||
return BottomToolbar.toggleFilmStrip();
|
||||
};
|
||||
|
||||
UI.toggleChat = function () {
|
||||
return BottomToolbar.toggleChat();
|
||||
};
|
||||
|
||||
UI.toggleContactList = function () {
|
||||
return BottomToolbar.toggleContactList();
|
||||
};
|
||||
|
||||
UI.inputDisplayNameHandler = function (value) {
|
||||
VideoLayout.inputDisplayNameHandler(value);
|
||||
};
|
||||
|
||||
|
||||
UI.getLargeVideoState = function()
|
||||
{
|
||||
return VideoLayout.getLargeVideoState();
|
||||
};
|
||||
|
||||
UI.generateRoomName = function() {
|
||||
if(roomName)
|
||||
return roomName;
|
||||
var roomnode = null;
|
||||
var path = window.location.pathname;
|
||||
|
||||
// determinde the room node from the url
|
||||
// TODO: just the roomnode or the whole bare jid?
|
||||
if (config.getroomnode && typeof config.getroomnode === 'function') {
|
||||
// custom function might be responsible for doing the pushstate
|
||||
roomnode = config.getroomnode(path);
|
||||
} else {
|
||||
/* fall back to default strategy
|
||||
* this is making assumptions about how the URL->room mapping happens.
|
||||
* It currently assumes deployment at root, with a rewrite like the
|
||||
* following one (for nginx):
|
||||
location ~ ^/([a-zA-Z0-9]+)$ {
|
||||
rewrite ^/(.*)$ / break;
|
||||
}
|
||||
*/
|
||||
if (path.length > 1) {
|
||||
roomnode = path.substr(1).toLowerCase();
|
||||
} else {
|
||||
var word = RoomNameGenerator.generateRoomWithoutSeparator();
|
||||
roomnode = word.toLowerCase();
|
||||
|
||||
window.history.pushState('VideoChat',
|
||||
'Room: ' + word, window.location.pathname + word);
|
||||
}
|
||||
}
|
||||
|
||||
roomName = roomnode + '@' + config.hosts.muc;
|
||||
return roomName;
|
||||
};
|
||||
|
||||
|
||||
UI.connectionIndicatorShowMore = function(id)
|
||||
{
|
||||
return VideoLayout.connectionIndicators[id].showMore();
|
||||
};
|
||||
|
||||
UI.getCredentials = function () {
|
||||
var settings = this.getSettings();
|
||||
return {
|
||||
bosh: document.getElementById('boshURL').value,
|
||||
password: document.getElementById('password').value,
|
||||
jid: document.getElementById('jid').value,
|
||||
email: settings.email,
|
||||
displayName: settings.displayName,
|
||||
uid: settings.uid
|
||||
};
|
||||
};
|
||||
|
||||
UI.disableConnect = function () {
|
||||
document.getElementById('connect').disabled = true;
|
||||
};
|
||||
|
||||
UI.showLoginPopup = function(callback)
|
||||
{
|
||||
console.log('password is required');
|
||||
var message = '<h2 data-i18n="dialog.passwordRequired">';
|
||||
message += APP.translation.translateString(
|
||||
"dialog.passwordRequired");
|
||||
message += '</h2>' +
|
||||
'<input id="passwordrequired.username" type="text" ' +
|
||||
'placeholder="user@domain.net" autofocus>' +
|
||||
'<input id="passwordrequired.password" ' +
|
||||
'type="password" data-i18n="[placeholder]dialog.userPassword"' +
|
||||
' placeholder="user password">';
|
||||
UI.messageHandler.openTwoButtonDialog(null, null, null, message,
|
||||
true,
|
||||
"dialog.Ok",
|
||||
function (e, v, m, f) {
|
||||
if (v) {
|
||||
var username = document.getElementById('passwordrequired.username');
|
||||
var password = document.getElementById('passwordrequired.password');
|
||||
|
||||
if (username.value !== null && password.value != null) {
|
||||
callback(username.value, password.value);
|
||||
}
|
||||
}
|
||||
},
|
||||
function (event) {
|
||||
document.getElementById('passwordrequired.username').focus();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
UI.checkForNicknameAndJoin = function () {
|
||||
|
||||
Authentication.closeAuthenticationDialog();
|
||||
Authentication.stopInterval();
|
||||
|
||||
var nick = null;
|
||||
if (config.useNicks) {
|
||||
nick = window.prompt('Your nickname (optional)');
|
||||
}
|
||||
APP.xmpp.joinRoom(roomName, config.useNicks, nick);
|
||||
};
|
||||
|
||||
|
||||
function dump(elem, filename) {
|
||||
elem = elem.parentNode;
|
||||
elem.download = filename || 'meetlog.json';
|
||||
elem.href = 'data:application/json;charset=utf-8,\n';
|
||||
var data = APP.xmpp.populateData();
|
||||
var metadata = {};
|
||||
metadata.time = new Date();
|
||||
metadata.url = window.location.href;
|
||||
metadata.ua = navigator.userAgent;
|
||||
var log = APP.xmpp.getLogger();
|
||||
if (log) {
|
||||
metadata.xmpp = log;
|
||||
}
|
||||
data.metadata = metadata;
|
||||
elem.href += encodeURIComponent(JSON.stringify(data, null, ' '));
|
||||
return false;
|
||||
}
|
||||
|
||||
UI.getRoomName = function () {
|
||||
return roomName;
|
||||
};
|
||||
|
||||
/**
|
||||
* Mutes/unmutes the local video.
|
||||
*/
|
||||
UI.toggleVideo = function () {
|
||||
UIUtil.buttonClick("#video", "icon-camera icon-camera-disabled");
|
||||
|
||||
setVideoMute(!APP.RTC.localVideo.isMuted());
|
||||
};
|
||||
|
||||
/**
|
||||
* Mutes / unmutes audio for the local participant.
|
||||
*/
|
||||
UI.toggleAudio = function() {
|
||||
UI.setAudioMuted(!APP.RTC.localAudio.isMuted());
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets muted audio state for the local participant.
|
||||
*/
|
||||
UI.setAudioMuted = function (mute) {
|
||||
|
||||
if(!APP.xmpp.setAudioMute(mute, function () {
|
||||
VideoLayout.showLocalAudioIndicator(mute);
|
||||
|
||||
UIUtil.buttonClick("#mute", "icon-microphone icon-mic-disabled");
|
||||
}))
|
||||
{
|
||||
// We still click the button.
|
||||
UIUtil.buttonClick("#mute", "icon-microphone icon-mic-disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
UI.addListener = function (type, listener) {
|
||||
eventEmitter.on(type, listener);
|
||||
}
|
||||
|
||||
UI.clickOnVideo = function (videoNumber) {
|
||||
var remoteVideos = $(".videocontainer:not(#mixedstream)");
|
||||
if (remoteVideos.length > videoNumber) {
|
||||
remoteVideos[videoNumber].click();
|
||||
}
|
||||
}
|
||||
|
||||
//Used by torture
|
||||
UI.showToolbar = function () {
|
||||
return ToolbarToggler.showToolbar();
|
||||
}
|
||||
|
||||
//Used by torture
|
||||
UI.dockToolbar = function (isDock) {
|
||||
return ToolbarToggler.dockToolbar(isDock);
|
||||
}
|
||||
|
||||
module.exports = UI;
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
var CanvasUtil = require("./CanvasUtils");
|
||||
|
||||
var ASDrawContext = $('#activeSpeakerAudioLevel')[0].getContext('2d');
|
||||
|
||||
function initActiveSpeakerAudioLevels() {
|
||||
var ASRadius = interfaceConfig.ACTIVE_SPEAKER_AVATAR_SIZE / 2;
|
||||
var ASCenter = (interfaceConfig.ACTIVE_SPEAKER_AVATAR_SIZE + ASRadius) / 2;
|
||||
|
||||
// Draw a circle.
|
||||
ASDrawContext.arc(ASCenter, ASCenter, ASRadius, 0, 2 * Math.PI);
|
||||
|
||||
// Add a shadow around the circle
|
||||
ASDrawContext.shadowColor = interfaceConfig.SHADOW_COLOR;
|
||||
ASDrawContext.shadowOffsetX = 0;
|
||||
ASDrawContext.shadowOffsetY = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* The audio Levels plugin.
|
||||
*/
|
||||
@@ -6,11 +23,15 @@ var AudioLevels = (function(my) {
|
||||
|
||||
my.LOCAL_LEVEL = 'local';
|
||||
|
||||
my.init = function () {
|
||||
initActiveSpeakerAudioLevels();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the audio level canvas for the given peerJid. If the canvas
|
||||
* didn't exist we create it.
|
||||
*/
|
||||
my.updateAudioLevelCanvas = function (peerJid) {
|
||||
my.updateAudioLevelCanvas = function (peerJid, VideoLayout) {
|
||||
var resourceJid = null;
|
||||
var videoSpanId = null;
|
||||
if (!peerJid)
|
||||
@@ -21,7 +42,7 @@ var AudioLevels = (function(my) {
|
||||
videoSpanId = 'participant_' + resourceJid;
|
||||
}
|
||||
|
||||
videoSpan = document.getElementById(videoSpanId);
|
||||
var videoSpan = document.getElementById(videoSpanId);
|
||||
|
||||
if (!videoSpan) {
|
||||
if (resourceJid)
|
||||
@@ -35,8 +56,7 @@ var AudioLevels = (function(my) {
|
||||
var audioLevelCanvas = $('#' + videoSpanId + '>canvas');
|
||||
|
||||
var videoSpaceWidth = $('#remoteVideos').width();
|
||||
var thumbnailSize
|
||||
= VideoLayout.calculateThumbnailSize(videoSpaceWidth);
|
||||
var thumbnailSize = VideoLayout.calculateThumbnailSize(videoSpaceWidth);
|
||||
var thumbnailWidth = thumbnailSize[0];
|
||||
var thumbnailHeight = thumbnailSize[1];
|
||||
|
||||
@@ -67,7 +87,7 @@ var AudioLevels = (function(my) {
|
||||
* which we draw the audio level
|
||||
* @param audioLevel the newAudio level to render
|
||||
*/
|
||||
my.updateAudioLevel = function (resourceJid, audioLevel) {
|
||||
my.updateAudioLevel = function (resourceJid, audioLevel, largeVideoResourceJid) {
|
||||
drawAudioLevelCanvas(resourceJid, audioLevel);
|
||||
|
||||
var videoSpanId = getVideoSpanId(resourceJid);
|
||||
@@ -84,6 +104,35 @@ var AudioLevels = (function(my) {
|
||||
drawContext.clearRect (0, 0,
|
||||
audioLevelCanvas.width, audioLevelCanvas.height);
|
||||
drawContext.drawImage(canvasCache, 0, 0);
|
||||
|
||||
if(resourceJid === AudioLevels.LOCAL_LEVEL) {
|
||||
if(!APP.xmpp.myJid()) {
|
||||
return;
|
||||
}
|
||||
resourceJid = APP.xmpp.myResource();
|
||||
}
|
||||
|
||||
if(resourceJid === largeVideoResourceJid) {
|
||||
window.requestAnimationFrame(function () {
|
||||
AudioLevels.updateActiveSpeakerAudioLevel(audioLevel);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
my.updateActiveSpeakerAudioLevel = function(audioLevel) {
|
||||
if($("#activeSpeaker").css("visibility") == "hidden")
|
||||
return;
|
||||
|
||||
|
||||
ASDrawContext.clearRect(0, 0, 300, 300);
|
||||
if(audioLevel == 0)
|
||||
return;
|
||||
|
||||
ASDrawContext.shadowBlur = getShadowLevel(audioLevel);
|
||||
|
||||
|
||||
// Fill the shape.
|
||||
ASDrawContext.fill();
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -94,7 +143,7 @@ var AudioLevels = (function(my) {
|
||||
thumbnailHeight) {
|
||||
audioLevelCanvas.width = thumbnailWidth + interfaceConfig.CANVAS_EXTRA;
|
||||
audioLevelCanvas.height = thumbnailHeight + interfaceConfig.CANVAS_EXTRA;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the audio level canvas into the cached canvas object.
|
||||
@@ -143,7 +192,7 @@ var AudioLevels = (function(my) {
|
||||
interfaceConfig.CANVAS_RADIUS,
|
||||
interfaceConfig.SHADOW_COLOR,
|
||||
shadowLevel);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the shadow/glow level for the given audio level.
|
||||
@@ -164,7 +213,7 @@ var AudioLevels = (function(my) {
|
||||
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
|
||||
@@ -173,14 +222,14 @@ var AudioLevels = (function(my) {
|
||||
function getVideoSpanId(resourceJid) {
|
||||
var videoSpanId = null;
|
||||
if (resourceJid === AudioLevels.LOCAL_LEVEL
|
||||
|| (connection.emuc.myroomjid && resourceJid
|
||||
=== Strophe.getResourceFromJid(connection.emuc.myroomjid)))
|
||||
|| (APP.xmpp.myResource() && resourceJid
|
||||
=== APP.xmpp.myResource()))
|
||||
videoSpanId = 'localVideoContainer';
|
||||
else
|
||||
videoSpanId = 'participant_' + resourceJid;
|
||||
|
||||
return videoSpanId;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that the remote video has been resized.
|
||||
@@ -212,3 +261,5 @@ var AudioLevels = (function(my) {
|
||||
return my;
|
||||
|
||||
})(AudioLevels || {});
|
||||
|
||||
module.exports = AudioLevels;
|
||||
@@ -107,3 +107,5 @@ var CanvasUtil = (function(my) {
|
||||
|
||||
return my;
|
||||
})(CanvasUtil || {});
|
||||
|
||||
module.exports = CanvasUtil;
|
||||
88
modules/UI/authentication/Authentication.js
Normal file
@@ -0,0 +1,88 @@
|
||||
/* Initial "authentication required" dialog */
|
||||
var authDialog = null;
|
||||
/* Loop retry ID that wits for other user to create the room */
|
||||
var authRetryId = null;
|
||||
var authenticationWindow = null;
|
||||
|
||||
var Authentication = {
|
||||
openAuthenticationDialog: function (roomName, intervalCallback, callback) {
|
||||
// This is the loop that will wait for the room to be created by
|
||||
// someone else. 'auth_required.moderator' will bring us back here.
|
||||
authRetryId = window.setTimeout(intervalCallback , 5000);
|
||||
// Show prompt only if it's not open
|
||||
if (authDialog !== null) {
|
||||
return;
|
||||
}
|
||||
// extract room name from 'room@muc.server.net'
|
||||
var room = roomName.substr(0, roomName.indexOf('@'));
|
||||
|
||||
var title = APP.translation.generateTranslatonHTML("dialog.Stop");
|
||||
var msg = APP.translation.generateTranslatonHTML("dialog.AuthMsg",
|
||||
{room: room});
|
||||
var button = APP.translation.generateTranslatonHTML(
|
||||
"dialog.Authenticate");
|
||||
var buttons = {};
|
||||
buttons.authenticate = {title: button, value: "authNow"};
|
||||
|
||||
authDialog = APP.UI.messageHandler.openDialog(
|
||||
title,
|
||||
msg,
|
||||
true,
|
||||
buttons,
|
||||
function (onSubmitEvent, submitValue) {
|
||||
|
||||
// Do not close the dialog yet
|
||||
onSubmitEvent.preventDefault();
|
||||
|
||||
// Open login popup
|
||||
if (submitValue === 'authNow') {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
closeAuthenticationWindow:function () {
|
||||
if (authenticationWindow) {
|
||||
authenticationWindow.close();
|
||||
authenticationWindow = null;
|
||||
}
|
||||
},
|
||||
focusAuthenticationWindow: function () {
|
||||
// If auth window exists just bring it to the front
|
||||
if (authenticationWindow) {
|
||||
authenticationWindow.focus();
|
||||
return;
|
||||
}
|
||||
},
|
||||
closeAuthenticationDialog: function () {
|
||||
// Close authentication dialog if opened
|
||||
if (authDialog) {
|
||||
APP.UI.messageHandler.closeDialog();
|
||||
authDialog = null;
|
||||
}
|
||||
},
|
||||
createAuthenticationWindow: function (callback, url) {
|
||||
authenticationWindow = APP.UI.messageHandler.openCenteredPopup(
|
||||
url, 910, 660,
|
||||
// On closed
|
||||
function () {
|
||||
// Close authentication dialog if opened
|
||||
if (authDialog) {
|
||||
messageHandler.closeDialog();
|
||||
authDialog = null;
|
||||
}
|
||||
callback();
|
||||
authenticationWindow = null;
|
||||
});
|
||||
return authenticationWindow;
|
||||
},
|
||||
stopInterval: function () {
|
||||
// Clear retry interval, so that we don't call 'doJoinAfterFocus' twice
|
||||
if (authRetryId) {
|
||||
window.clearTimeout(authRetryId);
|
||||
authRetryId = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = Authentication;
|
||||
155
modules/UI/avatar/Avatar.js
Normal file
@@ -0,0 +1,155 @@
|
||||
var Settings = require("../../settings/Settings");
|
||||
var MediaStreamType = require("../../../service/RTC/MediaStreamTypes");
|
||||
|
||||
var users = {};
|
||||
var activeSpeakerJid;
|
||||
|
||||
function setVisibility(selector, show) {
|
||||
if (selector && selector.length > 0) {
|
||||
selector.css("visibility", show ? "visible" : "hidden");
|
||||
}
|
||||
}
|
||||
|
||||
function isUserMuted(jid) {
|
||||
// XXX(gp) we may want to rename this method to something like
|
||||
// isUserStreaming, for example.
|
||||
if (jid && jid != APP.xmpp.myJid()) {
|
||||
var resource = Strophe.getResourceFromJid(jid);
|
||||
if (!require("../videolayout/VideoLayout").isInLastN(resource)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!APP.RTC.remoteStreams[jid] || !APP.RTC.remoteStreams[jid][MediaStreamType.VIDEO_TYPE]) {
|
||||
return null;
|
||||
}
|
||||
return APP.RTC.remoteStreams[jid][MediaStreamType.VIDEO_TYPE].muted;
|
||||
}
|
||||
|
||||
function getGravatarUrl(id, size) {
|
||||
if(id === APP.xmpp.myJid() || !id) {
|
||||
id = Settings.getSettings().uid;
|
||||
}
|
||||
return 'https://www.gravatar.com/avatar/' +
|
||||
MD5.hexdigest(id.trim().toLowerCase()) +
|
||||
"?d=wavatar&size=" + (size || "30");
|
||||
}
|
||||
|
||||
var Avatar = {
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
setUserAvatar: function (jid, id) {
|
||||
if (id) {
|
||||
if (users[jid] === id) {
|
||||
return;
|
||||
}
|
||||
users[jid] = id;
|
||||
}
|
||||
var thumbUrl = getGravatarUrl(users[jid] || jid, 100);
|
||||
var contactListUrl = 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 === APP.xmpp.myJid()) {
|
||||
$('#avatar').get(0).src = thumbUrl;
|
||||
thumbnail = $('#localVideoContainer');
|
||||
}
|
||||
|
||||
// set the avatar in the contact list
|
||||
var contact = $('#' + resourceJid + '>img');
|
||||
if (contact && contact.length > 0) {
|
||||
contact.get(0).src = contactListUrl;
|
||||
}
|
||||
|
||||
// set the avatar in the thumbnail
|
||||
if (avatar && avatar.length > 0) {
|
||||
avatar[0].src = thumbUrl;
|
||||
} else {
|
||||
if (thumbnail && thumbnail.length > 0) {
|
||||
avatar = document.createElement('img');
|
||||
avatar.id = 'avatar_' + resourceJid;
|
||||
avatar.className = 'userAvatar';
|
||||
avatar.src = thumbUrl;
|
||||
thumbnail.append(avatar);
|
||||
}
|
||||
}
|
||||
|
||||
//if the user is the current active speaker - update the active speaker
|
||||
// avatar
|
||||
if (jid === activeSpeakerJid) {
|
||||
this.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
|
||||
*/
|
||||
showUserAvatar: function (jid, show) {
|
||||
if (users[jid]) {
|
||||
var resourceJid = Strophe.getResourceFromJid(jid);
|
||||
var video = $('#participant_' + resourceJid + '>video');
|
||||
var avatar = $('#avatar_' + resourceJid);
|
||||
|
||||
if (jid === APP.xmpp.myJid()) {
|
||||
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 and the large video is
|
||||
//currently shown
|
||||
if (activeSpeakerJid === jid && require("../videolayout/VideoLayout").isLargeVideoOnTop()) {
|
||||
setVisibility($("#largeVideo"), !show);
|
||||
setVisibility($('#activeSpeaker'), 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
|
||||
*/
|
||||
updateActiveSpeakerAvatarSrc: function (jid) {
|
||||
if (!jid) {
|
||||
jid = APP.xmpp.findJidFromResource(
|
||||
require("../videolayout/VideoLayout").getLargeVideoState().userResourceJid);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
module.exports = Avatar;
|
||||
194
modules/UI/etherpad/Etherpad.js
Normal file
@@ -0,0 +1,194 @@
|
||||
/* global $, config,
|
||||
setLargeVideoVisible, Util */
|
||||
|
||||
var VideoLayout = require("../videolayout/VideoLayout");
|
||||
var Prezi = require("../prezi/Prezi");
|
||||
var UIUtil = require("../util/UIUtil");
|
||||
|
||||
var etherpadName = null;
|
||||
var etherpadIFrame = null;
|
||||
var domain = null;
|
||||
var options = "?showControls=true&showChat=false&showLineNumbers=true&useMonospaceFont=false";
|
||||
|
||||
|
||||
/**
|
||||
* Resizes the etherpad.
|
||||
*/
|
||||
function resize() {
|
||||
if ($('#etherpad>iframe').length) {
|
||||
var remoteVideos = $('#remoteVideos');
|
||||
var availableHeight
|
||||
= window.innerHeight - remoteVideos.outerHeight();
|
||||
var availableWidth = UIUtil.getAvailableVideoWidth();
|
||||
|
||||
$('#etherpad>iframe').width(availableWidth);
|
||||
$('#etherpad>iframe').height(availableHeight);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shares the Etherpad name with other participants.
|
||||
*/
|
||||
function shareEtherpad() {
|
||||
APP.xmpp.addToPresence("etherpad", etherpadName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the Etherpad button and adds it to the toolbar.
|
||||
*/
|
||||
function enableEtherpadButton() {
|
||||
if (!$('#etherpadButton').is(":visible"))
|
||||
$('#etherpadButton').css({display: 'inline-block'});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the IFrame for the etherpad.
|
||||
*/
|
||||
function createIFrame() {
|
||||
etherpadIFrame = document.createElement('iframe');
|
||||
etherpadIFrame.src = domain + etherpadName + options;
|
||||
etherpadIFrame.frameBorder = 0;
|
||||
etherpadIFrame.scrolling = "no";
|
||||
etherpadIFrame.width = $('#largeVideoContainer').width() || 640;
|
||||
etherpadIFrame.height = $('#largeVideoContainer').height() || 480;
|
||||
etherpadIFrame.setAttribute('style', 'visibility: hidden;');
|
||||
|
||||
document.getElementById('etherpad').appendChild(etherpadIFrame);
|
||||
|
||||
etherpadIFrame.onload = function() {
|
||||
|
||||
document.domain = document.domain;
|
||||
bubbleIframeMouseMove(etherpadIFrame);
|
||||
setTimeout(function() {
|
||||
// the iframes inside of the etherpad are
|
||||
// not yet loaded when the etherpad iframe is loaded
|
||||
var outer = etherpadIFrame.
|
||||
contentDocument.getElementsByName("ace_outer")[0];
|
||||
bubbleIframeMouseMove(outer);
|
||||
var inner = outer.
|
||||
contentDocument.getElementsByName("ace_inner")[0];
|
||||
bubbleIframeMouseMove(inner);
|
||||
}, 2000);
|
||||
};
|
||||
}
|
||||
|
||||
function bubbleIframeMouseMove(iframe){
|
||||
var existingOnMouseMove = iframe.contentWindow.onmousemove;
|
||||
iframe.contentWindow.onmousemove = function(e){
|
||||
if(existingOnMouseMove) existingOnMouseMove(e);
|
||||
var evt = document.createEvent("MouseEvents");
|
||||
var boundingClientRect = iframe.getBoundingClientRect();
|
||||
evt.initMouseEvent(
|
||||
"mousemove",
|
||||
true, // bubbles
|
||||
false, // not cancelable
|
||||
window,
|
||||
e.detail,
|
||||
e.screenX,
|
||||
e.screenY,
|
||||
e.clientX + boundingClientRect.left,
|
||||
e.clientY + boundingClientRect.top,
|
||||
e.ctrlKey,
|
||||
e.altKey,
|
||||
e.shiftKey,
|
||||
e.metaKey,
|
||||
e.button,
|
||||
null // no related element
|
||||
);
|
||||
iframe.dispatchEvent(evt);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* On video selected event.
|
||||
*/
|
||||
$(document).bind('video.selected', function (event, isPresentation) {
|
||||
if (config.etherpad_base && etherpadIFrame && etherpadIFrame.style.visibility !== 'hidden')
|
||||
Etherpad.toggleEtherpad(isPresentation);
|
||||
});
|
||||
|
||||
|
||||
var Etherpad = {
|
||||
/**
|
||||
* Initializes the etherpad.
|
||||
*/
|
||||
init: function (name) {
|
||||
|
||||
if (config.etherpad_base && !etherpadName) {
|
||||
|
||||
domain = config.etherpad_base;
|
||||
|
||||
if (!name) {
|
||||
// In case we're the focus we generate the name.
|
||||
etherpadName = Math.random().toString(36).substring(7) +
|
||||
'_' + (new Date().getTime()).toString();
|
||||
shareEtherpad();
|
||||
}
|
||||
else
|
||||
etherpadName = name;
|
||||
|
||||
enableEtherpadButton();
|
||||
|
||||
/**
|
||||
* Resizes the etherpad, when the window is resized.
|
||||
*/
|
||||
$(window).resize(function () {
|
||||
resize();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Opens/hides the Etherpad.
|
||||
*/
|
||||
toggleEtherpad: function (isPresentation) {
|
||||
if (!etherpadIFrame)
|
||||
createIFrame();
|
||||
|
||||
var largeVideo = null;
|
||||
if (Prezi.isPresentationVisible())
|
||||
largeVideo = $('#presentation>iframe');
|
||||
else
|
||||
largeVideo = $('#largeVideo');
|
||||
|
||||
if ($('#etherpad>iframe').css('visibility') === 'hidden') {
|
||||
$('#activeSpeaker').css('visibility', 'hidden');
|
||||
largeVideo.fadeOut(300, function () {
|
||||
if (Prezi.isPresentationVisible()) {
|
||||
largeVideo.css({opacity: '0'});
|
||||
} else {
|
||||
VideoLayout.setLargeVideoVisible(false);
|
||||
}
|
||||
});
|
||||
|
||||
$('#etherpad>iframe').fadeIn(300, function () {
|
||||
document.body.style.background = '#eeeeee';
|
||||
$('#etherpad>iframe').css({visibility: 'visible'});
|
||||
$('#etherpad').css({zIndex: 2});
|
||||
});
|
||||
}
|
||||
else if ($('#etherpad>iframe')) {
|
||||
$('#etherpad>iframe').fadeOut(300, function () {
|
||||
$('#etherpad>iframe').css({visibility: 'hidden'});
|
||||
$('#etherpad').css({zIndex: 0});
|
||||
document.body.style.background = 'black';
|
||||
});
|
||||
|
||||
if (!isPresentation) {
|
||||
$('#largeVideo').fadeIn(300, function () {
|
||||
VideoLayout.setLargeVideoVisible(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
resize();
|
||||
},
|
||||
|
||||
isVisible: function() {
|
||||
var etherpadIframe = $('#etherpad>iframe');
|
||||
return etherpadIframe && etherpadIframe.is(':visible');
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
module.exports = Etherpad;
|
||||
371
modules/UI/prezi/Prezi.js
Normal file
@@ -0,0 +1,371 @@
|
||||
var ToolbarToggler = require("../toolbars/ToolbarToggler");
|
||||
var UIUtil = require("../util/UIUtil");
|
||||
var VideoLayout = require("../videolayout/VideoLayout");
|
||||
var messageHandler = require("../util/MessageHandler");
|
||||
var PreziPlayer = require("./PreziPlayer");
|
||||
|
||||
var preziPlayer = null;
|
||||
|
||||
var Prezi = {
|
||||
|
||||
|
||||
/**
|
||||
* Reloads the current presentation.
|
||||
*/
|
||||
reloadPresentation: function() {
|
||||
var iframe = document.getElementById(preziPlayer.options.preziId);
|
||||
iframe.src = iframe.src;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns <tt>true</tt> if the presentation is visible, <tt>false</tt> -
|
||||
* otherwise.
|
||||
*/
|
||||
isPresentationVisible: function () {
|
||||
return ($('#presentation>iframe') != null
|
||||
&& $('#presentation>iframe').css('opacity') == 1);
|
||||
},
|
||||
|
||||
/**
|
||||
* Opens the Prezi dialog, from which the user could choose a presentation
|
||||
* to load.
|
||||
*/
|
||||
openPreziDialog: function() {
|
||||
var myprezi = APP.xmpp.getPrezi();
|
||||
if (myprezi) {
|
||||
messageHandler.openTwoButtonDialog("dialog.removePreziTitle",
|
||||
null,
|
||||
"dialog.removePreziMsg",
|
||||
null,
|
||||
false,
|
||||
"dialog.Remove",
|
||||
function(e,v,m,f) {
|
||||
if(v) {
|
||||
APP.xmpp.removePreziFromPresence();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
else if (preziPlayer != null) {
|
||||
messageHandler.openTwoButtonDialog("dialog.sharePreziTitle",
|
||||
null, "dialog.sharePreziMsg",
|
||||
null,
|
||||
false,
|
||||
"dialog.Ok",
|
||||
function(e,v,m,f) {
|
||||
$.prompt.close();
|
||||
}
|
||||
);
|
||||
}
|
||||
else {
|
||||
var html = APP.translation.generateTranslatonHTML(
|
||||
"dialog.sharePreziTitle");
|
||||
var cancelButton = APP.translation.generateTranslatonHTML(
|
||||
"dialog.Cancel");
|
||||
var shareButton = APP.translation.generateTranslatonHTML(
|
||||
"dialog.Share");
|
||||
var backButton = APP.translation.generateTranslatonHTML(
|
||||
"dialog.Back");
|
||||
var buttons = {};
|
||||
var buttons1 = {};
|
||||
buttons1.button1 = buttons.button1 = {title: cancelButton, value: false};
|
||||
buttons.button2 = {title: shareButton, value: true};
|
||||
buttons1.button2 = {title: backButton, value: true};
|
||||
var linkError = APP.translation.generateTranslatonHTML(
|
||||
"dialog.preziLinkError");
|
||||
var defaultUrl = APP.translation.translateString("defaultPreziLink",
|
||||
{url: "http://prezi.com/wz7vhjycl7e6/my-prezi"});
|
||||
var openPreziState = {
|
||||
state0: {
|
||||
html: '<h2>' + html + '</h2>' +
|
||||
'<input id="preziUrl" type="text" ' +
|
||||
'data-i18n="[placeholder]defaultPreziLink" data-i18n-options=\'' +
|
||||
JSON.stringify({"url": "http://prezi.com/wz7vhjycl7e6/my-prezi"}) +
|
||||
'\' placeholder="' + defaultUrl + '" autofocus>',
|
||||
persistent: false,
|
||||
buttons: buttons,
|
||||
defaultButton: 1,
|
||||
submit: function(e,v,m,f){
|
||||
e.preventDefault();
|
||||
if(v)
|
||||
{
|
||||
var preziUrl = document.getElementById('preziUrl');
|
||||
|
||||
if (preziUrl.value)
|
||||
{
|
||||
var urlValue
|
||||
= encodeURI(UIUtil.escapeHtml(preziUrl.value));
|
||||
|
||||
if (urlValue.indexOf('http://prezi.com/') != 0
|
||||
&& urlValue.indexOf('https://prezi.com/') != 0)
|
||||
{
|
||||
$.prompt.goToState('state1');
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
var presIdTmp = urlValue.substring(
|
||||
urlValue.indexOf("prezi.com/") + 10);
|
||||
if (!isAlphanumeric(presIdTmp)
|
||||
|| presIdTmp.indexOf('/') < 2) {
|
||||
$.prompt.goToState('state1');
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
APP.xmpp.addToPresence("prezi", urlValue);
|
||||
$.prompt.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
$.prompt.close();
|
||||
}
|
||||
},
|
||||
state1: {
|
||||
html: '<h2>' + html + '</h2>' +
|
||||
linkError,
|
||||
persistent: false,
|
||||
buttons: buttons1,
|
||||
defaultButton: 1,
|
||||
submit:function(e,v,m,f) {
|
||||
e.preventDefault();
|
||||
if(v==0)
|
||||
$.prompt.close();
|
||||
else
|
||||
$.prompt.goToState('state0');
|
||||
}
|
||||
}
|
||||
};
|
||||
var focusPreziUrl = function(e) {
|
||||
document.getElementById('preziUrl').focus();
|
||||
};
|
||||
messageHandler.openDialogWithStates(openPreziState, focusPreziUrl, focusPreziUrl);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* A new presentation has been added.
|
||||
*
|
||||
* @param event the event indicating the add of a presentation
|
||||
* @param jid the jid from which the presentation was added
|
||||
* @param presUrl url of the presentation
|
||||
* @param currentSlide the current slide to which we should move
|
||||
*/
|
||||
function presentationAdded(event, jid, presUrl, currentSlide) {
|
||||
console.log("presentation added", presUrl);
|
||||
|
||||
var presId = getPresentationId(presUrl);
|
||||
|
||||
var elementId = 'participant_'
|
||||
+ Strophe.getResourceFromJid(jid)
|
||||
+ '_' + presId;
|
||||
|
||||
// We explicitly don't specify the peer jid here, because we don't want
|
||||
// this video to be dealt with as a peer related one (for example we
|
||||
// don't want to show a mute/kick menu for this one, etc.).
|
||||
VideoLayout.addRemoteVideoContainer(null, elementId);
|
||||
VideoLayout.resizeThumbnails();
|
||||
|
||||
var controlsEnabled = false;
|
||||
if (jid === APP.xmpp.myJid())
|
||||
controlsEnabled = true;
|
||||
|
||||
setPresentationVisible(true);
|
||||
$('#largeVideoContainer').hover(
|
||||
function (event) {
|
||||
if (Prezi.isPresentationVisible()) {
|
||||
var reloadButtonRight = window.innerWidth
|
||||
- $('#presentation>iframe').offset().left
|
||||
- $('#presentation>iframe').width();
|
||||
|
||||
$('#reloadPresentation').css({ right: reloadButtonRight,
|
||||
display:'inline-block'});
|
||||
}
|
||||
},
|
||||
function (event) {
|
||||
if (!Prezi.isPresentationVisible())
|
||||
$('#reloadPresentation').css({display:'none'});
|
||||
else {
|
||||
var e = event.toElement || event.relatedTarget;
|
||||
|
||||
if (e && e.id != 'reloadPresentation' && e.id != 'header')
|
||||
$('#reloadPresentation').css({display:'none'});
|
||||
}
|
||||
});
|
||||
|
||||
preziPlayer = new PreziPlayer(
|
||||
'presentation',
|
||||
{preziId: presId,
|
||||
width: getPresentationWidth(),
|
||||
height: getPresentationHeihgt(),
|
||||
controls: controlsEnabled,
|
||||
debug: true
|
||||
});
|
||||
|
||||
$('#presentation>iframe').attr('id', preziPlayer.options.preziId);
|
||||
|
||||
preziPlayer.on(PreziPlayer.EVENT_STATUS, function(event) {
|
||||
console.log("prezi status", event.value);
|
||||
if (event.value == PreziPlayer.STATUS_CONTENT_READY) {
|
||||
if (jid != APP.xmpp.myJid())
|
||||
preziPlayer.flyToStep(currentSlide);
|
||||
}
|
||||
});
|
||||
|
||||
preziPlayer.on(PreziPlayer.EVENT_CURRENT_STEP, function(event) {
|
||||
console.log("event value", event.value);
|
||||
APP.xmpp.addToPresence("preziSlide", event.value);
|
||||
});
|
||||
|
||||
$("#" + elementId).css( 'background-image',
|
||||
'url(../images/avatarprezi.png)');
|
||||
$("#" + elementId).click(
|
||||
function () {
|
||||
setPresentationVisible(true);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* A presentation has been removed.
|
||||
*
|
||||
* @param event the event indicating the remove of a presentation
|
||||
* @param jid the jid for which the presentation was removed
|
||||
* @param the url of the presentation
|
||||
*/
|
||||
function presentationRemoved(event, jid, presUrl) {
|
||||
console.log('presentation removed', presUrl);
|
||||
var presId = getPresentationId(presUrl);
|
||||
setPresentationVisible(false);
|
||||
$('#participant_'
|
||||
+ Strophe.getResourceFromJid(jid)
|
||||
+ '_' + presId).remove();
|
||||
$('#presentation>iframe').remove();
|
||||
if (preziPlayer != null) {
|
||||
preziPlayer.destroy();
|
||||
preziPlayer = null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Indicates if the given string is an alphanumeric string.
|
||||
* Note that some special characters are also allowed (-, _ , /, &, ?, =, ;) for the
|
||||
* purpose of checking URIs.
|
||||
*/
|
||||
function isAlphanumeric(unsafeText) {
|
||||
var regex = /^[a-z0-9-_\/&\?=;]+$/i;
|
||||
return regex.test(unsafeText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the presentation id from the given url.
|
||||
*/
|
||||
function getPresentationId (presUrl) {
|
||||
var presIdTmp = presUrl.substring(presUrl.indexOf("prezi.com/") + 10);
|
||||
return presIdTmp.substring(0, presIdTmp.indexOf('/'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the presentation width.
|
||||
*/
|
||||
function getPresentationWidth() {
|
||||
var availableWidth = UIUtil.getAvailableVideoWidth();
|
||||
var availableHeight = getPresentationHeihgt();
|
||||
|
||||
var aspectRatio = 16.0 / 9.0;
|
||||
if (availableHeight < availableWidth / aspectRatio) {
|
||||
availableWidth = Math.floor(availableHeight * aspectRatio);
|
||||
}
|
||||
return availableWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the presentation height.
|
||||
*/
|
||||
function getPresentationHeihgt() {
|
||||
var remoteVideos = $('#remoteVideos');
|
||||
return window.innerHeight - remoteVideos.outerHeight();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resizes the presentation iframe.
|
||||
*/
|
||||
function resize() {
|
||||
if ($('#presentation>iframe')) {
|
||||
$('#presentation>iframe').width(getPresentationWidth());
|
||||
$('#presentation>iframe').height(getPresentationHeihgt());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows/hides a presentation.
|
||||
*/
|
||||
function setPresentationVisible(visible) {
|
||||
var prezi = $('#presentation>iframe');
|
||||
if (visible) {
|
||||
// Trigger the video.selected event to indicate a change in the
|
||||
// large video.
|
||||
$(document).trigger("video.selected", [true]);
|
||||
|
||||
$('#largeVideo').fadeOut(300);
|
||||
prezi.fadeIn(300, function() {
|
||||
prezi.css({opacity:'1'});
|
||||
ToolbarToggler.dockToolbar(true);
|
||||
VideoLayout.setLargeVideoVisible(false);
|
||||
});
|
||||
$('#activeSpeaker').css('visibility', 'hidden');
|
||||
}
|
||||
else {
|
||||
if (prezi.css('opacity') == '1') {
|
||||
prezi.fadeOut(300, function () {
|
||||
prezi.css({opacity:'0'});
|
||||
$('#reloadPresentation').css({display:'none'});
|
||||
$('#largeVideo').fadeIn(300, function() {
|
||||
VideoLayout.setLargeVideoVisible(true);
|
||||
ToolbarToggler.dockToolbar(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Presentation has been removed.
|
||||
*/
|
||||
$(document).bind('presentationremoved.muc', presentationRemoved);
|
||||
|
||||
/**
|
||||
* Presentation has been added.
|
||||
*/
|
||||
$(document).bind('presentationadded.muc', presentationAdded);
|
||||
|
||||
/*
|
||||
* Indicates presentation slide change.
|
||||
*/
|
||||
$(document).bind('gotoslide.muc', function (event, jid, presUrl, current) {
|
||||
if (preziPlayer && preziPlayer.getCurrentStep() != current) {
|
||||
preziPlayer.flyToStep(current);
|
||||
|
||||
var animationStepsArray = preziPlayer.getAnimationCountOnSteps();
|
||||
for (var i = 0; i < parseInt(animationStepsArray[current]); i++) {
|
||||
preziPlayer.flyToStep(current, i);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* On video selected event.
|
||||
*/
|
||||
$(document).bind('video.selected', function (event, isPresentation) {
|
||||
if (!isPresentation && $('#presentation>iframe')) {
|
||||
setPresentationVisible(false);
|
||||
}
|
||||
});
|
||||
|
||||
$(window).resize(function () {
|
||||
resize();
|
||||
});
|
||||
|
||||
module.exports = Prezi;
|
||||
@@ -33,21 +33,22 @@
|
||||
var message, item, player;
|
||||
try {
|
||||
message = JSON.parse(event.data);
|
||||
} catch (e) {}
|
||||
if (message.id && (player = PreziPlayer.players[message.id])){
|
||||
if (player.options.debug === true) {
|
||||
if (console && console.log) console.log('received', message);
|
||||
}
|
||||
if (message.type === "changes"){
|
||||
player.changesHandler(message);
|
||||
}
|
||||
for (var i=0; i<player.callbacks.length; i++) {
|
||||
item = player.callbacks[i];
|
||||
if (item && message.type === item.event){
|
||||
item.callback(message);
|
||||
if (message.id && (player = PreziPlayer.players[message.id])) {
|
||||
if (player.options.debug === true) {
|
||||
if (console && console.log)
|
||||
console.log('received', message);
|
||||
}
|
||||
if (message.type === "changes") {
|
||||
player.changesHandler(message);
|
||||
}
|
||||
for (var i = 0; i < player.callbacks.length; i++) {
|
||||
item = player.callbacks[i];
|
||||
if (item && message.type === item.event) {
|
||||
item.callback(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) { }
|
||||
};
|
||||
|
||||
function PreziPlayer(id, options) {
|
||||
@@ -289,3 +290,5 @@
|
||||
})();
|
||||
|
||||
})();
|
||||
|
||||
module.exports = PreziPlayer;
|
||||