浏览代码

Merge remote-tracking branch 'upstream/master'

master
turint 11 年前
父节点
当前提交
77cb10d6a1

+ 3
- 0
README.md 查看文件

14
 
14
 
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/) 
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
 ## Acknowledgements
20
 ## Acknowledgements
18
 
21
 
19
 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! 
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
文件差异内容过多而无法显示
查看文件


+ 51
- 3
chat.js 查看文件

39
         $('#usermsg').keydown(function (event) {
39
         $('#usermsg').keydown(function (event) {
40
             if (event.keyCode === 13) {
40
             if (event.keyCode === 13) {
41
                 event.preventDefault();
41
                 event.preventDefault();
42
-                var message = Util.escapeHtml(this.value);
42
+                var value = this.value;
43
                 $('#usermsg').val('').trigger('autosize.resize');
43
                 $('#usermsg').val('').trigger('autosize.resize');
44
                 this.focus();
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
                 { scrollTop: $('#chatconversation')[0].scrollHeight}, 1000);
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
      * Opens / closes the chat area.
142
      * Opens / closes the chat area.
95
      */
143
      */
242
         if (unreadMessages) {
290
         if (unreadMessages) {
243
             unreadMsgElement.innerHTML = unreadMessages.toString();
291
             unreadMsgElement.innerHTML = unreadMessages.toString();
244
 
292
 
245
-            showToolbar();
293
+            Toolbar.showToolbar();
246
 
294
 
247
             var chatButtonElement
295
             var chatButtonElement
248
                 = document.getElementById('chatButton').parentNode;
296
                 = document.getElementById('chatButton').parentNode;

+ 98
- 0
commands.js 查看文件

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 查看文件

11
     bosh: '//lambada.jitsi.net/http-bind', // FIXME: use xep-0156 for that
11
     bosh: '//lambada.jitsi.net/http-bind', // FIXME: use xep-0156 for that
12
     desktopSharing: 'ext', // Desktop sharing method. Can be set to 'ext', 'webrtc' or false to disable.
12
     desktopSharing: 'ext', // Desktop sharing method. Can be set to 'ext', 'webrtc' or false to disable.
13
     chromeExtensionId: 'diibjkoicjeejcmhdnailmkgecihlobk', // Id of desktop streamer Chrome extension
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 查看文件

43
     color: #087dba;
43
     color: #087dba;
44
 }
44
 }
45
 
45
 
46
+.errorMessage {
47
+    color: red;
48
+}
49
+
46
 .remoteuser {
50
 .remoteuser {
47
     color: #424242;
51
     color: #424242;
48
 }
52
 }

+ 124
- 0
css/popover.css 查看文件

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 查看文件

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

+ 39
- 17
css/videolayout_default.css 查看文件

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

+ 80
- 0
data_channels.js 查看文件

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 查看文件

1
-/* global $, config, connection, chrome, alert, getUserMediaWithConstraints, change_local_video, getConferenceHandler */
1
+/* global $, config, connection, chrome, alert, getUserMediaWithConstraints, changeLocalVideo, getConferenceHandler */
2
 /**
2
 /**
3
  * Indicates that desktop stream is currently in use(for toggle purpose).
3
  * Indicates that desktop stream is currently in use(for toggle purpose).
4
  * @type {boolean}
4
  * @type {boolean}
251
 
251
 
252
     var oldStream = connection.jingle.localVideo;
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
     var conferenceHandler = getConferenceHandler();
258
     var conferenceHandler = getConferenceHandler();
257
     if (conferenceHandler) {
259
     if (conferenceHandler) {

+ 4
- 4
etherpad.js 查看文件

45
                 if (Prezi.isPresentationVisible()) {
45
                 if (Prezi.isPresentationVisible()) {
46
                     largeVideo.css({opacity: '0'});
46
                     largeVideo.css({opacity: '0'});
47
                 } else {
47
                 } else {
48
-                    setLargeVideoVisible(false);
49
-                    dockToolbar(true);
48
+                    VideoLayout.setLargeVideoVisible(false);
49
+                    Toolbar.dockToolbar(true);
50
                 }
50
                 }
51
 
51
 
52
                 $('#etherpad>iframe').fadeIn(300, function () {
52
                 $('#etherpad>iframe').fadeIn(300, function () {
63
                 document.body.style.background = 'black';
63
                 document.body.style.background = 'black';
64
                 if (!isPresentation) {
64
                 if (!isPresentation) {
65
                     $('#largeVideo').fadeIn(300, function () {
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 查看文件

20
     <script src="libs/colibri/colibri.focus.js?v=8"></script><!-- colibri focus implementation -->
20
     <script src="libs/colibri/colibri.focus.js?v=8"></script><!-- colibri focus implementation -->
21
     <script src="libs/colibri/colibri.session.js?v=1"></script>
21
     <script src="libs/colibri/colibri.session.js?v=1"></script>
22
     <script src="//code.jquery.com/ui/1.10.4/jquery-ui.js"></script>
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
     <script src="estos_log.js?v=2"></script><!-- simple stanza logger -->
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
     <script src="analytics.js?v=1"></script><!-- google analytics plugin -->
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
     <link href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" rel="stylesheet">
44
     <link href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" rel="stylesheet">
37
     <link rel="stylesheet" href="css/font.css"/>
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
     <link rel="stylesheet" href="css/jquery-impromptu.css?v=4">
48
     <link rel="stylesheet" href="css/jquery-impromptu.css?v=4">
41
     <link rel="stylesheet" href="css/modaldialog.css?v=3">
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
         Link used for inline installation of chrome desktop streaming extension,
53
         Link used for inline installation of chrome desktop streaming extension,
45
         is updated automatically from the code with the value defined in config.js -->
54
         is updated automatically from the code with the value defined in config.js -->
49
     <script src="libs/prezi_player.js?v=2"></script>
58
     <script src="libs/prezi_player.js?v=2"></script>
50
   </head>
59
   </head>
51
   <body>
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
                 <div class="header_button_separator"></div>
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
                 <div class="header_button_separator"></div>
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
             </span>
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
     </div>
95
     </div>
82
     <div id="settings">
96
     <div id="settings">
83
       <h1>Connection Settings</h1>
97
       <h1>Connection Settings</h1>
89
       </form>
103
       </form>
90
     </div>
104
     </div>
91
     <div id="reloadPresentation"><a onclick='Prezi.reloadPresentation();'><i title="Reload Prezi" class="fa fa-repeat fa-lg"></i></a></div>
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
         <div id="largeVideoContainer" class="videocontainer">
107
         <div id="largeVideoContainer" class="videocontainer">
94
             <div id="presentation"></div>
108
             <div id="presentation"></div>
95
             <div id="etherpad"></div>
109
             <div id="etherpad"></div>
104
                     <!--<video id="localVideo" autoplay oncontextmenu="return false;" muted></video> - is now per stream generated -->
118
                     <!--<video id="localVideo" autoplay oncontextmenu="return false;" muted></video> - is now per stream generated -->
105
                 </span>
119
                 </span>
106
                 <audio id="localAudio" autoplay oncontextmenu="return false;" muted></audio>
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
             </span>
122
             </span>
109
             <audio id="userJoined" src="sounds/joined.wav" preload="auto"></audio>
123
             <audio id="userJoined" src="sounds/joined.wav" preload="auto"></audio>
110
             <audio id="userLeft" src="sounds/left.wav" preload="auto"></audio>
124
             <audio id="userLeft" src="sounds/left.wav" preload="auto"></audio>
123
         <audio id="chatNotification" src="sounds/incomingMessage.wav" preload="auto"></audio>
137
         <audio id="chatNotification" src="sounds/incomingMessage.wav" preload="auto"></audio>
124
         <textarea id="usermsg" placeholder='Enter text...' autofocus></textarea>
138
         <textarea id="usermsg" placeholder='Enter text...' autofocus></textarea>
125
     </div>
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
   </body>
141
   </body>
128
 </html>
142
 </html>

+ 311
- 81
libs/colibri/colibri.focus.js 查看文件

44
     this.peers = [];
44
     this.peers = [];
45
     this.confid = null;
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
     // media types of the conference
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
     this.connection.jingle.sessions[this.sid] = this;
69
     this.connection.jingle.sessions[this.sid] = this;
51
     this.mychannel = [];
70
     this.mychannel = [];
151
     elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri'});
170
     elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri'});
152
 
171
 
153
     this.media.forEach(function (name) {
172
     this.media.forEach(function (name) {
173
+        var isData = name === 'data';
174
+        var channel = isData ? 'sctpconnection' : 'channel';
175
+
154
         elem.c('content', {name: name});
176
         elem.c('content', {name: name});
155
-        elem.c('channel', {
177
+
178
+        elem.c(channel, {
156
             initiator: 'true',
179
             initiator: 'true',
157
             expire: '15',
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
         for (var j = 0; j < self.peers.length; j++) {
187
         for (var j = 0; j < self.peers.length; j++) {
160
-            elem.c('channel', {
188
+            elem.c(channel, {
161
                 initiator: 'true',
189
                 initiator: 'true',
162
                 expire: '15',
190
                 expire: '15',
163
                 endpoint: self.peers[j].substr(1 + self.peers[j].lastIndexOf('/'))
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
         elem.up(); // end of content
197
         elem.up(); // end of content
167
     });
198
     });
209
     this.confid = $(result).find('>conference').attr('id');
240
     this.confid = $(result).find('>conference').attr('id');
210
     var remotecontents = $(result).find('>conference>content').get();
241
     var remotecontents = $(result).find('>conference>content').get();
211
     var numparticipants = 0;
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
         this.mychannel.push($(tmp.shift()));
250
         this.mychannel.push($(tmp.shift()));
215
         numparticipants = tmp.length;
251
         numparticipants = tmp.length;
216
         for (j = 0; j < tmp.length; j++) {
252
         for (j = 0; j < tmp.length; j++) {
223
 
259
 
224
     console.log('remote channels', this.channels);
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
     bridgeSDP.media.length = this.mychannel.length;
311
     bridgeSDP.media.length = this.mychannel.length;
228
     var channel;
312
     var channel;
229
     /*
313
     /*
262
         // get the mixed ssrc
346
         // get the mixed ssrc
263
         tmp = $(this.mychannel[channel]).find('>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
347
         tmp = $(this.mychannel[channel]).find('>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
264
         // FIXME: check rtp-level-relay-type
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
             bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'cname:mixed' + '\r\n';
353
             bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'cname:mixed' + '\r\n';
267
             bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'label:mixedlabela0' + '\r\n';
354
             bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'label:mixedlabela0' + '\r\n';
268
             bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'msid:mixedmslabel mixedlabela0' + '\r\n';
355
             bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'msid:mixedmslabel mixedlabela0' + '\r\n';
269
             bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'mslabel:mixedmslabel' + '\r\n';
356
             bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'mslabel:mixedmslabel' + '\r\n';
270
-        } else {
357
+        }
358
+        else if (!isData)
359
+        {
271
             // make chrome happy... '3735928559' == 0xDEADBEEF
360
             // make chrome happy... '3735928559' == 0xDEADBEEF
272
             // FIXME: this currently appears as two streams, should be one
361
             // FIXME: this currently appears as two streams, should be one
273
             bridgeSDP.media[channel] += 'a=ssrc:' + '3735928559' + ' ' + 'cname:mixed' + '\r\n';
362
             bridgeSDP.media[channel] += 'a=ssrc:' + '3735928559' + ' ' + 'cname:mixed' + '\r\n';
308
                             elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: self.confid});
397
                             elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: self.confid});
309
                             var localSDP = new SDP(self.peerconnection.localDescription.sdp);
398
                             var localSDP = new SDP(self.peerconnection.localDescription.sdp);
310
                             localSDP.media.forEach(function (media, channel) {
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
                                 elem.c('content', {name: name});
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
                                 var mline = SDPUtil.parse_mline(media.split('\r\n')[0]);
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
                                 localSDP.TransportToJingle(channel, elem);
437
                                 localSDP.TransportToJingle(channel, elem);
336
                                     // ...
445
                                     // ...
337
                                 },
446
                                 },
338
                                 function (error) {
447
                                 function (error) {
339
-                                    console.warn(error);
448
+                                    console.error(
449
+                                        "ERROR setLocalDescription succeded",
450
+                                        error, elem);
340
                                 }
451
                                 }
341
                             );
452
                             );
342
 
453
 
344
                             for (var i = 0; i < numparticipants; i++) {
455
                             for (var i = 0; i < numparticipants; i++) {
345
                                 self.initiate(self.peers[i], true);
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
                         function (error) {
463
                         function (error) {
349
                             console.warn('setLocalDescription failed.', error);
464
                             console.warn('setLocalDescription failed.', error);
417
             sdp.media[j] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'label:mixedlabela0' + '\r\n';
532
             sdp.media[j] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'label:mixedlabela0' + '\r\n';
418
             sdp.media[j] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'msid:mixedmslabel mixedlabela0' + '\r\n';
533
             sdp.media[j] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'msid:mixedmslabel mixedlabela0' + '\r\n';
419
             sdp.media[j] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'mslabel:mixedmslabel' + '\r\n';
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
             // make chrome happy... '3735928559' == 0xDEADBEEF
539
             // make chrome happy... '3735928559' == 0xDEADBEEF
422
             sdp.media[j] += 'a=ssrc:' + '3735928559' + ' ' + 'cname:mixed' + '\r\n';
540
             sdp.media[j] += 'a=ssrc:' + '3735928559' + ' ' + 'cname:mixed' + '\r\n';
423
             sdp.media[j] += 'a=ssrc:' + '3735928559' + ' ' + 'label:mixedlabelv0' + '\r\n';
541
             sdp.media[j] += 'a=ssrc:' + '3735928559' + ' ' + 'label:mixedlabelv0' + '\r\n';
486
 // pull in a new participant into the conference
604
 // pull in a new participant into the conference
487
 ColibriFocus.prototype.addNewParticipant = function (peer) {
605
 ColibriFocus.prototype.addNewParticipant = function (peer) {
488
     var self = this;
606
     var self = this;
489
-    if (this.confid === 0) {
607
+    if (this.confid === 0 || !this.peerconnection.localDescription)
608
+    {
490
         // bad state
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
         window.setTimeout(function () {
618
         window.setTimeout(function () {
493
             self.addNewParticipant(peer);
619
             self.addNewParticipant(peer);
494
         }, 250);
620
         }, 250);
502
     elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
628
     elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
503
     var localSDP = new SDP(this.peerconnection.localDescription.sdp);
629
     var localSDP = new SDP(this.peerconnection.localDescription.sdp);
504
     localSDP.media.forEach(function (media, channel) {
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
         elem.c('content', {name: name});
632
         elem.c('content', {name: name});
507
-        elem.c('channel', {
633
+        if (name !== 'data')
634
+        {
635
+            elem.c('channel', {
508
                 initiator: 'true',
636
                 initiator: 'true',
509
-                expire:'15',
637
+                expire: self.channelExpire,
510
                 endpoint: peer.substr(1 + peer.lastIndexOf('/'))
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
         elem.up(); // end of content
651
         elem.up(); // end of content
514
     });
652
     });
515
 
653
 
517
         function (result) {
655
         function (result) {
518
             var contents = $(result).find('>conference>content').get();
656
             var contents = $(result).find('>conference>content').get();
519
             for (var i = 0; i < contents.length; i++) {
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
                 self.channels[index][i] = tmp[0];
667
                 self.channels[index][i] = tmp[0];
522
             }
668
             }
523
             self.initiate(peer, true);
669
             self.initiate(peer, true);
531
 // update the channel description (payload-types + dtls fp) for a participant
677
 // update the channel description (payload-types + dtls fp) for a participant
532
 ColibriFocus.prototype.updateChannel = function (remoteSDP, participant) {
678
 ColibriFocus.prototype.updateChannel = function (remoteSDP, participant) {
533
     console.log('change allocation for', this.confid);
679
     console.log('change allocation for', this.confid);
680
+    var self = this;
534
     var change = $iq({to: this.bridgejid, type: 'set'});
681
     var change = $iq({to: this.bridgejid, type: 'set'});
535
     change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
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
         // now add transport
722
         // now add transport
562
         remoteSDP.TransportToJingle(channel, change);
723
         remoteSDP.TransportToJingle(channel, change);
563
 
724
 
564
-        change.up(); // end of channel
725
+        change.up(); // end of channel/sctpconnection
565
         change.up(); // end of content
726
         change.up(); // end of content
566
     }
727
     }
567
     this.connection.sendIQ(change,
728
     this.connection.sendIQ(change,
605
 ColibriFocus.prototype.addSource = function (elem, fromJid) {
766
 ColibriFocus.prototype.addSource = function (elem, fromJid) {
606
 
767
 
607
     var self = this;
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
     this.peerconnection.addSource(elem);
782
     this.peerconnection.addSource(elem);
609
 
783
 
610
     var peerSsrc = this.remotessrc[fromJid];
784
     var peerSsrc = this.remotessrc[fromJid];
638
 ColibriFocus.prototype.removeSource = function (elem, fromJid) {
812
 ColibriFocus.prototype.removeSource = function (elem, fromJid) {
639
 
813
 
640
     var self = this;
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
     this.peerconnection.removeSource(elem);
828
     this.peerconnection.removeSource(elem);
642
 
829
 
643
     var peerSsrc = this.remotessrc[fromJid];
830
     var peerSsrc = this.remotessrc[fromJid];
675
     this.remotessrc[session.peerjid] = [];
862
     this.remotessrc[session.peerjid] = [];
676
     for (channel = 0; channel < this.channels[participant].length; channel++) {
863
     for (channel = 0; channel < this.channels[participant].length; channel++) {
677
         //if (channel == 0) continue; FIXME: does not work as intended
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
     change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
892
     change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
703
     $(elem).each(function () {
893
     $(elem).each(function () {
704
         var name = $(this).attr('name');
894
         var name = $(this).attr('name');
895
+
705
         var channel = name == 'audio' ? 0 : 1; // FIXME: search mlineindex in localdesc
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
         change.c('content', {name: name});
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
         $(this).find('>transport').each(function () {
916
         $(this).find('>transport').each(function () {
714
             change.c('transport', {
917
             change.c('transport', {
715
                 ufrag: $(this).attr('ufrag'),
918
                 ufrag: $(this).attr('ufrag'),
729
             });
932
             });
730
             change.up(); // end of transport
933
             change.up(); // end of transport
731
         });
934
         });
732
-        change.up(); // end of channel
935
+        change.up(); // end of channel/sctpconnection
733
         change.up(); // end of content
936
         change.up(); // end of content
734
     });
937
     });
735
     // FIXME: need to check if there is at least one candidate when filtering TCP ones
938
     // FIXME: need to check if there is at least one candidate when filtering TCP ones
769
     mycands.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
972
     mycands.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
770
     // FIXME: multi-candidate logic is taken from strophe.jingle, should be refactored there
973
     // FIXME: multi-candidate logic is taken from strophe.jingle, should be refactored there
771
     var localSDP = new SDP(this.peerconnection.localDescription.sdp);
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
         var cands = candidates.filter(function (el) { return el.sdpMLineIndex == mid; });
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
             mycands.c('transport', {xmlns: 'urn:xmpp:jingle:transports:ice-udp:1'});
998
             mycands.c('transport', {xmlns: 'urn:xmpp:jingle:transports:ice-udp:1'});
782
             for (var i = 0; i < cands.length; i++) {
999
             for (var i = 0; i < cands.length; i++) {
783
                 mycands.c('candidate', SDPUtil.candidateToJingle(cands[i].candidate)).up();
1000
                 mycands.c('candidate', SDPUtil.candidateToJingle(cands[i].candidate)).up();
784
             }
1001
             }
785
             mycands.up(); // transport
1002
             mycands.up(); // transport
786
-            mycands.up(); // channel
1003
+            mycands.up(); // channel / sctpconnection
787
             mycands.up(); // content
1004
             mycands.up(); // content
788
         }
1005
         }
789
     }
1006
     }
814
     var change = $iq({to: this.bridgejid, type: 'set'});
1031
     var change = $iq({to: this.bridgejid, type: 'set'});
815
     change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
1032
     change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
816
     for (var channel = 0; channel < this.channels[participant].length; channel++) {
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
         change.up(); // end of content
1054
         change.up(); // end of content
825
     }
1055
     }
826
     this.connection.sendIQ(change,
1056
     this.connection.sendIQ(change,

+ 110
- 0
libs/popover.js 查看文件

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 查看文件

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

+ 55
- 7
libs/strophe/strophe.jingle.sdp.js 查看文件

155
     }
155
     }
156
     for (i = 0; i < this.media.length; i++) {
156
     for (i = 0; i < this.media.length; i++) {
157
         mline = SDPUtil.parse_mline(this.media[i].split('\r\n')[0]);
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
             continue;
162
             continue;
160
         }
163
         }
161
         if (SDPUtil.find_line(this.media[i], 'a=ssrc:')) {
164
         if (SDPUtil.find_line(this.media[i], 'a=ssrc:')) {
171
             elem.attrs({ name: mid });
174
             elem.attrs({ name: mid });
172
 
175
 
173
             // old BUNDLE plan, to be removed
176
             // old BUNDLE plan, to be removed
174
-            if (bundle.indexOf(mid) != -1) {
177
+            if (bundle.indexOf(mid) !== -1) {
175
                 elem.c('bundle', {xmlns: 'http://estos.de/ns/bundle'}).up();
178
                 elem.c('bundle', {xmlns: 'http://estos.de/ns/bundle'}).up();
176
                 bundle.splice(bundle.indexOf(mid), 1);
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
             elem.c('description',
185
             elem.c('description',
181
                 {xmlns: 'urn:xmpp:jingle:apps:rtp:1',
186
                 {xmlns: 'urn:xmpp:jingle:apps:rtp:1',
182
                     media: mline.media });
187
                     media: mline.media });
304
     var self = this;
309
     var self = this;
305
     elem.c('transport');
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
     // XEP-0320
332
     // XEP-0320
308
     var fingerprints = SDPUtil.find_lines(this.media[mediaindex], 'a=fingerprint:', this.session);
333
     var fingerprints = SDPUtil.find_lines(this.media[mediaindex], 'a=fingerprint:', this.session);
309
     fingerprints.forEach(function(line) {
334
     fingerprints.forEach(function(line) {
438
         ssrc = desc.attr('ssrc'),
463
         ssrc = desc.attr('ssrc'),
439
         self = this,
464
         self = this,
440
         tmp;
465
         tmp;
466
+    var sctp = content.find(
467
+        '>transport>sctpmap[xmlns="urn:xmpp:jingle:transports:dtls-sctp:1"]');
441
 
468
 
442
     tmp = { media: desc.attr('media') };
469
     tmp = { media: desc.attr('media') };
443
     tmp.port = '1';
470
     tmp.port = '1';
446
         tmp.port = '0';
473
         tmp.port = '0';
447
     }
474
     }
448
     if (content.find('>transport>fingerprint').length || desc.find('encryption').length) {
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
     } else {
480
     } else {
451
         tmp.proto = 'RTP/AVPF';
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
     media += 'c=IN IP4 0.0.0.0\r\n';
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
     tmp = content.find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]');
505
     tmp = content.find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]');
458
     if (tmp.length) {
506
     if (tmp.length) {
459
         if (tmp.attr('ufrag')) {
507
         if (tmp.attr('ufrag')) {

+ 17
- 1
libs/strophe/strophe.jingle.sdp.util.js 查看文件

90
         data.channels = parts.length ? parts.shift() : '1';
90
         data.channels = parts.length ? parts.shift() : '1';
91
         return data;
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
     build_rtpmap: function (el) {
107
     build_rtpmap: function (el) {
94
         var line = 'a=rtpmap:' + el.getAttribute('id') + ' ' + el.getAttribute('name') + '/' + el.getAttribute('clockrate');
108
         var line = 'a=rtpmap:' + el.getAttribute('id') + ' ' + el.getAttribute('name') + '/' + el.getAttribute('clockrate');
95
         if (el.getAttribute('channels') && el.getAttribute('channels') != '1') {
109
         if (el.getAttribute('channels') && el.getAttribute('channels') != '1') {
269
     candidateToJingle: function (line) {
283
     candidateToJingle: function (line) {
270
         // a=candidate:2979166662 1 udp 2113937151 192.168.2.100 57698 typ host generation 0
284
         // a=candidate:2979166662 1 udp 2113937151 192.168.2.100 57698 typ host generation 0
271
         //      <candidate component=... foundation=... generation=... id=... ip=... network=... port=... priority=... protocol=... type=.../>
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
             console.log('parseCandidate called with a line that is not a candidate line');
289
             console.log('parseCandidate called with a line that is not a candidate line');
274
             console.log(line);
290
             console.log(line);
275
             return null;
291
             return null;

+ 1
- 0
libs/strophe/strophe.jingle.session.js 查看文件

420
         },
420
         },
421
         function (e) {
421
         function (e) {
422
             console.error('setRemoteDescription error', e);
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 查看文件

23
 
23
 
24
 SessionBase.prototype.addSource = function (elem, fromJid) {
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
     this.peerconnection.addSource(elem);
40
     this.peerconnection.addSource(elem);
27
 
41
 
28
     this.modifySources();
42
     this.modifySources();
30
 
44
 
31
 SessionBase.prototype.removeSource = function (elem, fromJid) {
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
     this.peerconnection.removeSource(elem);
61
     this.peerconnection.removeSource(elem);
34
 
62
 
35
     this.modifySources();
63
     this.modifySources();

+ 399
- 0
libs/tooltip.js 查看文件

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 查看文件

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 查看文件

21
     },
21
     },
22
     doJoin: function (jid, password) {
22
     doJoin: function (jid, password) {
23
         this.myroomjid = jid;
23
         this.myroomjid = jid;
24
+
25
+        console.info("Joined MUC as " + this.myroomjid);
26
+
24
         this.initPresenceMap(this.myroomjid);
27
         this.initPresenceMap(this.myroomjid);
25
 
28
 
26
         if (!this.roomjid) {
29
         if (!this.roomjid) {
167
         }
170
         }
168
         this.connection.send(msg);
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
     onMessage: function (msg) {
179
     onMessage: function (msg) {
171
-        var txt = $(msg).find('>body').text();
172
-        // TODO: <subject/>
173
         // FIXME: this is a hack. but jingle on muc makes nickchanges hard
180
         // FIXME: this is a hack. but jingle on muc makes nickchanges hard
174
         var from = msg.getAttribute('from');
181
         var from = msg.getAttribute('from');
175
         var nick = $(msg).find('>nick[xmlns="http://jabber.org/protocol/nick"]').text() || Strophe.getResourceFromJid(from);
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
         if (txt) {
203
         if (txt) {
177
             console.log('chat', nick, txt);
204
             console.log('chat', nick, txt);
178
 
205
 

+ 6
- 6
prezi.js 查看文件

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

+ 287
- 0
rtp_stats.js 查看文件

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 查看文件

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 查看文件

51
      * Returns the available video width.
51
      * Returns the available video width.
52
      */
52
      */
53
     my.getAvailableVideoWidth = function () {
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
         return window.innerWidth - chatspaceWidth;
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
     return my;
84
     return my;
60
 }(Util || {}));
85
 }(Util || {}));

+ 901
- 0
videolayout.js 查看文件

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 || {}));

正在加载...
取消
保存