Parcourir la source

Merge remote-tracking branch 'upstream/master'

master
turint il y a 11 ans
Parent
révision
77cb10d6a1

+ 3
- 0
README.md Voir le fichier

@@ -14,6 +14,9 @@ You can find information on how to deploy Jitsi Meet in the [installation instru
14 14
 
15 15
 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/) 
16 16
 
17
+## Discuss
18
+Please use the [Jitsi dev mailing list](http://lists.jitsi.org/pipermail/dev/) to discuss feature requests before opening an issue on github. 
19
+
17 20
 ## Acknowledgements
18 21
 
19 22
 Jitsi Meet started out as a sample conferencing application using Jitsi Videobridge. It was originally developed by Philipp Hancke who then contributed it to the community where development continues with joint forces! 

+ 226
- 945
app.js
Fichier diff supprimé car celui-ci est trop grand
Voir le fichier


+ 51
- 3
chat.js Voir le fichier

@@ -39,10 +39,19 @@ var Chat = (function (my) {
39 39
         $('#usermsg').keydown(function (event) {
40 40
             if (event.keyCode === 13) {
41 41
                 event.preventDefault();
42
-                var message = Util.escapeHtml(this.value);
42
+                var value = this.value;
43 43
                 $('#usermsg').val('').trigger('autosize.resize');
44 44
                 this.focus();
45
-                connection.emuc.sendMessage(message, nickname);
45
+                var command = new CommandsProcessor(value);
46
+                if(command.isCommand())
47
+                {
48
+                    command.processCommand();
49
+                }
50
+                else
51
+                {
52
+                    var message = Util.escapeHtml(value);
53
+                    connection.emuc.sendMessage(message, nickname);
54
+                }
46 55
             }
47 56
         });
48 57
 
@@ -90,6 +99,45 @@ var Chat = (function (my) {
90 99
                 { scrollTop: $('#chatconversation')[0].scrollHeight}, 1000);
91 100
     };
92 101
 
102
+    /**
103
+     * Appends error message to the conversation
104
+     * @param errorMessage the received error message.
105
+     * @param originalText the original message.
106
+     */
107
+    my.chatAddError = function(errorMessage, originalText)
108
+    {
109
+        errorMessage = Util.escapeHtml(errorMessage);
110
+        originalText = Util.escapeHtml(originalText);
111
+
112
+        $('#chatconversation').append('<div class="errorMessage"><b>Error: </b>'
113
+            + 'Your message' + (originalText? (' \"'+ originalText + '\"') : "")
114
+            + ' was not sent.' + (errorMessage? (' Reason: ' + errorMessage) : '')
115
+            +  '</div>');
116
+        $('#chatconversation').animate(
117
+            { scrollTop: $('#chatconversation')[0].scrollHeight}, 1000);
118
+
119
+    }
120
+
121
+    /**
122
+     * Sets the subject to the UI
123
+     * @param subject the subject
124
+     */
125
+    my.chatSetSubject = function(subject)
126
+    {
127
+        if(subject)
128
+            subject = subject.trim();
129
+        $('#subject').html(linkify(Util.escapeHtml(subject)));
130
+        if(subject == "")
131
+        {
132
+            $("#subject").css({display: "none"});
133
+        }
134
+        else
135
+        {
136
+            $("#subject").css({display: "block"});
137
+        }
138
+    }
139
+
140
+
93 141
     /**
94 142
      * Opens / closes the chat area.
95 143
      */
@@ -242,7 +290,7 @@ var Chat = (function (my) {
242 290
         if (unreadMessages) {
243 291
             unreadMsgElement.innerHTML = unreadMessages.toString();
244 292
 
245
-            showToolbar();
293
+            Toolbar.showToolbar();
246 294
 
247 295
             var chatButtonElement
248 296
                 = document.getElementById('chatButton').parentNode;

+ 98
- 0
commands.js Voir le fichier

@@ -0,0 +1,98 @@
1
+/**
2
+ * Handles commands received via chat messages.
3
+ */
4
+var CommandsProcessor = (function()
5
+{
6
+    /**
7
+     * Constructs new CommandProccessor instance from a message.
8
+     * @param message the message
9
+     * @constructor
10
+     */
11
+    function CommandsPrototype(message)
12
+    {
13
+        /**
14
+         * Extracts the command from the message.
15
+         * @param message the received message
16
+         * @returns {string} the command
17
+         */
18
+        function getCommand(message)
19
+        {
20
+            if(message)
21
+            {
22
+                for(var command in commands)
23
+                {
24
+                    if(message.indexOf("/" + command) == 0)
25
+                        return command;
26
+                }
27
+            }
28
+            return "";
29
+        };
30
+
31
+        var command = getCommand(message);
32
+
33
+        /**
34
+         * Returns the name of the command.
35
+         * @returns {String} the command
36
+         */
37
+        this.getCommand = function()
38
+        {
39
+            return command;
40
+        }
41
+
42
+
43
+        var messageArgument = message.substr(command.length + 2);
44
+
45
+        /**
46
+         * Returns the arguments of the command.
47
+         * @returns {string}
48
+         */
49
+        this.getArgument = function()
50
+        {
51
+            return messageArgument;
52
+        }
53
+    }
54
+
55
+    /**
56
+     * Checks whether this instance is valid command or not.
57
+     * @returns {boolean}
58
+     */
59
+    CommandsPrototype.prototype.isCommand = function()
60
+    {
61
+        if(this.getCommand())
62
+            return true;
63
+        return false;
64
+    }
65
+
66
+    /**
67
+     * Processes the command.
68
+     */
69
+    CommandsPrototype.prototype.processCommand = function()
70
+    {
71
+        if(!this.isCommand())
72
+            return;
73
+
74
+        commands[this.getCommand()](this.getArgument());
75
+
76
+    }
77
+
78
+    /**
79
+     * Processes the data for topic command.
80
+     * @param commandArguments the arguments of the topic command.
81
+     */
82
+    var processTopic = function(commandArguments)
83
+    {
84
+        var topic = Util.escapeHtml(commandArguments);
85
+        connection.emuc.setSubject(topic);
86
+    }
87
+
88
+    /**
89
+     * List with supported commands. The keys are the names of the commands and
90
+     * the value is the function that processes the message.
91
+     * @type {{String: function}}
92
+     */
93
+    var commands = {
94
+        "topic" : processTopic
95
+    };
96
+
97
+    return CommandsPrototype;
98
+})();

+ 3
- 1
config.js Voir le fichier

@@ -11,5 +11,7 @@ var config = {
11 11
     bosh: '//lambada.jitsi.net/http-bind', // FIXME: use xep-0156 for that
12 12
     desktopSharing: 'ext', // Desktop sharing method. Can be set to 'ext', 'webrtc' or false to disable.
13 13
     chromeExtensionId: 'diibjkoicjeejcmhdnailmkgecihlobk', // Id of desktop streamer Chrome extension
14
-    minChromeExtVersion: '0.1' // Required version of Chrome extension
14
+    minChromeExtVersion: '0.1', // Required version of Chrome extension
15
+    enableRtpStats: false, // Enables RTP stats processing
16
+    openSctp: true //Toggle to enable/disable SCTP channels
15 17
 };

+ 4
- 0
css/main.css Voir le fichier

@@ -43,6 +43,10 @@ html, body{
43 43
     color: #087dba;
44 44
 }
45 45
 
46
+.errorMessage {
47
+    color: red;
48
+}
49
+
46 50
 .remoteuser {
47 51
     color: #424242;
48 52
 }

+ 124
- 0
css/popover.css Voir le fichier

@@ -0,0 +1,124 @@
1
+.popover {
2
+  position: absolute;
3
+  top: 0;
4
+  left: 0;
5
+  z-index: 1010;
6
+  display: none;
7
+  max-width: 300px;
8
+  min-width: 100px;
9
+  padding: 1px;
10
+  text-align: left;
11
+  color: #428bca;
12
+  background-color: #ffffff;
13
+  background-clip: padding-box;
14
+  border: 1px solid #cccccc;
15
+  border: 1px solid rgba(0, 0, 0, 0.2);
16
+  border-radius: 6px;
17
+  -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
18
+  box-shadow: 0 5px 10px rgba(0, 0, 0, 0.4);
19
+  white-space: normal;
20
+}
21
+.popover.top {
22
+  margin-top: -10px;
23
+}
24
+.popover.right {
25
+  margin-left: 10px;
26
+}
27
+.popover.bottom {
28
+  margin-top: 10px;
29
+}
30
+.popover.left {
31
+  margin-left: -10px;
32
+}
33
+.popover-title {
34
+  margin: 0;
35
+  padding: 8px 14px;
36
+  font-size: 11pt;
37
+  font-weight: normal;
38
+  line-height: 18px;
39
+  background-color: #f7f7f7;
40
+  border-bottom: 1px solid #ebebeb;
41
+  border-radius: 5px 5px 0 0;
42
+}
43
+.popover-content {
44
+  padding: 9px 14px;
45
+  font-size: 10pt;
46
+  white-space:pre-wrap;
47
+  text-align: center;
48
+}
49
+.popover > .arrow,
50
+.popover > .arrow:after {
51
+  position: absolute;
52
+  display: block;
53
+  width: 0;
54
+  height: 0;
55
+  border-color: transparent;
56
+  border-style: solid;
57
+}
58
+.popover > .arrow {
59
+  border-width: 11px;
60
+}
61
+.popover > .arrow:after {
62
+  border-width: 10px;
63
+  content: "";
64
+}
65
+.popover.top > .arrow {
66
+  left: 50%;
67
+  margin-left: -11px;
68
+  border-bottom-width: 0;
69
+  border-top-color: #999999;
70
+  border-top-color: rgba(0, 0, 0, 0.25);
71
+  bottom: -11px;
72
+}
73
+.popover.top > .arrow:after {
74
+  content: " ";
75
+  bottom: 1px;
76
+  margin-left: -10px;
77
+  border-bottom-width: 0;
78
+  border-top-color: #ffffff;
79
+}
80
+.popover.right > .arrow {
81
+  top: 50%;
82
+  left: -11px;
83
+  margin-top: -11px;
84
+  border-left-width: 0;
85
+  border-right-color: #999999;
86
+  border-right-color: rgba(0, 0, 0, 0.25);
87
+}
88
+.popover.right > .arrow:after {
89
+  content: " ";
90
+  left: 1px;
91
+  bottom: -10px;
92
+  border-left-width: 0;
93
+  border-right-color: #ffffff;
94
+}
95
+.popover.bottom > .arrow {
96
+  left: 50%;
97
+  margin-left: -11px;
98
+  border-top-width: 0;
99
+  border-bottom-color: #999999;
100
+  border-bottom-color: rgba(0, 0, 0, 0.25);
101
+  top: -11px;
102
+}
103
+.popover.bottom > .arrow:after {
104
+  content: " ";
105
+  top: 1px;
106
+  margin-left: -10px;
107
+  border-top-width: 0;
108
+  border-bottom-color: #ffffff;
109
+}
110
+.popover.left > .arrow {
111
+  top: 50%;
112
+  right: -11px;
113
+  margin-top: -11px;
114
+  border-right-width: 0;
115
+  border-left-color: #999999;
116
+  border-left-color: rgba(0, 0, 0, 0.25);
117
+}
118
+.popover.left > .arrow:after {
119
+  content: " ";
120
+  right: 1px;
121
+  border-right-width: 0;
122
+  border-left-color: #ffffff;
123
+  bottom: -10px;
124
+}

+ 2
- 2
css/popup_menu.css Voir le fichier

@@ -9,7 +9,7 @@ ul.popupmenu {
9 9
     padding-bottom: 5px;
10 10
     padding-top: 5px;
11 11
     right: 10px;
12
-    left: 0px;
12
+    left: -5px;
13 13
     width: 100px;
14 14
     background-color: rgba(0,0,0,1);
15 15
     -webkit-box-shadow: 0 0 2px #000000, 0 0 10px #000000;
@@ -21,7 +21,7 @@ ul.popupmenu:after {
21 21
     display: block;
22 22
     position: absolute;
23 23
     bottom: -9px;
24
-    left: 13px;
24
+    left: 11px;
25 25
 }
26 26
 
27 27
 ul.popupmenu li {

+ 39
- 17
css/videolayout_default.css Voir le fichier

@@ -32,9 +32,11 @@
32 32
     background-size: contain;
33 33
     border-radius:8px;
34 34
     border: 2px solid #212425;
35
+    margin-right: 3px;
35 36
 }
36 37
 
37
-#remoteVideos .videocontainer:hover {
38
+#remoteVideos .videocontainer:hover,
39
+#remoteVideos .videocontainer.videoContainerFocused {
38 40
     width: 100%;
39 41
     height: 100%;
40 42
     content:"";
@@ -49,7 +51,6 @@
49 51
     -webkit-animation-iteration-count: 1;
50 52
     -webkit-box-shadow: 0 0 18px #388396;
51 53
     border: 2px solid #388396;
52
-    z-index: 3;
53 54
 }
54 55
 
55 56
 #localVideoWrapper {
@@ -93,6 +94,11 @@
93 94
     height: 100%;
94 95
 }
95 96
 
97
+.activespeaker {
98
+    -webkit-filter: grayscale(1);
99
+    filter: grayscale(1);
100
+}
101
+
96 102
 #etherpad,
97 103
 #presentation {
98 104
     text-align: center;
@@ -119,6 +125,7 @@
119 125
     text-shadow: 0px 1px 0px rgba(255,255,255,.3), 0px -1px 0px rgba(0,0,0,.7);
120 126
     border: 0px;
121 127
     z-index: 2;
128
+    text-align: center;
122 129
 }
123 130
 
124 131
 #remoteVideos .nick {
@@ -133,25 +140,22 @@
133 140
 
134 141
 .videocontainer>span.displayname,
135 142
 .videocontainer>input.displayname {
136
-    display: inline-block;
143
+    display: none;
137 144
     position: absolute;
138
-    background: -webkit-linear-gradient(left, rgba(0,0,0,.7), rgba(0,0,0,0));
139 145
     color: #FFFFFF;
140
-    bottom: 0;
141
-    left: 0;
142
-    padding: 3px 5px;
143
-    width: 100%;
144
-    height: auto;
145
-    max-height: 18px;
146
-    font-size: 9pt;
147
-    text-align: left;
146
+    background: rgba(0,0,0,.7);
147
+    text-align: center;
148 148
     text-overflow: ellipsis;
149
+    width: 70%;
150
+    height: 20%;
151
+    left: 15%;
152
+    top: 40%;
153
+    padding: 5px;
154
+    font-size: 11pt;
149 155
     overflow: hidden;
150 156
     white-space: nowrap;
151 157
     z-index: 2;
152
-    box-sizing: border-box;
153
-    border-bottom-left-radius:4px;
154
-    border-bottom-right-radius:4px;
158
+    border-radius:20px;
155 159
 }
156 160
 
157 161
 #localVideoContainer>span.displayname:hover {
@@ -162,6 +166,10 @@
162 166
     pointer-events: none;
163 167
 }
164 168
 
169
+.videocontainer>input.displayname {
170
+    height: auto;
171
+}
172
+
165 173
 #localDisplayName {
166 174
     pointer-events: auto !important;
167 175
 }
@@ -190,6 +198,7 @@
190 198
     text-shadow: 0px 1px 0px rgba(255,255,255,.3), 0px -1px 0px rgba(0,0,0,.7);
191 199
     border: 0px;
192 200
     z-index: 3;
201
+    text-align: center;
193 202
 }
194 203
 
195 204
 .videocontainer>span.videoMuted {
@@ -226,7 +235,6 @@
226 235
 #header{
227 236
     display:none;
228 237
     position:absolute;
229
-    height: 0px;
230 238
     text-align:center;
231 239
     top:0;
232 240
     left:0;
@@ -241,13 +249,27 @@
241 249
     margin-right:auto;
242 250
     height:39px;
243 251
     width:auto;
244
-    overflow: hidden;
245 252
     background: linear-gradient(to bottom, rgba(103,103,103,.65) , rgba(0,0,0,.65));
246 253
     -webkit-box-shadow: 0 0 2px #000000, 0 0 10px #000000;
247 254
     border-bottom-left-radius: 12px;
248 255
     border-bottom-right-radius: 12px;
249 256
 }
250 257
 
258
+#subject {
259
+    position: relative;
260
+    z-index: 3;
261
+    width: auto;
262
+    padding: 5px;
263
+    margin-left: 40%;
264
+    margin-right: 40%;
265
+    text-align: center;
266
+    background: linear-gradient(to bottom, rgba(255,255,255,.85) , rgba(255,255,255,.35));
267
+    -webkit-box-shadow: 0 0 2px #000000, 0 0 10px #000000;
268
+    border-bottom-left-radius: 12px;
269
+    border-bottom-right-radius: 12px;
270
+    display: none;
271
+}
272
+
251 273
 .watermark {
252 274
     display: block;
253 275
     position: absolute;

+ 80
- 0
data_channels.js Voir le fichier

@@ -0,0 +1,80 @@
1
+/* global connection, Strophe, updateLargeVideo, focusedVideoSrc*/
2
+/**
3
+ * Callback triggered by PeerConnection when new data channel is opened
4
+ * on the bridge.
5
+ * @param event the event info object.
6
+ */
7
+function onDataChannel(event)
8
+{
9
+    var dataChannel = event.channel;
10
+
11
+    dataChannel.onopen = function ()
12
+    {
13
+        console.info("Data channel opened by the bridge !!!", dataChannel);
14
+
15
+        // Code sample for sending string and/or binary data
16
+        // Sends String message to the bridge
17
+        //dataChannel.send("Hello bridge!");
18
+        // Sends 12 bytes binary message to the bridge
19
+        //dataChannel.send(new ArrayBuffer(12));
20
+    };
21
+
22
+    dataChannel.onerror = function (error)
23
+    {
24
+        console.error("Data Channel Error:", error, dataChannel);
25
+    };
26
+
27
+    dataChannel.onmessage = function (event)
28
+    {
29
+        var msgData = event.data;
30
+        console.info("Got Data Channel Message:", msgData, dataChannel);
31
+
32
+        // Active speaker event
33
+        if (msgData.indexOf('activeSpeaker') === 0)
34
+        {
35
+            // Endpoint ID from the bridge
36
+            var resourceJid = msgData.split(":")[1];
37
+
38
+            console.info(
39
+                "Data channel new active speaker event: " + resourceJid);
40
+            $(document).trigger('activespeakerchanged', [resourceJid]);
41
+        }
42
+    };
43
+
44
+    dataChannel.onclose = function ()
45
+    {
46
+        console.info("The Data Channel closed", dataChannel);
47
+    };
48
+}
49
+
50
+/**
51
+ * Binds "ondatachannel" event listener to given PeerConnection instance.
52
+ * @param peerConnection WebRTC peer connection instance.
53
+ */
54
+function bindDataChannelListener(peerConnection)
55
+{
56
+    peerConnection.ondatachannel = onDataChannel;
57
+
58
+    // Sample code for opening new data channel from Jitsi Meet to the bridge.
59
+    // Although it's not a requirement to open separate channels from both bridge
60
+    // and peer as single channel can be used for sending and receiving data.
61
+    // So either channel opened by the bridge or the one opened here is enough
62
+    // for communication with the bridge.
63
+    /*var dataChannelOptions =
64
+    {
65
+        reliable: true
66
+    };
67
+    var dataChannel
68
+       = peerConnection.createDataChannel("myChannel", dataChannelOptions);
69
+
70
+    // Can be used only when is in open state
71
+    dataChannel.onopen = function ()
72
+    {
73
+        dataChannel.send("My channel !!!");
74
+    };
75
+    dataChannel.onmessage = function (event)
76
+    {
77
+        var msgData = event.data;
78
+        console.info("Got My Data Channel Message:", msgData, dataChannel);
79
+    };*/
80
+}

+ 4
- 2
desktopsharing.js Voir le fichier

@@ -1,4 +1,4 @@
1
-/* global $, config, connection, chrome, alert, getUserMediaWithConstraints, change_local_video, getConferenceHandler */
1
+/* global $, config, connection, chrome, alert, getUserMediaWithConstraints, changeLocalVideo, getConferenceHandler */
2 2
 /**
3 3
  * Indicates that desktop stream is currently in use(for toggle purpose).
4 4
  * @type {boolean}
@@ -251,7 +251,9 @@ function newStreamCreated(stream) {
251 251
 
252 252
     var oldStream = connection.jingle.localVideo;
253 253
 
254
-    change_local_video(stream, !isUsingScreenStream);
254
+    connection.jingle.localVideo = stream;
255
+
256
+    VideoLayout.changeLocalVideo(stream, !isUsingScreenStream);
255 257
 
256 258
     var conferenceHandler = getConferenceHandler();
257 259
     if (conferenceHandler) {

+ 4
- 4
etherpad.js Voir le fichier

@@ -45,8 +45,8 @@ var Etherpad = (function (my) {
45 45
                 if (Prezi.isPresentationVisible()) {
46 46
                     largeVideo.css({opacity: '0'});
47 47
                 } else {
48
-                    setLargeVideoVisible(false);
49
-                    dockToolbar(true);
48
+                    VideoLayout.setLargeVideoVisible(false);
49
+                    Toolbar.dockToolbar(true);
50 50
                 }
51 51
 
52 52
                 $('#etherpad>iframe').fadeIn(300, function () {
@@ -63,8 +63,8 @@ var Etherpad = (function (my) {
63 63
                 document.body.style.background = 'black';
64 64
                 if (!isPresentation) {
65 65
                     $('#largeVideo').fadeIn(300, function () {
66
-                        setLargeVideoVisible(true);
67
-                        dockToolbar(false);
66
+                        VideoLayout.setLargeVideoVisible(true);
67
+                        Toolbar.dockToolbar(false);
68 68
                     });
69 69
                 }
70 70
             });

+ 57
- 43
index.html Voir le fichier

@@ -20,26 +20,35 @@
20 20
     <script src="libs/colibri/colibri.focus.js?v=8"></script><!-- colibri focus implementation -->
21 21
     <script src="libs/colibri/colibri.session.js?v=1"></script>
22 22
     <script src="//code.jquery.com/ui/1.10.4/jquery-ui.js"></script>
23
-    <script src="config.js"></script><!-- adapt to your needs, i.e. set hosts and bosh path -->
24
-    <script src="muc.js?v=10"></script><!-- simple MUC library -->
23
+    <script src="libs/tooltip.js?v=1"></script><!-- bootstrap tooltip lib -->
24
+    <script src="libs/popover.js?v=1"></script><!-- bootstrap tooltip lib -->
25
+    <script src="config.js?v=2"></script><!-- adapt to your needs, i.e. set hosts and bosh path -->
26
+    <script src="muc.js?v=12"></script><!-- simple MUC library -->
25 27
     <script src="estos_log.js?v=2"></script><!-- simple stanza logger -->
26
-    <script src="desktopsharing.js?v=1"></script><!-- desktop sharing -->
27
-    <script src="app.js?v=26"></script><!-- application logic -->
28
-    <script src="chat.js?v=4"></script><!-- chat logic -->
29
-    <script src="util.js?v=3"></script><!-- utility functions -->
30
-    <script src="etherpad.js?v=7"></script><!-- etherpad plugin -->
31
-    <script src="prezi.js?v=2"></script><!-- prezi plugin -->
32
-    <script src="smileys.js?v=1"></script><!-- smiley images -->
33
-    <script src="replacement.js?v=5"></script><!-- link and smiley replacement -->
34
-    <script src="moderatemuc.js?v=1"></script><!-- moderator plugin -->
28
+    <script src="desktopsharing.js?v=2"></script><!-- desktop sharing -->
29
+    <script src="data_channels.js?v=2"></script><!-- data channels -->
30
+    <script src="app.js?v=29"></script><!-- application logic -->
31
+    <script src="commands.js?v=1"></script><!-- application logic -->
32
+    <script src="chat.js?v=6"></script><!-- chat logic -->
33
+    <script src="util.js?v=5"></script><!-- utility functions -->
34
+    <script src="etherpad.js?v=8"></script><!-- etherpad plugin -->
35
+    <script src="prezi.js?v=4"></script><!-- prezi plugin -->
36
+    <script src="smileys.js?v=2"></script><!-- smiley images -->
37
+    <script src="replacement.js?v=6"></script><!-- link and smiley replacement -->
38
+    <script src="moderatemuc.js?v=3"></script><!-- moderator plugin -->
35 39
     <script src="analytics.js?v=1"></script><!-- google analytics plugin -->
40
+    <script src="rtp_stats.js?v=1"></script><!-- RTP stats processing -->
41
+    <script src="local_stats.js?v=1"></script><!-- Local stats processing -->
42
+    <script src="videolayout.js?v=4"></script><!-- video ui -->
43
+    <script src="toolbar.js?v=2"></script><!-- toolbar ui -->
36 44
     <link href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" rel="stylesheet">
37 45
     <link rel="stylesheet" href="css/font.css"/>
38
-    <link rel="stylesheet" type="text/css" media="screen" href="css/main.css?v=20"/>
39
-    <link rel="stylesheet" type="text/css" media="screen" href="css/videolayout_default.css?v=4" id="videolayout_default"/>
46
+    <link rel="stylesheet" type="text/css" media="screen" href="css/main.css?v=21"/>
47
+    <link rel="stylesheet" type="text/css" media="screen" href="css/videolayout_default.css?v=7" id="videolayout_default"/>
40 48
     <link rel="stylesheet" href="css/jquery-impromptu.css?v=4">
41 49
     <link rel="stylesheet" href="css/modaldialog.css?v=3">
42
-    <link rel="stylesheet" href="css/popup_menu.css?v=1">
50
+    <link rel="stylesheet" href="css/popup_menu.css?v=2">
51
+    <link rel="stylesheet" href="css/popover.css?v=1">
43 52
     <!--
44 53
         Link used for inline installation of chrome desktop streaming extension,
45 54
         is updated automatically from the code with the value defined in config.js -->
@@ -49,35 +58,40 @@
49 58
     <script src="libs/prezi_player.js?v=2"></script>
50 59
   </head>
51 60
   <body>
52
-    <div id="header">
53
-        <span id="toolbar">
54
-            <a class="button" onclick='toggleAudio();'>
55
-                <i id="mute" title="Mute / unmute" class="icon-microphone"></i></a>
56
-            <div class="header_button_separator"></div>
57
-            <a class="button" onclick='buttonClick("#video", "icon-camera icon-camera-disabled");toggleVideo();'>
58
-                <i id="video" title="Start / stop camera" class="icon-camera"></i></a>
59
-            <div class="header_button_separator"></div>
60
-            <a class="button" onclick="openLockDialog();" title="Lock/unlock room"><i id="lockIcon" class="icon-security"></i></a>
61
-            <div class="header_button_separator"></div>
62
-            <a class="button" onclick="openLinkDialog();" title="Invite others"><i class="icon-link"></i></a>
63
-            <div class="header_button_separator"></div>
64
-            <span class="toolbar_span">
65
-                <a class="button" onclick='Chat.toggleChat();' title="Open chat"><i id="chatButton" class="icon-chat"></i></a>
66
-                <span id="unreadMessages"></span>
67
-            </span>
68
-            <div class="header_button_separator"></div>
69
-            <a class="button" onclick='Prezi.openPreziDialog();' title="Share Prezi"><i class="icon-prezi"></i></a>
70
-            <span id="etherpadButton">
61
+    <div style="position: relative;" id="header_container">
62
+        <div id="header">
63
+            <span id="toolbar">
64
+                <a class="button" data-toggle="popover" data-placement="bottom" data-content="Mute / Unmute" onclick='toggleAudio();'>
65
+                    <i id="mute" class="icon-microphone"></i></a>
71 66
                 <div class="header_button_separator"></div>
72
-                <a class="button" onclick='Etherpad.toggleEtherpad(0);' title="Open shared document"><i class="icon-share-doc"></i></a>
73
-            </span>
74
-            <div class="header_button_separator"></div>
75
-            <span id="desktopsharing" style="display: none">
76
-                <a class="button" onclick="toggleScreenSharing();" title="Share screen"><i class="icon-share-desktop"></i></a>
67
+                <a class="button" data-toggle="popover" data-placement="bottom" data-content="Start / stop camera" onclick='buttonClick("#video", "icon-camera icon-camera-disabled");toggleVideo();'>
68
+                    <i id="video" class="icon-camera"></i></a>
69
+                <div class="header_button_separator"></div>
70
+                <a class="button" data-toggle="popover" data-placement="bottom" data-content="Lock / unlock room" onclick="Toolbar.openLockDialog();">
71
+                    <i id="lockIcon" class="icon-security"></i></a>
72
+                <div class="header_button_separator"></div>
73
+                <a class="button" data-toggle="popover" data-placement="bottom" data-content="Invite others" onclick="Toolbar.openLinkDialog();"><i class="icon-link"></i></a>
74
+                <div class="header_button_separator"></div>
75
+                <span class="toolbar_span">
76
+                    <a class="button" data-toggle="popover" data-placement="bottom" data-content="Open / close chat" onclick='Chat.toggleChat();'><i id="chatButton" class="icon-chat"></i></a>
77
+                    <span id="unreadMessages"></span>
78
+                </span>
79
+                <div class="header_button_separator"></div>
80
+                <a class="button" data-toggle="popover" data-placement="bottom" data-content="Share Prezi" onclick='Prezi.openPreziDialog();'><i class="icon-prezi"></i></a>
81
+                <span id="etherpadButton">
82
+                    <div class="header_button_separator"></div>
83
+                    <a class="button" data-toggle="popover" data-placement="bottom" data-content="Shared document" onclick='Etherpad.toggleEtherpad(0);'><i class="icon-share-doc"></i></a>
84
+                </span>
77 85
                 <div class="header_button_separator"></div>
86
+                <span id="desktopsharing" style="display: none">
87
+                    <a class="button" data-toggle="popover" data-placement="bottom" data-content="Share screen" onclick="toggleScreenSharing();"><i class="icon-share-desktop"></i></a>
88
+                    <div class="header_button_separator"></div>
89
+                </span>
90
+                <a class="button" data-toggle="popover" data-placement="bottom" data-content="Enter / Exit Full Screen" onclick='buttonClick("#fullScreen", "icon-full-screen icon-exit-full-screen");Toolbar.toggleFullScreen();'>
91
+                    <i id="fullScreen" class="icon-full-screen"></i></a>
78 92
             </span>
79
-            <a class="button" onclick='buttonClick("#fullScreen", "icon-full-screen icon-exit-full-screen");toggleFullScreen();'><i id="fullScreen" title="Enter / Exit Full Screen" class="icon-full-screen"></i></a>
80
-        </span>
93
+        </div>
94
+        <div id="subject"></div>
81 95
     </div>
82 96
     <div id="settings">
83 97
       <h1>Connection Settings</h1>
@@ -89,7 +103,7 @@
89 103
       </form>
90 104
     </div>
91 105
     <div id="reloadPresentation"><a onclick='Prezi.reloadPresentation();'><i title="Reload Prezi" class="fa fa-repeat fa-lg"></i></a></div>
92
-    <div id="videospace" onmousemove="showToolbar();">
106
+    <div id="videospace" onmousemove="Toolbar.showToolbar();">
93 107
         <div id="largeVideoContainer" class="videocontainer">
94 108
             <div id="presentation"></div>
95 109
             <div id="etherpad"></div>
@@ -104,7 +118,7 @@
104 118
                     <!--<video id="localVideo" autoplay oncontextmenu="return false;" muted></video> - is now per stream generated -->
105 119
                 </span>
106 120
                 <audio id="localAudio" autoplay oncontextmenu="return false;" muted></audio>
107
-                <span class="focusindicator"></span>
121
+                <span class="focusindicator" data-content="The owner of&#10;this conference" data-toggle="popover" data-placement="top"></span>
108 122
             </span>
109 123
             <audio id="userJoined" src="sounds/joined.wav" preload="auto"></audio>
110 124
             <audio id="userLeft" src="sounds/left.wav" preload="auto"></audio>
@@ -123,6 +137,6 @@
123 137
         <audio id="chatNotification" src="sounds/incomingMessage.wav" preload="auto"></audio>
124 138
         <textarea id="usermsg" placeholder='Enter text...' autofocus></textarea>
125 139
     </div>
126
-    <a id="downloadlog" onclick='dump(event.target);'><i title="Download support information" class="fa fa-cloud-download"></i></a>
140
+    <a id="downloadlog" onclick='dump(event.target);' data-toggle="popover" data-placement="right" data-content="Download logs" ><i class="fa fa-cloud-download"></i></a>
127 141
   </body>
128 142
 </html>

+ 311
- 81
libs/colibri/colibri.focus.js Voir le fichier

@@ -44,8 +44,27 @@ function ColibriFocus(connection, bridgejid) {
44 44
     this.peers = [];
45 45
     this.confid = null;
46 46
 
47
+    /**
48
+     * Local XMPP resource used to join the multi user chat.
49
+     * @type {*}
50
+     */
51
+    this.myMucResource = Strophe.getResourceFromJid(connection.emuc.myroomjid);
52
+
53
+    /**
54
+     * Default channel expire value in seconds.
55
+     * @type {number}
56
+     */
57
+    this.channelExpire = 60;
58
+
47 59
     // media types of the conference
48
-    this.media = ['audio', 'video'];
60
+    if (config.openSctp)
61
+    {
62
+        this.media = ['audio', 'video', 'data'];
63
+    }
64
+    else
65
+    {
66
+        this.media = ['audio', 'video'];
67
+    }
49 68
 
50 69
     this.connection.jingle.sessions[this.sid] = this;
51 70
     this.mychannel = [];
@@ -151,17 +170,29 @@ ColibriFocus.prototype._makeConference = function () {
151 170
     elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri'});
152 171
 
153 172
     this.media.forEach(function (name) {
173
+        var isData = name === 'data';
174
+        var channel = isData ? 'sctpconnection' : 'channel';
175
+
154 176
         elem.c('content', {name: name});
155
-        elem.c('channel', {
177
+
178
+        elem.c(channel, {
156 179
             initiator: 'true',
157 180
             expire: '15',
158
-            endpoint: 'fix_me_focus_endpoint'}).up();
181
+            endpoint: self.myMucResource
182
+        });
183
+        if (isData)
184
+            elem.attrs({port:  5000});
185
+        elem.up();// end of channel
186
+
159 187
         for (var j = 0; j < self.peers.length; j++) {
160
-            elem.c('channel', {
188
+            elem.c(channel, {
161 189
                 initiator: 'true',
162 190
                 expire: '15',
163 191
                 endpoint: self.peers[j].substr(1 + self.peers[j].lastIndexOf('/'))
164
-            }).up();
192
+            });
193
+            if (isData)
194
+                elem.attrs({port: 5000});
195
+            elem.up(); // end of channel
165 196
         }
166 197
         elem.up(); // end of content
167 198
     });
@@ -209,8 +240,13 @@ ColibriFocus.prototype.createdConference = function (result) {
209 240
     this.confid = $(result).find('>conference').attr('id');
210 241
     var remotecontents = $(result).find('>conference>content').get();
211 242
     var numparticipants = 0;
212
-    for (var i = 0; i < remotecontents.length; i++) {
213
-        tmp = $(remotecontents[i]).find('>channel').get();
243
+    for (var i = 0; i < remotecontents.length; i++)
244
+    {
245
+        var contentName = $(remotecontents[i]).attr('name');
246
+        var channelName
247
+            = contentName !== 'data' ? '>channel' : '>sctpconnection';
248
+
249
+        tmp = $(remotecontents[i]).find(channelName).get();
214 250
         this.mychannel.push($(tmp.shift()));
215 251
         numparticipants = tmp.length;
216 252
         for (j = 0; j < tmp.length; j++) {
@@ -223,7 +259,55 @@ ColibriFocus.prototype.createdConference = function (result) {
223 259
 
224 260
     console.log('remote channels', this.channels);
225 261
 
226
-    var bridgeSDP = new SDP('v=0\r\no=- 5151055458874951233 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\nm=audio 1 RTP/SAVPF 111 103 104 0 8 106 105 13 126\r\nc=IN IP4 0.0.0.0\r\na=rtcp:1 IN IP4 0.0.0.0\r\na=mid:audio\r\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\na=sendrecv\r\na=rtpmap:111 opus/48000/2\r\na=fmtp:111 minptime=10\r\na=rtpmap:103 ISAC/16000\r\na=rtpmap:104 ISAC/32000\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:106 CN/32000\r\na=rtpmap:105 CN/16000\r\na=rtpmap:13 CN/8000\r\na=rtpmap:126 telephone-event/8000\r\na=maxptime:60\r\nm=video 1 RTP/SAVPF 100 116 117\r\nc=IN IP4 0.0.0.0\r\na=rtcp:1 IN IP4 0.0.0.0\r\na=mid:video\r\na=extmap:2 urn:ietf:params:rtp-hdrext:toffset\r\na=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=sendrecv\r\na=rtpmap:100 VP8/90000\r\na=rtcp-fb:100 ccm fir\r\na=rtcp-fb:100 nack\r\na=rtcp-fb:100 goog-remb\r\na=rtpmap:116 red/90000\r\na=rtpmap:117 ulpfec/90000\r\n');
262
+    // Notify that the focus has created the conference on the bridge
263
+    $(document).trigger('conferenceCreated.jingle', [self]);
264
+
265
+    var bridgeSDP = new SDP(
266
+        'v=0\r\n' +
267
+        'o=- 5151055458874951233 2 IN IP4 127.0.0.1\r\n' +
268
+        's=-\r\n' +
269
+        't=0 0\r\n' +
270
+        /* Audio */
271
+        'm=audio 1 RTP/SAVPF 111 103 104 0 8 106 105 13 126\r\n' +
272
+        'c=IN IP4 0.0.0.0\r\n' +
273
+        'a=rtcp:1 IN IP4 0.0.0.0\r\n' +
274
+        'a=mid:audio\r\n' +
275
+        'a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n' +
276
+        'a=sendrecv\r\n' +
277
+        'a=rtpmap:111 opus/48000/2\r\n' +
278
+        'a=fmtp:111 minptime=10\r\n' +
279
+        'a=rtpmap:103 ISAC/16000\r\n' +
280
+        'a=rtpmap:104 ISAC/32000\r\n' +
281
+        'a=rtpmap:0 PCMU/8000\r\n' +
282
+        'a=rtpmap:8 PCMA/8000\r\n' +
283
+        'a=rtpmap:106 CN/32000\r\n' +
284
+        'a=rtpmap:105 CN/16000\r\n' +
285
+        'a=rtpmap:13 CN/8000\r\n' +
286
+        'a=rtpmap:126 telephone-event/8000\r\n' +
287
+        'a=maxptime:60\r\n' +
288
+        /* Video */
289
+        'm=video 1 RTP/SAVPF 100 116 117\r\n' +
290
+        'c=IN IP4 0.0.0.0\r\n' +
291
+        'a=rtcp:1 IN IP4 0.0.0.0\r\n' +
292
+        'a=mid:video\r\n' +
293
+        'a=extmap:2 urn:ietf:params:rtp-hdrext:toffset\r\n' +
294
+        'a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n' +
295
+        'a=sendrecv\r\n' +
296
+        'a=rtpmap:100 VP8/90000\r\n' +
297
+        'a=rtcp-fb:100 ccm fir\r\n' +
298
+        'a=rtcp-fb:100 nack\r\n' +
299
+        'a=rtcp-fb:100 goog-remb\r\n' +
300
+        'a=rtpmap:116 red/90000\r\n' +
301
+        'a=rtpmap:117 ulpfec/90000\r\n' +
302
+        /* Data SCTP */
303
+        (config.openSctp ?
304
+            'm=application 1 DTLS/SCTP 5000\r\n' +
305
+            'c=IN IP4 0.0.0.0\r\n' +
306
+            'a=sctpmap:5000 webrtc-datachannel\r\n' +
307
+            'a=mid:data\r\n'
308
+            : '')
309
+    );
310
+
227 311
     bridgeSDP.media.length = this.mychannel.length;
228 312
     var channel;
229 313
     /*
@@ -262,12 +346,17 @@ ColibriFocus.prototype.createdConference = function (result) {
262 346
         // get the mixed ssrc
263 347
         tmp = $(this.mychannel[channel]).find('>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
264 348
         // FIXME: check rtp-level-relay-type
265
-        if (tmp.length) {
349
+
350
+        var isData = bridgeSDP.media[channel].indexOf('application') !== -1;
351
+        if (!isData && tmp.length)
352
+        {
266 353
             bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'cname:mixed' + '\r\n';
267 354
             bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'label:mixedlabela0' + '\r\n';
268 355
             bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'msid:mixedmslabel mixedlabela0' + '\r\n';
269 356
             bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'mslabel:mixedmslabel' + '\r\n';
270
-        } else {
357
+        }
358
+        else if (!isData)
359
+        {
271 360
             // make chrome happy... '3735928559' == 0xDEADBEEF
272 361
             // FIXME: this currently appears as two streams, should be one
273 362
             bridgeSDP.media[channel] += 'a=ssrc:' + '3735928559' + ' ' + 'cname:mixed' + '\r\n';
@@ -308,21 +397,41 @@ ColibriFocus.prototype.createdConference = function (result) {
308 397
                             elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: self.confid});
309 398
                             var localSDP = new SDP(self.peerconnection.localDescription.sdp);
310 399
                             localSDP.media.forEach(function (media, channel) {
311
-                                var name = SDPUtil.parse_mline(media.split('\r\n')[0]).media;
400
+                                var name = SDPUtil.parse_mid(SDPUtil.find_line(media, 'a=mid:'));
312 401
                                 elem.c('content', {name: name});
313
-                                elem.c('channel', {
314
-                                    initiator: 'true',
315
-                                    expire: '15',
316
-                                    id: self.mychannel[channel].attr('id'),
317
-                                    endpoint: 'fix_me_focus_endpoint'
318
-                                });
319
-
320
-                                // FIXME: should reuse code from .toJingle
321 402
                                 var mline = SDPUtil.parse_mline(media.split('\r\n')[0]);
322
-                                for (var j = 0; j < mline.fmt.length; j++) {
323
-                                    var rtpmap = SDPUtil.find_line(media, 'a=rtpmap:' + mline.fmt[j]);
324
-                                    elem.c('payload-type', SDPUtil.parse_rtpmap(rtpmap));
325
-                                    elem.up();
403
+                                if (name !== 'data')
404
+                                {
405
+                                    elem.c('channel', {
406
+                                        initiator: 'true',
407
+                                        expire: self.channelExpire,
408
+                                        id: self.mychannel[channel].attr('id'),
409
+                                        endpoint: self.myMucResource
410
+                                    });
411
+
412
+                                    // FIXME: should reuse code from .toJingle
413
+                                    for (var j = 0; j < mline.fmt.length; j++)
414
+                                    {
415
+                                        var rtpmap = SDPUtil.find_line(media, 'a=rtpmap:' + mline.fmt[j]);
416
+                                        if (rtpmap)
417
+                                        {
418
+                                            elem.c('payload-type', SDPUtil.parse_rtpmap(rtpmap));
419
+                                            elem.up();
420
+                                        }
421
+                                    }
422
+                                }
423
+                                else
424
+                                {
425
+                                    var sctpmap = SDPUtil.find_line(media, 'a=sctpmap:' + mline.fmt[0]);
426
+                                    var sctpPort = SDPUtil.parse_sctpmap(sctpmap)[0];
427
+                                    elem.c("sctpconnection",
428
+                                        {
429
+                                            initiator: 'true',
430
+                                            expire: self.channelExpire,
431
+                                            endpoint: self.myMucResource,
432
+                                            port: sctpPort
433
+                                        }
434
+                                    );
326 435
                                 }
327 436
 
328 437
                                 localSDP.TransportToJingle(channel, elem);
@@ -336,7 +445,9 @@ ColibriFocus.prototype.createdConference = function (result) {
336 445
                                     // ...
337 446
                                 },
338 447
                                 function (error) {
339
-                                    console.warn(error);
448
+                                    console.error(
449
+                                        "ERROR setLocalDescription succeded",
450
+                                        error, elem);
340 451
                                 }
341 452
                             );
342 453
 
@@ -344,6 +455,10 @@ ColibriFocus.prototype.createdConference = function (result) {
344 455
                             for (var i = 0; i < numparticipants; i++) {
345 456
                                 self.initiate(self.peers[i], true);
346 457
                             }
458
+
459
+                            // Notify we've created the conference
460
+                            $(document).trigger(
461
+                                'conferenceCreated.jingle', self);
347 462
                         },
348 463
                         function (error) {
349 464
                             console.warn('setLocalDescription failed.', error);
@@ -417,7 +532,10 @@ ColibriFocus.prototype.initiate = function (peer, isInitiator) {
417 532
             sdp.media[j] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'label:mixedlabela0' + '\r\n';
418 533
             sdp.media[j] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'msid:mixedmslabel mixedlabela0' + '\r\n';
419 534
             sdp.media[j] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'mslabel:mixedmslabel' + '\r\n';
420
-        } else {
535
+        }
536
+        // No SSRCs for 'data', comes when j == 2
537
+        else if (j < 2)
538
+        {
421 539
             // make chrome happy... '3735928559' == 0xDEADBEEF
422 540
             sdp.media[j] += 'a=ssrc:' + '3735928559' + ' ' + 'cname:mixed' + '\r\n';
423 541
             sdp.media[j] += 'a=ssrc:' + '3735928559' + ' ' + 'label:mixedlabelv0' + '\r\n';
@@ -486,9 +604,17 @@ ColibriFocus.prototype.initiate = function (peer, isInitiator) {
486 604
 // pull in a new participant into the conference
487 605
 ColibriFocus.prototype.addNewParticipant = function (peer) {
488 606
     var self = this;
489
-    if (this.confid === 0) {
607
+    if (this.confid === 0 || !this.peerconnection.localDescription)
608
+    {
490 609
         // bad state
491
-        console.log('confid does not exist yet, postponing', peer);
610
+        if (this.confid === 0)
611
+        {
612
+            console.error('confid does not exist yet, postponing', peer);
613
+        }
614
+        else
615
+        {
616
+            console.error('local description not ready yet, postponing', peer);
617
+        }
492 618
         window.setTimeout(function () {
493 619
             self.addNewParticipant(peer);
494 620
         }, 250);
@@ -502,14 +628,26 @@ ColibriFocus.prototype.addNewParticipant = function (peer) {
502 628
     elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
503 629
     var localSDP = new SDP(this.peerconnection.localDescription.sdp);
504 630
     localSDP.media.forEach(function (media, channel) {
505
-        var name = SDPUtil.parse_mline(media.split('\r\n')[0]).media;
631
+        var name = SDPUtil.parse_mid(SDPUtil.find_line(media, 'a=mid:'));
506 632
         elem.c('content', {name: name});
507
-        elem.c('channel', {
633
+        if (name !== 'data')
634
+        {
635
+            elem.c('channel', {
508 636
                 initiator: 'true',
509
-                expire:'15',
637
+                expire: self.channelExpire,
510 638
                 endpoint: peer.substr(1 + peer.lastIndexOf('/'))
511
-        });
512
-        elem.up(); // end of channel
639
+            });
640
+        }
641
+        else
642
+        {
643
+            elem.c('sctpconnection', {
644
+                endpoint: peer.substr(1 + peer.lastIndexOf('/')),
645
+                initiator: 'true',
646
+                expire: self.channelExpire,
647
+                port: 5000
648
+            });
649
+        }
650
+        elem.up(); // end of channel/sctpconnection
513 651
         elem.up(); // end of content
514 652
     });
515 653
 
@@ -517,7 +655,15 @@ ColibriFocus.prototype.addNewParticipant = function (peer) {
517 655
         function (result) {
518 656
             var contents = $(result).find('>conference>content').get();
519 657
             for (var i = 0; i < contents.length; i++) {
520
-                tmp = $(contents[i]).find('>channel').get();
658
+                var channelXml = $(contents[i]).find('>channel');
659
+                if (channelXml.length)
660
+                {
661
+                    tmp = channelXml.get();
662
+                }
663
+                else
664
+                {
665
+                    tmp = $(contents[i]).find('>sctpconnection').get();
666
+                }
521 667
                 self.channels[index][i] = tmp[0];
522 668
             }
523 669
             self.initiate(peer, true);
@@ -531,37 +677,52 @@ ColibriFocus.prototype.addNewParticipant = function (peer) {
531 677
 // update the channel description (payload-types + dtls fp) for a participant
532 678
 ColibriFocus.prototype.updateChannel = function (remoteSDP, participant) {
533 679
     console.log('change allocation for', this.confid);
680
+    var self = this;
534 681
     var change = $iq({to: this.bridgejid, type: 'set'});
535 682
     change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
536
-    for (channel = 0; channel < this.channels[participant].length; channel++) {
537
-        change.c('content', {name: channel === 0 ? 'audio' : 'video'});
538
-        change.c('channel', {
539
-            id: $(this.channels[participant][channel]).attr('id'),
540
-            endpoint: $(this.channels[participant][channel]).attr('endpoint'),
541
-            expire: '15'
542
-        });
683
+    for (channel = 0; channel < this.channels[participant].length; channel++)
684
+    {
685
+        var name = SDPUtil.parse_mid(SDPUtil.find_line(remoteSDP.media[channel], 'a=mid:'));
686
+        change.c('content', {name: name});
687
+        if (name !== 'data')
688
+        {
689
+            change.c('channel', {
690
+                id: $(this.channels[participant][channel]).attr('id'),
691
+                endpoint: $(this.channels[participant][channel]).attr('endpoint'),
692
+                expire: self.channelExpire
693
+            });
543 694
 
544
-        var rtpmap = SDPUtil.find_lines(remoteSDP.media[channel], 'a=rtpmap:');
545
-        rtpmap.forEach(function (val) {
546
-            // TODO: too much copy-paste
547
-            var rtpmap = SDPUtil.parse_rtpmap(val);
548
-            change.c('payload-type', rtpmap);
549
-            //
550
-            // put any 'a=fmtp:' + mline.fmt[j] lines into <param name=foo value=bar/>
551
-            /*
552
-            if (SDPUtil.find_line(remoteSDP.media[channel], 'a=fmtp:' + rtpmap.id)) {
553
-                tmp = SDPUtil.parse_fmtp(SDPUtil.find_line(remoteSDP.media[channel], 'a=fmtp:' + rtpmap.id));
554
-                for (var k = 0; k < tmp.length; k++) {
555
-                    change.c('parameter', tmp[k]).up();
695
+            var rtpmap = SDPUtil.find_lines(remoteSDP.media[channel], 'a=rtpmap:');
696
+            rtpmap.forEach(function (val) {
697
+                // TODO: too much copy-paste
698
+                var rtpmap = SDPUtil.parse_rtpmap(val);
699
+                change.c('payload-type', rtpmap);
700
+                //
701
+                // put any 'a=fmtp:' + mline.fmt[j] lines into <param name=foo value=bar/>
702
+                /*
703
+                if (SDPUtil.find_line(remoteSDP.media[channel], 'a=fmtp:' + rtpmap.id)) {
704
+                    tmp = SDPUtil.parse_fmtp(SDPUtil.find_line(remoteSDP.media[channel], 'a=fmtp:' + rtpmap.id));
705
+                    for (var k = 0; k < tmp.length; k++) {
706
+                        change.c('parameter', tmp[k]).up();
707
+                    }
556 708
                 }
557
-            }
558
-            */
559
-            change.up();
560
-        });
709
+                */
710
+                change.up();
711
+            });
712
+        }
713
+        else
714
+        {
715
+            var sctpmap = SDPUtil.find_line(remoteSDP.media[channel], 'a=sctpmap:');
716
+            change.c('sctpconnection', {
717
+                endpoint: $(this.channels[participant][channel]).attr('endpoint'),
718
+                expire: self.channelExpire,
719
+                port: SDPUtil.parse_sctpmap(sctpmap)[0]
720
+            });
721
+        }
561 722
         // now add transport
562 723
         remoteSDP.TransportToJingle(channel, change);
563 724
 
564
-        change.up(); // end of channel
725
+        change.up(); // end of channel/sctpconnection
565 726
         change.up(); // end of content
566 727
     }
567 728
     this.connection.sendIQ(change,
@@ -605,6 +766,19 @@ ColibriFocus.prototype.sendSSRCUpdate = function (sdpMediaSsrcs, fromJid, isadd)
605 766
 ColibriFocus.prototype.addSource = function (elem, fromJid) {
606 767
 
607 768
     var self = this;
769
+    // FIXME: dirty waiting
770
+    if (!this.peerconnection.localDescription)
771
+    {
772
+        console.warn("addSource - localDescription not ready yet")
773
+        setTimeout(function()
774
+            {
775
+                self.addSource(elem, fromJid);
776
+            },
777
+            200
778
+        );
779
+        return;
780
+    }
781
+
608 782
     this.peerconnection.addSource(elem);
609 783
 
610 784
     var peerSsrc = this.remotessrc[fromJid];
@@ -638,6 +812,19 @@ ColibriFocus.prototype.addSource = function (elem, fromJid) {
638 812
 ColibriFocus.prototype.removeSource = function (elem, fromJid) {
639 813
 
640 814
     var self = this;
815
+    // FIXME: dirty waiting
816
+    if (!self.peerconnection.localDescription)
817
+    {
818
+        console.warn("removeSource - localDescription not ready yet");
819
+        setTimeout(function()
820
+            {
821
+                self.removeSource(elem, fromJid);
822
+            },
823
+            200
824
+        );
825
+        return;
826
+    }
827
+
641 828
     this.peerconnection.removeSource(elem);
642 829
 
643 830
     var peerSsrc = this.remotessrc[fromJid];
@@ -675,8 +862,11 @@ ColibriFocus.prototype.setRemoteDescription = function (session, elem, desctype)
675 862
     this.remotessrc[session.peerjid] = [];
676 863
     for (channel = 0; channel < this.channels[participant].length; channel++) {
677 864
         //if (channel == 0) continue; FIXME: does not work as intended
678
-        if (SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:').length) {
679
-            this.remotessrc[session.peerjid][channel] = SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:').join('\r\n') + '\r\n';
865
+        if (SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:').length)
866
+        {
867
+            this.remotessrc[session.peerjid][channel] =
868
+                SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:')
869
+                        .join('\r\n') + '\r\n';
680 870
         }
681 871
     }
682 872
 
@@ -702,14 +892,27 @@ ColibriFocus.prototype.addIceCandidate = function (session, elem) {
702 892
     change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
703 893
     $(elem).each(function () {
704 894
         var name = $(this).attr('name');
895
+
705 896
         var channel = name == 'audio' ? 0 : 1; // FIXME: search mlineindex in localdesc
897
+        if (name != 'audio' && name != 'video')
898
+            channel = 2; // name == 'data'
706 899
 
707 900
         change.c('content', {name: name});
708
-        change.c('channel', {
709
-            id: $(self.channels[participant][channel]).attr('id'),
710
-            endpoint: $(self.channels[participant][channel]).attr('endpoint'),
711
-            expire: '15'
712
-        });
901
+        if (name !== 'data')
902
+        {
903
+            change.c('channel', {
904
+                id: $(self.channels[participant][channel]).attr('id'),
905
+                endpoint: $(self.channels[participant][channel]).attr('endpoint'),
906
+                expire: self.channelExpire
907
+            });
908
+        }
909
+        else
910
+        {
911
+            change.c('sctpconnection', {
912
+                endpoint: $(self.channels[participant][channel]).attr('endpoint'),
913
+                expire: self.channelExpire
914
+            });
915
+        }
713 916
         $(this).find('>transport').each(function () {
714 917
             change.c('transport', {
715 918
                 ufrag: $(this).attr('ufrag'),
@@ -729,7 +932,7 @@ ColibriFocus.prototype.addIceCandidate = function (session, elem) {
729 932
             });
730 933
             change.up(); // end of transport
731 934
         });
732
-        change.up(); // end of channel
935
+        change.up(); // end of channel/sctpconnection
733 936
         change.up(); // end of content
734 937
     });
735 938
     // FIXME: need to check if there is at least one candidate when filtering TCP ones
@@ -769,21 +972,35 @@ ColibriFocus.prototype.sendIceCandidates = function (candidates) {
769 972
     mycands.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
770 973
     // FIXME: multi-candidate logic is taken from strophe.jingle, should be refactored there
771 974
     var localSDP = new SDP(this.peerconnection.localDescription.sdp);
772
-    for (var mid = 0; mid < localSDP.media.length; mid++) {
975
+    for (var mid = 0; mid < localSDP.media.length; mid++)
976
+    {
773 977
         var cands = candidates.filter(function (el) { return el.sdpMLineIndex == mid; });
774
-        if (cands.length > 0) {
775
-            mycands.c('content', {name: cands[0].sdpMid });
776
-            mycands.c('channel', {
777
-                id: $(this.mychannel[cands[0].sdpMLineIndex]).attr('id'),
778
-                endpoint: $(this.mychannel[cands[0].sdpMLineIndex]).attr('endpoint'),
779
-                expire: '15'
780
-            });
978
+        if (cands.length > 0)
979
+        {
980
+            var name = cands[0].sdpMid;
981
+            mycands.c('content', {name: name });
982
+            if (name !== 'data')
983
+            {
984
+                mycands.c('channel', {
985
+                    id: $(this.mychannel[cands[0].sdpMLineIndex]).attr('id'),
986
+                    endpoint: $(this.mychannel[cands[0].sdpMLineIndex]).attr('endpoint'),
987
+                    expire: self.channelExpire
988
+                });
989
+            }
990
+            else
991
+            {
992
+                mycands.c('sctpconnection', {
993
+                    endpoint: $(this.mychannel[cands[0].sdpMLineIndex]).attr('endpoint'),
994
+                    port: $(this.mychannel[cands[0].sdpMLineIndex]).attr('port'),
995
+                    expire: self.channelExpire
996
+                });
997
+            }
781 998
             mycands.c('transport', {xmlns: 'urn:xmpp:jingle:transports:ice-udp:1'});
782 999
             for (var i = 0; i < cands.length; i++) {
783 1000
                 mycands.c('candidate', SDPUtil.candidateToJingle(cands[i].candidate)).up();
784 1001
             }
785 1002
             mycands.up(); // transport
786
-            mycands.up(); // channel
1003
+            mycands.up(); // channel / sctpconnection
787 1004
             mycands.up(); // content
788 1005
         }
789 1006
     }
@@ -814,13 +1031,26 @@ ColibriFocus.prototype.terminate = function (session, reason) {
814 1031
     var change = $iq({to: this.bridgejid, type: 'set'});
815 1032
     change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
816 1033
     for (var channel = 0; channel < this.channels[participant].length; channel++) {
817
-        change.c('content', {name: channel === 0 ? 'audio' : 'video'});
818
-        change.c('channel', {
819
-            id: $(this.channels[participant][channel]).attr('id'),
820
-            endpoint: $(this.channels[participant][channel]).attr('endpoint'),
821
-            expire: '0'
822
-        });
823
-        change.up(); // end of channel
1034
+        var name = channel === 0 ? 'audio' : 'video';
1035
+        if (channel == 2)
1036
+            name = 'data';
1037
+        change.c('content', {name: name});
1038
+        if (name !== 'data')
1039
+        {
1040
+            change.c('channel', {
1041
+                id: $(this.channels[participant][channel]).attr('id'),
1042
+                endpoint: $(this.channels[participant][channel]).attr('endpoint'),
1043
+                expire: '0'
1044
+            });
1045
+        }
1046
+        else
1047
+        {
1048
+            change.c('sctpconnection', {
1049
+                endpoint: $(this.channels[participant][channel]).attr('endpoint'),
1050
+                expire: '0'
1051
+            });
1052
+        }
1053
+        change.up(); // end of channel/sctpconnection
824 1054
         change.up(); // end of content
825 1055
     }
826 1056
     this.connection.sendIQ(change,

+ 110
- 0
libs/popover.js Voir le fichier

@@ -0,0 +1,110 @@
1
+/* ========================================================================
2
+ * Bootstrap: popover.js v3.1.1
3
+ * http://getbootstrap.com/javascript/#popovers
4
+ * ========================================================================
5
+ * Copyright 2011-2014 Twitter, Inc.
6
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
7
+ * ======================================================================== */
8
+
9
+
10
++function ($) {
11
+  'use strict';
12
+
13
+  // POPOVER PUBLIC CLASS DEFINITION
14
+  // ===============================
15
+
16
+  var Popover = function (element, options) {
17
+    this.init('popover', element, options)
18
+  }
19
+
20
+  if (!$.fn.tooltip) throw new Error('Popover requires tooltip.js')
21
+
22
+  Popover.DEFAULTS = $.extend({}, $.fn.tooltip.Constructor.DEFAULTS, {
23
+    placement: 'right',
24
+    trigger: 'click',
25
+    content: '',
26
+    template: '<div class="popover"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>'
27
+  })
28
+
29
+
30
+  // NOTE: POPOVER EXTENDS tooltip.js
31
+  // ================================
32
+
33
+  Popover.prototype = $.extend({}, $.fn.tooltip.Constructor.prototype)
34
+
35
+  Popover.prototype.constructor = Popover
36
+
37
+  Popover.prototype.getDefaults = function () {
38
+    return Popover.DEFAULTS
39
+  }
40
+
41
+  Popover.prototype.setContent = function () {
42
+    var $tip    = this.tip()
43
+    var title   = this.getTitle()
44
+    var content = this.getContent()
45
+
46
+    $tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title)
47
+    $tip.find('.popover-content')[ // we use append for html objects to maintain js events
48
+      this.options.html ? (typeof content == 'string' ? 'html' : 'append') : 'text'
49
+    ](content)
50
+
51
+    $tip.removeClass('fade top bottom left right in')
52
+
53
+    // IE8 doesn't accept hiding via the `:empty` pseudo selector, we have to do
54
+    // this manually by checking the contents.
55
+    if (!$tip.find('.popover-title').html()) $tip.find('.popover-title').hide()
56
+  }
57
+
58
+  Popover.prototype.hasContent = function () {
59
+    return this.getTitle() || this.getContent()
60
+  }
61
+
62
+  Popover.prototype.getContent = function () {
63
+    var $e = this.$element
64
+    var o  = this.options
65
+
66
+    return $e.attr('data-content')
67
+      || (typeof o.content == 'function' ?
68
+            o.content.call($e[0]) :
69
+            o.content)
70
+  }
71
+
72
+  Popover.prototype.arrow = function () {
73
+    return this.$arrow = this.$arrow || this.tip().find('.arrow')
74
+  }
75
+
76
+  Popover.prototype.tip = function () {
77
+    if (!this.$tip) this.$tip = $(this.options.template)
78
+    return this.$tip
79
+  }
80
+
81
+
82
+  // POPOVER PLUGIN DEFINITION
83
+  // =========================
84
+
85
+  var old = $.fn.popover
86
+
87
+  $.fn.popover = function (option) {
88
+    return this.each(function () {
89
+      var $this   = $(this)
90
+      var data    = $this.data('bs.popover')
91
+      var options = typeof option == 'object' && option
92
+
93
+      if (!data && option == 'destroy') return
94
+      if (!data) $this.data('bs.popover', (data = new Popover(this, options)))
95
+      if (typeof option == 'string') data[option]()
96
+    })
97
+  }
98
+
99
+  $.fn.popover.Constructor = Popover
100
+
101
+
102
+  // POPOVER NO CONFLICT
103
+  // ===================
104
+
105
+  $.fn.popover.noConflict = function () {
106
+    $.fn.popover = old
107
+    return this
108
+  }
109
+
110
+}(jQuery);

+ 6
- 6
libs/strophe/strophe.jingle.adapter.js Voir le fichier

@@ -5,7 +5,7 @@ function TraceablePeerConnection(ice_config, constraints) {
5 5
     this.updateLog = [];
6 6
     this.stats = {};
7 7
     this.statsinterval = null;
8
-    this.maxstats = 300; // limit to 300 values, i.e. 5 minutes; set to 0 to disable
8
+    this.maxstats = 0; // limit to 300 values, i.e. 5 minutes; set to 0 to disable
9 9
 
10 10
     /**
11 11
      * Array of ssrcs that will be added on next modifySources call.
@@ -32,7 +32,7 @@ function TraceablePeerConnection(ice_config, constraints) {
32 32
     this.switchstreams = false;
33 33
 
34 34
     // override as desired
35
-    this.trace = function(what, info) {
35
+    this.trace = function (what, info) {
36 36
         //console.warn('WTRACE', what, info);
37 37
         self.updateLog.push({
38 38
             time: new Date(),
@@ -88,8 +88,8 @@ function TraceablePeerConnection(ice_config, constraints) {
88 88
         if (self.ondatachannel !== null) {
89 89
             self.ondatachannel(event);
90 90
         }
91
-    }
92
-    if (!navigator.mozGetUserMedia) {
91
+    };
92
+    if (!navigator.mozGetUserMedia && this.maxstats) {
93 93
         this.statsinterval = window.setInterval(function() {
94 94
             self.peerconnection.getStats(function(stats) {
95 95
                 var results = stats.result();
@@ -144,8 +144,8 @@ TraceablePeerConnection.prototype.removeStream = function (stream) {
144 144
 
145 145
 TraceablePeerConnection.prototype.createDataChannel = function (label, opts) {
146 146
     this.trace('createDataChannel', label, opts);
147
-    this.peerconnection.createDataChannel(label, opts);
148
-}
147
+    return this.peerconnection.createDataChannel(label, opts);
148
+};
149 149
 
150 150
 TraceablePeerConnection.prototype.setLocalDescription = function (description, successCallback, failureCallback) {
151 151
     var self = this;

+ 55
- 7
libs/strophe/strophe.jingle.sdp.js Voir le fichier

@@ -155,7 +155,10 @@ SDP.prototype.toJingle = function (elem, thecreator) {
155 155
     }
156 156
     for (i = 0; i < this.media.length; i++) {
157 157
         mline = SDPUtil.parse_mline(this.media[i].split('\r\n')[0]);
158
-        if (!(mline.media == 'audio' || mline.media == 'video')) {
158
+        if (!(mline.media === 'audio' ||
159
+              mline.media === 'video' ||
160
+              mline.media === 'application'))
161
+        {
159 162
             continue;
160 163
         }
161 164
         if (SDPUtil.find_line(this.media[i], 'a=ssrc:')) {
@@ -171,12 +174,14 @@ SDP.prototype.toJingle = function (elem, thecreator) {
171 174
             elem.attrs({ name: mid });
172 175
 
173 176
             // old BUNDLE plan, to be removed
174
-            if (bundle.indexOf(mid) != -1) {
177
+            if (bundle.indexOf(mid) !== -1) {
175 178
                 elem.c('bundle', {xmlns: 'http://estos.de/ns/bundle'}).up();
176 179
                 bundle.splice(bundle.indexOf(mid), 1);
177 180
             }
178 181
         }
179
-        if (SDPUtil.find_line(this.media[i], 'a=rtpmap:').length) {
182
+
183
+        if (SDPUtil.find_line(this.media[i], 'a=rtpmap:').length)
184
+        {
180 185
             elem.c('description',
181 186
                 {xmlns: 'urn:xmpp:jingle:apps:rtp:1',
182 187
                     media: mline.media });
@@ -304,6 +309,26 @@ SDP.prototype.TransportToJingle = function (mediaindex, elem) {
304 309
     var self = this;
305 310
     elem.c('transport');
306 311
 
312
+    // XEP-0343 DTLS/SCTP
313
+    if (SDPUtil.find_line(this.media[mediaindex], 'a=sctpmap:').length)
314
+    {
315
+        var sctpmap = SDPUtil.find_line(
316
+            this.media[i], 'a=sctpmap:', self.session);
317
+        if (sctpmap)
318
+        {
319
+            var sctpAttrs = SDPUtil.parse_sctpmap(sctpmap);
320
+            elem.c('sctpmap',
321
+                {
322
+                    xmlns: 'urn:xmpp:jingle:transports:dtls-sctp:1',
323
+                    number: sctpAttrs[0], /* SCTP port */
324
+                    protocol: sctpAttrs[1], /* protocol */
325
+                });
326
+            // Optional stream count attribute
327
+            if (sctpAttrs.length > 2)
328
+                elem.attrs({ streams: sctpAttrs[2]});
329
+            elem.up();
330
+        }
331
+    }
307 332
     // XEP-0320
308 333
     var fingerprints = SDPUtil.find_lines(this.media[mediaindex], 'a=fingerprint:', this.session);
309 334
     fingerprints.forEach(function(line) {
@@ -438,6 +463,8 @@ SDP.prototype.jingle2media = function (content) {
438 463
         ssrc = desc.attr('ssrc'),
439 464
         self = this,
440 465
         tmp;
466
+    var sctp = content.find(
467
+        '>transport>sctpmap[xmlns="urn:xmpp:jingle:transports:dtls-sctp:1"]');
441 468
 
442 469
     tmp = { media: desc.attr('media') };
443 470
     tmp.port = '1';
@@ -446,14 +473,35 @@ SDP.prototype.jingle2media = function (content) {
446 473
         tmp.port = '0';
447 474
     }
448 475
     if (content.find('>transport>fingerprint').length || desc.find('encryption').length) {
449
-        tmp.proto = 'RTP/SAVPF';
476
+        if (sctp.length)
477
+            tmp.proto = 'DTLS/SCTP';
478
+        else
479
+            tmp.proto = 'RTP/SAVPF';
450 480
     } else {
451 481
         tmp.proto = 'RTP/AVPF';
452 482
     }
453
-    tmp.fmt = desc.find('payload-type').map(function () { return this.getAttribute('id'); }).get();
454
-    media += SDPUtil.build_mline(tmp) + '\r\n';
483
+    if (!sctp.length)
484
+    {
485
+        tmp.fmt = desc.find('payload-type').map(
486
+            function () { return this.getAttribute('id'); }).get();
487
+        media += SDPUtil.build_mline(tmp) + '\r\n';
488
+    }
489
+    else
490
+    {
491
+        media += 'm=application 1 DTLS/SCTP ' + sctp.attr('number') + '\r\n';
492
+        media += 'a=sctpmap:' + sctp.attr('number') +
493
+            ' ' + sctp.attr('protocol');
494
+
495
+        var streamCount = sctp.attr('streams');
496
+        if (streamCount)
497
+            media += ' ' + streamCount + '\r\n';
498
+        else
499
+            media += '\r\n';
500
+    }
501
+
455 502
     media += 'c=IN IP4 0.0.0.0\r\n';
456
-    media += 'a=rtcp:1 IN IP4 0.0.0.0\r\n';
503
+    if (!sctp.length)
504
+        media += 'a=rtcp:1 IN IP4 0.0.0.0\r\n';
457 505
     tmp = content.find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]');
458 506
     if (tmp.length) {
459 507
         if (tmp.attr('ufrag')) {

+ 17
- 1
libs/strophe/strophe.jingle.sdp.util.js Voir le fichier

@@ -90,6 +90,20 @@ SDPUtil = {
90 90
         data.channels = parts.length ? parts.shift() : '1';
91 91
         return data;
92 92
     },
93
+    /**
94
+     * Parses SDP line "a=sctpmap:..." and extracts SCTP port from it.
95
+     * @param line eg. "a=sctpmap:5000 webrtc-datachannel"
96
+     * @returns [SCTP port number, protocol, streams]
97
+     */
98
+    parse_sctpmap: function (line)
99
+    {
100
+        var parts = line.substring(10).split(' ');
101
+        var sctpPort = parts[0];
102
+        var protocol = parts[1];
103
+        // Stream count is optional
104
+        var streamCount = parts.length > 2 ? parts[2] : null;
105
+        return [sctpPort, protocol, streamCount];// SCTP port
106
+    },
93 107
     build_rtpmap: function (el) {
94 108
         var line = 'a=rtpmap:' + el.getAttribute('id') + ' ' + el.getAttribute('name') + '/' + el.getAttribute('clockrate');
95 109
         if (el.getAttribute('channels') && el.getAttribute('channels') != '1') {
@@ -269,7 +283,9 @@ SDPUtil = {
269 283
     candidateToJingle: function (line) {
270 284
         // a=candidate:2979166662 1 udp 2113937151 192.168.2.100 57698 typ host generation 0
271 285
         //      <candidate component=... foundation=... generation=... id=... ip=... network=... port=... priority=... protocol=... type=.../>
272
-        if (line.substring(0, 12) != 'a=candidate:') {
286
+        if (line.indexOf('candidate:') == 0) {
287
+            line = 'a=' + line;
288
+        } else if (line.substring(0, 12) != 'a=candidate:') {
273 289
             console.log('parseCandidate called with a line that is not a candidate line');
274 290
             console.log(line);
275 291
             return null;

+ 1
- 0
libs/strophe/strophe.jingle.session.js Voir le fichier

@@ -420,6 +420,7 @@ JingleSession.prototype.setRemoteDescription = function (elem, desctype) {
420 420
         },
421 421
         function (e) {
422 422
             console.error('setRemoteDescription error', e);
423
+            $(document).trigger('fatalError.jingle', [self, e]);
423 424
         }
424 425
     );
425 426
 };

+ 28
- 0
libs/strophe/strophe.jingle.sessionbase.js Voir le fichier

@@ -23,6 +23,20 @@ SessionBase.prototype.modifySources = function (successCallback) {
23 23
 
24 24
 SessionBase.prototype.addSource = function (elem, fromJid) {
25 25
 
26
+    var self = this;
27
+    // FIXME: dirty waiting
28
+    if (!this.peerconnection.localDescription)
29
+    {
30
+        console.warn("addSource - localDescription not ready yet")
31
+        setTimeout(function()
32
+            {
33
+                self.addSource(elem, fromJid);
34
+            },
35
+            200
36
+        );
37
+        return;
38
+    }
39
+
26 40
     this.peerconnection.addSource(elem);
27 41
 
28 42
     this.modifySources();
@@ -30,6 +44,20 @@ SessionBase.prototype.addSource = function (elem, fromJid) {
30 44
 
31 45
 SessionBase.prototype.removeSource = function (elem, fromJid) {
32 46
 
47
+    var self = this;
48
+    // FIXME: dirty waiting
49
+    if (!this.peerconnection.localDescription)
50
+    {
51
+        console.warn("removeSource - localDescription not ready yet")
52
+        setTimeout(function()
53
+            {
54
+                self.removeSource(elem, fromJid);
55
+            },
56
+            200
57
+        );
58
+        return;
59
+    }
60
+
33 61
     this.peerconnection.removeSource(elem);
34 62
 
35 63
     this.modifySources();

+ 399
- 0
libs/tooltip.js Voir le fichier

@@ -0,0 +1,399 @@
1
+/* ========================================================================
2
+ * Bootstrap: tooltip.js v3.1.1
3
+ * http://getbootstrap.com/javascript/#tooltip
4
+ * Inspired by the original jQuery.tipsy by Jason Frame
5
+ * ========================================================================
6
+ * Copyright 2011-2014 Twitter, Inc.
7
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
8
+ * ======================================================================== */
9
+
10
+
11
++function ($) {
12
+  'use strict';
13
+
14
+  // TOOLTIP PUBLIC CLASS DEFINITION
15
+  // ===============================
16
+
17
+  var Tooltip = function (element, options) {
18
+    this.type       =
19
+    this.options    =
20
+    this.enabled    =
21
+    this.timeout    =
22
+    this.hoverState =
23
+    this.$element   = null
24
+
25
+    this.init('tooltip', element, options)
26
+  }
27
+
28
+  Tooltip.DEFAULTS = {
29
+    animation: true,
30
+    placement: 'top',
31
+    selector: false,
32
+    template: '<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',
33
+    trigger: 'hover focus',
34
+    title: '',
35
+    delay: 0,
36
+    html: false,
37
+    container: false
38
+  }
39
+
40
+  Tooltip.prototype.init = function (type, element, options) {
41
+    this.enabled  = true
42
+    this.type     = type
43
+    this.$element = $(element)
44
+    this.options  = this.getOptions(options)
45
+
46
+    var triggers = this.options.trigger.split(' ')
47
+
48
+    for (var i = triggers.length; i--;) {
49
+      var trigger = triggers[i]
50
+
51
+      if (trigger == 'click') {
52
+        this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this))
53
+      } else if (trigger != 'manual') {
54
+        var eventIn  = trigger == 'hover' ? 'mouseenter' : 'focusin'
55
+        var eventOut = trigger == 'hover' ? 'mouseleave' : 'focusout'
56
+
57
+        this.$element.on(eventIn  + '.' + this.type, this.options.selector, $.proxy(this.enter, this))
58
+        this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this))
59
+      }
60
+    }
61
+
62
+    this.options.selector ?
63
+      (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) :
64
+      this.fixTitle()
65
+  }
66
+
67
+  Tooltip.prototype.getDefaults = function () {
68
+    return Tooltip.DEFAULTS
69
+  }
70
+
71
+  Tooltip.prototype.getOptions = function (options) {
72
+    options = $.extend({}, this.getDefaults(), this.$element.data(), options)
73
+
74
+    if (options.delay && typeof options.delay == 'number') {
75
+      options.delay = {
76
+        show: options.delay,
77
+        hide: options.delay
78
+      }
79
+    }
80
+
81
+    return options
82
+  }
83
+
84
+  Tooltip.prototype.getDelegateOptions = function () {
85
+    var options  = {}
86
+    var defaults = this.getDefaults()
87
+
88
+    this._options && $.each(this._options, function (key, value) {
89
+      if (defaults[key] != value) options[key] = value
90
+    })
91
+
92
+    return options
93
+  }
94
+
95
+  Tooltip.prototype.enter = function (obj) {
96
+    var self = obj instanceof this.constructor ?
97
+      obj : $(obj.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type)
98
+
99
+    clearTimeout(self.timeout)
100
+
101
+    self.hoverState = 'in'
102
+
103
+    if (!self.options.delay || !self.options.delay.show) return self.show()
104
+
105
+    self.timeout = setTimeout(function () {
106
+      if (self.hoverState == 'in') self.show()
107
+    }, self.options.delay.show)
108
+  }
109
+
110
+  Tooltip.prototype.leave = function (obj) {
111
+    var self = obj instanceof this.constructor ?
112
+      obj : $(obj.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type)
113
+
114
+    clearTimeout(self.timeout)
115
+
116
+    self.hoverState = 'out'
117
+
118
+    if (!self.options.delay || !self.options.delay.hide) return self.hide()
119
+
120
+    self.timeout = setTimeout(function () {
121
+      if (self.hoverState == 'out') self.hide()
122
+    }, self.options.delay.hide)
123
+  }
124
+
125
+  Tooltip.prototype.show = function () {
126
+    var e = $.Event('show.bs.' + this.type)
127
+
128
+    if (this.hasContent() && this.enabled) {
129
+      this.$element.trigger(e)
130
+
131
+      if (e.isDefaultPrevented()) return
132
+      var that = this;
133
+
134
+      var $tip = this.tip()
135
+
136
+      this.setContent()
137
+
138
+      if (this.options.animation) $tip.addClass('fade')
139
+
140
+      var placement = typeof this.options.placement == 'function' ?
141
+        this.options.placement.call(this, $tip[0], this.$element[0]) :
142
+        this.options.placement
143
+
144
+      var autoToken = /\s?auto?\s?/i
145
+      var autoPlace = autoToken.test(placement)
146
+      if (autoPlace) placement = placement.replace(autoToken, '') || 'top'
147
+
148
+      $tip
149
+        .detach()
150
+        .css({ top: 0, left: 0, display: 'block' })
151
+        .addClass(placement)
152
+
153
+      this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element)
154
+
155
+      var pos          = this.getPosition()
156
+      var actualWidth  = $tip[0].offsetWidth
157
+      var actualHeight = $tip[0].offsetHeight
158
+
159
+      if (autoPlace) {
160
+        var $parent = this.$element.parent()
161
+
162
+        var orgPlacement = placement
163
+        var docScroll    = document.documentElement.scrollTop || document.body.scrollTop
164
+        var parentWidth  = this.options.container == 'body' ? window.innerWidth  : $parent.outerWidth()
165
+        var parentHeight = this.options.container == 'body' ? window.innerHeight : $parent.outerHeight()
166
+        var parentLeft   = this.options.container == 'body' ? 0 : $parent.offset().left
167
+
168
+        placement = placement == 'bottom' && pos.top   + pos.height  + actualHeight - docScroll > parentHeight  ? 'top'    :
169
+                    placement == 'top'    && pos.top   - docScroll   - actualHeight < 0                         ? 'bottom' :
170
+                    placement == 'right'  && pos.right + actualWidth > parentWidth                              ? 'left'   :
171
+                    placement == 'left'   && pos.left  - actualWidth < parentLeft                               ? 'right'  :
172
+                    placement
173
+
174
+        $tip
175
+          .removeClass(orgPlacement)
176
+          .addClass(placement)
177
+      }
178
+
179
+      var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight)
180
+
181
+      this.applyPlacement(calculatedOffset, placement)
182
+      this.hoverState = null
183
+
184
+      var complete = function() {
185
+        that.$element.trigger('shown.bs.' + that.type)
186
+      }
187
+
188
+      $.support.transition && this.$tip.hasClass('fade') ?
189
+        $tip
190
+          .one($.support.transition.end, complete)
191
+          .emulateTransitionEnd(150) :
192
+        complete()
193
+    }
194
+  }
195
+
196
+  Tooltip.prototype.applyPlacement = function (offset, placement) {
197
+    var replace
198
+    var $tip   = this.tip()
199
+    var width  = $tip[0].offsetWidth
200
+    var height = $tip[0].offsetHeight
201
+
202
+    // manually read margins because getBoundingClientRect includes difference
203
+    var marginTop = parseInt($tip.css('margin-top'), 10)
204
+    var marginLeft = parseInt($tip.css('margin-left'), 10)
205
+
206
+    // we must check for NaN for ie 8/9
207
+    if (isNaN(marginTop))  marginTop  = 0
208
+    if (isNaN(marginLeft)) marginLeft = 0
209
+
210
+    offset.top  = offset.top  + marginTop
211
+    offset.left = offset.left + marginLeft
212
+
213
+    // $.fn.offset doesn't round pixel values
214
+    // so we use setOffset directly with our own function B-0
215
+    $.offset.setOffset($tip[0], $.extend({
216
+      using: function (props) {
217
+        $tip.css({
218
+          top: Math.round(props.top),
219
+          left: Math.round(props.left)
220
+        })
221
+      }
222
+    }, offset), 0)
223
+
224
+    $tip.addClass('in')
225
+
226
+    // check to see if placing tip in new offset caused the tip to resize itself
227
+    var actualWidth  = $tip[0].offsetWidth
228
+    var actualHeight = $tip[0].offsetHeight
229
+
230
+    if (placement == 'top' && actualHeight != height) {
231
+      replace = true
232
+      offset.top = offset.top + height - actualHeight
233
+    }
234
+
235
+    if (/bottom|top/.test(placement)) {
236
+      var delta = 0
237
+
238
+      if (offset.left < 0) {
239
+        delta       = offset.left * -2
240
+        offset.left = 0
241
+
242
+        $tip.offset(offset)
243
+
244
+        actualWidth  = $tip[0].offsetWidth
245
+        actualHeight = $tip[0].offsetHeight
246
+      }
247
+
248
+      this.replaceArrow(delta - width + actualWidth, actualWidth, 'left')
249
+    } else {
250
+      this.replaceArrow(actualHeight - height, actualHeight, 'top')
251
+    }
252
+
253
+    if (replace) $tip.offset(offset)
254
+  }
255
+
256
+  Tooltip.prototype.replaceArrow = function (delta, dimension, position) {
257
+    this.arrow().css(position, delta ? (50 * (1 - delta / dimension) + '%') : '')
258
+  }
259
+
260
+  Tooltip.prototype.setContent = function () {
261
+    var $tip  = this.tip()
262
+    var title = this.getTitle()
263
+
264
+    $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title)
265
+    $tip.removeClass('fade in top bottom left right')
266
+  }
267
+
268
+  Tooltip.prototype.hide = function () {
269
+    var that = this
270
+    var $tip = this.tip()
271
+    var e    = $.Event('hide.bs.' + this.type)
272
+
273
+    function complete() {
274
+      if (that.hoverState != 'in') $tip.detach()
275
+      that.$element.trigger('hidden.bs.' + that.type)
276
+    }
277
+
278
+    this.$element.trigger(e)
279
+
280
+    if (e.isDefaultPrevented()) return
281
+
282
+    $tip.removeClass('in')
283
+
284
+    $.support.transition && this.$tip.hasClass('fade') ?
285
+      $tip
286
+        .one($.support.transition.end, complete)
287
+        .emulateTransitionEnd(150) :
288
+      complete()
289
+
290
+    this.hoverState = null
291
+
292
+    return this
293
+  }
294
+
295
+  Tooltip.prototype.fixTitle = function () {
296
+    var $e = this.$element
297
+    if ($e.attr('title') || typeof($e.attr('data-original-title')) != 'string') {
298
+      $e.attr('data-original-title', $e.attr('title') || '').attr('title', '')
299
+    }
300
+  }
301
+
302
+  Tooltip.prototype.hasContent = function () {
303
+    return this.getTitle()
304
+  }
305
+
306
+  Tooltip.prototype.getPosition = function () {
307
+    var el = this.$element[0]
308
+    return $.extend({}, (typeof el.getBoundingClientRect == 'function') ? el.getBoundingClientRect() : {
309
+      width: el.offsetWidth,
310
+      height: el.offsetHeight
311
+    }, this.$element.offset())
312
+  }
313
+
314
+  Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth, actualHeight) {
315
+    return placement == 'bottom' ? { top: pos.top + pos.height,   left: pos.left + pos.width / 2 - actualWidth / 2  } :
316
+           placement == 'top'    ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2  } :
317
+           placement == 'left'   ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } :
318
+        /* placement == 'right' */ { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width   }
319
+  }
320
+
321
+  Tooltip.prototype.getTitle = function () {
322
+    var title
323
+    var $e = this.$element
324
+    var o  = this.options
325
+
326
+    title = $e.attr('data-original-title')
327
+      || (typeof o.title == 'function' ? o.title.call($e[0]) :  o.title)
328
+
329
+    return title
330
+  }
331
+
332
+  Tooltip.prototype.tip = function () {
333
+    return this.$tip = this.$tip || $(this.options.template)
334
+  }
335
+
336
+  Tooltip.prototype.arrow = function () {
337
+    return this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow')
338
+  }
339
+
340
+  Tooltip.prototype.validate = function () {
341
+    if (!this.$element[0].parentNode) {
342
+      this.hide()
343
+      this.$element = null
344
+      this.options  = null
345
+    }
346
+  }
347
+
348
+  Tooltip.prototype.enable = function () {
349
+    this.enabled = true
350
+  }
351
+
352
+  Tooltip.prototype.disable = function () {
353
+    this.enabled = false
354
+  }
355
+
356
+  Tooltip.prototype.toggleEnabled = function () {
357
+    this.enabled = !this.enabled
358
+  }
359
+
360
+  Tooltip.prototype.toggle = function (e) {
361
+    var self = e ? $(e.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type) : this
362
+    self.tip().hasClass('in') ? self.leave(self) : self.enter(self)
363
+  }
364
+
365
+  Tooltip.prototype.destroy = function () {
366
+    clearTimeout(this.timeout)
367
+    this.hide().$element.off('.' + this.type).removeData('bs.' + this.type)
368
+  }
369
+
370
+
371
+  // TOOLTIP PLUGIN DEFINITION
372
+  // =========================
373
+
374
+  var old = $.fn.tooltip
375
+
376
+  $.fn.tooltip = function (option) {
377
+    return this.each(function () {
378
+      var $this   = $(this)
379
+      var data    = $this.data('bs.tooltip')
380
+      var options = typeof option == 'object' && option
381
+
382
+      if (!data && option == 'destroy') return
383
+      if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options)))
384
+      if (typeof option == 'string') data[option]()
385
+    })
386
+  }
387
+
388
+  $.fn.tooltip.Constructor = Tooltip
389
+
390
+
391
+  // TOOLTIP NO CONFLICT
392
+  // ===================
393
+
394
+  $.fn.tooltip.noConflict = function () {
395
+    $.fn.tooltip = old
396
+    return this
397
+  }
398
+
399
+}(jQuery);

+ 97
- 0
local_stats.js Voir le fichier

@@ -0,0 +1,97 @@
1
+/**
2
+ * Provides statistics for the local stream.
3
+ */
4
+var LocalStatsCollector = (function() {
5
+    /**
6
+     * Size of the webaudio analizer buffer.
7
+     * @type {number}
8
+     */
9
+    var WEBAUDIO_ANALIZER_FFT_SIZE = 512;
10
+
11
+    /**
12
+     * Value of the webaudio analizer smoothing time parameter.
13
+     * @type {number}
14
+     */
15
+    var WEBAUDIO_ANALIZER_SMOOTING_TIME = 0.1;
16
+
17
+    /**
18
+     * <tt>LocalStatsCollector</tt> calculates statistics for the local stream.
19
+     *
20
+     * @param stream the local stream
21
+     * @param interval stats refresh interval given in ms.
22
+     * @param {function(LocalStatsCollector)} updateCallback the callback called on stats
23
+     *                                   update.
24
+     * @constructor
25
+     */
26
+    function LocalStatsCollectorProto(stream, interval, updateCallback) {
27
+        window.AudioContext = window.AudioContext || window.webkitAudioContext;
28
+        this.stream = stream;
29
+        this.intervalId = null;
30
+        this.intervalMilis = interval;
31
+        this.updateCallback = updateCallback;
32
+        this.audioLevel = 0;
33
+    }
34
+
35
+
36
+    /**
37
+     * Starts the collecting the statistics.
38
+     */
39
+    LocalStatsCollectorProto.prototype.start = function () {
40
+        if (!window.AudioContext)
41
+            return;
42
+
43
+        var context = new AudioContext();
44
+        var analyser = context.createAnalyser();
45
+        analyser.smoothingTimeConstant = WEBAUDIO_ANALIZER_SMOOTING_TIME;
46
+        analyser.fftSize = WEBAUDIO_ANALIZER_FFT_SIZE;
47
+
48
+
49
+        var source = context.createMediaStreamSource(this.stream);
50
+        source.connect(analyser);
51
+
52
+
53
+        var self = this;
54
+
55
+        this.intervalId = setInterval(
56
+            function () {
57
+                var array = new Uint8Array(analyser.frequencyBinCount);
58
+                analyser.getByteFrequencyData(array);
59
+                self.audioLevel = FrequencyDataToAudioLevel(array);
60
+                self.updateCallback(self);
61
+            },
62
+            this.intervalMilis
63
+        );
64
+
65
+    }
66
+
67
+    /**
68
+     * Stops collecting the statistics.
69
+     */
70
+    LocalStatsCollectorProto.prototype.stop = function () {
71
+        if (this.intervalId) {
72
+            clearInterval(this.intervalId);
73
+            this.intervalId = null;
74
+        }
75
+    }
76
+
77
+
78
+    /**
79
+     * Converts frequency data array to audio level.
80
+     * @param array the frequency data array.
81
+     * @returns {number} the audio level
82
+     */
83
+    var FrequencyDataToAudioLevel = function (array) {
84
+        var maxVolume = 0;
85
+
86
+        var length = array.length;
87
+
88
+        for (var i = 0; i < length; i++) {
89
+            if (maxVolume < array[i])
90
+                maxVolume = array[i];
91
+        }
92
+
93
+        return maxVolume / 255;
94
+    }
95
+
96
+    return LocalStatsCollectorProto;
97
+})();

+ 29
- 2
muc.js Voir le fichier

@@ -21,6 +21,9 @@ Strophe.addConnectionPlugin('emuc', {
21 21
     },
22 22
     doJoin: function (jid, password) {
23 23
         this.myroomjid = jid;
24
+
25
+        console.info("Joined MUC as " + this.myroomjid);
26
+
24 27
         this.initPresenceMap(this.myroomjid);
25 28
 
26 29
         if (!this.roomjid) {
@@ -167,12 +170,36 @@ Strophe.addConnectionPlugin('emuc', {
167 170
         }
168 171
         this.connection.send(msg);
169 172
     },
173
+    setSubject: function (subject){
174
+        var msg = $msg({to: this.roomjid, type: 'groupchat'});
175
+        msg.c('subject', subject);
176
+        this.connection.send(msg);
177
+        console.log("topic changed to " + subject);
178
+    },
170 179
     onMessage: function (msg) {
171
-        var txt = $(msg).find('>body').text();
172
-        // TODO: <subject/>
173 180
         // FIXME: this is a hack. but jingle on muc makes nickchanges hard
174 181
         var from = msg.getAttribute('from');
175 182
         var nick = $(msg).find('>nick[xmlns="http://jabber.org/protocol/nick"]').text() || Strophe.getResourceFromJid(from);
183
+
184
+        var txt = $(msg).find('>body').text();
185
+        var type = msg.getAttribute("type");
186
+        if(type == "error")
187
+        {
188
+            Chat.chatAddError($(msg).find('>text').text(), txt);
189
+            return true;
190
+        }
191
+
192
+        var subject = $(msg).find('>subject');
193
+        if(subject.length)
194
+        {
195
+            var subjectText = subject.text();
196
+            if(subjectText || subjectText == "") {
197
+                Chat.chatSetSubject(subjectText);
198
+                console.log("Subject is changed to " + subjectText);
199
+            }
200
+        }
201
+
202
+
176 203
         if (txt) {
177 204
             console.log('chat', nick, txt);
178 205
 

+ 6
- 6
prezi.js Voir le fichier

@@ -19,10 +19,10 @@ var Prezi = (function (my) {
19 19
             $(document).trigger("video.selected", [true]);
20 20
 
21 21
             $('#largeVideo').fadeOut(300, function () {
22
-                setLargeVideoVisible(false);
22
+                VideoLayout.setLargeVideoVisible(false);
23 23
                 $('#presentation>iframe').fadeIn(300, function() {
24 24
                     $('#presentation>iframe').css({opacity:'1'});
25
-                    dockToolbar(true);
25
+                    Toolbar.dockToolbar(true);
26 26
                 });
27 27
             });
28 28
         }
@@ -32,8 +32,8 @@ var Prezi = (function (my) {
32 32
                     $('#presentation>iframe').css({opacity:'0'});
33 33
                     $('#reloadPresentation').css({display:'none'});
34 34
                     $('#largeVideo').fadeIn(300, function() {
35
-                        setLargeVideoVisible(true);
36
-                        dockToolbar(false);
35
+                        VideoLayout.setLargeVideoVisible(true);
36
+                        Toolbar.dockToolbar(false);
37 37
                     });
38 38
                 });
39 39
             }
@@ -177,8 +177,8 @@ var Prezi = (function (my) {
177 177
         // We explicitly don't specify the peer jid here, because we don't want
178 178
         // this video to be dealt with as a peer related one (for example we
179 179
         // don't want to show a mute/kick menu for this one, etc.).
180
-        addRemoteVideoContainer(null, elementId);
181
-        resizeThumbnails();
180
+        VideoLayout.addRemoteVideoContainer(null, elementId);
181
+        VideoLayout.resizeThumbnails();
182 182
 
183 183
         var controlsEnabled = false;
184 184
         if (jid === connection.emuc.myroomjid)

+ 287
- 0
rtp_stats.js Voir le fichier

@@ -0,0 +1,287 @@
1
+/* global ssrc2jid */
2
+
3
+/**
4
+ * Function object which once created can be used to calculate moving average of
5
+ * given period. Example for SMA3:</br>
6
+ * var sma3 = new SimpleMovingAverager(3);
7
+ * while(true) // some update loop
8
+ * {
9
+ *   var currentSma3Value = sma3(nextInputValue);
10
+ * }
11
+ *
12
+ * @param period moving average period that will be used by created instance.
13
+ * @returns {Function} SMA calculator function of given <tt>period</tt>.
14
+ * @constructor
15
+ */
16
+function SimpleMovingAverager(period)
17
+{
18
+    var nums = [];
19
+    return function (num)
20
+    {
21
+        nums.push(num);
22
+        if (nums.length > period)
23
+            nums.splice(0, 1);
24
+        var sum = 0;
25
+        for (var i in nums)
26
+            sum += nums[i];
27
+        var n = period;
28
+        if (nums.length < period)
29
+            n = nums.length;
30
+        return (sum / n);
31
+    };
32
+}
33
+
34
+/**
35
+ * Peer statistics data holder.
36
+ * @constructor
37
+ */
38
+function PeerStats()
39
+{
40
+    this.ssrc2Loss = {};
41
+    this.ssrc2AudioLevel = {};
42
+}
43
+
44
+/**
45
+ * Sets packets loss rate for given <tt>ssrc</tt> that blong to the peer
46
+ * represented by this instance.
47
+ * @param ssrc audio or video RTP stream SSRC.
48
+ * @param lossRate new packet loss rate value to be set.
49
+ */
50
+PeerStats.prototype.setSsrcLoss = function (ssrc, lossRate)
51
+{
52
+    this.ssrc2Loss[ssrc] = lossRate;
53
+};
54
+
55
+/**
56
+ * Sets new audio level(input or output) for given <tt>ssrc</tt> that identifies
57
+ * the stream which belongs to the peer represented by this instance.
58
+ * @param ssrc RTP stream SSRC for which current audio level value will be
59
+ *        updated.
60
+ * @param audioLevel the new audio level value to be set. Value is truncated to
61
+ *        fit the range from 0 to 1.
62
+ */
63
+PeerStats.prototype.setSsrcAudioLevel = function (ssrc, audioLevel)
64
+{
65
+    // Range limit 0 - 1
66
+    this.ssrc2AudioLevel[ssrc] = Math.min(Math.max(audioLevel, 0), 1);
67
+};
68
+
69
+/**
70
+ * Calculates average packet loss for all streams that belong to the peer
71
+ * represented by this instance.
72
+ * @returns {number} average packet loss for all streams that belong to the peer
73
+ *                   represented by this instance.
74
+ */
75
+PeerStats.prototype.getAvgLoss = function ()
76
+{
77
+    var self = this;
78
+    var avg = 0;
79
+    var count = Object.keys(this.ssrc2Loss).length;
80
+    Object.keys(this.ssrc2Loss).forEach(
81
+        function (ssrc)
82
+        {
83
+            avg += self.ssrc2Loss[ssrc];
84
+        }
85
+    );
86
+    return count > 0 ? avg / count : 0;
87
+};
88
+
89
+/**
90
+ * <tt>StatsCollector</tt> registers for stats updates of given
91
+ * <tt>peerconnection</tt> in given <tt>interval</tt>. On each update particular
92
+ * stats are extracted and put in {@link PeerStats} objects. Once the processing
93
+ * is done <tt>updateCallback</tt> is called with <tt>this</tt> instance as
94
+ * an event source.
95
+ *
96
+ * @param peerconnection webRTC peer connection object.
97
+ * @param interval stats refresh interval given in ms.
98
+ * @param {function(StatsCollector)} updateCallback the callback called on stats
99
+ *                                   update.
100
+ * @constructor
101
+ */
102
+function StatsCollector(peerconnection, interval, updateCallback)
103
+{
104
+    this.peerconnection = peerconnection;
105
+    this.baselineReport = null;
106
+    this.currentReport = null;
107
+    this.intervalId = null;
108
+    // Updates stats interval
109
+    this.intervalMilis = interval;
110
+    // Use SMA 3 to average packet loss changes over time
111
+    this.sma3 = new SimpleMovingAverager(3);
112
+    // Map of jids to PeerStats
113
+    this.jid2stats = {};
114
+
115
+    this.updateCallback = updateCallback;
116
+}
117
+
118
+/**
119
+ * Stops stats updates.
120
+ */
121
+StatsCollector.prototype.stop = function ()
122
+{
123
+    if (this.intervalId)
124
+    {
125
+        clearInterval(this.intervalId);
126
+        this.intervalId = null;
127
+    }
128
+};
129
+
130
+/**
131
+ * Callback passed to <tt>getStats</tt> method.
132
+ * @param error an error that occurred on <tt>getStats</tt> call.
133
+ */
134
+StatsCollector.prototype.errorCallback = function (error)
135
+{
136
+    console.error("Get stats error", error);
137
+    this.stop();
138
+};
139
+
140
+/**
141
+ * Starts stats updates.
142
+ */
143
+StatsCollector.prototype.start = function ()
144
+{
145
+    var self = this;
146
+    this.intervalId = setInterval(
147
+        function ()
148
+        {
149
+            // Interval updates
150
+            self.peerconnection.getStats(
151
+                function (report)
152
+                {
153
+                    var results = report.result();
154
+                    //console.error("Got interval report", results);
155
+                    self.currentReport = results;
156
+                    self.processReport();
157
+                    self.baselineReport = self.currentReport;
158
+                },
159
+                self.errorCallback
160
+            );
161
+        },
162
+        self.intervalMilis
163
+    );
164
+};
165
+
166
+/**
167
+ * Stats processing logic.
168
+ */
169
+StatsCollector.prototype.processReport = function ()
170
+{
171
+    if (!this.baselineReport)
172
+    {
173
+        return;
174
+    }
175
+
176
+    for (var idx in this.currentReport)
177
+    {
178
+        var now = this.currentReport[idx];
179
+        if (now.type != 'ssrc')
180
+        {
181
+            continue;
182
+        }
183
+
184
+        var before = this.baselineReport[idx];
185
+        if (!before)
186
+        {
187
+            console.warn(now.stat('ssrc') + ' not enough data');
188
+            continue;
189
+        }
190
+
191
+        var ssrc = now.stat('ssrc');
192
+        var jid = ssrc2jid[ssrc];
193
+        if (!jid)
194
+        {
195
+            console.warn("No jid for ssrc: " + ssrc);
196
+            continue;
197
+        }
198
+
199
+        var jidStats = this.jid2stats[jid];
200
+        if (!jidStats)
201
+        {
202
+            jidStats = new PeerStats();
203
+            this.jid2stats[jid] = jidStats;
204
+        }
205
+
206
+        // Audio level
207
+        var audioLevel = now.stat('audioInputLevel');
208
+        if (!audioLevel)
209
+            audioLevel = now.stat('audioOutputLevel');
210
+        if (audioLevel)
211
+        {
212
+            // TODO: can't find specs about what this value really is,
213
+            // but it seems to vary between 0 and around 32k.
214
+            audioLevel = audioLevel / 32767;
215
+            jidStats.setSsrcAudioLevel(ssrc, audioLevel);
216
+        }
217
+
218
+        var key = 'packetsReceived';
219
+        if (!now.stat(key))
220
+        {
221
+            key = 'packetsSent';
222
+            if (!now.stat(key))
223
+            {
224
+                console.error("No packetsReceived nor packetSent stat found");
225
+                this.stop();
226
+                return;
227
+            }
228
+        }
229
+        var packetsNow = now.stat(key);
230
+        var packetsBefore = before.stat(key);
231
+        var packetRate = packetsNow - packetsBefore;
232
+
233
+        var currentLoss = now.stat('packetsLost');
234
+        var previousLoss = before.stat('packetsLost');
235
+        var lossRate = currentLoss - previousLoss;
236
+
237
+        var packetsTotal = (packetRate + lossRate);
238
+        var lossPercent;
239
+
240
+        if (packetsTotal > 0)
241
+            lossPercent = lossRate / packetsTotal;
242
+        else
243
+            lossPercent = 0;
244
+
245
+        //console.info(jid + " ssrc: " + ssrc + " " + key + ": " + packetsNow);
246
+
247
+        jidStats.setSsrcLoss(ssrc, lossPercent);
248
+    }
249
+
250
+    var self = this;
251
+    // Jid stats
252
+    var allPeersAvg = 0;
253
+    var jids = Object.keys(this.jid2stats);
254
+    jids.forEach(
255
+        function (jid)
256
+        {
257
+            var peerAvg = self.jid2stats[jid].getAvgLoss(
258
+                function (avg)
259
+                {
260
+                    //console.info(jid + " stats: " + (avg * 100) + " %");
261
+                    allPeersAvg += avg;
262
+                }
263
+            );
264
+        }
265
+    );
266
+
267
+    if (jids.length > 1)
268
+    {
269
+        // Our streams loss is reported as 0 always, so -1 to length
270
+        allPeersAvg = allPeersAvg / (jids.length - 1);
271
+
272
+        /**
273
+         * Calculates number of connection quality bars from 4(hi) to 0(lo).
274
+         */
275
+        var outputAvg = self.sma3(allPeersAvg);
276
+        // Linear from 4(0%) to 0(25%).
277
+        var quality = Math.round(4 - outputAvg * 16);
278
+        quality = Math.max(quality, 0); // lower limit 0
279
+        quality = Math.min(quality, 4); // upper limit 4
280
+        // TODO: quality can be used to indicate connection quality using 4 step
281
+        // bar indicator
282
+        //console.info("Loss SMA3: " + outputAvg + " Q: " + quality);
283
+    }
284
+
285
+    self.updateCallback(self);
286
+};
287
+

+ 234
- 0
toolbar.js Voir le fichier

@@ -0,0 +1,234 @@
1
+var Toolbar = (function (my) {
2
+    var INITIAL_TOOLBAR_TIMEOUT = 20000;
3
+    var TOOLBAR_TIMEOUT = INITIAL_TOOLBAR_TIMEOUT;
4
+
5
+    /**
6
+     * Opens the lock room dialog.
7
+     */
8
+    my.openLockDialog = function() {
9
+        // Only the focus is able to set a shared key.
10
+        if (focus === null) {
11
+            if (sharedKey)
12
+                $.prompt("This conversation is currently protected by"
13
+                        + " a shared secret key.",
14
+                    {
15
+                        title: "Secrect key",
16
+                        persistent: false
17
+                    }
18
+                );
19
+            else
20
+                $.prompt("This conversation isn't currently protected by"
21
+                        + " a secret key. Only the owner of the conference" +
22
+                        + " could set a shared key.",
23
+                    {
24
+                        title: "Secrect key",
25
+                        persistent: false
26
+                    }
27
+                );
28
+        } else {
29
+            if (sharedKey) {
30
+                $.prompt("Are you sure you would like to remove your secret key?",
31
+                    {
32
+                        title: "Remove secrect key",
33
+                        persistent: false,
34
+                        buttons: { "Remove": true, "Cancel": false},
35
+                        defaultButton: 1,
36
+                        submit: function (e, v, m, f) {
37
+                            if (v) {
38
+                                setSharedKey('');
39
+                                lockRoom(false);
40
+                            }
41
+                        }
42
+                    }
43
+                );
44
+            } else {
45
+                $.prompt('<h2>Set a secrect key to lock your room</h2>' +
46
+                         '<input id="lockKey" type="text" placeholder="your shared key" autofocus>',
47
+                    {
48
+                        persistent: false,
49
+                        buttons: { "Save": true, "Cancel": false},
50
+                        defaultButton: 1,
51
+                        loaded: function (event) {
52
+                            document.getElementById('lockKey').focus();
53
+                        },
54
+                        submit: function (e, v, m, f) {
55
+                            if (v) {
56
+                                var lockKey = document.getElementById('lockKey');
57
+
58
+                                if (lockKey.value) {
59
+                                    setSharedKey(Util.escapeHtml(lockKey.value));
60
+                                    lockRoom(true);
61
+                                }
62
+                            }
63
+                        }
64
+                    }
65
+                );
66
+            }
67
+        }
68
+    };
69
+
70
+    /**
71
+     * Opens the invite link dialog.
72
+     */
73
+    my.openLinkDialog = function() {
74
+        $.prompt('<input id="inviteLinkRef" type="text" value="' +
75
+            encodeURI(roomUrl) + '" onclick="this.select();" readonly>',
76
+            {
77
+                title: "Share this link with everyone you want to invite",
78
+                persistent: false,
79
+                buttons: { "Cancel": false},
80
+                loaded: function (event) {
81
+                    document.getElementById('inviteLinkRef').select();
82
+                }
83
+            }
84
+        );
85
+    };
86
+
87
+    /**
88
+     * Opens the settings dialog.
89
+     */
90
+    my.openSettingsDialog = function() {
91
+        $.prompt('<h2>Configure your conference</h2>' +
92
+            '<input type="checkbox" id="initMuted"> Participants join muted<br/>' +
93
+            '<input type="checkbox" id="requireNicknames"> Require nicknames<br/><br/>' +
94
+            'Set a secrect key to lock your room: <input id="lockKey" type="text" placeholder="your shared key" autofocus>',
95
+            {
96
+                persistent: false,
97
+                buttons: { "Save": true, "Cancel": false},
98
+                defaultButton: 1,
99
+                loaded: function (event) {
100
+                    document.getElementById('lockKey').focus();
101
+                },
102
+                submit: function (e, v, m, f) {
103
+                    if (v) {
104
+                        if ($('#initMuted').is(":checked")) {
105
+                            // it is checked
106
+                        }
107
+
108
+                        if ($('#requireNicknames').is(":checked")) {
109
+                            // it is checked
110
+                        }
111
+                        /*
112
+                        var lockKey = document.getElementById('lockKey');
113
+
114
+                        if (lockKey.value)
115
+                        {
116
+                            setSharedKey(lockKey.value);
117
+                            lockRoom(true);
118
+                        }
119
+                        */
120
+                    }
121
+                }
122
+            }
123
+        );
124
+    };
125
+
126
+    /**
127
+     * Toggles the application in and out of full screen mode
128
+     * (a.k.a. presentation mode in Chrome).
129
+     */
130
+    my.toggleFullScreen = function() {
131
+        var fsElement = document.documentElement;
132
+
133
+        if (!document.mozFullScreen && !document.webkitIsFullScreen) {
134
+            //Enter Full Screen
135
+            if (fsElement.mozRequestFullScreen) {
136
+                fsElement.mozRequestFullScreen();
137
+            }
138
+            else {
139
+                fsElement.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT);
140
+            }
141
+        } else {
142
+            //Exit Full Screen
143
+            if (document.mozCancelFullScreen) {
144
+                document.mozCancelFullScreen();
145
+            } else {
146
+                document.webkitCancelFullScreen();
147
+            }
148
+        }
149
+    };
150
+
151
+    /**
152
+     * Shows the main toolbar.
153
+     */
154
+    my.showToolbar = function() {
155
+        if (!$('#header').is(':visible')) {
156
+            $('#header').show("slide", { direction: "up", duration: 300});
157
+            $('#subject').animate({top: "+=40"}, 300);
158
+
159
+            if (toolbarTimeout) {
160
+                clearTimeout(toolbarTimeout);
161
+                toolbarTimeout = null;
162
+            }
163
+            toolbarTimeout = setTimeout(hideToolbar, TOOLBAR_TIMEOUT);
164
+            TOOLBAR_TIMEOUT = 4000;
165
+        }
166
+
167
+        if (focus != null)
168
+        {
169
+//            TODO: Enable settings functionality. Need to uncomment the settings button in index.html.
170
+//            $('#settingsButton').css({visibility:"visible"});
171
+        }
172
+
173
+        // Show/hide desktop sharing button
174
+        showDesktopSharingButton();
175
+    };
176
+
177
+    /**
178
+     * Docks/undocks the toolbar.
179
+     *
180
+     * @param isDock indicates what operation to perform
181
+     */
182
+    my.dockToolbar = function(isDock) {
183
+        if (isDock) {
184
+            // First make sure the toolbar is shown.
185
+            if (!$('#header').is(':visible')) {
186
+                Toolbar.showToolbar();
187
+            }
188
+            // Then clear the time out, to dock the toolbar.
189
+            clearTimeout(toolbarTimeout);
190
+            toolbarTimeout = null;
191
+        }
192
+        else {
193
+            if (!$('#header').is(':visible')) {
194
+                Toolbar.showToolbar();
195
+            }
196
+            else {
197
+                toolbarTimeout = setTimeout(hideToolbar, TOOLBAR_TIMEOUT);
198
+            }
199
+        }
200
+    };
201
+
202
+    /**
203
+     * Updates the lock button state.
204
+     */
205
+    my.updateLockButton = function() {
206
+        buttonClick("#lockIcon", "icon-security icon-security-locked");
207
+    };
208
+
209
+    /**
210
+     * Hides the toolbar.
211
+     */
212
+    var hideToolbar = function () {
213
+        var isToolbarHover = false;
214
+        $('#header').find('*').each(function () {
215
+            var id = $(this).attr('id');
216
+            if ($("#" + id + ":hover").length > 0) {
217
+                isToolbarHover = true;
218
+            }
219
+        });
220
+
221
+        clearTimeout(toolbarTimeout);
222
+        toolbarTimeout = null;
223
+
224
+        if (!isToolbarHover) {
225
+            $('#header').hide("slide", { direction: "up", duration: 300});
226
+            $('#subject').animate({top: "-=40"}, 300);
227
+        }
228
+        else {
229
+            toolbarTimeout = setTimeout(hideToolbar, TOOLBAR_TIMEOUT);
230
+        }
231
+    };
232
+
233
+    return my;
234
+}(Toolbar || {}));

+ 26
- 1
util.js Voir le fichier

@@ -51,10 +51,35 @@ var Util = (function (my) {
51 51
      * Returns the available video width.
52 52
      */
53 53
     my.getAvailableVideoWidth = function () {
54
-        var chatspaceWidth = $('#chatspace').is(":visible") ? $('#chatspace').width() : 0;
54
+        var chatspaceWidth
55
+            = $('#chatspace').is(":visible") ? $('#chatspace').width() : 0;
55 56
 
56 57
         return window.innerWidth - chatspaceWidth;
57 58
     };
58 59
 
60
+    my.imageToGrayScale = function (canvas) {
61
+        var context = canvas.getContext('2d');
62
+        var imgData = context.getImageData(0, 0, canvas.width, canvas.height);
63
+        var pixels  = imgData.data;
64
+
65
+        for (var i = 0, n = pixels.length; i < n; i += 4) {
66
+            var grayscale
67
+                = pixels[i] * .3 + pixels[i+1] * .59 + pixels[i+2] * .11;
68
+            pixels[i  ] = grayscale;        // red
69
+            pixels[i+1] = grayscale;        // green
70
+            pixels[i+2] = grayscale;        // blue
71
+            // pixels[i+3]              is alpha
72
+        }
73
+        // redraw the image in black & white
74
+        context.putImageData(imgData, 0, 0);
75
+    };
76
+
77
+    my.setTooltip = function (element, tooltipText, position) {
78
+        element.setAttribute("data-content", tooltipText);
79
+        element.setAttribute("data-toggle", "popover");
80
+        element.setAttribute("data-placement", position);
81
+        element.setAttribute("data-html", true);
82
+    };
83
+
59 84
     return my;
60 85
 }(Util || {}));

+ 901
- 0
videolayout.js Voir le fichier

@@ -0,0 +1,901 @@
1
+var VideoLayout = (function (my) {
2
+    var preMuted = false;
3
+    var currentActiveSpeaker = null;
4
+
5
+    my.changeLocalAudio = function(stream) {
6
+        connection.jingle.localAudio = stream;
7
+
8
+        RTC.attachMediaStream($('#localAudio'), stream);
9
+        document.getElementById('localAudio').autoplay = true;
10
+        document.getElementById('localAudio').volume = 0;
11
+        if (preMuted) {
12
+            toggleAudio();
13
+            preMuted = false;
14
+        }
15
+    };
16
+
17
+    my.changeLocalVideo = function(stream, flipX) {
18
+        connection.jingle.localVideo = stream;
19
+
20
+        var localVideo = document.createElement('video');
21
+        localVideo.id = 'localVideo_' + stream.id;
22
+        localVideo.autoplay = true;
23
+        localVideo.volume = 0; // is it required if audio is separated ?
24
+        localVideo.oncontextmenu = function () { return false; };
25
+
26
+        var localVideoContainer = document.getElementById('localVideoWrapper');
27
+        localVideoContainer.appendChild(localVideo);
28
+
29
+        var localVideoSelector = $('#' + localVideo.id);
30
+        // Add click handler
31
+        localVideoSelector.click(function () {
32
+            VideoLayout.handleVideoThumbClicked(localVideo.src);
33
+        });
34
+        // Add hover handler
35
+        $('#localVideoContainer').hover(
36
+            function() {
37
+                VideoLayout.showDisplayName('localVideoContainer', true);
38
+            },
39
+            function() {
40
+                if (focusedVideoSrc !== localVideo.src)
41
+                    VideoLayout.showDisplayName('localVideoContainer', false);
42
+            }
43
+        );
44
+        // Add stream ended handler
45
+        stream.onended = function () {
46
+            localVideoContainer.removeChild(localVideo);
47
+            VideoLayout.checkChangeLargeVideo(localVideo.src);
48
+        };
49
+        // Flip video x axis if needed
50
+        flipXLocalVideo = flipX;
51
+        if (flipX) {
52
+            localVideoSelector.addClass("flipVideoX");
53
+        }
54
+        // Attach WebRTC stream
55
+        RTC.attachMediaStream(localVideoSelector, stream);
56
+
57
+        localVideoSrc = localVideo.src;
58
+        VideoLayout.updateLargeVideo(localVideoSrc, 0);
59
+    };
60
+
61
+    /**
62
+     * Checks if removed video is currently displayed and tries to display
63
+     * another one instead.
64
+     * @param removedVideoSrc src stream identifier of the video.
65
+     */
66
+    my.checkChangeLargeVideo = function(removedVideoSrc) {
67
+        if (removedVideoSrc === $('#largeVideo').attr('src')) {
68
+            // this is currently displayed as large
69
+            // pick the last visible video in the row
70
+            // if nobody else is left, this picks the local video
71
+            var pick
72
+                = $('#remoteVideos>span[id!="mixedstream"]:visible:last>video')
73
+                    .get(0);
74
+
75
+            if (!pick) {
76
+                console.info("Last visible video no longer exists");
77
+                pick = $('#remoteVideos>span[id!="mixedstream"]>video').get(0);
78
+                if (!pick) {
79
+                    // Try local video
80
+                    console.info("Fallback to local video...");
81
+                    pick = $('#remoteVideos>span>span>video').get(0);
82
+                }
83
+            }
84
+
85
+            // mute if localvideo
86
+            if (pick) {
87
+                VideoLayout.updateLargeVideo(pick.src, pick.volume);
88
+            } else {
89
+                console.warn("Failed to elect large video");
90
+            }
91
+        }
92
+    };
93
+
94
+
95
+    /**
96
+     * Updates the large video with the given new video source.
97
+     */
98
+    my.updateLargeVideo = function(newSrc, vol) {
99
+        console.log('hover in', newSrc);
100
+
101
+        if ($('#largeVideo').attr('src') != newSrc) {
102
+
103
+            var isVisible = $('#largeVideo').is(':visible');
104
+
105
+            $('#largeVideo').fadeOut(300, function () {
106
+                $(this).attr('src', newSrc);
107
+
108
+                // Screen stream is already rotated
109
+                var flipX = (newSrc === localVideoSrc) && flipXLocalVideo;
110
+
111
+                var videoTransform = document.getElementById('largeVideo')
112
+                                        .style.webkitTransform;
113
+
114
+                if (flipX && videoTransform !== 'scaleX(-1)') {
115
+                    document.getElementById('largeVideo').style.webkitTransform
116
+                        = "scaleX(-1)";
117
+                }
118
+                else if (!flipX && videoTransform === 'scaleX(-1)') {
119
+                    document.getElementById('largeVideo').style.webkitTransform
120
+                        = "none";
121
+                }
122
+
123
+                // Change the way we'll be measuring and positioning large video
124
+                var isDesktop = isVideoSrcDesktop(newSrc);
125
+                getVideoSize = isDesktop
126
+                                ? getDesktopVideoSize
127
+                                : getCameraVideoSize;
128
+                getVideoPosition = isDesktop
129
+                                    ? getDesktopVideoPosition
130
+                                    : getCameraVideoPosition;
131
+
132
+                if (isVisible)
133
+                    $(this).fadeIn(300);
134
+            });
135
+        }
136
+    };
137
+
138
+    my.handleVideoThumbClicked = function(videoSrc) {
139
+        // Restore style for previously focused video
140
+        var focusJid = getJidFromVideoSrc(focusedVideoSrc);
141
+        var oldContainer = getParticipantContainer(focusJid);
142
+
143
+        if (oldContainer) {
144
+            oldContainer.removeClass("videoContainerFocused");
145
+            VideoLayout.enableActiveSpeaker(
146
+                    Strophe.getResourceFromJid(focusJid), false);
147
+        }
148
+
149
+        // Unlock current focused.
150
+        if (focusedVideoSrc === videoSrc)
151
+        {
152
+            focusedVideoSrc = null;
153
+            // Enable the currently set active speaker.
154
+            if (currentActiveSpeaker) {
155
+                VideoLayout.enableActiveSpeaker(currentActiveSpeaker, true);
156
+            }
157
+
158
+            return;
159
+        }
160
+        // Remove style for current active speaker if we're going to lock
161
+        // another video.
162
+        else if (currentActiveSpeaker) {
163
+            VideoLayout.enableActiveSpeaker(currentActiveSpeaker, false);
164
+        }
165
+
166
+        // Lock new video
167
+        focusedVideoSrc = videoSrc;
168
+
169
+        var userJid = getJidFromVideoSrc(videoSrc);
170
+        if (userJid)
171
+        {
172
+            var container = getParticipantContainer(userJid);
173
+            container.addClass("videoContainerFocused");
174
+
175
+            var resourceJid = Strophe.getResourceFromJid(userJid);
176
+            VideoLayout.enableActiveSpeaker(resourceJid, true);
177
+        }
178
+
179
+        $(document).trigger("video.selected", [false]);
180
+
181
+        VideoLayout.updateLargeVideo(videoSrc, 1);
182
+
183
+        $('audio').each(function (idx, el) {
184
+            if (el.id.indexOf('mixedmslabel') !== -1) {
185
+                el.volume = 0;
186
+                el.volume = 1;
187
+            }
188
+        });
189
+    };
190
+
191
+    /**
192
+     * Positions the large video.
193
+     *
194
+     * @param videoWidth the stream video width
195
+     * @param videoHeight the stream video height
196
+     */
197
+    my.positionLarge = function (videoWidth, videoHeight) {
198
+        var videoSpaceWidth = $('#videospace').width();
199
+        var videoSpaceHeight = window.innerHeight;
200
+
201
+        var videoSize = getVideoSize(videoWidth,
202
+                                     videoHeight,
203
+                                     videoSpaceWidth,
204
+                                     videoSpaceHeight);
205
+
206
+        var largeVideoWidth = videoSize[0];
207
+        var largeVideoHeight = videoSize[1];
208
+
209
+        var videoPosition = getVideoPosition(largeVideoWidth,
210
+                                             largeVideoHeight,
211
+                                             videoSpaceWidth,
212
+                                             videoSpaceHeight);
213
+
214
+        var horizontalIndent = videoPosition[0];
215
+        var verticalIndent = videoPosition[1];
216
+
217
+        positionVideo($('#largeVideo'),
218
+                      largeVideoWidth,
219
+                      largeVideoHeight,
220
+                      horizontalIndent, verticalIndent);
221
+    };
222
+
223
+    /**
224
+     * Shows/hides the large video.
225
+     */
226
+    my.setLargeVideoVisible = function(isVisible) {
227
+        if (isVisible) {
228
+            $('#largeVideo').css({visibility: 'visible'});
229
+            $('.watermark').css({visibility: 'visible'});
230
+        }
231
+        else {
232
+            $('#largeVideo').css({visibility: 'hidden'});
233
+            $('.watermark').css({visibility: 'hidden'});
234
+        }
235
+    };
236
+
237
+
238
+    /**
239
+     * Checks if container for participant identified by given peerJid exists
240
+     * in the document and creates it eventually.
241
+     * 
242
+     * @param peerJid peer Jid to check.
243
+     */
244
+    my.ensurePeerContainerExists = function(peerJid) {
245
+        var peerResource = Strophe.getResourceFromJid(peerJid);
246
+        var videoSpanId = 'participant_' + peerResource;
247
+
248
+        if ($('#' + videoSpanId).length > 0) {
249
+            // If there's been a focus change, make sure we add focus related
250
+            // interface!!
251
+            if (focus && $('#remote_popupmenu_' + peerResource).length <= 0)
252
+                addRemoteVideoMenu( peerJid,
253
+                                    document.getElementById(videoSpanId));
254
+            return;
255
+        }
256
+
257
+        var container
258
+            = VideoLayout.addRemoteVideoContainer(peerJid, videoSpanId);
259
+
260
+        var nickfield = document.createElement('span');
261
+        nickfield.className = "nick";
262
+        nickfield.appendChild(document.createTextNode(peerResource));
263
+        container.appendChild(nickfield);
264
+        VideoLayout.resizeThumbnails();
265
+    };
266
+
267
+    my.addRemoteVideoContainer = function(peerJid, spanId) {
268
+        var container = document.createElement('span');
269
+        container.id = spanId;
270
+        container.className = 'videocontainer';
271
+        var remotes = document.getElementById('remoteVideos');
272
+
273
+        // If the peerJid is null then this video span couldn't be directly
274
+        // associated with a participant (this could happen in the case of prezi).
275
+        if (focus && peerJid != null)
276
+            addRemoteVideoMenu(peerJid, container);
277
+
278
+        remotes.appendChild(container);
279
+        return container;
280
+    };
281
+
282
+    /**
283
+     * Shows the display name for the given video.
284
+     */
285
+    my.setDisplayName = function(videoSpanId, displayName) {
286
+        var nameSpan = $('#' + videoSpanId + '>span.displayname');
287
+
288
+        // If we already have a display name for this video.
289
+        if (nameSpan.length > 0) {
290
+            var nameSpanElement = nameSpan.get(0);
291
+
292
+            if (nameSpanElement.id === 'localDisplayName' &&
293
+                $('#localDisplayName').text() !== displayName) {
294
+                $('#localDisplayName').text(displayName);
295
+            } else {
296
+                $('#' + videoSpanId + '_name').text(displayName);
297
+            }
298
+        } else {
299
+            var editButton = null;
300
+
301
+            if (videoSpanId === 'localVideoContainer') {
302
+                editButton = createEditDisplayNameButton();
303
+            }
304
+            if (displayName.length) {
305
+                nameSpan = document.createElement('span');
306
+                nameSpan.className = 'displayname';
307
+                nameSpan.innerText = displayName;
308
+                $('#' + videoSpanId)[0].appendChild(nameSpan);
309
+            }
310
+
311
+            if (!editButton) {
312
+                nameSpan.id = videoSpanId + '_name';
313
+            } else {
314
+                nameSpan.id = 'localDisplayName';
315
+                $('#' + videoSpanId)[0].appendChild(editButton);
316
+
317
+                var editableText = document.createElement('input');
318
+                editableText.className = 'displayname';
319
+                editableText.id = 'editDisplayName';
320
+
321
+                if (displayName.length) {
322
+                    editableText.value
323
+                        = displayName.substring(0, displayName.indexOf(' (me)'));
324
+                }
325
+
326
+                editableText.setAttribute('style', 'display:none;');
327
+                editableText.setAttribute('placeholder', 'ex. Jane Pink');
328
+                $('#' + videoSpanId)[0].appendChild(editableText);
329
+
330
+                $('#localVideoContainer .displayname').bind("click", function (e) {
331
+                    e.preventDefault();
332
+                    $('#localDisplayName').hide();
333
+                    $('#editDisplayName').show();
334
+                    $('#editDisplayName').focus();
335
+                    $('#editDisplayName').select();
336
+
337
+                    var inputDisplayNameHandler = function (name) {
338
+                        if (nickname !== name) {
339
+                            nickname = name;
340
+                            window.localStorage.displayname = nickname;
341
+                            connection.emuc.addDisplayNameToPresence(nickname);
342
+                            connection.emuc.sendPresence();
343
+
344
+                            Chat.setChatConversationMode(true);
345
+                        }
346
+
347
+                        if (!$('#localDisplayName').is(":visible")) {
348
+                            if (nickname) {
349
+                                $('#localDisplayName').text(nickname + " (me)");
350
+                                $('#localDisplayName').show();
351
+                            }
352
+                            else {
353
+                                $('#localDisplayName').text(nickname);
354
+                            }
355
+
356
+                            $('#editDisplayName').hide();
357
+                        }
358
+                    };
359
+
360
+                    $('#editDisplayName').one("focusout", function (e) {
361
+                        inputDisplayNameHandler(this.value);
362
+                    });
363
+
364
+                    $('#editDisplayName').on('keydown', function (e) {
365
+                        if (e.keyCode === 13) {
366
+                            e.preventDefault();
367
+                            inputDisplayNameHandler(this.value);
368
+                        }
369
+                    });
370
+                });
371
+            }
372
+        }
373
+    };
374
+
375
+    /**
376
+     * Shows/hides the display name on the remote video.
377
+     * @param videoSpanId the identifier of the video span element
378
+     * @param isShow indicates if the display name should be shown or hidden
379
+     */
380
+    my.showDisplayName = function(videoSpanId, isShow) {
381
+        var nameSpan = $('#' + videoSpanId + '>span.displayname').get(0);
382
+
383
+        if (isShow) {
384
+            if (nameSpan && nameSpan.innerHTML && nameSpan.innerHTML.length) 
385
+                nameSpan.setAttribute("style", "display:inline-block;");
386
+        }
387
+        else {
388
+            if (nameSpan)
389
+                nameSpan.setAttribute("style", "display:none;");
390
+        }
391
+    };
392
+
393
+    /**
394
+     * Shows a visual indicator for the focus of the conference.
395
+     * Currently if we're not the owner of the conference we obtain the focus
396
+     * from the connection.jingle.sessions.
397
+     */
398
+    my.showFocusIndicator = function() {
399
+        if (focus !== null) {
400
+            var indicatorSpan = $('#localVideoContainer .focusindicator');
401
+
402
+            if (indicatorSpan.children().length === 0)
403
+            {
404
+                createFocusIndicatorElement(indicatorSpan[0]);
405
+            }
406
+        }
407
+        else if (Object.keys(connection.jingle.sessions).length > 0) {
408
+            // If we're only a participant the focus will be the only session we have.
409
+            var session
410
+                = connection.jingle.sessions
411
+                    [Object.keys(connection.jingle.sessions)[0]];
412
+            var focusId
413
+                = 'participant_' + Strophe.getResourceFromJid(session.peerjid);
414
+
415
+            var focusContainer = document.getElementById(focusId);
416
+            if (!focusContainer) {
417
+                console.error("No focus container!");
418
+                return;
419
+            }
420
+            var indicatorSpan = $('#' + focusId + ' .focusindicator');
421
+
422
+            if (!indicatorSpan || indicatorSpan.length === 0) {
423
+                indicatorSpan = document.createElement('span');
424
+                indicatorSpan.className = 'focusindicator';
425
+                Util.setTooltip(indicatorSpan,
426
+                                "The owner of<br/>this conference",
427
+                                "top");
428
+                focusContainer.appendChild(indicatorSpan);
429
+
430
+                createFocusIndicatorElement(indicatorSpan);
431
+            }
432
+        }
433
+    };
434
+
435
+    /**
436
+     * Shows video muted indicator over small videos.
437
+     */
438
+    my.showVideoIndicator = function(videoSpanId, isMuted) {
439
+        var videoMutedSpan = $('#' + videoSpanId + '>span.videoMuted');
440
+
441
+        if (isMuted === 'false') {
442
+            if (videoMutedSpan.length > 0) {
443
+                videoMutedSpan.remove();
444
+            }
445
+        }
446
+        else {
447
+            var audioMutedSpan = $('#' + videoSpanId + '>span.audioMuted');
448
+
449
+            videoMutedSpan = document.createElement('span');
450
+            videoMutedSpan.className = 'videoMuted';
451
+            if (audioMutedSpan) {
452
+                videoMutedSpan.right = '30px';
453
+            }
454
+            $('#' + videoSpanId)[0].appendChild(videoMutedSpan);
455
+
456
+            var mutedIndicator = document.createElement('i');
457
+            mutedIndicator.className = 'icon-camera-disabled';
458
+            Util.setTooltip(mutedIndicator,
459
+                    "Participant has<br/>stopped the camera.",
460
+                    "top");
461
+            videoMutedSpan.appendChild(mutedIndicator);
462
+        }
463
+    };
464
+
465
+    /**
466
+     * Shows audio muted indicator over small videos.
467
+     */
468
+    my.showAudioIndicator = function(videoSpanId, isMuted) {
469
+        var audioMutedSpan = $('#' + videoSpanId + '>span.audioMuted');
470
+
471
+        if (isMuted === 'false') {
472
+            if (audioMutedSpan.length > 0) {
473
+                audioMutedSpan.remove();
474
+            }
475
+        }
476
+        else {
477
+            var videoMutedSpan = $('#' + videoSpanId + '>span.videoMuted');
478
+
479
+            audioMutedSpan = document.createElement('span');
480
+            audioMutedSpan.className = 'audioMuted';
481
+            Util.setTooltip(audioMutedSpan,
482
+                    "Participant is muted",
483
+                    "top");
484
+
485
+            if (videoMutedSpan) {
486
+                audioMutedSpan.right = '30px';
487
+            }
488
+            $('#' + videoSpanId)[0].appendChild(audioMutedSpan);
489
+
490
+            var mutedIndicator = document.createElement('i');
491
+            mutedIndicator.className = 'icon-mic-disabled';
492
+            audioMutedSpan.appendChild(mutedIndicator);
493
+        }
494
+    };
495
+
496
+    /**
497
+     * Resizes the large video container.
498
+     */
499
+    my.resizeLargeVideoContainer = function () {
500
+        Chat.resizeChat();
501
+        var availableHeight = window.innerHeight;
502
+        var availableWidth = Util.getAvailableVideoWidth();
503
+
504
+        if (availableWidth < 0 || availableHeight < 0) return;
505
+
506
+        $('#videospace').width(availableWidth);
507
+        $('#videospace').height(availableHeight);
508
+        $('#largeVideoContainer').width(availableWidth);
509
+        $('#largeVideoContainer').height(availableHeight);
510
+
511
+        VideoLayout.resizeThumbnails();
512
+    };
513
+
514
+    /**
515
+     * Resizes thumbnails.
516
+     */
517
+    my.resizeThumbnails = function() {
518
+        var thumbnailSize = calculateThumbnailSize();
519
+        var width = thumbnailSize[0];
520
+        var height = thumbnailSize[1];
521
+
522
+        // size videos so that while keeping AR and max height, we have a
523
+        // nice fit
524
+        $('#remoteVideos').height(height);
525
+        $('#remoteVideos>span').width(width);
526
+        $('#remoteVideos>span').height(height);
527
+    };
528
+
529
+    /**
530
+     * Enables the active speaker UI.
531
+     *
532
+     * @param resourceJid the jid indicating the video element to
533
+     * activate/deactivate
534
+     * @param isEnable indicates if the active speaker should be enabled or
535
+     * disabled
536
+     */
537
+    my.enableActiveSpeaker = function(resourceJid, isEnable) {
538
+        var displayName = resourceJid;
539
+        var nameSpan = $('#participant_' + resourceJid + '>span.displayname');
540
+        if (nameSpan.length > 0)
541
+            displayName = nameSpan.text();
542
+
543
+        console.log("Enable active speaker", displayName, isEnable);
544
+
545
+        var videoSpanId = null;
546
+        if (resourceJid
547
+                === Strophe.getResourceFromJid(connection.emuc.myroomjid))
548
+            videoSpanId = 'localVideoWrapper';
549
+        else
550
+            videoSpanId = 'participant_' + resourceJid;
551
+
552
+        videoSpan = document.getElementById(videoSpanId);
553
+
554
+        if (!videoSpan) {
555
+            console.error("No video element for jid", resourceJid);
556
+            return;
557
+        }
558
+
559
+        var video = $('#' + videoSpanId + '>video');
560
+
561
+        if (video && video.length > 0) {
562
+            var videoElement = video.get(0);
563
+            if (isEnable) {
564
+                if (!videoElement.classList.contains("activespeaker"))
565
+                    videoElement.classList.add("activespeaker");
566
+
567
+                VideoLayout.showDisplayName(videoSpanId, true);
568
+            }
569
+            else {
570
+                VideoLayout.showDisplayName(videoSpanId, false);
571
+
572
+                if (videoElement.classList.contains("activespeaker"))
573
+                    videoElement.classList.remove("activespeaker");
574
+            }
575
+        }
576
+    };
577
+
578
+    /**
579
+     * Gets the selector of video thumbnail container for the user identified by
580
+     * given <tt>userJid</tt>
581
+     * @param userJid user's Jid for whom we want to get the video container.
582
+     */
583
+    function getParticipantContainer(userJid)
584
+    {
585
+        if (!userJid)
586
+            return null;
587
+
588
+        if (userJid === connection.emuc.myroomjid)
589
+            return $("#localVideoContainer");
590
+        else
591
+            return $("#participant_" + Strophe.getResourceFromJid(userJid));
592
+    }
593
+
594
+    /**
595
+     * Sets the size and position of the given video element.
596
+     *
597
+     * @param video the video element to position
598
+     * @param width the desired video width
599
+     * @param height the desired video height
600
+     * @param horizontalIndent the left and right indent
601
+     * @param verticalIndent the top and bottom indent
602
+     */
603
+    function positionVideo(video,
604
+                           width,
605
+                           height,
606
+                           horizontalIndent,
607
+                           verticalIndent) {
608
+        video.width(width);
609
+        video.height(height);
610
+        video.css({  top: verticalIndent + 'px',
611
+                     bottom: verticalIndent + 'px',
612
+                     left: horizontalIndent + 'px',
613
+                     right: horizontalIndent + 'px'});
614
+    }
615
+
616
+    /**
617
+     * Calculates the thumbnail size.
618
+     */
619
+    var calculateThumbnailSize = function () {
620
+        // Calculate the available height, which is the inner window height minus
621
+       // 39px for the header minus 2px for the delimiter lines on the top and
622
+       // bottom of the large video, minus the 36px space inside the remoteVideos
623
+       // container used for highlighting shadow.
624
+       var availableHeight = 100;
625
+
626
+       var numvids = $('#remoteVideos>span:visible').length;
627
+
628
+       // Remove the 1px borders arround videos and the chat width.
629
+       var availableWinWidth = $('#remoteVideos').width() - 2 * numvids - 50;
630
+       var availableWidth = availableWinWidth / numvids;
631
+       var aspectRatio = 16.0 / 9.0;
632
+       var maxHeight = Math.min(160, availableHeight);
633
+       availableHeight = Math.min(maxHeight, availableWidth / aspectRatio);
634
+       if (availableHeight < availableWidth / aspectRatio) {
635
+           availableWidth = Math.floor(availableHeight * aspectRatio);
636
+       }
637
+
638
+       return [availableWidth, availableHeight];
639
+   };
640
+
641
+   /**
642
+    * Returns an array of the video dimensions, so that it keeps it's aspect
643
+    * ratio and fits available area with it's larger dimension. This method
644
+    * ensures that whole video will be visible and can leave empty areas.
645
+    *
646
+    * @return an array with 2 elements, the video width and the video height
647
+    */
648
+   function getDesktopVideoSize(videoWidth,
649
+                                videoHeight,
650
+                                videoSpaceWidth,
651
+                                videoSpaceHeight) {
652
+       if (!videoWidth)
653
+           videoWidth = currentVideoWidth;
654
+       if (!videoHeight)
655
+           videoHeight = currentVideoHeight;
656
+
657
+       var aspectRatio = videoWidth / videoHeight;
658
+
659
+       var availableWidth = Math.max(videoWidth, videoSpaceWidth);
660
+       var availableHeight = Math.max(videoHeight, videoSpaceHeight);
661
+
662
+       videoSpaceHeight -= $('#remoteVideos').outerHeight();
663
+
664
+       if (availableWidth / aspectRatio >= videoSpaceHeight)
665
+       {
666
+           availableHeight = videoSpaceHeight;
667
+           availableWidth = availableHeight * aspectRatio;
668
+       }
669
+
670
+       if (availableHeight * aspectRatio >= videoSpaceWidth)
671
+       {
672
+           availableWidth = videoSpaceWidth;
673
+           availableHeight = availableWidth / aspectRatio;
674
+       }
675
+
676
+       return [availableWidth, availableHeight];
677
+   }
678
+
679
+   /**
680
+    * Creates the edit display name button.
681
+    * 
682
+    * @returns the edit button
683
+    */
684
+    function createEditDisplayNameButton() {
685
+        var editButton = document.createElement('a');
686
+        editButton.className = 'displayname';
687
+        Util.setTooltip(editButton,
688
+                        'Click to edit your<br/>display name',
689
+                        "top");
690
+        editButton.innerHTML = '<i class="fa fa-pencil"></i>';
691
+
692
+        return editButton;
693
+    }
694
+
695
+    /**
696
+     * Creates the element indicating the focus of the conference.
697
+     *
698
+     * @param parentElement the parent element where the focus indicator will
699
+     * be added
700
+     */
701
+    function createFocusIndicatorElement(parentElement) {
702
+        var focusIndicator = document.createElement('i');
703
+        focusIndicator.className = 'fa fa-star';
704
+        parentElement.appendChild(focusIndicator);
705
+    }
706
+
707
+    /**
708
+     * Updates the remote video menu.
709
+     *
710
+     * @param jid the jid indicating the video for which we're adding a menu.
711
+     * @param isMuted indicates the current mute state
712
+     */
713
+    my.updateRemoteVideoMenu = function(jid, isMuted) {
714
+        var muteMenuItem
715
+            = $('#remote_popupmenu_'
716
+                    + Strophe.getResourceFromJid(jid)
717
+                    + '>li>a.mutelink');
718
+
719
+        var mutedIndicator = "<i class='icon-mic-disabled'></i>";
720
+
721
+        if (muteMenuItem.length) {
722
+            var muteLink = muteMenuItem.get(0);
723
+
724
+            if (isMuted === 'true') {
725
+                muteLink.innerHTML = mutedIndicator + ' Muted';
726
+                muteLink.className = 'mutelink disabled';
727
+            }
728
+            else {
729
+                muteLink.innerHTML = mutedIndicator + ' Mute';
730
+                muteLink.className = 'mutelink';
731
+            }
732
+        }
733
+    };
734
+
735
+    /**
736
+     * Returns the current active speaker.
737
+     */
738
+    my.getActiveSpeakerContainerId = function () {
739
+        return 'participant_' + currentActiveSpeaker;
740
+    };
741
+
742
+    /**
743
+     * Adds the remote video menu element for the given <tt>jid</tt> in the
744
+     * given <tt>parentElement</tt>.
745
+     *
746
+     * @param jid the jid indicating the video for which we're adding a menu.
747
+     * @param parentElement the parent element where this menu will be added
748
+     */
749
+    function addRemoteVideoMenu(jid, parentElement) {
750
+        var spanElement = document.createElement('span');
751
+        spanElement.className = 'remotevideomenu';
752
+
753
+        parentElement.appendChild(spanElement);
754
+
755
+        var menuElement = document.createElement('i');
756
+        menuElement.className = 'fa fa-angle-down';
757
+        menuElement.title = 'Remote user controls';
758
+        spanElement.appendChild(menuElement);
759
+
760
+//        <ul class="popupmenu">
761
+//        <li><a href="#">Mute</a></li>
762
+//        <li><a href="#">Eject</a></li>
763
+//        </ul>
764
+        var popupmenuElement = document.createElement('ul');
765
+        popupmenuElement.className = 'popupmenu';
766
+        popupmenuElement.id
767
+            = 'remote_popupmenu_' + Strophe.getResourceFromJid(jid);
768
+        spanElement.appendChild(popupmenuElement);
769
+
770
+        var muteMenuItem = document.createElement('li');
771
+        var muteLinkItem = document.createElement('a');
772
+
773
+        var mutedIndicator = "<i class='icon-mic-disabled'></i>";
774
+
775
+        if (!mutedAudios[jid]) {
776
+            muteLinkItem.innerHTML = mutedIndicator + 'Mute';
777
+            muteLinkItem.className = 'mutelink';
778
+        }
779
+        else {
780
+            muteLinkItem.innerHTML = mutedIndicator + ' Muted';
781
+            muteLinkItem.className = 'mutelink disabled';
782
+        }
783
+
784
+        muteLinkItem.onclick = function(){
785
+            if ($(this).attr('disabled') != undefined) {
786
+                event.preventDefault();
787
+            }
788
+            var isMute = !mutedAudios[jid];
789
+            connection.moderate.setMute(jid, isMute);
790
+            popupmenuElement.setAttribute('style', 'display:none;');
791
+
792
+            if (isMute) {
793
+                this.innerHTML = mutedIndicator + ' Muted';
794
+                this.className = 'mutelink disabled';
795
+            }
796
+            else {
797
+                this.innerHTML = mutedIndicator + ' Mute';
798
+                this.className = 'mutelink';
799
+            }
800
+        };
801
+
802
+        muteMenuItem.appendChild(muteLinkItem);
803
+        popupmenuElement.appendChild(muteMenuItem);
804
+
805
+        var ejectIndicator = "<i class='fa fa-eject'></i>";
806
+
807
+        var ejectMenuItem = document.createElement('li');
808
+        var ejectLinkItem = document.createElement('a');
809
+        ejectLinkItem.innerHTML = ejectIndicator + ' Kick out';
810
+        ejectLinkItem.onclick = function(){
811
+            connection.moderate.eject(jid);
812
+            popupmenuElement.setAttribute('style', 'display:none;');
813
+        };
814
+
815
+        ejectMenuItem.appendChild(ejectLinkItem);
816
+        popupmenuElement.appendChild(ejectMenuItem);
817
+    }
818
+
819
+    /**
820
+     * On audio muted event.
821
+     */
822
+    $(document).bind('audiomuted.muc', function (event, jid, isMuted) {
823
+        var videoSpanId = null;
824
+        if (jid === connection.emuc.myroomjid) {
825
+            videoSpanId = 'localVideoContainer';
826
+        } else {
827
+            VideoLayout.ensurePeerContainerExists(jid);
828
+            videoSpanId = 'participant_' + Strophe.getResourceFromJid(jid);
829
+        }
830
+
831
+        if (focus) {
832
+            mutedAudios[jid] = isMuted;
833
+            VideoLayout.updateRemoteVideoMenu(jid, isMuted);
834
+        }
835
+
836
+        if (videoSpanId)
837
+            VideoLayout.showAudioIndicator(videoSpanId, isMuted);
838
+    });
839
+
840
+    /**
841
+     * On video muted event.
842
+     */
843
+    $(document).bind('videomuted.muc', function (event, jid, isMuted) {
844
+        var videoSpanId = null;
845
+        if (jid === connection.emuc.myroomjid) {
846
+            videoSpanId = 'localVideoContainer';
847
+        } else {
848
+            VideoLayout.ensurePeerContainerExists(jid);
849
+            videoSpanId = 'participant_' + Strophe.getResourceFromJid(jid);
850
+        }
851
+
852
+        if (videoSpanId)
853
+            VideoLayout.showVideoIndicator(videoSpanId, isMuted);
854
+    });
855
+
856
+    /**
857
+     * On active speaker changed event.
858
+     */
859
+    $(document).bind('activespeakerchanged', function (event, resourceJid) {
860
+        // We ignore local user events.
861
+        if (resourceJid
862
+                === Strophe.getResourceFromJid(connection.emuc.myroomjid))
863
+            return;
864
+
865
+        // Disable style for previous active speaker.
866
+        if (currentActiveSpeaker
867
+                && currentActiveSpeaker !== resourceJid
868
+                && !focusedVideoSrc) {
869
+            var oldContainer  = document.getElementById(
870
+                    'participant_' + currentActiveSpeaker);
871
+
872
+            if (oldContainer) {
873
+                VideoLayout.enableActiveSpeaker(currentActiveSpeaker, false);
874
+            }
875
+        }
876
+
877
+        // Obtain container for new active speaker.
878
+        var container  = document.getElementById(
879
+                'participant_' + resourceJid);
880
+
881
+        // Update the current active speaker.
882
+        if (resourceJid !== currentActiveSpeaker)
883
+            currentActiveSpeaker = resourceJid;
884
+        else
885
+            return;
886
+
887
+        // Local video will not have container found, but that's ok
888
+        // since we don't want to switch to local video.
889
+        if (container && !focusedVideoSrc)
890
+        {
891
+            var video = container.getElementsByTagName("video");
892
+            if (video.length)
893
+            {
894
+                VideoLayout.updateLargeVideo(video[0].src);
895
+                VideoLayout.enableActiveSpeaker(resourceJid, true);
896
+            }
897
+        }
898
+    });
899
+
900
+    return my;
901
+}(VideoLayout || {}));

Chargement…
Annuler
Enregistrer