Browse Source

initial commit

master
Philipp Hancke 11 years ago
parent
commit
62530ef123
17 changed files with 4915 additions and 2 deletions
  1. 1
    0
      LICENSE
  2. 3
    2
      README.md
  3. 427
    0
      app.js
  4. 1
    0
      chromeonly.html
  5. 9
    0
      config.js
  6. 125
    0
      css/jquery-impromptu.css
  7. 318
    0
      css/main.css
  8. 22
    0
      css/modaldialog.css
  9. BIN
      images/estoslogo.png
  10. BIN
      images/jitsilogo.png
  11. 68
    0
      index.html
  12. 814
    0
      libs/colibri.js
  13. 666
    0
      libs/jquery-impromptu.js
  14. 250
    0
      libs/jquery.autosize.js
  15. 2071
    0
      libs/strophejingle.bundle.js
  16. 139
    0
      muc.js
  17. 1
    0
      webrtcrequired.html

+ 1
- 0
LICENSE View File

@@ -1,6 +1,7 @@
1 1
 The MIT License (MIT)
2 2
 
3 3
 Copyright (c) 2013 ESTOS GmbH
4
+Copyright (c) 2013 BlueJimp SARL
4 5
 
5 6
 Permission is hereby granted, free of charge, to any person obtaining a copy of
6 7
 this software and associated documentation files (the "Software"), to deal in

+ 3
- 2
README.md View File

@@ -1,4 +1,5 @@
1
-meet
1
+meet - a colibri.js sample application
2 2
 ====
3
+A WebRTC-powered multi-user videochat. For a live demo, check out either https://meet.estos.de/ or https://meet.jit.si/.
3 4
 
4
-colibri.js sample application
5
+Built using [colibri.js](https://github.com/ESTOS/colibri.js) and [strophe.jingle](https://github.com/ESTOS/strophe.jingle), powered by the [jitsi-videobridge](https://github.com/jitsi/jitsi-videobridge) and [prosody](http://prosody.im/).

+ 427
- 0
app.js View File

@@ -0,0 +1,427 @@
1
+/* jshint -W117 */
2
+/* application specific logic */
3
+var connection = null;
4
+var focus = null;
5
+var RTC;
6
+var RTCPeerConnection = null;
7
+var nickname = null;
8
+var sharedKey = '';
9
+var roomUrl = null;
10
+
11
+function init() {
12
+    RTC = setupRTC();
13
+    if (RTC === null) {
14
+        window.location.href = '/webrtcrequired.html';
15
+        return;
16
+    } else if (RTC.browser != 'chrome') {
17
+        window.location.href = '/chromeonly.html';
18
+        return;
19
+    }
20
+    RTCPeerconnection = RTC.peerconnection;
21
+
22
+    connection = new Strophe.Connection(document.getElementById('boshURL').value || config.bosh || '/http-bind');
23
+    /*
24
+    connection.rawInput = function (data) { console.log('RECV: ' + data); };
25
+    connection.rawOutput = function (data) { console.log('SEND: ' + data); };
26
+    */
27
+    connection.jingle.pc_constraints = RTC.pc_constraints;
28
+
29
+    var jid = document.getElementById('jid').value || config.hosts.domain || window.location.hostname;
30
+
31
+    connection.connect(jid, document.getElementById('password').value, function (status) {
32
+        if (status == Strophe.Status.CONNECTED) {
33
+            console.log('connected');
34
+            getUserMediaWithConstraints(['audio', 'video'], '360');
35
+            document.getElementById('connect').disabled = true;
36
+        } else {
37
+            console.log('status', status);
38
+        }
39
+    });
40
+}
41
+
42
+function doJoin() {
43
+    var roomnode = null;
44
+    var path = window.location.pathname;
45
+    var roomjid;
46
+    if (path.length > 1) {
47
+        roomnode = path.substr(1).toLowerCase();
48
+    } else {
49
+        roomnode = Math.random().toString(36).substr(2, 20);
50
+        window.history.pushState('VideoChat', 'Room: ' + roomnode, window.location.pathname + roomnode);
51
+    }
52
+    roomjid = roomnode + '@' + config.hosts.muc;
53
+
54
+    if (config.useNicks) {
55
+        var nick = window.prompt('Your nickname (optional)');
56
+        if (nick) {
57
+            roomjid += '/' + nick;
58
+        } else {
59
+            roomjid += '/' + Strophe.getNodeFromJid(connection.jid);
60
+        }
61
+    } else {
62
+        roomjid += '/' + Strophe.getNodeFromJid(connection.jid);
63
+    }
64
+    connection.emuc.doJoin(roomjid);
65
+}
66
+
67
+$(document).bind('mediaready.jingle', function (event, stream) {
68
+    connection.jingle.localStream = stream;
69
+    RTC.attachMediaStream($('#localVideo'), stream);
70
+    document.getElementById('localVideo').muted = true;
71
+    document.getElementById('localVideo').autoplay = true;
72
+    document.getElementById('localVideo').volume = 0;
73
+
74
+    document.getElementById('largeVideo').volume = 0;
75
+    document.getElementById('largeVideo').src = document.getElementById('localVideo').src;
76
+    doJoin();
77
+});
78
+
79
+$(document).bind('mediafailure.jingle', function () {
80
+    // FIXME
81
+});
82
+  
83
+$(document).bind('remotestreamadded.jingle', function (event, data, sid) {
84
+    function waitForRemoteVideo(selector, sid) {
85
+        var sess = connection.jingle.sessions[sid];
86
+        videoTracks = data.stream.getVideoTracks();
87
+        if (videoTracks.length === 0 || selector[0].currentTime > 0) {
88
+            RTC.attachMediaStream(selector, data.stream); // FIXME: why do i have to do this for FF?
89
+            $(document).trigger('callactive.jingle', [selector, sid]);
90
+            console.log('waitForremotevideo', sess.peerconnection.iceConnectionState, sess.peerconnection.signalingState);
91
+        } else {
92
+            setTimeout(function () { waitForRemoteVideo(selector, sid); }, 100);
93
+        }
94
+    }
95
+    var sess = connection.jingle.sessions[sid];
96
+    var vid = document.createElement('video');
97
+    var id = 'remoteVideo_' + sid + '_' + data.stream.id;
98
+    vid.id = id;
99
+    vid.autoplay = true;
100
+    vid.oncontextmenu = function () { return false; };
101
+    var remotes = document.getElementById('remoteVideos');
102
+    remotes.appendChild(vid);
103
+    var sel = $('#' + id);
104
+    sel.hide();
105
+    RTC.attachMediaStream(sel, data.stream);
106
+    waitForRemoteVideo(sel, sid);
107
+    data.stream.onended = function () {
108
+        console.log('stream ended', this.id);
109
+        var src = $('#' + id).attr('src');
110
+        $('#' + id).remove();
111
+        if (src === $('#largeVideo').attr('src')) {
112
+            // this is currently displayed as large
113
+            // pick the last visible video in the row
114
+            // if nobody else is left, this picks the local video
115
+            var pick = $('#remoteVideos :visible:last').get(0);
116
+            // mute if localvideo
117
+            document.getElementById('largeVideo').volume = pick.volume;
118
+            document.getElementById('largeVideo').src = pick.src;
119
+        }
120
+        resizeThumbnails();
121
+    };
122
+    sel.click(
123
+        function () {
124
+            console.log('hover in', $(this).attr('src'));
125
+            var newSrc = $(this).attr('src');
126
+            if ($('#largeVideo').attr('src') != newSrc) {
127
+                document.getElementById('largeVideo').volume = 1;
128
+                $('#largeVideo').fadeOut(300, function () {
129
+                    $(this).attr('src', newSrc);
130
+                    $(this).fadeIn(300);
131
+                });
132
+            }
133
+        }
134
+    );
135
+});
136
+
137
+$(document).bind('callincoming.jingle', function (event, sid) {
138
+    var sess = connection.jingle.sessions[sid];
139
+    // TODO: check affiliation and/or role
140
+    console.log('emuc data for', sess.peerjid, connection.emuc.members[sess.peerjid]);
141
+    sess.sendAnswer();
142
+    sess.accept();
143
+});
144
+
145
+$(document).bind('callactive.jingle', function (event, videoelem, sid) {
146
+    console.log('call active');
147
+    if (videoelem.attr('id').indexOf('mixedmslabel') == -1) {
148
+        // ignore mixedmslabela0 and v0
149
+        videoelem.show();
150
+        resizeThumbnails();
151
+
152
+        document.getElementById('largeVideo').volume = 1;
153
+        $('#largeVideo').attr('src', videoelem.attr('src'));
154
+    }
155
+});
156
+
157
+$(document).bind('callterminated.jingle', function (event, sid, reason) {
158
+    // FIXME
159
+});
160
+
161
+
162
+$(document).bind('joined.muc', function (event, jid, info) {
163
+    console.log('onJoinComplete', info);
164
+    updateRoomUrl(window.location.href);
165
+    if (Object.keys(connection.emuc.members).length < 1) {
166
+        focus = new ColibriFocus(connection, config.hosts.bridge);
167
+        return;
168
+    }
169
+});
170
+
171
+$(document).bind('entered.muc', function (event, jid, info) {
172
+    console.log('entered', jid, info);
173
+    console.log(focus);
174
+    if (focus !== null) {
175
+        // FIXME: this should prepare the video
176
+        if (focus.confid === null) {
177
+            console.log('make new conference with', jid);
178
+            focus.makeConference(Object.keys(connection.emuc.members));
179
+        } else {
180
+            console.log('invite', jid, 'into conference');
181
+            focus.addNewParticipant(jid);
182
+        }
183
+    }
184
+});
185
+
186
+$(document).bind('left.muc', function (event, jid) {
187
+    console.log('left', jid);
188
+    connection.jingle.terminateByJid(jid);
189
+    // FIXME: this should actually hide the video already for a nicer UX
190
+
191
+    if (Object.keys(connection.emuc.members).length === 0) {
192
+        console.log('everyone left');
193
+        if (focus !== null) {
194
+            // FIXME: closing the connection is a hack to avoid some 
195
+            // problemswith reinit
196
+            if (focus.peerconnection !== null) {
197
+                focus.peerconnection.close();
198
+            }
199
+            focus = new ColibriFocus(connection, config.hosts.bridge);
200
+        }
201
+    }
202
+});
203
+
204
+function toggleVideo() {
205
+    if (!(connection && connection.jingle.localStream)) return;
206
+    for (var idx = 0; idx < connection.jingle.localStream.getVideoTracks().length; idx++) {
207
+        connection.jingle.localStream.getVideoTracks()[idx].enabled = !connection.jingle.localStream.getVideoTracks()[idx].enabled;
208
+    }
209
+}
210
+
211
+function toggleAudio() {
212
+    if (!(connection && connection.jingle.localStream)) return;
213
+    for (var idx = 0; idx < connection.jingle.localStream.getAudioTracks().length; idx++) {
214
+        connection.jingle.localStream.getAudioTracks()[idx].enabled = !connection.jingle.localStream.getAudioTracks()[idx].enabled;
215
+    }
216
+}
217
+
218
+function resizeLarge() {
219
+    var availableHeight = window.innerHeight;
220
+    var chatspaceWidth = $('#chatspace').width();
221
+
222
+    var numvids = $('#remoteVideos>video:visible').length;
223
+    if (numvids < 5)
224
+        availableHeight -= 100; // min thumbnail height for up to 4 videos
225
+    else
226
+        availableHeight -= 50; // min thumbnail height for more than 5 videos
227
+
228
+    availableHeight -= 79; // padding + link ontop
229
+    var availableWidth = window.innerWidth - chatspaceWidth;
230
+    var aspectRatio = 16.0 / 9.0;
231
+    if (availableHeight < availableWidth / aspectRatio) {
232
+        availableWidth = Math.floor(availableHeight * aspectRatio);
233
+    }
234
+    if (availableWidth < 0 || availableHeight < 0) return;
235
+    $('#largeVideo').width(availableWidth);
236
+    $('#largeVideo').height(availableWidth / aspectRatio);
237
+    resizeThumbnails();
238
+}
239
+
240
+function resizeThumbnails() {
241
+    // Calculate the available height, which is the inner window height minus 39px for the header
242
+    // minus 4px for the delimiter lines on the top and bottom of the large video,
243
+    // minus the 36px space inside the remoteVideos container used for highlighting shadow.
244
+    var availableHeight = window.innerHeight - $('#largeVideo').height() - 79;
245
+    var numvids = $('#remoteVideos>video:visible').length;
246
+    // Remove the 1px borders arround videos.
247
+    var availableWinWidth = $('#remoteVideos').width() - 2 * numvids;
248
+    var availableWidth = availableWinWidth / numvids;
249
+    var aspectRatio = 16.0 / 9.0;
250
+    var maxHeight = Math.min(160, availableHeight);
251
+    availableHeight = Math.min(maxHeight, availableWidth / aspectRatio);
252
+    if (availableHeight < availableWidth / aspectRatio) {
253
+        availableWidth = Math.floor(availableHeight * aspectRatio);
254
+    }
255
+    // size videos so that while keeping AR and max height, we have a nice fit
256
+    $('#remoteVideos').height(availableHeight + 36); // add the 2*18px border used for highlighting shadow.
257
+    $('#remoteVideos>video:visible').width(availableWidth);
258
+    $('#remoteVideos>video:visible').height(availableHeight);
259
+}
260
+
261
+$(document).ready(function () {
262
+    $('#nickinput').keydown(function(event) {
263
+        if (event.keyCode == 13) {
264
+            event.preventDefault();
265
+            var val = this.value;
266
+            this.value = '';
267
+            if (!nickname) {
268
+                nickname = val;
269
+                $('#nickname').css({visibility:"hidden"});
270
+                $('#chatconversation').css({visibility:'visible'});
271
+                $('#usermsg').css({visibility:'visible'});
272
+                $('#usermsg').focus();
273
+                return;
274
+            }
275
+        }
276
+    });
277
+
278
+    $('#usermsg').keydown(function(event) {
279
+        if (event.keyCode == 13) {
280
+            event.preventDefault();
281
+            var message = this.value;
282
+            $('#usermsg').val('').trigger('autosize.resize');
283
+            this.focus();
284
+            connection.emuc.sendMessage(message, nickname);
285
+        }
286
+    });
287
+
288
+    $('#usermsg').autosize();
289
+
290
+    resizeLarge();
291
+    $(window).resize(function () {
292
+        resizeLarge();
293
+    });
294
+    if (!$('#settings').is(':visible')) {
295
+        console.log('init');
296
+        init();
297
+    } else {
298
+        loginInfo.onsubmit = function (e) {
299
+            if (e.preventDefault) e.preventDefault();
300
+            $('#settings').hide();
301
+            init();
302
+        };
303
+    }
304
+});
305
+
306
+$(window).bind('beforeunload', function () {
307
+    if (connection && connection.connected) {
308
+        // ensure signout
309
+        $.ajax({
310
+            type: 'POST',
311
+            url: config.bosh,
312
+            async: false,
313
+            cache: false,
314
+            contentType: 'application/xml',
315
+            data: "<body rid='" + (connection.rid || connection._proto.rid) + "' xmlns='http://jabber.org/protocol/httpbind' sid='" + (connection.sid || connection._proto.sid) + "' type='terminate'><presence xmlns='jabber:client' type='unavailable'/></body>",
316
+            success: function (data) {
317
+                console.log('signed out');
318
+                console.log(data);
319
+            },
320
+            error: function (XMLHttpRequest, textStatus, errorThrown) {
321
+                console.log('signout error', textStatus + ' (' + errorThrown + ')');
322
+            }
323
+        });
324
+    }
325
+});
326
+
327
+function updateChatConversation(nick, message)
328
+{
329
+    var divClassName = '';
330
+    if (nickname == nick)
331
+        divClassName = "localuser";
332
+    else
333
+        divClassName = "remoteuser";
334
+    
335
+    $('#chatconversation').append('<div class="' + divClassName + '"><b>' + nick + ': </b>' + message + '</div>');
336
+    $('#chatconversation').animate({ scrollTop: $('#chatconversation')[0].scrollHeight}, 1000);
337
+}
338
+
339
+function buttonClick(id, classname) {
340
+    $(id).toggleClass(classname); // add the class to the clicked element
341
+}
342
+
343
+function openLockDialog() {
344
+    if (sharedKey)
345
+        $.prompt("Are you sure you would like to remove your secret key?",
346
+        {
347
+            title: "Remove secrect key",
348
+            persistent: false,
349
+            buttons: { "Remove": true, "Cancel": false},
350
+            defaultButton: 1,
351
+            submit: function(e,v,m,f){
352
+                if(v)
353
+                {
354
+                    sharedKey = '';
355
+                    lockRoom();
356
+                }
357
+            }
358
+            });
359
+    else
360
+        $.prompt('<h2>Set a secrect key to lock your room</h2>' +
361
+                 '<input id="lockKey" type="text" placeholder="your shared key" autofocus>',
362
+                {
363
+                    persistent: false,
364
+                    buttons: { "Save": true , "Cancel": false},
365
+                    defaultButton: 1,
366
+                    loaded: function(event) {
367
+                        document.getElementById('lockKey').focus();
368
+                    },
369
+                    submit: function(e,v,m,f){
370
+                    if(v)
371
+                    {
372
+                        var lockKey = document.getElementById('lockKey');
373
+
374
+                    if (lockKey.value != null)
375
+                    {
376
+                        sharedKey = lockKey.value;
377
+                        lockRoom(true);
378
+                    }
379
+                }
380
+            }
381
+        });
382
+}
383
+
384
+function openLinkDialog() {
385
+    $.prompt('<input id="inviteLinkRef" type="text" value="' + roomUrl + '" onclick="this.select();">',
386
+             {
387
+             title: "Share this link with everyone you want to invite",
388
+             persistent: false,
389
+             buttons: { "Cancel": false},
390
+             loaded: function(event) {
391
+             document.getElementById('inviteLinkRef').select();
392
+             }
393
+             });
394
+}
395
+
396
+function lockRoom(lock) {
397
+    connection.emuc.lockRoom(sharedKey);
398
+    
399
+    buttonClick("#lockIcon", "fa fa-unlock fa-lg fa fa-lock fa-lg");
400
+}
401
+
402
+function openChat() {
403
+    var chatspace = $('#chatspace');
404
+    var videospace = $('#videospace');
405
+    var chatspaceWidth = chatspace.width();
406
+
407
+    if (chatspace.css("opacity") == 1) {
408
+        chatspace.animate({opacity: 0}, "fast");
409
+        chatspace.animate({width: 0}, "slow");
410
+        videospace.animate({right: 0, width:"100%"}, "slow");
411
+    }
412
+    else {
413
+        chatspace.animate({width:"20%"}, "slow");
414
+        chatspace.animate({opacity: 1}, "slow");
415
+        videospace.animate({right:chatspaceWidth, width:"80%"}, "slow");
416
+    }
417
+    
418
+    // Request the focus in the nickname field or the chat input field.
419
+    if ($('#nickinput').is(':visible'))
420
+        $('#nickinput').focus();
421
+    else
422
+        $('#usermsg').focus();
423
+}
424
+
425
+function updateRoomUrl(newRoomUrl) {
426
+    roomUrl = newRoomUrl;
427
+}

+ 1
- 0
chromeonly.html View File

@@ -0,0 +1 @@
1
+Sorry, this currently only works with chrome because it uses "Plan B".

+ 9
- 0
config.js View File

@@ -0,0 +1,9 @@
1
+var config = {
2
+    hosts: {
3
+        domain: 'your.domain.example',
4
+        muc: 'conference.your.domain.example', // FIXME: use XEP-0030
5
+        bridge: 'jitsi-videobridge.your.domain.example' // FIXME: use XEP-0030
6
+    },
7
+    useNicks: false,
8
+    bosh: '/http-bind' // FIXME: use xep-0156 for that
9
+};

+ 125
- 0
css/jquery-impromptu.css View File

@@ -0,0 +1,125 @@
1
+/*
2
+------------------------------
3
+	Impromptu
4
+------------------------------
5
+*/
6
+.jqifade{
7
+	position: absolute; 
8
+	background-color: #000;
9
+}
10
+div.jqi{ 
11
+	width: 400px; 
12
+	font-family: Verdana, Geneva, Arial, Helvetica, sans-serif; 
13
+	position: absolute; 
14
+	background-color: #ffffff; 
15
+	font-size: 11px; 
16
+	text-align: left; 
17
+	border: solid 1px #eeeeee;
18
+	border-radius: 6px;
19
+	-moz-border-radius: 6px;
20
+	-webkit-border-radius: 6px;
21
+	padding: 7px;
22
+}
23
+div.jqi .jqicontainer{ 
24
+}
25
+div.jqi .jqiclose{ 
26
+	position: absolute;
27
+	top: 4px; right: -2px; 
28
+	width: 18px; 
29
+	cursor: default; 
30
+	color: #bbbbbb; 
31
+	font-weight: bold; 
32
+}
33
+div.jqi .jqistate{
34
+	background-color: #fff;
35
+}
36
+div.jqi .jqititle{
37
+	padding: 5px 10px;
38
+	font-size: 16px; 
39
+	line-height: 20px; 
40
+	border-bottom: solid 1px #eeeeee;
41
+}
42
+div.jqi .jqimessage{ 
43
+	padding: 10px; 
44
+	line-height: 20px; 
45
+	color: #444444; 
46
+}
47
+div.jqi .jqibuttons{ 
48
+	text-align: right; 
49
+	margin: 0 -7px -7px -7px;
50
+	border-top: solid 1px #e4e4e4; 
51
+	background-color: #f4f4f4;
52
+	border-radius: 0 0 6px 6px;
53
+	-moz-border-radius: 0 0 6px 6px;
54
+	-webkit-border-radius: 0 0 6px 6px;
55
+}
56
+div.jqi .jqibuttons button{ 
57
+	margin: 0;
58
+	padding: 5px 20px;
59
+	background-color: transparent;
60
+	font-weight: normal; 
61
+	border: none;
62
+	border-left: solid 1px #e4e4e4; 
63
+	color: #777; 
64
+	font-weight: bold; 
65
+	font-size: 12px; 
66
+}
67
+div.jqi .jqibuttons button.jqidefaultbutton{
68
+	color: #489afe;
69
+}
70
+div.jqi .jqibuttons button:hover,
71
+div.jqi .jqibuttons button:focus{
72
+	color: #287ade;
73
+	outline: none;
74
+}
75
+.jqiwarning .jqi .jqibuttons{ 
76
+	background-color: #b95656;
77
+}
78
+
79
+/* sub states */
80
+div.jqi .jqiparentstate::after{ 
81
+	background-color: #777;
82
+	opacity: 0.6;
83
+	filter: alpha(opacity=60);
84
+	content: '';
85
+	position: absolute;
86
+	top:0;left:0;bottom:0;right:0;
87
+	border-radius: 6px;
88
+	-moz-border-radius: 6px;
89
+	-webkit-border-radius: 6px;
90
+}
91
+div.jqi .jqisubstate{
92
+	position: absolute;
93
+	top:0;
94
+	left: 20%;
95
+	width: 60%;
96
+	padding: 7px;
97
+	border: solid 1px #eeeeee;
98
+	border-top: none;
99
+	border-radius: 0 0 6px 6px;
100
+	-moz-border-radius: 0 0 6px 6px;
101
+	-webkit-border-radius: 0 0 6px 6px;
102
+}
103
+div.jqi .jqisubstate .jqibuttons button{
104
+	padding: 10px 18px;
105
+}
106
+
107
+/* arrows for tooltips/tours */
108
+.jqi .jqiarrow{ position: absolute; height: 0; width:0; line-height: 0; font-size: 0; border: solid 10px transparent;}
109
+
110
+.jqi .jqiarrowtl{ left: 10px; top: -20px; border-bottom-color: #ffffff; }
111
+.jqi .jqiarrowtc{ left: 50%; top: -20px; border-bottom-color: #ffffff; margin-left: -10px; }
112
+.jqi .jqiarrowtr{ right: 10px; top: -20px; border-bottom-color: #ffffff; }
113
+
114
+.jqi .jqiarrowbl{ left: 10px; bottom: -20px; border-top-color: #ffffff; }
115
+.jqi .jqiarrowbc{ left: 50%; bottom: -20px; border-top-color: #ffffff; margin-left: -10px; }
116
+.jqi .jqiarrowbr{ right: 10px; bottom: -20px; border-top-color: #ffffff; }
117
+
118
+.jqi .jqiarrowlt{ left: -20px; top: 10px; border-right-color: #ffffff; }
119
+.jqi .jqiarrowlm{ left: -20px; top: 50%; border-right-color: #ffffff; margin-top: -10px; }
120
+.jqi .jqiarrowlb{ left: -20px; bottom: 10px; border-right-color: #ffffff; }
121
+
122
+.jqi .jqiarrowrt{ right: -20px; top: 10px; border-left-color: #ffffff; }
123
+.jqi .jqiarrowrm{ right: -20px; top: 50%; border-left-color: #ffffff; margin-top: -10px; }
124
+.jqi .jqiarrowrb{ right: -20px; bottom: 10px; border-left-color: #ffffff; }
125
+

+ 318
- 0
css/main.css View File

@@ -0,0 +1,318 @@
1
+html, body{
2
+    margin:0px;
3
+    height:100%;
4
+    color: #424242;
5
+    font-family:'YanoneKaffeesatzLight',Verdana,Tahoma,Arial;
6
+	font-weight: 400;
7
+    background: #e9e9e9;
8
+}
9
+
10
+
11
+#videospace {
12
+    display: block;
13
+    position: absolute;
14
+    top: 39px;
15
+    left: 0px;
16
+    right: 0px;
17
+    float: left;
18
+}
19
+
20
+#largeVideo {
21
+    display:block;
22
+    position:relative;
23
+    width:1280px;
24
+    height:720px;
25
+    margin-left:auto;
26
+    margin-right:auto;
27
+    z-index: 0;
28
+}
29
+
30
+#remoteVideos {
31
+    display:block;
32
+    position:relative;
33
+    text-align:center;
34
+    height:196px;
35
+    width:auto;
36
+    overflow: hidden;
37
+    border:1px solid transparent;
38
+    font-size:0;
39
+    z-index: 2;
40
+}
41
+
42
+#remoteVideos video {
43
+    position:relative;
44
+    top:18px;
45
+    height:160px;
46
+    width:auto;
47
+    z-index:0;
48
+    border:1px solid #FFFFFF;
49
+}
50
+
51
+#remoteVideos video:hover {
52
+    cursor: pointer;
53
+    cursor: hand;
54
+    transform:scale(1.08, 1.08);
55
+    -webkit-transform:scale(1.08, 1.08);
56
+    transition-duration: 0.5s;
57
+    -webkit-transition-duration: 0.5s;
58
+    background-color: #FFFFFF;
59
+    -webkit-animation-name: greyPulse;
60
+    -webkit-animation-duration: 2s;
61
+    -webkit-animation-iteration-count: 1;
62
+    -webkit-box-shadow: 0 0 18px #515151;
63
+    border:1px solid #FFFFFF;
64
+    z-index: 10;
65
+}
66
+
67
+#chatspace {
68
+    display:block;
69
+    position:absolute;
70
+    float: right;
71
+    top: 40px;
72
+    bottom: 0px;
73
+    right: 0px;
74
+    width:0;
75
+    opacity: 0;
76
+    overflow: hidden;
77
+    background-color:#f6f6f6;
78
+    border-left:1px solid #424242;
79
+}
80
+
81
+#chatconversation {
82
+    display:block;
83
+    position:relative;
84
+    top: -120px;
85
+    float:top;
86
+    text-align:left;
87
+    line-height:20px;
88
+    font-size:14px;
89
+    padding:5px;
90
+    height:90%;
91
+    overflow:scroll;
92
+    visibility:hidden;
93
+}
94
+
95
+div.localuser {
96
+    color: #087dba;
97
+}
98
+
99
+div.remoteuser {
100
+    color: #424242;
101
+}
102
+
103
+#usermsg {
104
+    position:absolute;
105
+    bottom: 5px;
106
+    left: 5px;
107
+    right: 5px;
108
+    width: 95%;
109
+    height: 40px;
110
+    z-index: 5;
111
+    visibility:hidden;
112
+    max-height:150px;
113
+}
114
+
115
+#nickname {
116
+    position:relative;
117
+    text-align:center;
118
+    color: #9d9d9d;
119
+    font-size: 18;
120
+    top: 100px;
121
+    left: 5px;
122
+    right: 5px;
123
+    width: 95%;
124
+}
125
+
126
+#nickinput {
127
+    margin-top: 20px;
128
+    font-size: 14;
129
+}
130
+
131
+div#spacer {
132
+    height:5px;
133
+}
134
+
135
+#settings {
136
+    display:none;
137
+}
138
+
139
+#nowebrtc {
140
+    display:none;
141
+}
142
+
143
+div#header{
144
+	display:block;
145
+    position:relative;
146
+	width:100%;
147
+    height:39px;
148
+    z-index: 1;
149
+    text-align:center;
150
+    background-color:#087dba;
151
+}
152
+
153
+div#left {
154
+	display:block;
155
+    position: absolute;
156
+	left: 0px;
157
+    top: 0px;
158
+    width: 100px;
159
+    height: 39px;
160
+	background-image:url(../images/left1.png);
161
+	background-repeat:no-repeat;
162
+	margin: 0;
163
+	padding: 0;
164
+}
165
+
166
+div#leftlogo {
167
+    position:absolute;
168
+    top: 5px;
169
+    left: 15px;
170
+    background-image:url(../images/jitsilogo.png);
171
+    background-repeat:no-repeat;
172
+    height: 31px;
173
+    width: 68px;
174
+    z-index:1;
175
+}
176
+
177
+div#link {
178
+    display:block;
179
+    position:relative;
180
+    height:39px;
181
+    width:auto;
182
+    overflow: hidden;
183
+    z-index:0;
184
+}
185
+
186
+.button {
187
+    display: inline-block;
188
+    position: relative;
189
+    color: #FFFFFF;
190
+    top: 0;
191
+    padding: 10px 0px;
192
+    height: 19px;
193
+    width: 39px;
194
+    cursor: pointer;
195
+    font-size: 19px;
196
+    text-align: center;
197
+    text-shadow: 0px 1px 0px rgba(255,255,255,.3), 0px -1px 0px rgba(0,0,0,.7);
198
+}
199
+
200
+a.button:hover {
201
+    top: 0;
202
+    cursor: pointer;
203
+    background: rgba(0, 0, 0, 0.3);
204
+    border-radius: 5px;
205
+    background-clip: padding-box;
206
+    -webkit-border-radius: 5px;
207
+    -webkit-background-clip: padding-box;
208
+}
209
+
210
+.no-fa-video-camera, .fa-microphone-slash {
211
+    color: #636363;
212
+}
213
+
214
+.fade_line {
215
+    height: 1px;
216
+    background: black;
217
+    background: -webkit-gradient(linear, 0 0, 100% 0, from(#e9e9e9), to(#e9e9e9), color-stop(50%, black));
218
+}
219
+
220
+.header_button_separator {
221
+    display: inline-block;
222
+    position:relative;
223
+    top: 7;
224
+    width: 1px;
225
+    height: 25px;
226
+    background: white;
227
+    background: -webkit-gradient(linear, 0 0, 0 100%, from(#087dba), to(#087dba), color-stop(50%, white));
228
+}
229
+
230
+div#right {
231
+	display:block;
232
+    position:absolute;
233
+    right: 0px;
234
+    top: 0px;
235
+	background-image:url(../images/right1.png);
236
+	background-repeat:no-repeat;
237
+	margin:0;
238
+	padding:0;
239
+	width:100px;
240
+    height:39px;
241
+}
242
+div#rightlogo {
243
+    position:absolute;
244
+    top: 6px;
245
+    right: 15px;
246
+    background-image:url(../images/estoslogo.png);
247
+    background-repeat:no-repeat;
248
+    height: 25px;
249
+    width: 62px;
250
+    z-index:1;
251
+}
252
+
253
+input, textarea {
254
+    border: 0px none;
255
+    display: inline-block;
256
+    font-size: 14px;
257
+    padding: 5px;
258
+    background: #f3f3f3;
259
+    border-radius: 3px;
260
+    font-weight: 100;
261
+    line-height: 20px;
262
+    height: 40px;
263
+    color: #333;
264
+    font-weight: bold;
265
+    text-align: left;
266
+    border:1px solid #ACD8F0;
267
+    outline: none; /* removes the default outline */
268
+    resize: none; /* prevents the user-resizing, adjust to taste */
269
+}
270
+
271
+input, textarea:focus {
272
+    box-shadow: inset 0 0 3px 2px #ACD8F0; /* provides a more style-able
273
+                                         replacement to the outline */
274
+}
275
+
276
+textarea {
277
+    overflow: hidden;
278
+    word-wrap: break-word;
279
+    resize: horizontal;
280
+}
281
+
282
+button.no-icon {
283
+    padding: 0 1em;
284
+}
285
+
286
+button {
287
+    border: none;
288
+    height: 35px;
289
+    padding: 0 1em 0 2em;
290
+    position: relative;
291
+    border-radius: 3px;
292
+    font-weight: bold;
293
+    color: #fff;
294
+    line-height: 35px;
295
+    background: #2c8ad2;
296
+}
297
+
298
+button, input, select, textarea {
299
+    font-size: 100%;
300
+    margin: 0;
301
+    vertical-align: baseline;
302
+}
303
+
304
+button, input[type="button"], input[type="reset"], input[type="submit"] {
305
+    cursor: pointer;
306
+    -webkit-appearance: button;
307
+}
308
+
309
+form {
310
+    display: block;
311
+}
312
+
313
+/* Animated text area. */
314
+.animated {
315
+    -webkit-transition: height 0.2s;
316
+    -moz-transition: height 0.2s;
317
+    transition: height 0.2s;
318
+}

+ 22
- 0
css/modaldialog.css View File

@@ -0,0 +1,22 @@
1
+.jqistates h2 {
2
+    padding-bottom: 10px;
3
+    border-bottom: 1px solid #eee;
4
+    font-size: 18px;
5
+    line-height: 25px;
6
+    text-align: center;
7
+    color: #424242;
8
+}
9
+
10
+.jqistates input {
11
+    width: 100%;
12
+    margin: 20px 0;
13
+}
14
+
15
+.jqibuttons button {
16
+    margin-right: 5px;
17
+    float:right;
18
+}
19
+
20
+button.jqidefaultbutton #inviteLinkRef {
21
+    color: #2c8ad2;
22
+}

BIN
images/estoslogo.png View File


BIN
images/jitsilogo.png View File


+ 68
- 0
index.html View File

@@ -0,0 +1,68 @@
1
+<html>
2
+  <head>
3
+    <title>WebRTC, meet the Jitsi Videobridge</title>
4
+    <script src="//ajax.googleapis.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
5
+    <script src="libs/strophejingle.bundle.js"></script><!-- strophe.jingle bundle -->
6
+    <script src="libs/colibri.js"></script><!-- colibri focus implementation -->
7
+    <script src="muc.js"></script><!-- simple MUC library -->
8
+    <script src="app.js"></script><!-- application logic -->
9
+    <link href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" rel="stylesheet">
10
+    <link rel="stylesheet" type="text/css" media="screen" href="css/main.css" />
11
+    <link rel="stylesheet" href="css/jquery-impromptu.css">
12
+    <link rel="stylesheet" href="css/modaldialog.css">
13
+    <script src="libs/jquery-impromptu.js"></script>
14
+    <script src="libs/jquery.autosize.js"></script>
15
+    <script src="config.js"></script><!-- adapt to your needs, i.e. set hosts and bosh path -->
16
+  </head>
17
+  <body>
18
+    <div id="header">
19
+        <a href="http://jitsi.org" target="_blank"><div id="leftlogo"></div></a>
20
+        <a href="http://www.estos.com/" target="_blank"><div id="rightlogo"></div></a>
21
+        <div id="link">
22
+            <a class="button" onclick='buttonClick("#mute", "fa fa-microphone fa-lg fa fa-microphone-slash fa-lg");toggleAudio();'><i id="mute" title="Mute / unmute" class="fa fa-microphone fa-lg"></i></a>
23
+            <div class="header_button_separator"></div>
24
+            <a class="button" onclick='buttonClick("#video", "fa fa-video-camera fa-lg fa fa-video-camera no-fa-video-camera fa-lg");toggleVideo();'><i id="video" title="Start / stop camera" class="fa fa-video-camera fa-lg"></i></a>
25
+            <div class="header_button_separator"></div>
26
+            <a class="button" onclick="openLockDialog();"><i id="lockIcon" title="Lock/unlock room" class="fa fa-unlock fa-lg"></i></a>
27
+            <div class="header_button_separator"></div>
28
+            <a class="button" onclick="openLinkDialog();"><i title="Invite others" class="fa fa-link fa-lg"></i></a>
29
+            <div class="header_button_separator"></div>
30
+            <a class="button" onclick='openChat();'><i id="chat" title="Open chat" class="fa fa-comments fa-lg"></i></a>
31
+            <!--i class='fa fa-external-link'>&nbsp;</i>Others can join you by just going to <span id='roomurl'></span-->
32
+        </div>
33
+    </div>
34
+    <div id="settings">
35
+      <h1>Connection Settings</h1>
36
+      <form id="loginInfo">
37
+        <label>JID: <input id="jid" type="text" name="jid" placeholder="me@example.com"/></label>
38
+        <label>Password: <input id="password" type="password" name="password" placeholder="secret"/></label>
39
+        <label>BOSH URL: <input id="boshURL" type="text" name="boshURL" placeholder="/http-bind"/></label>
40
+        <input id="connect" type="submit" value="Connect" />
41
+      </form>
42
+    </div>
43
+
44
+    <div id="videospace">
45
+        <div class="fade_line"></div>
46
+        <video id="largeVideo" autoplay oncontextmenu="return false;"></video>
47
+        <div class="fade_line"></div>
48
+        <div id="remoteVideos">
49
+            <video id="localVideo" autoplay oncontextmenu="return false;" muted/>
50
+        </div>
51
+    </div>
52
+    <div id="chatspace">
53
+        <div id="nickname">
54
+            Enter a nickname in the box below
55
+            <form>
56
+                <input type='text' id="nickinput" placeholder='Choose a nickname' autofocus>
57
+            </form>
58
+        </div>
59
+        
60
+        <!--div><i class="fa fa-comments">&nbsp;</i><span class='nick'></span>:&nbsp;<span class='chattext'></span></div-->
61
+        <div id="chatconversation"></div>
62
+        <textarea id="usermsg" class= "animated" placeholder='Enter text...' autofocus></textarea>
63
+    </div>
64
+
65
+    <script>
66
+    </script>
67
+  </body>
68
+</html>

+ 814
- 0
libs/colibri.js View File

@@ -0,0 +1,814 @@
1
+/* colibri.js -- a COLIBRI focus 
2
+ * The colibri spec has been submitted to the XMPP Standards Foundation
3
+ * for publications as a XMPP extensions:
4
+ * http://xmpp.org/extensions/inbox/colibri.html
5
+ *
6
+ * colibri.js is a participating focus, i.e. the focus participates
7
+ * in the conference. The conference itself can be ad-hoc, through a
8
+ * MUC, through PubSub, etc.
9
+ *
10
+ * colibri.js relies heavily on the strophe.jingle library available 
11
+ * from https://github.com/ESTOS/strophe.jingle
12
+ * and interoperates with the Jitsi videobridge available from
13
+ * https://jitsi.org/Projects/JitsiVideobridge
14
+ */
15
+/*
16
+Copyright (c) 2013 ESTOS GmbH
17
+
18
+Permission is hereby granted, free of charge, to any person obtaining a copy
19
+of this software and associated documentation files (the "Software"), to deal
20
+in the Software without restriction, including without limitation the rights
21
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
22
+copies of the Software, and to permit persons to whom the Software is
23
+furnished to do so, subject to the following conditions:
24
+
25
+The above copyright notice and this permission notice shall be included in
26
+all copies or substantial portions of the Software.
27
+
28
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
29
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
30
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
31
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
32
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
33
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
34
+THE SOFTWARE.
35
+*/
36
+/* jshint -W117 */
37
+function ColibriFocus(connection, bridgejid) {
38
+    this.connection = connection;
39
+    this.bridgejid = bridgejid;
40
+    this.peers = [];
41
+    this.confid = null;
42
+
43
+    this.peerconnection = null;
44
+
45
+    this.sid = Math.random().toString(36).substr(2, 12);
46
+    this.connection.jingle.sessions[this.sid] = this;
47
+    this.mychannel = [];
48
+    this.channels = [];
49
+    this.remotessrc = {};
50
+
51
+    // ssrc lines to be added on next update
52
+    this.addssrc = [];
53
+    // ssrc lines to be removed on next update
54
+    this.removessrc = [];
55
+
56
+    // silly wait flag
57
+    this.wait = true;
58
+}
59
+
60
+// creates a conferences with an initial set of peers
61
+ColibriFocus.prototype.makeConference = function (peers) {
62
+    var ob = this;
63
+    if (this.confid !== null) {
64
+        console.error('makeConference called twice? Ignoring...');
65
+        // FIXME: just invite peers?
66
+        return;
67
+    }
68
+    this.confid = 0; // !null
69
+    this.peers = [];
70
+    peers.forEach(function (peer) {
71
+        ob.peers.push(peer);
72
+        ob.channels.push([]);
73
+    });
74
+
75
+    this.peerconnection = new RTC.peerconnection(this.connection.jingle.ice_config, this.connection.jingle.pc_constraints);
76
+    this.peerconnection.addStream(this.connection.jingle.localStream);
77
+    this.peerconnection.oniceconnectionstatechange = function (event) {
78
+        console.warn('ice connection state changed to', ob.peerconnection.iceConnectionState);
79
+        /*
80
+        if (ob.peerconnection.signalingState == 'stable' && ob.peerconnection.iceConnectionState == 'connected') {
81
+            console.log('adding new remote SSRCs from iceconnectionstatechange');
82
+            window.setTimeout(function() { ob.modifySources(); }, 1000);
83
+        }
84
+        */
85
+    };
86
+    this.peerconnection.onsignalingstatechange = function (event) {
87
+        console.warn(ob.peerconnection.signalingState);
88
+        /*
89
+        if (ob.peerconnection.signalingState == 'stable' && ob.peerconnection.iceConnectionState == 'connected') {
90
+            console.log('adding new remote SSRCs from signalingstatechange');
91
+            window.setTimeout(function() { ob.modifySources(); }, 1000);
92
+        }
93
+        */
94
+    };
95
+    this.peerconnection.onaddstream = function (event) {
96
+        ob.remoteStream = event.stream;
97
+        $(document).trigger('remotestreamadded.jingle', [event, ob.sid]);
98
+    };
99
+    this.peerconnection.onicecandidate = function (event) {
100
+        ob.sendIceCandidate(event.candidate);
101
+    };
102
+    this.peerconnection.createOffer(
103
+        function (offer) {
104
+            ob.peerconnection.setLocalDescription(
105
+                offer,
106
+                function () {
107
+                    // success
108
+                    // FIXME: could call _makeConference here and trickle candidates later
109
+                },
110
+                function (error) {
111
+                    console.log('setLocalDescription failed', error);
112
+                }
113
+            );
114
+        },
115
+        function (error) {
116
+            console.warn(error);
117
+        }
118
+    );
119
+    this.peerconnection.onicecandidate = function (event) {
120
+        console.log('candidate', event.candidate);
121
+        if (!event.candidate) {
122
+            console.log('end of candidates');
123
+            ob._makeConference();
124
+            return;
125
+        }
126
+    };
127
+};
128
+
129
+ColibriFocus.prototype._makeConference = function () {
130
+    var ob = this;
131
+    var elem = $iq({to: this.bridgejid, type: 'get'});
132
+    elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri'});
133
+
134
+    var localSDP = new SDP(this.peerconnection.localDescription.sdp);
135
+    var contents = SDPUtil.find_lines(localSDP.raw, 'a=mid:').map(SDPUtil.parse_mid);
136
+    localSDP.media.forEach(function (media, channel) {
137
+        var name = SDPUtil.parse_mid(SDPUtil.find_line(media, 'a=mid:'));
138
+        elem.c('content', {name: name});
139
+        elem.c('channel', {initiator: 'false', expire: '15'});
140
+
141
+        // FIXME: should reuse code from .toJingle
142
+        var mline = SDPUtil.parse_mline(media.split('\r\n')[0]);
143
+        for (var j = 0; j < mline.fmt.length; j++) {
144
+            var rtpmap = SDPUtil.find_line(media, 'a=rtpmap:' + mline.fmt[j]);
145
+            elem.c('payload-type', SDPUtil.parse_rtpmap(rtpmap));
146
+            elem.up();
147
+        }
148
+
149
+        // FIXME: should reuse code from .toJingle
150
+        elem.c('transport', {xmlns: 'urn:xmpp:jingle:transports:ice-udp:1'});
151
+        var tmp = SDPUtil.iceparams(media, localSDP.session);
152
+        if (tmp) {
153
+            elem.attrs(tmp);
154
+            var fingerprints = SDPUtil.find_lines(media, 'a=fingerprint:', localSDP.session);
155
+            fingerprints.forEach(function (line) {
156
+                tmp = SDPUtil.parse_fingerprint(line);
157
+                //tmp.xmlns = 'urn:xmpp:tmp:jingle:apps:dtls:0';
158
+                tmp.xmlns = 'urn:xmpp:jingle:apps:dtls:0';
159
+                elem.c('fingerprint').t(tmp.fingerprint);
160
+                delete tmp.fingerprint;
161
+                line = SDPUtil.find_line(media, 'a=setup:', ob.session);
162
+                if (line) {
163
+                    tmp.setup = line.substr(8);
164
+                }
165
+                elem.attrs(tmp);
166
+                elem.up();
167
+            });
168
+            // XEP-0176
169
+            if (SDPUtil.find_line(media, 'a=candidate:', localSDP.session)) { // add any a=candidate lines
170
+                lines = SDPUtil.find_lines(media, 'a=candidate:', localSDP.session);
171
+                for (j = 0; j < lines.length; j++) {
172
+                    tmp = SDPUtil.candidateToJingle(lines[j]);
173
+                    elem.c('candidate', tmp).up();
174
+                }
175
+            }
176
+            elem.up(); // end of transport
177
+        }
178
+        elem.up(); // end of channel
179
+        for (j = 0; j < ob.peers.length; j++) {
180
+            elem.c('channel', {initiator: 'true', expire:'15' }).up();
181
+        }
182
+        elem.up(); // end of content
183
+    });
184
+
185
+    this.connection.sendIQ(elem,
186
+        function (result) {
187
+            ob.createdConference(result);
188
+        },
189
+        function (error) {
190
+            console.warn(error);
191
+        }
192
+    );
193
+};
194
+
195
+// callback when a conference was created
196
+ColibriFocus.prototype.createdConference = function (result) {
197
+    console.log('created a conference on the bridge');
198
+    var tmp;
199
+
200
+    this.confid = $(result).find('>conference').attr('id');
201
+    var remotecontents = $(result).find('>conference>content').get();
202
+    for (var i = 0; i < remotecontents.length; i++) {
203
+        tmp = $(remotecontents[i]).find('>channel').get();
204
+        this.mychannel.push($(tmp.shift()));
205
+        for (j = 0; j < tmp.length; j++) {
206
+            if (this.channels[j] === undefined) {
207
+                this.channels[j] = [];
208
+            }
209
+            this.channels[j].push(tmp[j]);
210
+        }
211
+    }
212
+    console.log('remote channels', this.channels);
213
+
214
+    // establish our channel with the bridge
215
+    // static answer taken from chrome M31, should be replaced by a 
216
+    // dynamic one that is based on our offer FIXME
217
+    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');
218
+    // get the mixed ssrc
219
+    for (var channel = 0; channel < remotecontents.length; channel++) {
220
+        tmp = $(this.mychannel[channel]).find('>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
221
+        // FIXME: check rtp-level-relay-type
222
+        if (tmp.length) {
223
+            bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'cname:mixed' + '\r\n';
224
+            bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'label:mixedlabela0' + '\r\n';
225
+            bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'msid:mixedmslabela0 mixedlabela0' + '\r\n';
226
+            bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'mslabel:mixedmslabela0' + '\r\n';
227
+        } else {
228
+            // make chrome happy... '3735928559' == 0xDEADBEEF
229
+            bridgeSDP.media[channel] += 'a=ssrc:' + '3735928559' + ' ' + 'cname:mixed' + '\r\n';
230
+            bridgeSDP.media[channel] += 'a=ssrc:' + '3735928559' + ' ' + 'label:mixedlabelv0' + '\r\n';
231
+            bridgeSDP.media[channel] += 'a=ssrc:' + '3735928559' + ' ' + 'msid:mixedmslabelv0 mixedlabelv0' + '\r\n';
232
+            bridgeSDP.media[channel] += 'a=ssrc:' + '3735928559' + ' ' + 'mslabel:mixedmslabelv0' + '\r\n';
233
+        }
234
+
235
+        // FIXME: should take code from .fromJingle
236
+        tmp = $(this.mychannel[channel]).find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]');
237
+        if (tmp.length) {
238
+            bridgeSDP.media[channel] += 'a=ice-ufrag:' + tmp.attr('ufrag') + '\r\n';
239
+            bridgeSDP.media[channel] += 'a=ice-pwd:' + tmp.attr('pwd') + '\r\n';
240
+            tmp.find('>candidate').each(function () {
241
+                bridgeSDP.media[channel] += SDPUtil.candidateFromJingle(this);
242
+            });
243
+            tmp = tmp.find('>fingerprint');
244
+            if (tmp.length) {
245
+                bridgeSDP.media[channel] += 'a=fingerprint:' + tmp.attr('hash') + ' ' + tmp.text() + '\r\n';
246
+                if (tmp.attr('setup')) {
247
+                    bridgeSDP.media[channel] += 'a=setup:' + tmp.attr('setup') + '\r\n';
248
+                }
249
+            }
250
+        }
251
+    }
252
+    bridgeSDP.raw = bridgeSDP.session + bridgeSDP.media.join('');
253
+
254
+    var ob = this;
255
+    this.peerconnection.setRemoteDescription(
256
+        new RTCSessionDescription({type: 'answer', sdp: bridgeSDP.raw}),
257
+        function () {
258
+            console.log('setRemoteDescription success');
259
+            // remote channels == remotecontents length - 1!
260
+            for (var i = 0; i < remotecontents.length - 1; i++) {
261
+                ob.initiate(ob.peers[i], true);
262
+            }
263
+        },
264
+        function (error) {
265
+            console.log('setRemoteDescription failed');
266
+        }
267
+    );
268
+
269
+};
270
+
271
+// send a session-initiate to a new participant
272
+ColibriFocus.prototype.initiate = function (peer, isInitiator) {
273
+    var participant = this.peers.indexOf(peer);
274
+    console.log('tell', peer, participant);
275
+    var sdp;
276
+    if (this.peerconnection != null && this.peerconnection.signalingState == 'stable') {
277
+        sdp = new SDP(this.peerconnection.remoteDescription.sdp);
278
+        var localSDP = new SDP(this.peerconnection.localDescription.sdp);
279
+        // throw away stuff we don't want
280
+        // not needed with static offer
281
+        sdp.removeSessionLines('a=group:');
282
+        sdp.removeSessionLines('a=msid-semantic:'); // FIXME: not mapped over jingle anyway...
283
+        for (var i = 0; i < sdp.media.length; i++) {
284
+            sdp.removeMediaLines(i, 'a=rtcp-mux');
285
+            sdp.removeMediaLines(i, 'a=ssrc:');
286
+            sdp.removeMediaLines(i, 'a=crypto:');
287
+            sdp.removeMediaLines(i, 'a=candidate:');
288
+            sdp.removeMediaLines(i, 'a=ice-options:google-ice');
289
+            sdp.removeMediaLines(i, 'a=ice-ufrag:');
290
+            sdp.removeMediaLines(i, 'a=ice-pwd:');
291
+            sdp.removeMediaLines(i, 'a=fingerprint:');
292
+            sdp.removeMediaLines(i, 'a=setup:');
293
+
294
+            // re-add all remote a=ssrcs
295
+            for (var jid in this.remotessrc) {
296
+                if (jid == peer) continue;
297
+                sdp.media[i] += this.remotessrc[jid][i];
298
+            }
299
+            // and local a=ssrc lines
300
+            sdp.media[i] += SDPUtil.find_lines(localSDP.media[i], 'a=ssrc').join('\r\n') + '\r\n';
301
+        }
302
+        sdp.raw = sdp.session + sdp.media.join('');
303
+    } else {
304
+        console.error('can not initiate a new session without a stable peerconnection');
305
+        return;
306
+    }
307
+
308
+    // add stuff we got from the bridge
309
+    for (var j = 0; j < sdp.media.length; j++) {
310
+        var chan = $(this.channels[participant][j]);
311
+        console.log('channel id', chan.attr('id'));
312
+
313
+        tmp = chan.find('>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
314
+        if (tmp.length) {
315
+            sdp.media[j] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'cname:mixed' + '\r\n';
316
+            sdp.media[j] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'label:mixedlabela0' + '\r\n';
317
+            sdp.media[j] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'msid:mixedmslabela0 mixedlabela0' + '\r\n';
318
+            sdp.media[j] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'mslabel:mixedmslabela0' + '\r\n';
319
+        } else {
320
+            // make chrome happy... '3735928559' == 0xDEADBEEF
321
+            sdp.media[j] += 'a=ssrc:' + '3735928559' + ' ' + 'cname:mixed' + '\r\n';
322
+            sdp.media[j] += 'a=ssrc:' + '3735928559' + ' ' + 'label:mixedlabelv0' + '\r\n';
323
+            sdp.media[j] += 'a=ssrc:' + '3735928559' + ' ' + 'msid:mixedmslabelv0 mixedlabelv0' + '\r\n';
324
+            sdp.media[j] += 'a=ssrc:' + '3735928559' + ' ' + 'mslabel:mixedmslabelv0' + '\r\n';
325
+        }
326
+
327
+        tmp = chan.find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]');
328
+        if (tmp.length) {
329
+            if (tmp.attr('ufrag'))
330
+                sdp.media[j] += 'a=ice-ufrag:' + tmp.attr('ufrag') + '\r\n';
331
+            if (tmp.attr('pwd'))
332
+                sdp.media[j] += 'a=ice-pwd:' + tmp.attr('pwd') + '\r\n';
333
+            // and the candidates...
334
+            tmp.find('>candidate').each(function () {
335
+                sdp.media[j] += SDPUtil.candidateFromJingle(this);
336
+            });
337
+            tmp = tmp.find('>fingerprint');
338
+            if (tmp.length) {
339
+                sdp.media[j] += 'a=fingerprint:' + tmp.attr('hash') + ' ' + tmp.text() + '\r\n';
340
+                /*
341
+                if (tmp.attr('direction')) {
342
+                    sdp.media[j] += 'a=setup:' + tmp.attr('direction') + '\r\n';
343
+                }
344
+                */
345
+                sdp.media[j] += 'a=setup:actpass\r\n';
346
+            }
347
+        }
348
+    }
349
+    // make a new colibri session and configure it
350
+    // FIXME: is it correct to use this.connection.jid when used in a MUC?
351
+    var sess = new ColibriSession(this.connection.jid,
352
+                                  Math.random().toString(36).substr(2, 12), // random string
353
+                                  this.connection);
354
+    sess.initiate(peer);
355
+    sess.colibri = this;
356
+    sess.localStream = this.connection.jingle.localStream;
357
+    sess.media_constraints = this.connection.jingle.media_constraints;
358
+    sess.pc_constraints = this.connection.jingle.pc_constraints;
359
+    sess.ice_config = this.connection.ice_config;
360
+
361
+    this.connection.jingle.sessions[sess.sid] = sess;
362
+    this.connection.jingle.jid2session[sess.peerjid] = sess;
363
+
364
+    // send a session-initiate
365
+    var init = $iq({to: peer, type: 'set'})
366
+        .c('jingle',
367
+            {xmlns: 'urn:xmpp:jingle:1',
368
+             action: 'session-initiate',
369
+             initiator: sess.me,
370
+             sid: sess.sid
371
+            }
372
+    );
373
+    sdp.toJingle(init, 'initiator');
374
+    this.connection.sendIQ(init,
375
+        function (res) {
376
+            console.log('got result');
377
+        },
378
+        function (err) {
379
+            console.log('got error');
380
+        }
381
+    );
382
+};
383
+
384
+// pull in a new participant into the conference
385
+ColibriFocus.prototype.addNewParticipant = function (peer) {
386
+    var ob = this;
387
+    if (this.confid === 0) {
388
+        // bad state
389
+        console.log('confid does not exist yet, postponing', peer);
390
+        window.setTimeout(function () {
391
+            ob.addNewParticipant(peer);
392
+        }, 250);
393
+        return;
394
+    }
395
+    var index = this.channels.length;
396
+    this.channels.push([]);
397
+    this.peers.push(peer);
398
+
399
+    var elem = $iq({to: this.bridgejid, type: 'get'});
400
+    elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
401
+    var localSDP = new SDP(this.peerconnection.localDescription.sdp);
402
+    var contents = SDPUtil.find_lines(localSDP.raw, 'a=mid:').map(SDPUtil.parse_mid);
403
+    contents.forEach(function (name) {
404
+        elem.c('content', {name: name});
405
+        elem.c('channel', {initiator: 'true', expire:'15'});
406
+        elem.up(); // end of channel
407
+        elem.up(); // end of content
408
+    });
409
+
410
+    this.connection.sendIQ(elem,
411
+        function (result) {
412
+            var contents = $(result).find('>conference>content').get();
413
+            for (var i = 0; i < contents.length; i++) {
414
+                tmp = $(contents[i]).find('>channel').get();
415
+                ob.channels[index][i] = tmp[0];
416
+            }
417
+            ob.initiate(peer, true);
418
+        },
419
+        function (error) {
420
+            console.warn(error);
421
+        }
422
+    );
423
+};
424
+
425
+// update the channel description (payload-types + dtls fp) for a participant
426
+ColibriFocus.prototype.updateChannel = function (remoteSDP, participant) {
427
+    console.log('change allocation for', this.confid);
428
+    var change = $iq({to: this.bridgejid, type: 'set'});
429
+    change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
430
+    for (channel = 0; channel < this.channels[participant].length; channel++) {
431
+        change.c('content', {name: channel === 0 ? 'audio' : 'video'});
432
+        change.c('channel', {id: $(this.channels[participant][channel]).attr('id')});
433
+
434
+        var rtpmap = SDPUtil.find_lines(remoteSDP.media[channel], 'a=rtpmap:');
435
+        rtpmap.forEach(function (val) {
436
+            // TODO: too much copy-paste
437
+            var rtpmap = SDPUtil.parse_rtpmap(val);
438
+            change.c('payload-type', rtpmap);
439
+            // 
440
+            // put any 'a=fmtp:' + mline.fmt[j] lines into <param name=foo value=bar/>
441
+            /*
442
+            if (SDPUtil.find_line(remoteSDP.media[channel], 'a=fmtp:' + rtpmap.id)) {
443
+                tmp = SDPUtil.parse_fmtp(SDPUtil.find_line(remoteSDP.media[channel], 'a=fmtp:' + rtpmap.id));
444
+                for (var k = 0; k < tmp.length; k++) {
445
+                    change.c('parameter', tmp[k]).up();
446
+                }
447
+            }
448
+            */
449
+            change.up();
450
+        });
451
+
452
+        // now add transport
453
+        change.c('transport', {xmlns: 'urn:xmpp:jingle:transports:ice-udp:1'});
454
+        var fingerprints = SDPUtil.find_lines(remoteSDP.media[channel], 'a=fingerprint:', remoteSDP.session);
455
+        fingerprints.forEach(function (line) {
456
+            tmp = SDPUtil.parse_fingerprint(line);
457
+            tmp.xmlns = 'urn:xmpp:jingle:apps:dtls:0';
458
+            change.c('fingerprint').t(tmp.fingerprint);
459
+            delete tmp.fingerprint;
460
+            line = SDPUtil.find_line(remoteSDP.media[channel], 'a=setup:', remoteSDP.session);
461
+            if (line) {
462
+                tmp.setup = line.substr(8);
463
+            }
464
+            change.attrs(tmp);
465
+            change.up();
466
+        });
467
+        var candidates = SDPUtil.find_lines(remoteSDP.media[channel], 'a=candidate:', remoteSDP.session);
468
+        candidates.forEach(function (line) {
469
+            var tmp = SDPUtil.candidateToJingle(line);
470
+            change.c('candidate', tmp).up();
471
+        });
472
+        tmp = SDPUtil.iceparams(remoteSDP.media[channel], remoteSDP.session);
473
+        if (tmp) {
474
+            change.attrs(tmp);
475
+
476
+        }
477
+        change.up(); // end of transport
478
+        change.up(); // end of channel
479
+        change.up(); // end of content
480
+    }
481
+    this.connection.sendIQ(change,
482
+        function (res) {
483
+            console.log('got result');
484
+        },
485
+        function (err) {
486
+            console.log('got error');
487
+        }
488
+    );
489
+};
490
+
491
+// tell everyone about a new participants a=ssrc lines (isadd is true)
492
+// or a leaving participants a=ssrc lines
493
+// FIXME: should not take an SDP, but rather the a=ssrc lines and probably a=mid
494
+ColibriFocus.prototype.sendSSRCUpdate = function (sdp, exclude, isadd) {
495
+    var ob = this;
496
+    this.peers.forEach(function (peerjid) {
497
+        if (peerjid == exclude) return;
498
+        console.log('tell', peerjid, 'about ' + (isadd ? 'new' : 'removed') + ' ssrcs from', exclude);
499
+        if (!ob.remotessrc[peerjid]) {
500
+            // FIXME: this should only send to participants that are stable, i.e. who have sent a session-accept
501
+            // possibly, this.remoteSSRC[session.peerjid] does not exist yet
502
+            console.warn('do we really want to bother', peerjid, 'with updates yet?');
503
+        }
504
+        var channel;
505
+        var peersess = ob.connection.jingle.jid2session[peerjid];
506
+        var modify = $iq({to: peerjid, type: 'set'})
507
+            .c('jingle', {
508
+                xmlns: 'urn:xmpp:jingle:1',
509
+                action: isadd ? 'addsource' : 'removesource',
510
+                initiator: peersess.initiator,
511
+                sid: peersess.sid
512
+            }
513
+        );
514
+        for (channel = 0; channel < sdp.media.length; channel++) {
515
+            tmp = SDPUtil.find_lines(sdp.media[channel], 'a=ssrc:');
516
+            modify.c('content', {name: SDPUtil.parse_mid(SDPUtil.find_line(sdp.media[channel], 'a=mid:'))});
517
+            modify.c('source', { xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
518
+            // FIXME: not completly sure this operates on blocks and / or handles different ssrcs correctly
519
+            tmp.forEach(function (line) {
520
+                var idx = line.indexOf(' ');
521
+                var linessrc = line.substr(0, idx).substr(7);
522
+                modify.attrs({ssrc: linessrc});
523
+
524
+                var kv = line.substr(idx + 1);
525
+                modify.c('parameter');
526
+                if (kv.indexOf(':') == -1) {
527
+                    modify.attrs({ name: kv });
528
+                } else {
529
+                    modify.attrs({ name: kv.split(':', 2)[0] });
530
+                    modify.attrs({ value: kv.split(':', 2)[1] });
531
+                }
532
+                modify.up();
533
+            });
534
+            modify.up(); // end of source
535
+            modify.up(); // end of content
536
+        }
537
+        ob.connection.sendIQ(modify,
538
+            function (res) {
539
+                console.warn('got modify result');
540
+            },
541
+            function (err) {
542
+                console.warn('got modify error');
543
+            }
544
+        );
545
+    });
546
+};
547
+
548
+ColibriFocus.prototype.setRemoteDescription = function (session, elem, desctype) {
549
+    var participant = this.peers.indexOf(session.peerjid);
550
+    console.log('Colibri.setRemoteDescription from', session.peerjid, participant);
551
+    var ob = this;
552
+    var remoteSDP = new SDP('');
553
+    var tmp;
554
+    var channel;
555
+    remoteSDP.fromJingle(elem);
556
+
557
+    // ACT 1: change allocation on bridge
558
+    this.updateChannel(remoteSDP, participant);
559
+
560
+    // ACT 2: tell anyone else about the new SSRCs
561
+    this.sendSSRCUpdate(remoteSDP, session.peerjid, true);
562
+
563
+    // ACT 3: note the SSRCs
564
+    this.remotessrc[session.peerjid] = [];
565
+    for (channel = 0; channel < this.channels[participant].length; channel++) {
566
+        this.remotessrc[session.peerjid][channel] = SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:').join('\r\n') + '\r\n';
567
+    }
568
+
569
+    // ACT 4: add new a=ssrc lines to local remotedescription
570
+    for (channel = 0; channel < this.channels[participant].length; channel++) {
571
+        if (!this.addssrc[channel]) this.addssrc[channel] = '';
572
+        this.addssrc[channel] += SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:').join('\r\n') + '\r\n';
573
+    }
574
+    this.modifySources();
575
+};
576
+
577
+// relay ice candidates to bridge using trickle
578
+ColibriFocus.prototype.addIceCandidate = function (session, elem) {
579
+    var ob = this;
580
+    var participant = this.peers.indexOf(session.peerjid);
581
+    console.log('change transport allocation for', this.confid, session.peerjid, participant);
582
+    var change = $iq({to: this.bridgejid, type: 'set'});
583
+    change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
584
+    $(elem).each(function () {
585
+        var name = $(this).attr('name');
586
+        var channel = name == 'audio' ? 0 : 1; // FIXME: search mlineindex in localdesc
587
+
588
+        change.c('content', {name: name});
589
+        change.c('channel', {id: $(ob.channels[participant][channel]).attr('id')});
590
+        $(this).find('>transport').each(function () {
591
+            change.c('transport', {
592
+                ufrag: $(this).attr('ufrag'),
593
+                pwd: $(this).attr('pwd'),
594
+                xmlns: $(this).attr('xmlns')
595
+            });
596
+
597
+            $(this).find('>candidate').each(function () {
598
+                /* not yet
599
+                if (this.getAttribute('protocol') == 'tcp' && this.getAttribute('port') == 0) {
600
+                    // chrome generates TCP candidates with port 0
601
+                    return;
602
+                }
603
+                */
604
+                var line = SDPUtil.candidateFromJingle(this);
605
+                change.c('candidate', SDPUtil.candidateToJingle(line)).up();
606
+            });
607
+            change.up(); // end of transport
608
+        });
609
+        change.up(); // end of channel
610
+        change.up(); // end of content
611
+    });
612
+    // FIXME: need to check if there is at least one candidate when filtering TCP ones
613
+    this.connection.sendIQ(change,
614
+        function (res) {
615
+            console.log('got result');
616
+        },
617
+        function (err) {
618
+            console.warn('got error');
619
+        }
620
+    );
621
+};
622
+
623
+// send our own candidate to the bridge
624
+ColibriFocus.prototype.sendIceCandidate = function (candidate) {
625
+    //console.log('candidate', candidate);
626
+    if (!candidate) {
627
+        console.log('end of candidates');
628
+        return;
629
+    }
630
+    var mycands = $iq({to: this.bridgejid, type: 'set'});
631
+    mycands.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
632
+    mycands.c('content', {name: candidate.sdpMid });
633
+    mycands.c('channel', {id: $(this.mychannel[candidate.sdpMLineIndex]).attr('id')});
634
+    mycands.c('transport', {xmlns: 'urn:xmpp:jingle:transports:ice-udp:1'});
635
+    tmp = SDPUtil.candidateToJingle(candidate.candidate);
636
+    mycands.c('candidate', tmp).up();
637
+    this.connection.sendIQ(mycands,
638
+        function (res) {
639
+            console.log('got result');
640
+        },
641
+        function (err) {
642
+            console.warn('got error');
643
+        }
644
+    );
645
+};
646
+
647
+ColibriFocus.prototype.terminate = function (session, reason) {
648
+    console.log('remote session terminated from', session.peerjid);
649
+    var participant = this.peers.indexOf(session.peerjid);
650
+    if (!this.remotessrc[session.peerjid] || participant == -1) {
651
+        return;
652
+    }
653
+    console.log('remote ssrcs:', this.remotessrc[session.peerjid]);
654
+    var ssrcs = this.remotessrc[session.peerjid];
655
+    for (var i = 0; i < ssrcs.length; i++) {
656
+        if (!this.removessrc[i]) this.removessrc[i] = '';
657
+        this.removessrc[i] += ssrcs[i];
658
+    }
659
+    // remove from this.peers
660
+    this.peers.splice(participant, 1);
661
+    // expire channel on bridge
662
+    var change = $iq({to: this.bridgejid, type: 'set'});
663
+    change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
664
+    for (var channel = 0; channel < this.channels[participant].length; channel++) {
665
+        change.c('content', {name: channel === 0 ? 'audio' : 'video'});
666
+        change.c('channel', {id: $(this.channels[participant][channel]).attr('id'), expire: '0'});
667
+        change.up(); // end of channel
668
+        change.up(); // end of content
669
+    }
670
+    this.connection.sendIQ(change,
671
+        function (res) {
672
+            console.log('got result');
673
+        },
674
+        function (err) {
675
+            console.log('got error');
676
+        }
677
+    );
678
+    // and remove from channels
679
+    this.channels.splice(participant, 1);
680
+
681
+    // tell everyone about the ssrcs to be removed
682
+    var sdp = new SDP('');
683
+    var localSDP = new SDP(this.peerconnection.localDescription.sdp);
684
+    var contents = SDPUtil.find_lines(localSDP.raw, 'a=mid:').map(SDPUtil.parse_mid);
685
+    for (var j = 0; j < ssrcs.length; j++) {
686
+        sdp.media[j] = 'a=mid:' + contents[j] + '\r\n';
687
+        sdp.media[j] += ssrcs[j];
688
+        this.removessrc[j] += ssrcs[j];
689
+    }
690
+    this.sendSSRCUpdate(sdp, session.peerjid, false);
691
+
692
+    delete this.remotessrc[session.peerjid];
693
+    this.modifySources();
694
+};
695
+
696
+ColibriFocus.prototype.modifySources = function () {
697
+    var ob = this;
698
+    if (!(this.addssrc.length || this.removessrc.length)) return;
699
+    if (this.peerconnection.signalingState == 'closed') return;
700
+
701
+    // FIXME: this is a big hack
702
+    // https://code.google.com/p/webrtc/issues/detail?id=2688
703
+    if (!(this.peerconnection.signalingState == 'stable' && this.peerconnection.iceConnectionState == 'connected')) {
704
+        console.warn('modifySources not yet', this.peerconnection.signalingState, this.peerconnection.iceConnectionState);
705
+        window.setTimeout(function () { ob.modifySources(); }, 250);
706
+        this.wait = true;
707
+        return;
708
+    }
709
+    if (this.wait) {
710
+        window.setTimeout(function () { ob.modifySources(); }, 2500);
711
+        this.wait = false;
712
+        return;
713
+    }
714
+    var sdp = new SDP(this.peerconnection.remoteDescription.sdp);
715
+
716
+    // add sources
717
+    this.addssrc.forEach(function (lines, idx) {
718
+        sdp.media[idx] += lines;
719
+    });
720
+    this.addssrc = [];
721
+
722
+    // remove sources
723
+    this.removessrc.forEach(function (lines, idx) {
724
+        lines = lines.split('\r\n');
725
+        lines.pop(); // remove empty last element;
726
+        lines.forEach(function (line) {
727
+            sdp.media[idx] = sdp.media[idx].replace(line + '\r\n', '');
728
+        });
729
+    });
730
+    this.removessrc = [];
731
+
732
+    sdp.raw = sdp.session + sdp.media.join('');
733
+    this.peerconnection.setRemoteDescription(
734
+        new RTCSessionDescription({type: 'offer', sdp: sdp.raw }),
735
+        function () {
736
+            console.log('setModifiedRemoteDescription ok');
737
+            ob.peerconnection.createAnswer(
738
+                function (modifiedAnswer) {
739
+                    console.log('modifiedAnswer created');
740
+                    // FIXME: pushing down an answer while ice connection state 
741
+                    // is still checking is bad...
742
+                    console.log(ob.peerconnection.iceConnectionState);
743
+                    ob.peerconnection.setLocalDescription(modifiedAnswer,
744
+                        function () {
745
+                            console.log('setModifiedLocalDescription ok');
746
+                        },
747
+                        function (error) {
748
+                            console.log('setModifiedLocalDescription failed');
749
+                        }
750
+                    );
751
+                },
752
+                function (error) {
753
+                    console.log('createModifiedAnswer failed');
754
+                }
755
+            );
756
+        },
757
+        function (error) {
758
+            console.log('setModifiedRemoteDescription failed');
759
+        }
760
+    );
761
+};
762
+
763
+
764
+// A colibri session is similar to a jingle session, it just implements some things differently
765
+// FIXME: inherit jinglesession, see https://github.com/legastero/Jingle-RTCPeerConnection/blob/master/index.js
766
+function ColibriSession(me, sid, connection) {
767
+    this.me = me;
768
+    this.sid = sid;
769
+    this.connection = connection;
770
+    //this.peerconnection = null;
771
+    //this.mychannel = null;
772
+    //this.channels = null;
773
+    this.peerjid = null;
774
+
775
+    this.colibri = null;
776
+}
777
+
778
+// implementation of JingleSession interface
779
+ColibriSession.prototype.initiate = function (peerjid, isInitiator) {
780
+    this.peerjid = peerjid;
781
+};
782
+
783
+ColibriSession.prototype.sendOffer = function (offer) {
784
+    console.log('ColibriSession.sendOffer');
785
+};
786
+
787
+
788
+ColibriSession.prototype.accept = function () {
789
+    console.log('ColibriSession.accept');
790
+};
791
+
792
+ColibriSession.prototype.terminate = function (reason) {
793
+    this.colibri.terminate(this, reason);
794
+};
795
+
796
+ColibriSession.prototype.active = function () {
797
+    console.log('ColibriSession.active');
798
+};
799
+
800
+ColibriSession.prototype.setRemoteDescription = function (elem, desctype) {
801
+    this.colibri.setRemoteDescription(this, elem, desctype);
802
+};
803
+
804
+ColibriSession.prototype.addIceCandidate = function (elem) {
805
+    this.colibri.addIceCandidate(this, elem);
806
+};
807
+
808
+ColibriSession.prototype.sendAnswer = function (sdp, provisional) {
809
+    console.log('ColibriSession.sendAnswer');
810
+};
811
+
812
+ColibriSession.prototype.sendTerminate = function (reason, text) {
813
+    console.log('ColibriSession.sendTerminate');
814
+};

+ 666
- 0
libs/jquery-impromptu.js View File

@@ -0,0 +1,666 @@
1
+/*! jQuery-Impromptu - v5.1.1
2
+* http://trentrichardson.com/Impromptu
3
+* Copyright (c) 2013 Trent Richardson; Licensed MIT */
4
+(function($) {
5
+	"use strict";
6
+
7
+	/**
8
+	* setDefaults - Sets the default options
9
+	* @param message String/Object - String of html or Object of states
10
+	* @param options Object - Options to set the prompt
11
+	* @return jQuery - container with overlay and prompt 
12
+	*/
13
+	$.prompt = function(message, options) {
14
+		// only for backwards compat, to be removed in future version
15
+		if(options !== undefined && options.classes !== undefined && typeof options.classes === 'string'){
16
+			options = { box: options.classes };
17
+		}
18
+
19
+		$.prompt.options = $.extend({},$.prompt.defaults,options);
20
+		$.prompt.currentPrefix = $.prompt.options.prefix;
21
+		
22
+		// Be sure any previous timeouts are destroyed
23
+		if($.prompt.timeout){
24
+			clearTimeout($.prompt.timeout);
25
+		}
26
+		$.prompt.timeout = false;
27
+
28
+		var opts = $.prompt.options,
29
+			$body = $(document.body),
30
+			$window = $(window);
31
+					
32
+		//build the box and fade
33
+		var msgbox = '<div class="'+ $.prompt.options.prefix +'box '+ opts.classes.box +'">';
34
+		if(opts.useiframe && ($('object, applet').length > 0)) {
35
+			msgbox += '<iframe src="javascript:false;" style="display:block;position:absolute;z-index:-1;" class="'+ opts.prefix +'fade '+ opts.classes.fade +'"></iframe>';
36
+		} else {
37
+			msgbox +='<div class="'+ opts.prefix +'fade '+ opts.classes.fade +'"></div>';
38
+		}
39
+		msgbox += '<div class="'+ opts.prefix +' '+ opts.classes.prompt +'">'+
40
+					'<form action="javascript:false;" onsubmit="return false;" class="'+ opts.prefix +'form">'+
41
+						'<div class="'+ opts.prefix +'close '+ opts.classes.close +'">'+ opts.closeText +'</div>'+
42
+						'<div class="'+ opts.prefix +'states"></div>'+
43
+					'</form>'+
44
+				'</div>'+
45
+			'</div>';
46
+
47
+		$.prompt.jqib = $(msgbox).appendTo($body);
48
+		$.prompt.jqi = $.prompt.jqib.children('.'+ opts.prefix);//.data('jqi',opts);
49
+		$.prompt.jqif = $.prompt.jqib.children('.'+ opts.prefix +'fade');
50
+
51
+		//if a string was passed, convert to a single state
52
+		if(message.constructor === String){
53
+			message = {
54
+				state0: {
55
+					title: opts.title,
56
+					html: message,
57
+					buttons: opts.buttons,
58
+					position: opts.position,
59
+					focus: opts.focus,
60
+					submit: opts.submit
61
+				}
62
+			};
63
+		}
64
+
65
+		//build the states
66
+		$.prompt.options.states = {};
67
+		var k,v;
68
+		for(k in message){
69
+			v = $.extend({},$.prompt.defaults.state,{name:k},message[k]);
70
+			$.prompt.addState(v.name, v);
71
+
72
+			if($.prompt.currentStateName === ''){
73
+				$.prompt.currentStateName = v.name;
74
+			}
75
+		}
76
+
77
+		// Go ahead and transition to the first state. It won't be visible just yet though until we show the prompt
78
+		var $firstState = $.prompt.jqi.find('.'+ opts.prefix +'states .'+ opts.prefix +'state').eq(0);
79
+		$.prompt.goToState($firstState.data('jqi-name'));
80
+
81
+		//Events
82
+		$.prompt.jqi.on('click', '.'+ opts.prefix +'buttons button', function(e){
83
+			var $t = $(this),
84
+				$state = $t.parents('.'+ opts.prefix +'state'),
85
+				stateobj = $.prompt.options.states[$state.data('jqi-name')],
86
+				msg = $state.children('.'+ opts.prefix +'message'),
87
+				clicked = stateobj.buttons[$t.text()] || stateobj.buttons[$t.html()],
88
+				forminputs = {};
89
+
90
+			// if for some reason we couldn't get the value
91
+			if(clicked === undefined){
92
+				for(var i in stateobj.buttons){
93
+					if(stateobj.buttons[i].title === $t.text() || stateobj.buttons[i].title === $t.html()){
94
+						clicked = stateobj.buttons[i].value;
95
+					}
96
+				}
97
+			}
98
+
99
+			//collect all form element values from all states.
100
+			$.each($.prompt.jqi.children('form').serializeArray(),function(i,obj){
101
+				if (forminputs[obj.name] === undefined) {
102
+					forminputs[obj.name] = obj.value;
103
+				} else if (typeof forminputs[obj.name] === Array || typeof forminputs[obj.name] === 'object') {
104
+					forminputs[obj.name].push(obj.value);
105
+				} else {
106
+					forminputs[obj.name] = [forminputs[obj.name],obj.value];	
107
+				} 
108
+			});
109
+
110
+			// trigger an event
111
+			var promptsubmite = new $.Event('impromptu:submit');
112
+			promptsubmite.stateName = stateobj.name;
113
+			promptsubmite.state = $state;
114
+			$state.trigger(promptsubmite, [clicked, msg, forminputs]);
115
+			
116
+			if(!promptsubmite.isDefaultPrevented()){
117
+				$.prompt.close(true, clicked,msg,forminputs);
118
+			}
119
+		});
120
+
121
+		// if the fade is clicked blink the prompt
122
+		var fadeClicked = function(){
123
+			if(opts.persistent){
124
+				var offset = (opts.top.toString().indexOf('%') >= 0? ($window.height()*(parseInt(opts.top,10)/100)) : parseInt(opts.top,10)),
125
+					top = parseInt($.prompt.jqi.css('top').replace('px',''),10) - offset;
126
+
127
+				//$window.scrollTop(top);
128
+				$('html,body').animate({ scrollTop: top }, 'fast', function(){
129
+					var i = 0;
130
+					$.prompt.jqib.addClass(opts.prefix +'warning');
131
+					var intervalid = setInterval(function(){
132
+						$.prompt.jqib.toggleClass(opts.prefix +'warning');
133
+						if(i++ > 1){
134
+							clearInterval(intervalid);
135
+							$.prompt.jqib.removeClass(opts.prefix +'warning');
136
+						}
137
+					}, 100);
138
+				});
139
+			}
140
+			else {
141
+				$.prompt.close(true);
142
+			}
143
+		};
144
+		
145
+		// listen for esc or tab keys
146
+		var keyPressEventHandler = function(e){
147
+			var key = (window.event) ? event.keyCode : e.keyCode;
148
+			
149
+			//escape key closes
150
+			if(key===27) {
151
+				fadeClicked();	
152
+			}
153
+			
154
+			//constrain tabs, tabs should iterate through the state and not leave
155
+			if (key === 9){
156
+				var $inputels = $('input,select,textarea,button',$.prompt.getCurrentState());
157
+				var fwd = !e.shiftKey && e.target === $inputels[$inputels.length-1];
158
+				var back = e.shiftKey && e.target === $inputels[0];
159
+				if (fwd || back) {
160
+					setTimeout(function(){ 
161
+						if (!$inputels){
162
+							return;
163
+						}
164
+						var el = $inputels[back===true ? $inputels.length-1 : 0];
165
+
166
+						if (el){
167
+							el.focus();
168
+						}
169
+					},10);
170
+					return false;
171
+				}
172
+			}
173
+		};
174
+		
175
+		$.prompt.position();
176
+		$.prompt.style();
177
+		
178
+		$.prompt.jqif.click(fadeClicked);
179
+		$window.resize({animate:false}, $.prompt.position);
180
+		$.prompt.jqi.find('.'+ opts.prefix +'close').click($.prompt.close);
181
+		$.prompt.jqib.on("keydown",keyPressEventHandler)
182
+					.on('impromptu:loaded', opts.loaded)
183
+					.on('impromptu:close', opts.close)
184
+					.on('impromptu:statechanging', opts.statechanging)
185
+					.on('impromptu:statechanged', opts.statechanged);
186
+
187
+		// Show it
188
+		$.prompt.jqif[opts.show](opts.overlayspeed);
189
+		$.prompt.jqi[opts.show](opts.promptspeed, function(){
190
+			$.prompt.jqib.trigger('impromptu:loaded');
191
+		});
192
+		
193
+		// Timeout
194
+		if(opts.timeout > 0){
195
+			$.prompt.timeout = setTimeout(function(){ $.prompt.close(true); },opts.timeout);
196
+		}
197
+
198
+		return $.prompt.jqib;
199
+	};
200
+	
201
+	$.prompt.defaults = {
202
+		prefix:'jqi',
203
+		classes: {
204
+			box: '',
205
+			fade: '',
206
+			prompt: '',
207
+			close: '',
208
+			title: '',
209
+			message: '',
210
+			buttons: '',
211
+			button: '',
212
+			defaultButton: ''
213
+		},
214
+		title: '',
215
+		closeText: '&times;',
216
+		buttons: {
217
+			Ok: true
218
+		},
219
+		loaded: function(e){},
220
+		submit: function(e,v,m,f){},
221
+		close: function(e,v,m,f){},
222
+		statechanging: function(e, from, to){},
223
+		statechanged: function(e, to){},
224
+		opacity: 0.6,
225
+		zIndex: 999,
226
+		overlayspeed: 'slow',
227
+		promptspeed: 'fast',
228
+		show: 'fadeIn',
229
+		focus: 0,
230
+		defaultButton: 0,
231
+		useiframe: false,
232
+		top: '15%',
233
+		position: { 
234
+			container: null, 
235
+			x: null, 
236
+			y: null,
237
+			arrow: null,
238
+			width: null
239
+		},
240
+		persistent: true,
241
+		timeout: 0,
242
+		states: {},
243
+		state: {
244
+			name: null,
245
+			title: '',
246
+			html: '',
247
+			buttons: {
248
+				Ok: true
249
+			},
250
+			focus: 0,
251
+			defaultButton: 0,
252
+			position: { 
253
+				container: null, 
254
+				x: null, 
255
+				y: null,
256
+				arrow: null,
257
+				width: null
258
+			},
259
+			submit: function(e,v,m,f){
260
+				return true;
261
+			}
262
+		}
263
+	};
264
+	
265
+	/**
266
+	* currentPrefix String - At any time this show be the prefix 
267
+	* of the current prompt ex: "jqi"
268
+	*/
269
+	$.prompt.currentPrefix = $.prompt.defaults.prefix;
270
+	
271
+	/**
272
+	* currentStateName String - At any time this is the current state
273
+	* of the current prompt ex: "state0"
274
+	*/
275
+	$.prompt.currentStateName = "";
276
+		
277
+	/**
278
+	* setDefaults - Sets the default options
279
+	* @param o Object - Options to set as defaults
280
+	* @return void
281
+	*/
282
+	$.prompt.setDefaults = function(o) {
283
+		$.prompt.defaults = $.extend({}, $.prompt.defaults, o);
284
+	};
285
+	
286
+	/**
287
+	* setStateDefaults - Sets the default options for a state
288
+	* @param o Object - Options to set as defaults
289
+	* @return void
290
+	*/
291
+	$.prompt.setStateDefaults = function(o) {
292
+		$.prompt.defaults.state = $.extend({}, $.prompt.defaults.state, o);
293
+	};
294
+
295
+	/**
296
+	* position - Repositions the prompt (Used internally)
297
+	* @return void
298
+	*/
299
+	$.prompt.position = function(e){
300
+		var restoreFx = $.fx.off,
301
+			$state = $.prompt.getCurrentState(),
302
+			stateObj = $.prompt.options.states[$state.data('jqi-name')],
303
+			pos = stateObj? stateObj.position : undefined,
304
+			$window = $(window),
305
+			bodyHeight = document.body.scrollHeight, //$(document.body).outerHeight(true),
306
+			windowHeight = $(window).height(),
307
+			documentHeight = $(document).height(),
308
+			height = bodyHeight > windowHeight ? bodyHeight : windowHeight,
309
+			top = parseInt($window.scrollTop(),10) + ($.prompt.options.top.toString().indexOf('%') >= 0? 
310
+					(windowHeight*(parseInt($.prompt.options.top,10)/100)) : parseInt($.prompt.options.top,10));
311
+
312
+		// when resizing the window turn off animation
313
+		if(e !== undefined && e.data.animate === false){
314
+			$.fx.off = true;
315
+		}
316
+		
317
+		$.prompt.jqib.css({
318
+			position: "absolute",
319
+			height: height,
320
+			width: "100%",
321
+			top: 0,
322
+			left: 0,
323
+			right: 0,
324
+			bottom: 0
325
+		});
326
+		$.prompt.jqif.css({
327
+			position: "fixed",
328
+			height: height,
329
+			width: "100%",
330
+			top: 0,
331
+			left: 0,
332
+			right: 0,
333
+			bottom: 0
334
+		});
335
+
336
+		// tour positioning
337
+		if(pos && pos.container){
338
+			var offset = $(pos.container).offset();
339
+			
340
+			if($.isPlainObject(offset) && offset.top !== undefined){
341
+				$.prompt.jqi.css({
342
+					position: "absolute"
343
+				});
344
+				$.prompt.jqi.animate({
345
+					top: offset.top + pos.y,
346
+					left: offset.left + pos.x,
347
+					marginLeft: 0,
348
+					width: (pos.width !== undefined)? pos.width : null
349
+				});
350
+				top = (offset.top + pos.y) - ($.prompt.options.top.toString().indexOf('%') >= 0? (windowHeight*(parseInt($.prompt.options.top,10)/100)) : parseInt($.prompt.options.top,10));
351
+				$('html,body').animate({ scrollTop: top }, 'slow', 'swing', function(){});
352
+			}
353
+		}
354
+		// custom state width animation
355
+		else if(pos && pos.width){
356
+			$.prompt.jqi.css({
357
+					position: "absolute",
358
+					left: '50%'
359
+				});
360
+			$.prompt.jqi.animate({
361
+					top: pos.y || top,
362
+					left: pos.x || '50%',
363
+					marginLeft: ((pos.width/2)*-1),
364
+					width: pos.width
365
+				});
366
+		}
367
+		// standard prompt positioning
368
+		else{
369
+			$.prompt.jqi.css({
370
+				position: "absolute",
371
+				top: top,
372
+				left: '50%',//$window.width()/2,
373
+				marginLeft: (($.prompt.jqi.outerWidth(false)/2)*-1)
374
+			});
375
+		}
376
+
377
+		// restore fx settings
378
+		if(e !== undefined && e.data.animate === false){
379
+			$.fx.off = restoreFx;
380
+		}
381
+	};
382
+	
383
+	/**
384
+	* style - Restyles the prompt (Used internally)
385
+	* @return void
386
+	*/
387
+	$.prompt.style = function(){
388
+		$.prompt.jqif.css({
389
+			zIndex: $.prompt.options.zIndex,
390
+			display: "none",
391
+			opacity: $.prompt.options.opacity
392
+		});
393
+		$.prompt.jqi.css({
394
+			zIndex: $.prompt.options.zIndex+1,
395
+			display: "none"
396
+		});
397
+		$.prompt.jqib.css({
398
+			zIndex: $.prompt.options.zIndex
399
+		});
400
+	};
401
+
402
+	/**
403
+	* get - Get the prompt
404
+	* @return jQuery - the prompt
405
+	*/
406
+	$.prompt.get = function(state) {
407
+		return $('.'+ $.prompt.currentPrefix);
408
+	};
409
+
410
+	/**
411
+	* addState - Injects a state into the prompt
412
+	* @param statename String - Name of the state
413
+	* @param stateobj Object - options for the state
414
+	* @param afterState String - selector of the state to insert after
415
+	* @return jQuery - the newly created state
416
+	*/
417
+	$.prompt.addState = function(statename, stateobj, afterState) {
418
+		var state = "",
419
+			$state = null,
420
+			arrow = "",
421
+			title = "",
422
+			opts = $.prompt.options,
423
+			$jqistates = $('.'+ $.prompt.currentPrefix +'states'),
424
+			defbtn,k,v,i=0;
425
+
426
+		stateobj = $.extend({},$.prompt.defaults.state, {name:statename}, stateobj);
427
+
428
+		if(stateobj.position.arrow !== null){
429
+			arrow = '<div class="'+ opts.prefix + 'arrow '+ opts.prefix + 'arrow'+ stateobj.position.arrow +'"></div>';
430
+		}
431
+		if(stateobj.title && stateobj.title !== ''){
432
+			title = '<div class="lead '+ opts.prefix + 'title '+ opts.classes.title +'">'+  stateobj.title +'</div>';
433
+		}
434
+		state += '<div id="'+ opts.prefix +'state_'+ statename +'" class="'+ opts.prefix + 'state" data-jqi-name="'+ statename +'" style="display:none;">'+ 
435
+					arrow + title +
436
+					'<div class="'+ opts.prefix +'message '+ opts.classes.message +'">' + stateobj.html +'</div>'+
437
+					'<div class="'+ opts.prefix +'buttons '+ opts.classes.buttons +'"'+ ($.isEmptyObject(stateobj.buttons)? 'style="display:none;"':'') +'>';
438
+		
439
+		for(k in stateobj.buttons){
440
+			v = stateobj.buttons[k],
441
+			defbtn = stateobj.focus === i || (isNaN(stateobj.focus) && stateobj.defaultButton === i) ? ($.prompt.currentPrefix + 'defaultbutton ' + opts.classes.defaultButton) : '';
442
+
443
+			if(typeof v === 'object'){
444
+				state += '<button class="'+ opts.classes.button +' '+ defbtn;
445
+				
446
+				if(typeof v.classes !== "undefined"){
447
+					state += ' '+ ($.isArray(v.classes)? v.classes.join(' ') : v.classes) + ' ';
448
+				}
449
+				
450
+				state += '" name="' + opts.prefix + '_' + statename + '_button' + v.title.replace(/[^a-z0-9]+/gi,'') + '" id="' + opts.prefix + '_' + statename + '_button' + v.title.replace(/[^a-z0-9]+/gi,'') + '" value="' + v.value + '">' + v.title + '</button>';
451
+				
452
+			} else {
453
+				state += '<button class="'+ opts.classes.button +' '+ defbtn +'" name="' + opts.prefix + '_' + statename + '_button' + k + '" id="' + opts.prefix +  '_' + statename + '_button' + k + '" value="' + v + '">' + k + '</button>';
454
+				
455
+			}
456
+			i++;
457
+		}
458
+		state += '</div></div>';
459
+		
460
+		$state = $(state);
461
+
462
+		$state.on('impromptu:submit', stateobj.submit);
463
+
464
+		if(afterState !== undefined){
465
+			$jqistates.find('#'+ $.prompt.currentPrefix +'state_'+ afterState).after($state);
466
+		}
467
+		else{
468
+			$jqistates.append($state);
469
+		}
470
+
471
+		$.prompt.options.states[statename] = stateobj;
472
+
473
+		return $state;
474
+	};
475
+	
476
+	/**
477
+	* removeState - Removes a state from the promt
478
+	* @param state String - Name of the state
479
+	* @return Boolean - returns true on success, false on failure
480
+	*/
481
+	$.prompt.removeState = function(state) {
482
+		var $state = $.prompt.getState(state),
483
+			rm = function(){ $state.remove(); };
484
+
485
+		if($state.length === 0){
486
+			return false;
487
+		}
488
+
489
+		// transition away from it before deleting
490
+		if($state.is(':visible')){
491
+			if($state.next().length > 0){
492
+				$.prompt.nextState(rm);
493
+			}
494
+			else{
495
+				$.prompt.prevState(rm);
496
+			}
497
+		}
498
+		else{
499
+			$state.slideUp('slow', rm);
500
+		}
501
+
502
+		return true;
503
+	};
504
+
505
+	/**
506
+	* getState - Get the state by its name
507
+	* @param state String - Name of the state
508
+	* @return jQuery - the state
509
+	*/
510
+	$.prompt.getState = function(state) {
511
+		return $('#'+ $.prompt.currentPrefix +'state_'+ state);
512
+	};
513
+	$.prompt.getStateContent = function(state) {
514
+		return $.prompt.getState(state);
515
+	};
516
+	
517
+	/**
518
+	* getCurrentState - Get the current visible state
519
+	* @return jQuery - the current visible state
520
+	*/
521
+	$.prompt.getCurrentState = function() {
522
+		return $.prompt.getState($.prompt.getCurrentStateName());
523
+	};
524
+		
525
+	/**
526
+	* getCurrentStateName - Get the name of the current visible state
527
+	* @return String - the current visible state's name
528
+	*/
529
+	$.prompt.getCurrentStateName = function() {
530
+		return $.prompt.currentStateName;
531
+	};
532
+	
533
+	/**
534
+	* goToState - Goto the specified state
535
+	* @param state String - name of the state to transition to
536
+	* @param subState Boolean - true to be a sub state within the currently open state
537
+	* @param callback Function - called when the transition is complete
538
+	* @return jQuery - the newly active state
539
+	*/	
540
+	$.prompt.goToState = function(state, subState, callback) {
541
+		var $jqi = $.prompt.get(),
542
+			jqiopts = $.prompt.options,
543
+			$state = $.prompt.getState(state),
544
+			stateobj = jqiopts.states[$state.data('jqi-name')],
545
+			promptstatechanginge = new $.Event('impromptu:statechanging');
546
+
547
+		// subState can be ommitted
548
+		if(typeof subState === 'function'){
549
+			callback = subState;
550
+			subState = false;
551
+		}
552
+
553
+		$.prompt.jqib.trigger(promptstatechanginge, [$.prompt.getCurrentStateName(), state]);
554
+		
555
+		if(!promptstatechanginge.isDefaultPrevented() && $state.length > 0){
556
+			$.prompt.jqi.find('.'+ $.prompt.currentPrefix +'parentstate').removeClass($.prompt.currentPrefix +'parentstate');
557
+
558
+			if(subState){ // hide any open substates
559
+				// get rid of any substates
560
+				$.prompt.jqi.find('.'+ $.prompt.currentPrefix +'substate').not($state)
561
+					.slideUp(jqiopts.promptspeed)
562
+					.removeClass('.'+ $.prompt.currentPrefix +'substate')
563
+					.find('.'+ $.prompt.currentPrefix +'arrow').hide();
564
+
565
+				// add parent state class so it can be visible, but blocked
566
+				$.prompt.jqi.find('.'+ $.prompt.currentPrefix +'state:visible').addClass($.prompt.currentPrefix +'parentstate');
567
+
568
+				// add substate class so we know it will be smaller
569
+				$state.addClass($.prompt.currentPrefix +'substate');
570
+			}
571
+			else{ // hide any open states
572
+				$.prompt.jqi.find('.'+ $.prompt.currentPrefix +'state').not($state)
573
+					.slideUp(jqiopts.promptspeed)
574
+					.find('.'+ $.prompt.currentPrefix +'arrow').hide();
575
+			}
576
+			$.prompt.currentStateName = stateobj.name;
577
+
578
+			$state.slideDown(jqiopts.promptspeed,function(){
579
+				var $t = $(this);
580
+
581
+				// if focus is a selector, find it, else its button index
582
+				if(typeof(stateobj.focus) === 'string'){
583
+					$t.find(stateobj.focus).eq(0).focus();
584
+				}
585
+				else{
586
+					$t.find('.'+ $.prompt.currentPrefix +'defaultbutton').focus();
587
+				}
588
+
589
+				$t.find('.'+ $.prompt.currentPrefix +'arrow').show(jqiopts.promptspeed);
590
+				
591
+				if (typeof callback === 'function'){
592
+					$.prompt.jqib.on('impromptu:statechanged', callback);
593
+				}
594
+				$.prompt.jqib.trigger('impromptu:statechanged', [state]);
595
+				if (typeof callback === 'function'){
596
+					$.prompt.jqib.off('impromptu:statechanged', callback);
597
+				}
598
+			});
599
+			if(!subState){
600
+				$.prompt.position();
601
+			}
602
+		}
603
+		return $state;
604
+	};
605
+
606
+	/**
607
+	* nextState - Transition to the next state
608
+	* @param callback Function - called when the transition is complete
609
+	* @return jQuery - the newly active state
610
+	*/	
611
+	$.prompt.nextState = function(callback) {
612
+		var $next = $('#'+ $.prompt.currentPrefix +'state_'+ $.prompt.getCurrentStateName()).next();
613
+		return $.prompt.goToState( $next.attr('id').replace($.prompt.currentPrefix +'state_',''), callback );
614
+	};
615
+	
616
+	/**
617
+	* prevState - Transition to the previous state
618
+	* @param callback Function - called when the transition is complete
619
+	* @return jQuery - the newly active state
620
+	*/	
621
+	$.prompt.prevState = function(callback) {
622
+		var $prev = $('#'+ $.prompt.currentPrefix +'state_'+ $.prompt.getCurrentStateName()).prev();
623
+		$.prompt.goToState( $prev.attr('id').replace($.prompt.currentPrefix +'state_',''), callback );
624
+	};
625
+	
626
+	/**
627
+	* close - Closes the prompt
628
+	* @param callback Function - called when the transition is complete
629
+	* @param clicked String - value of the button clicked (only used internally)
630
+	* @param msg jQuery - The state message body (only used internally)
631
+	* @param forvals Object - key/value pairs of all form field names and values (only used internally)
632
+	* @return jQuery - the newly active state
633
+	*/	
634
+	$.prompt.close = function(callCallback, clicked, msg, formvals){
635
+		if($.prompt.timeout){
636
+			clearTimeout($.prompt.timeout);
637
+			$.prompt.timeout = false;
638
+		}
639
+
640
+		$.prompt.jqib.fadeOut('fast',function(){
641
+
642
+			if(callCallback) {
643
+				$.prompt.jqib.trigger('impromptu:close', [clicked,msg,formvals]);
644
+			}
645
+			$.prompt.jqib.remove();
646
+			
647
+			$(window).off('resize',$.prompt.position);
648
+		});
649
+	};
650
+	
651
+	/**
652
+	* Enable using $('.selector').prompt({});
653
+	* This will grab the html within the prompt as the prompt message
654
+	*/
655
+	$.fn.prompt = function(options){
656
+		if(options === undefined){
657
+			options = {};
658
+		}
659
+		if(options.withDataAndEvents === undefined){
660
+			options.withDataAndEvents = false;
661
+		}
662
+		
663
+		$.prompt($(this).clone(options.withDataAndEvents).html(),options);
664
+	};
665
+	
666
+})(jQuery);

+ 250
- 0
libs/jquery.autosize.js View File

@@ -0,0 +1,250 @@
1
+/*!
2
+	Autosize v1.18.1 - 2013-11-05
3
+	Automatically adjust textarea height based on user input.
4
+	(c) 2013 Jack Moore - http://www.jacklmoore.com/autosize
5
+	license: http://www.opensource.org/licenses/mit-license.php
6
+*/
7
+(function ($) {
8
+	var
9
+	defaults = {
10
+		className: 'autosizejs',
11
+		append: '',
12
+		callback: false,
13
+		resizeDelay: 10
14
+	},
15
+
16
+	// border:0 is unnecessary, but avoids a bug in Firefox on OSX
17
+	copy = '<textarea tabindex="-1" style="position:absolute; top:-999px; left:0; right:auto; bottom:auto; border:0; padding: 0; -moz-box-sizing:content-box; -webkit-box-sizing:content-box; box-sizing:content-box; word-wrap:break-word; height:0 !important; min-height:0 !important; overflow:hidden; transition:none; -webkit-transition:none; -moz-transition:none;"/>',
18
+
19
+	// line-height is conditionally included because IE7/IE8/old Opera do not return the correct value.
20
+	typographyStyles = [
21
+		'fontFamily',
22
+		'fontSize',
23
+		'fontWeight',
24
+		'fontStyle',
25
+		'letterSpacing',
26
+		'textTransform',
27
+		'wordSpacing',
28
+		'textIndent'
29
+	],
30
+
31
+	// to keep track which textarea is being mirrored when adjust() is called.
32
+	mirrored,
33
+
34
+	// the mirror element, which is used to calculate what size the mirrored element should be.
35
+	mirror = $(copy).data('autosize', true)[0];
36
+
37
+	// test that line-height can be accurately copied.
38
+	mirror.style.lineHeight = '99px';
39
+	if ($(mirror).css('lineHeight') === '99px') {
40
+		typographyStyles.push('lineHeight');
41
+	}
42
+	mirror.style.lineHeight = '';
43
+
44
+	$.fn.autosize = function (options) {
45
+		if (!this.length) {
46
+			return this;
47
+		}
48
+
49
+		options = $.extend({}, defaults, options || {});
50
+
51
+		if (mirror.parentNode !== document.body) {
52
+			$(document.body).append(mirror);
53
+		}
54
+
55
+		return this.each(function () {
56
+			var
57
+			ta = this,
58
+			$ta = $(ta),
59
+			maxHeight,
60
+			minHeight,
61
+			boxOffset = 0,
62
+			callback = $.isFunction(options.callback),
63
+			originalStyles = {
64
+				height: ta.style.height,
65
+				overflow: ta.style.overflow,
66
+				overflowY: ta.style.overflowY,
67
+				wordWrap: ta.style.wordWrap,
68
+				resize: ta.style.resize
69
+			},
70
+			timeout,
71
+			width = $ta.width();
72
+
73
+			if ($ta.data('autosize')) {
74
+				// exit if autosize has already been applied, or if the textarea is the mirror element.
75
+				return;
76
+			}
77
+			$ta.data('autosize', true);
78
+
79
+			if ($ta.css('box-sizing') === 'border-box' || $ta.css('-moz-box-sizing') === 'border-box' || $ta.css('-webkit-box-sizing') === 'border-box'){
80
+				boxOffset = $ta.outerHeight() - $ta.height();
81
+			}
82
+
83
+			// IE8 and lower return 'auto', which parses to NaN, if no min-height is set.
84
+			minHeight = Math.max(parseInt($ta.css('minHeight'), 10) - boxOffset || 0, $ta.height());
85
+
86
+			$ta.css({
87
+				overflow: 'hidden',
88
+				overflowY: 'hidden',
89
+				wordWrap: 'break-word', // horizontal overflow is hidden, so break-word is necessary for handling words longer than the textarea width
90
+				resize: ($ta.css('resize') === 'none' || $ta.css('resize') === 'vertical') ? 'none' : 'horizontal'
91
+			});
92
+
93
+			// The mirror width must exactly match the textarea width, so using getBoundingClientRect because it doesn't round the sub-pixel value.
94
+			function setWidth() {
95
+				var style, width;
96
+				
97
+				if ('getComputedStyle' in window) {
98
+					style = window.getComputedStyle(ta, null);
99
+					width = ta.getBoundingClientRect().width;
100
+
101
+					$.each(['paddingLeft', 'paddingRight', 'borderLeftWidth', 'borderRightWidth'], function(i,val){
102
+						width -= parseInt(style[val],10);
103
+					});
104
+
105
+					mirror.style.width = width + 'px';
106
+				}
107
+				else {
108
+					// window.getComputedStyle, getBoundingClientRect returning a width are unsupported and unneeded in IE8 and lower.
109
+					mirror.style.width = Math.max($ta.width(), 0) + 'px';
110
+				}
111
+			}
112
+
113
+			function initMirror() {
114
+				var styles = {};
115
+
116
+				mirrored = ta;
117
+				mirror.className = options.className;
118
+				maxHeight = parseInt($ta.css('maxHeight'), 10);
119
+
120
+				// mirror is a duplicate textarea located off-screen that
121
+				// is automatically updated to contain the same text as the
122
+				// original textarea.  mirror always has a height of 0.
123
+				// This gives a cross-browser supported way getting the actual
124
+				// height of the text, through the scrollTop property.
125
+				$.each(typographyStyles, function(i,val){
126
+					styles[val] = $ta.css(val);
127
+				});
128
+				$(mirror).css(styles);
129
+
130
+				setWidth();
131
+
132
+				// Chrome-specific fix:
133
+				// When the textarea y-overflow is hidden, Chrome doesn't reflow the text to account for the space
134
+				// made available by removing the scrollbar. This workaround triggers the reflow for Chrome.
135
+				if (window.chrome) {
136
+					var width = ta.style.width;
137
+					ta.style.width = '0px';
138
+					var ignore = ta.offsetWidth;
139
+					ta.style.width = width;
140
+				}
141
+			}
142
+
143
+			// Using mainly bare JS in this function because it is going
144
+			// to fire very often while typing, and needs to very efficient.
145
+			function adjust() {
146
+				var height, original;
147
+
148
+				if (mirrored !== ta) {
149
+					initMirror();
150
+				} else {
151
+					setWidth();
152
+				}
153
+
154
+				mirror.value = ta.value + options.append;
155
+				mirror.style.overflowY = ta.style.overflowY;
156
+				original = parseInt(ta.style.height,10);
157
+
158
+				// Setting scrollTop to zero is needed in IE8 and lower for the next step to be accurately applied
159
+				mirror.scrollTop = 0;
160
+
161
+				mirror.scrollTop = 9e4;
162
+
163
+				// Using scrollTop rather than scrollHeight because scrollHeight is non-standard and includes padding.
164
+				height = mirror.scrollTop;
165
+
166
+				if (maxHeight && height > maxHeight) {
167
+					ta.style.overflowY = 'scroll';
168
+					height = maxHeight;
169
+				} else {
170
+					ta.style.overflowY = 'hidden';
171
+					if (height < minHeight) {
172
+						height = minHeight;
173
+					}
174
+				}
175
+
176
+				height += boxOffset;
177
+
178
+				if (original !== height) {
179
+					ta.style.height = height + 'px';
180
+					if (callback) {
181
+						options.callback.call(ta,ta);
182
+					}
183
+				}
184
+			}
185
+
186
+			function resize () {
187
+				clearTimeout(timeout);
188
+				timeout = setTimeout(function(){
189
+					var newWidth = $ta.width();
190
+
191
+					if (newWidth !== width) {
192
+						width = newWidth;
193
+						adjust();
194
+					}
195
+				}, parseInt(options.resizeDelay,10));
196
+			}
197
+
198
+			if ('onpropertychange' in ta) {
199
+				if ('oninput' in ta) {
200
+					// Detects IE9.  IE9 does not fire onpropertychange or oninput for deletions,
201
+					// so binding to onkeyup to catch most of those occasions.  There is no way that I
202
+					// know of to detect something like 'cut' in IE9.
203
+					$ta.on('input.autosize keyup.autosize', adjust);
204
+				} else {
205
+					// IE7 / IE8
206
+					$ta.on('propertychange.autosize', function(){
207
+						if(event.propertyName === 'value'){
208
+							adjust();
209
+						}
210
+					});
211
+				}
212
+			} else {
213
+				// Modern Browsers
214
+				$ta.on('input.autosize', adjust);
215
+			}
216
+
217
+			// Set options.resizeDelay to false if using fixed-width textarea elements.
218
+			// Uses a timeout and width check to reduce the amount of times adjust needs to be called after window resize.
219
+
220
+			if (options.resizeDelay !== false) {
221
+				$(window).on('resize.autosize', resize);
222
+			}
223
+
224
+			// Event for manual triggering if needed.
225
+			// Should only be needed when the value of the textarea is changed through JavaScript rather than user input.
226
+			$ta.on('autosize.resize', adjust);
227
+
228
+			// Event for manual triggering that also forces the styles to update as well.
229
+			// Should only be needed if one of typography styles of the textarea change, and the textarea is already the target of the adjust method.
230
+			$ta.on('autosize.resizeIncludeStyle', function() {
231
+				mirrored = null;
232
+				adjust();
233
+			});
234
+
235
+			$ta.on('autosize.destroy', function(){
236
+				mirrored = null;
237
+				clearTimeout(timeout);
238
+				$(window).off('resize', resize);
239
+				$ta
240
+					.off('autosize')
241
+					.off('.autosize')
242
+					.css(originalStyles)
243
+					.removeData('autosize');
244
+			});
245
+
246
+			// Call adjust in case the textarea already contains text.
247
+			adjust();
248
+		});
249
+	};
250
+}(window.jQuery || window.$)); // jQuery or jQuery-like library, such as Zepto

+ 2071
- 0
libs/strophejingle.bundle.js
File diff suppressed because it is too large
View File


+ 139
- 0
muc.js View File

@@ -0,0 +1,139 @@
1
+/* jshint -W117 */
2
+/* a simple MUC connection plugin 
3
+ * can only handle a single MUC room
4
+ */
5
+Strophe.addConnectionPlugin('emuc', {
6
+    connection: null,
7
+    roomjid: null,
8
+    myroomjid: null,
9
+    members: {},
10
+    isOwner: false,
11
+    init: function (conn) {
12
+        this.connection = conn;
13
+    },
14
+    doJoin: function (jid, password) {
15
+        this.myroomjid = jid;
16
+        if (!this.roomjid) {
17
+            this.roomjid = Strophe.getBareJidFromJid(jid);
18
+            // add handlers (just once)
19
+            this.connection.addHandler(this.onPresence.bind(this), null, 'presence', null, null, this.roomjid, {matchBare: true});
20
+            this.connection.addHandler(this.onPresenceUnavailable.bind(this), null, 'presence', 'unavailable', null, this.roomjid, {matchBare: true});
21
+            this.connection.addHandler(this.onPresenceError.bind(this), null, 'presence', 'error', null, this.roomjid, {matchBare: true});
22
+            this.connection.addHandler(this.onMessage.bind(this), null, 'message', null, null, this.roomjid, {matchBare: true});
23
+        }
24
+
25
+        var join = $pres({to: this.myroomjid }).c('x', {xmlns: 'http://jabber.org/protocol/muc'});
26
+        if (password !== null) {
27
+            join.c('password').t(password);
28
+        }
29
+        this.connection.send(join);
30
+    },
31
+    onPresence: function (pres) {
32
+        var from = pres.getAttribute('from');
33
+        var type = pres.getAttribute('type');
34
+        if (type != null) {
35
+            return true;
36
+        }
37
+        if ($(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>status[code="201"]').length) {
38
+            // http://xmpp.org/extensions/xep-0045.html#createroom-instant
39
+            this.isOwner = true;
40
+            var create = $iq({type: 'set', to: this.roomjid})
41
+                    .c('query', {xmlns: 'http://jabber.org/protocol/muc#owner'})
42
+                    .c('x', {xmlns: 'jabber:x:data', type: 'submit'});
43
+            this.connection.send(create); // fire away
44
+        }
45
+
46
+        var member = {};
47
+        member.show = $(pres).find('>show').text();
48
+        member.status = $(pres).find('>status').text();
49
+        var tmp = $(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>item');
50
+        member.affilication = tmp.attr('affiliation');
51
+        member.role = tmp.attr('role');
52
+        if (from == this.myroomjid) {
53
+            $(document).trigger('joined.muc', [from, member]);
54
+        } else if (this.members[from] === undefined) {
55
+            // new participant
56
+            this.members[from] = member;
57
+            $(document).trigger('entered.muc', [from, member]);
58
+        } else {
59
+            console.log('presence change from', from);
60
+        }
61
+        return true;
62
+    },
63
+    onPresenceUnavailable: function (pres) {
64
+        var from = pres.getAttribute('from');
65
+        delete this.members[from];
66
+        $(document).trigger('left.muc', [from]);
67
+        return true;
68
+    },
69
+    onPresenceError: function (pres) {
70
+        var from = pres.getAttribute('from');
71
+        if ($(pres).find('>error[type="auth"]>not-authorized[xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]').length) {
72
+            $(document).trigger('passwordrequired.muc', [from]);
73
+
74
+            // FIXME: remove once moved to passwordrequired which should reuse dojoin
75
+            var ob = this;
76
+            window.setTimeout(function () {
77
+                var given = window.prompt('Password required');
78
+                if (given != null) {
79
+                    // FIXME: reuse doJoin?
80
+                    ob.connection.send($pres({to: ob.myroomjid }).c('x', {xmlns: 'http://jabber.org/protocol/muc'}).c('password').t(given));
81
+                } else {
82
+                    // user aborted
83
+                }
84
+            }, 50);
85
+        } else {
86
+            console.warn('onPresError ', pres);
87
+        }
88
+        return true;
89
+    },
90
+    sendMessage: function (body, nickname) {
91
+        var msg = $msg({to: this.roomjid, type: 'groupchat'});
92
+        msg.c('body', body).up();
93
+        if (nickname) {
94
+            msg.c('nick', {xmlns: 'http://jabber.org/protocol/nick'}).t(nickname).up().up();
95
+        }
96
+        this.connection.send(msg);
97
+    },
98
+    onMessage: function (msg) {
99
+        var txt = $(msg).find('>body').text();
100
+        // TODO: <subject/>
101
+        // FIXME: this is a hack. but jingle on muc makes nickchanges hard
102
+        var nick = $(msg).find('>nick[xmlns="http://jabber.org/protocol/nick"]').text() || Strophe.getResourceFromJid(msg.getAttribute('from'));
103
+        if (txt) {
104
+            console.log('chat', nick, txt);
105
+
106
+            updateChatConversation(nick, txt);
107
+        }
108
+        return true;
109
+    },
110
+    lockRoom: function (key) {
111
+        //http://xmpp.org/extensions/xep-0045.html#roomconfig
112
+        var ob = this;
113
+        this.connection.sendIQ($iq({to: this.roomjid, type: 'get'}).c('query', {xmlns: 'http://jabber.org/protocol/muc#owner'}),
114
+            function (res) {
115
+                if ($(res).find('>query>x[xmlns="jabber:x:data"]>field[var="muc#roomconfig_roomsecret"]').length) {
116
+                    var formsubmit = $iq({to: ob.roomjid, type: 'set'}).c('query', {xmlns: 'http://jabber.org/protocol/muc#owner'});
117
+                    formsubmit.c('x', {xmlns: 'jabber:x:data', type: 'submit'});
118
+                    formsubmit.c('field', {'var': 'FORM_TYPE'}).c('value').t('http://jabber.org/protocol/muc#roomconfig').up().up();
119
+                    formsubmit.c('field', {'var': 'muc#roomconfig_roomsecret'}).c('value').t(key).up().up();
120
+                    // FIXME: is muc#roomconfig_passwordprotectedroom required?
121
+                    this.connection.sendIQ(formsubmit,
122
+                        function (res) {
123
+                            console.log('set room password');
124
+                        },
125
+                        function (err) {
126
+                            console.warn('setting password failed', err);
127
+                        }
128
+                    );
129
+                } else {
130
+                    console.warn('room passwords not supported');
131
+                }
132
+            },
133
+            function (err) {
134
+                console.warn('setting password failed', err);
135
+            }
136
+        );
137
+    }
138
+});
139
+

+ 1
- 0
webrtcrequired.html View File

@@ -0,0 +1 @@
1
+Sorry, webrtc is required for this and your browser does not seem to support it.

Loading…
Cancel
Save