├── LICENSE
├── README.md
├── app.js
├── chromeonly.html
├── config.js
├── css
├── jquery-impromptu.css
├── main.css
└── modaldialog.css
├── estos_log.js
├── images
├── estoslogo.png
└── jitsilogo.png
├── index.html
├── libs
├── colibri.js
├── jquery-impromptu.js
├── jquery.autosize.js
└── strophejingle.bundle.js
├── muc.js
└── webrtcrequired.html
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2013 ESTOS GmbH
4 | Copyright (c) 2013 BlueJimp SARL
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy of
7 | this software and associated documentation files (the "Software"), to deal in
8 | the Software without restriction, including without limitation the rights to
9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
10 | the Software, and to permit persons to whom the Software is furnished to do so,
11 | subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
18 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
19 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
20 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
21 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | meet - a colibri.js sample application
2 | ====
3 | A WebRTC-powered multi-user videochat. For a live demo, check out either https://meet.estos.de/ or https://meet.jit.si/.
4 |
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/).
6 |
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
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 | var ssrc2jid = {};
11 |
12 | function init() {
13 | RTC = setupRTC();
14 | if (RTC === null) {
15 | window.location.href = 'webrtcrequired.html';
16 | return;
17 | } else if (RTC.browser != 'chrome') {
18 | window.location.href = 'chromeonly.html';
19 | return;
20 | }
21 | RTCPeerconnection = TraceablePeerConnection;
22 |
23 | connection = new Strophe.Connection(document.getElementById('boshURL').value || config.bosh || '/http-bind');
24 | if (connection.disco) {
25 | // for chrome, add multistream cap
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 | connection.jingle.getStunAndTurnCredentials();
35 | if (RTC.browser == 'firefox') {
36 | getUserMediaWithConstraints(['audio']);
37 | } else {
38 | getUserMediaWithConstraints(['audio', 'video'], '360');
39 | }
40 | document.getElementById('connect').disabled = true;
41 | } else {
42 | console.log('status', status);
43 | }
44 | });
45 | }
46 |
47 | function doJoin() {
48 | var roomnode = null;
49 | var path = window.location.pathname;
50 | var roomjid;
51 |
52 | // determinde the room node from the url
53 | // TODO: just the roomnode or the whole bare jid?
54 | if (config.getroomnode && typeof config.getroomnode === 'function') {
55 | // custom function might be responsible for doing the pushstate
56 | roomnode = config.getroomnode(path);
57 | } else {
58 | /* fall back to default strategy
59 | * this is making assumptions about how the URL->room mapping happens.
60 | * It currently assumes deployment at root, with a rewrite like the
61 | * following one (for nginx):
62 | location ~ ^/([a-zA-Z0-9]+)$ {
63 | rewrite ^/(.*)$ / break;
64 | }
65 | */
66 | if (path.length > 1) {
67 | roomnode = path.substr(1).toLowerCase();
68 | } else {
69 | roomnode = Math.random().toString(36).substr(2, 20);
70 | window.history.pushState('VideoChat', 'Room: ' + roomnode, window.location.pathname + roomnode);
71 | }
72 | }
73 | roomjid = roomnode + '@' + config.hosts.muc;
74 |
75 | if (config.useNicks) {
76 | var nick = window.prompt('Your nickname (optional)');
77 | if (nick) {
78 | roomjid += '/' + nick;
79 | } else {
80 | roomjid += '/' + Strophe.getNodeFromJid(connection.jid);
81 | }
82 | } else {
83 | roomjid += '/' + Strophe.getNodeFromJid(connection.jid).substr(0,8);
84 | }
85 | connection.emuc.doJoin(roomjid);
86 | }
87 |
88 | $(document).bind('mediaready.jingle', function (event, stream) {
89 | connection.jingle.localStream = stream;
90 | RTC.attachMediaStream($('#localVideo'), stream);
91 | document.getElementById('localVideo').muted = true;
92 | document.getElementById('localVideo').autoplay = true;
93 | document.getElementById('localVideo').volume = 0;
94 |
95 | document.getElementById('largeVideo').volume = 0;
96 | document.getElementById('largeVideo').src = document.getElementById('localVideo').src;
97 | doJoin();
98 | });
99 |
100 | $(document).bind('mediafailure.jingle', function () {
101 | // FIXME
102 | });
103 |
104 | $(document).bind('remotestreamadded.jingle', function (event, data, sid) {
105 | function waitForRemoteVideo(selector, sid) {
106 | var sess = connection.jingle.sessions[sid];
107 | videoTracks = data.stream.getVideoTracks();
108 | if (videoTracks.length === 0 || selector[0].currentTime > 0) {
109 | RTC.attachMediaStream(selector, data.stream); // FIXME: why do i have to do this for FF?
110 | $(document).trigger('callactive.jingle', [selector, sid]);
111 | console.log('waitForremotevideo', sess.peerconnection.iceConnectionState, sess.peerconnection.signalingState);
112 | } else {
113 | setTimeout(function () { waitForRemoteVideo(selector, sid); }, 100);
114 | }
115 | }
116 | var sess = connection.jingle.sessions[sid];
117 |
118 | // look up an associated JID for a stream id
119 | if (data.stream.id.indexOf('mixedmslabel') == -1) {
120 | var ssrclines = SDPUtil.find_lines(sess.peerconnection.remoteDescription.sdp, 'a=ssrc');
121 | ssrclines = ssrclines.filter(function (line) {
122 | return line.indexOf('mslabel:' + data.stream.label) != -1;
123 | })
124 | if (ssrclines.length) {
125 | thessrc = ssrclines[0].substring(7).split(' ')[0];
126 | // ok to overwrite the one from focus? might save work in colibri.js
127 | console.log('associated jid', ssrc2jid[thessrc], data.peerjid);
128 | if (ssrc2jid[thessrc]) {
129 | data.peerjid = ssrc2jid[thessrc];
130 | }
131 | }
132 | }
133 |
134 | var container;
135 | var remotes = document.getElementById('remoteVideos');
136 | if (data.peerjid) {
137 | container = document.getElementById('participant_' + Strophe.getResourceFromJid(data.peerjid));
138 | if (!container) {
139 | console.warn('no container for', data.peerjid);
140 | // create for now...
141 | // FIXME: should be removed
142 | container = document.createElement('span');
143 | container.id = 'participant_' + Strophe.getResourceFromJid(data.peerjid);
144 | container.className = 'videocontainer';
145 | remotes.appendChild(container);
146 | } else {
147 | //console.log('found container for', data.peerjid);
148 | }
149 | } else {
150 | if (data.stream.id != 'mixedmslabel') {
151 | console.warn('can not associate stream', data.stream.id, 'with a participant');
152 | }
153 | // FIXME: for the mixed ms we dont need a video -- currently
154 | container = document.createElement('span');
155 | container.className = 'videocontainer';
156 | remotes.appendChild(container);
157 | }
158 | var vid = document.createElement('video');
159 | var id = 'remoteVideo_' + sid + '_' + data.stream.id;
160 | vid.id = id;
161 | vid.autoplay = true;
162 | vid.oncontextmenu = function () { return false; };
163 | container.appendChild(vid);
164 | // TODO: make mixedstream display:none via css?
165 | if (id.indexOf('mixedmslabel') != -1) {
166 | container.id = 'mixedstream';
167 | $(container).hide();
168 | }
169 | var sel = $('#' + id);
170 | sel.hide();
171 | RTC.attachMediaStream(sel, data.stream);
172 | waitForRemoteVideo(sel, sid);
173 | data.stream.onended = function () {
174 | console.log('stream ended', this.id);
175 | var src = $('#' + id).attr('src');
176 | if (src === $('#largeVideo').attr('src')) {
177 | // this is currently displayed as large
178 | // pick the last visible video in the row
179 | // if nobody else is left, this picks the local video
180 | var pick = $('#remoteVideos>span[id!="mixedstream"]:visible:last>video').get(0);
181 | // mute if localvideo
182 | document.getElementById('largeVideo').volume = pick.volume;
183 | document.getElementById('largeVideo').src = pick.src;
184 | }
185 | $('#' + id).parent().remove();
186 | resizeThumbnails();
187 | };
188 | sel.click(
189 | function () {
190 | console.log('hover in', $(this).attr('src'));
191 | var newSrc = $(this).attr('src');
192 | if ($('#largeVideo').attr('src') != newSrc) {
193 | document.getElementById('largeVideo').volume = 1;
194 | $('#largeVideo').fadeOut(300, function () {
195 | $(this).attr('src', newSrc);
196 | $(this).fadeIn(300);
197 | });
198 | }
199 | }
200 | );
201 | });
202 |
203 | $(document).bind('callincoming.jingle', function (event, sid) {
204 | var sess = connection.jingle.sessions[sid];
205 | // TODO: check affiliation and/or role
206 | console.log('emuc data for', sess.peerjid, connection.emuc.members[sess.peerjid]);
207 | sess.sendAnswer();
208 | sess.accept();
209 | });
210 |
211 | $(document).bind('callactive.jingle', function (event, videoelem, sid) {
212 | if (videoelem.attr('id').indexOf('mixedmslabel') == -1) {
213 | // ignore mixedmslabela0 and v0
214 | videoelem.show();
215 | resizeThumbnails();
216 |
217 | document.getElementById('largeVideo').volume = 1;
218 | $('#largeVideo').attr('src', videoelem.attr('src'));
219 | }
220 | });
221 |
222 | $(document).bind('callterminated.jingle', function (event, sid, reason) {
223 | // FIXME
224 | });
225 |
226 | $(document).bind('setLocalDescription.jingle', function (event, sid) {
227 | // put our ssrcs into presence so other clients can identify our stream
228 | var sess = connection.jingle.sessions[sid];
229 | var newssrcs = {};
230 | var localSDP = new SDP(sess.peerconnection.localDescription.sdp);
231 | localSDP.media.forEach(function (media) {
232 | var type = SDPUtil.parse_mline(media.split('\r\n')[0]).media;
233 | var ssrc = SDPUtil.find_line(media, 'a=ssrc:').substring(7).split(' ')[0];
234 | // assumes a single local ssrc
235 | newssrcs[type] = ssrc;
236 | });
237 | console.log('new ssrcs', newssrcs);
238 | // just blast off presence for everything -- TODO: optimize
239 | var pres = $pres({to: connection.emuc.myroomjid });
240 | pres.c('x', {xmlns: 'http://jabber.org/protocol/muc'}).up();
241 |
242 | pres.c('media', {xmlns: 'http://estos.de/ns/mjs'});
243 | Object.keys(newssrcs).forEach(function (mtype) {
244 | pres.c('source', {type: mtype, ssrc: newssrcs[mtype]}).up();
245 | });
246 | pres.up();
247 | connection.send(pres);
248 | });
249 |
250 | $(document).bind('joined.muc', function (event, jid, info) {
251 | updateRoomUrl(window.location.href);
252 | showToolbar();
253 | document.getElementById('localNick').appendChild(
254 | document.createTextNode(Strophe.getResourceFromJid(jid) + ' (you)')
255 | );
256 | if (Object.keys(connection.emuc.members).length < 1) {
257 | focus = new ColibriFocus(connection, config.hosts.bridge);
258 | }
259 | });
260 |
261 | $(document).bind('entered.muc', function (event, jid, info, pres) {
262 | console.log('entered', jid, info);
263 | console.log(focus);
264 |
265 | var container = document.createElement('span');
266 | container.id = 'participant_' + Strophe.getResourceFromJid(jid);
267 | container.className = 'videocontainer';
268 | var remotes = document.getElementById('remoteVideos');
269 | remotes.appendChild(container);
270 | var nickfield = document.createElement('span');
271 | nickfield.appendChild(document.createTextNode(Strophe.getResourceFromJid(jid)));
272 | container.appendChild(nickfield);
273 | resizeThumbnails();
274 |
275 | if (focus !== null) {
276 | if (focus.confid === null) {
277 | console.log('make new conference with', jid);
278 | focus.makeConference(Object.keys(connection.emuc.members));
279 | } else {
280 | console.log('invite', jid, 'into conference');
281 | focus.addNewParticipant(jid);
282 | }
283 | }
284 | $(pres).find('>media[xmlns="http://estos.de/ns/mjs"]>source').each(function (idx, ssrc) {
285 | //console.log(jid, 'assoc ssrc', ssrc.getAttribute('type'), ssrc.getAttribute('ssrc'));
286 | ssrc2jid[ssrc.getAttribute('ssrc')] = jid;
287 | });
288 | });
289 |
290 | $(document).bind('left.muc', function (event, jid) {
291 | console.log('left', jid);
292 | connection.jingle.terminateByJid(jid);
293 | var container = document.getElementById('participant_' + Strophe.getResourceFromJid(jid));
294 | if (container) {
295 | // hide here, wait for video to close before removing
296 | $(container).hide();
297 | resizeThumbnails();
298 | }
299 |
300 | if (Object.keys(connection.emuc.members).length === 0) {
301 | console.log('everyone left');
302 | if (focus !== null) {
303 | // FIXME: closing the connection is a hack to avoid some
304 | // problemswith reinit
305 | if (focus.peerconnection !== null) {
306 | focus.peerconnection.close();
307 | }
308 | focus = new ColibriFocus(connection, config.hosts.bridge);
309 | }
310 | }
311 | });
312 |
313 | $(document).bind('presence.muc', function (event, jid, info, pres) {
314 | $(pres).find('>media[xmlns="http://estos.de/ns/mjs"]>source').each(function (idx, ssrc) {
315 | //console.log(jid, 'assoc ssrc', ssrc.getAttribute('type'), ssrc.getAttribute('ssrc'));
316 | ssrc2jid[ssrc.getAttribute('ssrc')] = jid;
317 | });
318 | });
319 |
320 | function toggleVideo() {
321 | if (!(connection && connection.jingle.localStream)) return;
322 | for (var idx = 0; idx < connection.jingle.localStream.getVideoTracks().length; idx++) {
323 | connection.jingle.localStream.getVideoTracks()[idx].enabled = !connection.jingle.localStream.getVideoTracks()[idx].enabled;
324 | }
325 | }
326 |
327 | function toggleAudio() {
328 | if (!(connection && connection.jingle.localStream)) return;
329 | for (var idx = 0; idx < connection.jingle.localStream.getAudioTracks().length; idx++) {
330 | connection.jingle.localStream.getAudioTracks()[idx].enabled = !connection.jingle.localStream.getAudioTracks()[idx].enabled;
331 | }
332 | }
333 |
334 | function resizeLarge() {
335 | var availableHeight = window.innerHeight;
336 | var chatspaceWidth = $('#chatspace').width();
337 |
338 | var numvids = $('#remoteVideos>video:visible').length;
339 | if (numvids < 5)
340 | availableHeight -= 100; // min thumbnail height for up to 4 videos
341 | else
342 | availableHeight -= 50; // min thumbnail height for more than 5 videos
343 |
344 | availableHeight -= 79; // padding + link ontop
345 | var availableWidth = window.innerWidth - chatspaceWidth;
346 | var aspectRatio = 16.0 / 9.0;
347 | if (availableHeight < availableWidth / aspectRatio) {
348 | availableWidth = Math.floor(availableHeight * aspectRatio);
349 | }
350 | if (availableWidth < 0 || availableHeight < 0) return;
351 | $('#largeVideo').parent().width(availableWidth);
352 | $('#largeVideo').parent().height(availableWidth / aspectRatio);
353 | resizeThumbnails();
354 | }
355 |
356 | function resizeThumbnails() {
357 | // Calculate the available height, which is the inner window height minus 39px for the header
358 | // minus 4px for the delimiter lines on the top and bottom of the large video,
359 | // minus the 36px space inside the remoteVideos container used for highlighting shadow.
360 | var availableHeight = window.innerHeight - $('#largeVideo').height() - 79;
361 | var numvids = $('#remoteVideos>span:visible').length;
362 | // Remove the 1px borders arround videos.
363 | var availableWinWidth = $('#remoteVideos').width() - 2 * numvids;
364 | var availableWidth = availableWinWidth / numvids;
365 | var aspectRatio = 16.0 / 9.0;
366 | var maxHeight = Math.min(160, availableHeight);
367 | availableHeight = Math.min(maxHeight, availableWidth / aspectRatio);
368 | if (availableHeight < availableWidth / aspectRatio) {
369 | availableWidth = Math.floor(availableHeight * aspectRatio);
370 | }
371 | // size videos so that while keeping AR and max height, we have a nice fit
372 | $('#remoteVideos').height(availableHeight+26); // add the 2*18px-padding-top border used for highlighting shadow.
373 | $('#remoteVideos>span').width(availableWidth);
374 | $('#remoteVideos>span').height(availableHeight);
375 | }
376 |
377 | $(document).ready(function () {
378 | $('#nickinput').keydown(function(event) {
379 | if (event.keyCode == 13) {
380 | event.preventDefault();
381 | var val = this.value;
382 | this.value = '';
383 | if (!nickname) {
384 | nickname = val;
385 | $('#nickname').css({visibility:"hidden"});
386 | $('#chatconversation').css({visibility:'visible'});
387 | $('#usermsg').css({visibility:'visible'});
388 | $('#usermsg').focus();
389 | return;
390 | }
391 | }
392 | });
393 |
394 | $('#usermsg').keydown(function(event) {
395 | if (event.keyCode == 13) {
396 | event.preventDefault();
397 | var message = this.value;
398 | $('#usermsg').val('').trigger('autosize.resize');
399 | this.focus();
400 | connection.emuc.sendMessage(message, nickname);
401 | }
402 | });
403 |
404 | $('#usermsg').autosize();
405 |
406 | resizeLarge();
407 | $(window).resize(function () {
408 | resizeLarge();
409 | });
410 | if (!$('#settings').is(':visible')) {
411 | console.log('init');
412 | init();
413 | } else {
414 | loginInfo.onsubmit = function (e) {
415 | if (e.preventDefault) e.preventDefault();
416 | $('#settings').hide();
417 | init();
418 | };
419 | }
420 | });
421 |
422 | $(window).bind('beforeunload', function () {
423 | if (connection && connection.connected) {
424 | // ensure signout
425 | $.ajax({
426 | type: 'POST',
427 | url: config.bosh,
428 | async: false,
429 | cache: false,
430 | contentType: 'application/xml',
431 | data: "
",
432 | success: function (data) {
433 | console.log('signed out');
434 | console.log(data);
435 | },
436 | error: function (XMLHttpRequest, textStatus, errorThrown) {
437 | console.log('signout error', textStatus + ' (' + errorThrown + ')');
438 | }
439 | });
440 | }
441 | });
442 |
443 | function dump(elem, filename){
444 | elem = elem.parentNode;
445 | elem.download = filename || 'meetlog.json';
446 | elem.href = 'data:application/json;charset=utf-8,\n';
447 | var data = {};
448 | if (connection.jingle) {
449 | Object.keys(connection.jingle.sessions).forEach(function (sid) {
450 | var session = connection.jingle.sessions[sid];
451 | if (session.peerconnection && session.peerconnection.updateLog) {
452 | // FIXME: should probably be a .dump call
453 | /* well, if I need to modify the output format anyway...
454 | var stats = JSON.parse(JSON.stringify(session.peerconnection.stats));
455 | Object.keys(stats).forEach(function (name) {
456 | stats[name].values = JSON.stringify(stats[name].values);
457 | });
458 | */
459 |
460 | data["jingle_" + session.sid] = {
461 | updateLog: session.peerconnection.updateLog,
462 | stats: session.peerconnection.stats,
463 | url: window.location.href}
464 | ;
465 | }
466 | });
467 | }
468 | metadata = {};
469 | metadata.time = new Date();
470 | metadata.url = window.location.href;
471 | metadata.ua = navigator.userAgent;
472 | if (connection.logger) {
473 | metadata.xmpp = connection.logger.log;
474 | }
475 | data.metadata = metadata;
476 | elem.href += encodeURIComponent(JSON.stringify(data, null, ' '));
477 | return false;
478 | }
479 |
480 | function updateChatConversation(nick, message)
481 | {
482 | var divClassName = '';
483 | if (nickname == nick)
484 | divClassName = "localuser";
485 | else
486 | divClassName = "remoteuser";
487 |
488 | $('#chatconversation').append('' + nick + ': ' + message + '
');
489 | $('#chatconversation').animate({ scrollTop: $('#chatconversation')[0].scrollHeight}, 1000);
490 | }
491 |
492 | function buttonClick(id, classname) {
493 | $(id).toggleClass(classname); // add the class to the clicked element
494 | }
495 |
496 | function openLockDialog() {
497 | if (sharedKey)
498 | $.prompt("Are you sure you would like to remove your secret key?",
499 | {
500 | title: "Remove secrect key",
501 | persistent: false,
502 | buttons: { "Remove": true, "Cancel": false},
503 | defaultButton: 1,
504 | submit: function(e,v,m,f){
505 | if(v)
506 | {
507 | sharedKey = '';
508 | lockRoom();
509 | }
510 | }
511 | });
512 | else
513 | $.prompt('Set a secrect key to lock your room
' +
514 | '',
515 | {
516 | persistent: false,
517 | buttons: { "Save": true , "Cancel": false},
518 | defaultButton: 1,
519 | loaded: function(event) {
520 | document.getElementById('lockKey').focus();
521 | },
522 | submit: function(e,v,m,f){
523 | if(v)
524 | {
525 | var lockKey = document.getElementById('lockKey');
526 |
527 | if (lockKey.value != null)
528 | {
529 | sharedKey = lockKey.value;
530 | lockRoom(true);
531 | }
532 | }
533 | }
534 | });
535 | }
536 |
537 | function openLinkDialog() {
538 | $.prompt('',
539 | {
540 | title: "Share this link with everyone you want to invite",
541 | persistent: false,
542 | buttons: { "Cancel": false},
543 | loaded: function(event) {
544 | document.getElementById('inviteLinkRef').select();
545 | }
546 | });
547 | }
548 |
549 | function lockRoom(lock) {
550 | connection.emuc.lockRoom(sharedKey);
551 |
552 | buttonClick("#lockIcon", "fa fa-unlock fa-lg fa fa-lock fa-lg");
553 | }
554 |
555 | function openChat() {
556 | var chatspace = $('#chatspace');
557 | var videospace = $('#videospace');
558 | var chatspaceWidth = chatspace.width();
559 |
560 | if (chatspace.css("opacity") == 1) {
561 | chatspace.animate({opacity: 0}, "fast");
562 | chatspace.animate({width: 0}, "slow");
563 | videospace.animate({right: 0, width:"100%"}, "slow");
564 | }
565 | else {
566 | chatspace.animate({width:"20%"}, "slow");
567 | chatspace.animate({opacity: 1}, "slow");
568 | videospace.animate({right:chatspaceWidth, width:"80%"}, "slow");
569 | }
570 |
571 | // Request the focus in the nickname field or the chat input field.
572 | if ($('#nickinput').is(':visible'))
573 | $('#nickinput').focus();
574 | else
575 | $('#usermsg').focus();
576 | }
577 |
578 | function updateRoomUrl(newRoomUrl) {
579 | roomUrl = newRoomUrl;
580 | }
581 |
582 | function showToolbar() {
583 | $('#toolbar').css({visibility:"visible"});
584 | }
585 |
--------------------------------------------------------------------------------
/chromeonly.html:
--------------------------------------------------------------------------------
1 | Sorry, this currently only works with chrome because it uses "Plan B".
2 |
--------------------------------------------------------------------------------
/config.js:
--------------------------------------------------------------------------------
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 | // getroomnode: function (path) { return 'someprefixpossiblybasedonpath'; },
8 | useNicks: false,
9 | bosh: '/http-bind' // FIXME: use xep-0156 for that
10 | };
11 |
--------------------------------------------------------------------------------
/css/jquery-impromptu.css:
--------------------------------------------------------------------------------
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 |
126 |
--------------------------------------------------------------------------------
/css/main.css:
--------------------------------------------------------------------------------
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 | .videocontainer {
21 | position: relative;
22 | margin-left: auto;
23 | margin-right: auto;
24 | }
25 | .videocontainer>video {
26 | position: absolute;
27 | left: 0px;
28 | top: 0px;
29 | z-index: 0;
30 | width: 100%;
31 | height: 100%;
32 | }
33 | .videocontainer>span {
34 | display: none; /* enable when you want nicks to be shown */
35 | position: absolute;
36 | left: 0px;
37 | bottom: -20px;
38 | z-index: 0;
39 | width: 100%;
40 | font-size: 10pt;
41 | }
42 |
43 | #largeVideo {
44 | }
45 |
46 | #remoteVideos {
47 | display:block;
48 | position:relative;
49 | text-align:center;
50 | height:196px;
51 | padding-top:10px;
52 | width:auto;
53 | overflow: hidden;
54 | border:1px solid transparent;
55 | z-index: 2;
56 | }
57 |
58 | #remoteVideos>span {
59 | display: inline-block;
60 | }
61 |
62 | #remoteVideos video {
63 | z-index:0;
64 | border:1px solid #FFFFFF;
65 | }
66 |
67 | #remoteVideos>span:hover {
68 | cursor: pointer;
69 | cursor: hand;
70 | transform:scale(1.08, 1.08);
71 | -webkit-transform:scale(1.08, 1.08);
72 | transition-duration: 0.5s;
73 | -webkit-transition-duration: 0.5s;
74 | background-color: #FFFFFF;
75 | -webkit-animation-name: greyPulse;
76 | -webkit-animation-duration: 2s;
77 | -webkit-animation-iteration-count: 1;
78 | -webkit-box-shadow: 0 0 18px #515151;
79 | border:1px solid #FFFFFF;
80 | z-index: 10;
81 | }
82 |
83 | #chatspace {
84 | display:block;
85 | position:absolute;
86 | float: right;
87 | top: 40px;
88 | bottom: 0px;
89 | right: 0px;
90 | width:0;
91 | opacity: 0;
92 | overflow: hidden;
93 | background-color:#f6f6f6;
94 | border-left:1px solid #424242;
95 | }
96 |
97 | #chatconversation {
98 | display:block;
99 | position:relative;
100 | top: -120px;
101 | float:top;
102 | text-align:left;
103 | line-height:20px;
104 | font-size:14px;
105 | padding:5px;
106 | height:90%;
107 | overflow:scroll;
108 | visibility:hidden;
109 | }
110 |
111 | .localuser {
112 | color: #087dba;
113 | }
114 |
115 | .remoteuser {
116 | color: #424242;
117 | }
118 |
119 | #usermsg {
120 | position:absolute;
121 | bottom: 5px;
122 | left: 5px;
123 | right: 5px;
124 | width: 95%;
125 | height: 40px;
126 | z-index: 5;
127 | visibility:hidden;
128 | max-height:150px;
129 | }
130 |
131 | #nickname {
132 | position:relative;
133 | text-align:center;
134 | color: #9d9d9d;
135 | font-size: 18;
136 | top: 100px;
137 | left: 5px;
138 | right: 5px;
139 | width: 95%;
140 | }
141 |
142 | #nickinput {
143 | margin-top: 20px;
144 | font-size: 14;
145 | }
146 |
147 | #spacer {
148 | height:5px;
149 | }
150 |
151 | #settings {
152 | display:none;
153 | }
154 |
155 | #nowebrtc {
156 | display:none;
157 | }
158 |
159 | #header {
160 | height:39px;
161 | text-align:center;
162 | background-color:#087dba;
163 | }
164 |
165 | #toolbar {
166 | visibility:hidden;
167 | height:39px;
168 | }
169 |
170 | #left {
171 | display:block;
172 | position: absolute;
173 | left: 0px;
174 | top: 0px;
175 | width: 100px;
176 | height: 39px;
177 | background-image:url(../images/left1.png);
178 | background-repeat:no-repeat;
179 | margin: 0;
180 | padding: 0;
181 | }
182 |
183 | #leftlogo {
184 | position:absolute;
185 | top: 5px;
186 | left: 15px;
187 | background-image:url(../images/jitsilogo.png);
188 | background-repeat:no-repeat;
189 | height: 31px;
190 | width: 68px;
191 | z-index:1;
192 | }
193 |
194 | #link {
195 | display:block;
196 | position:relative;
197 | height:39px;
198 | width:auto;
199 | overflow: hidden;
200 | z-index:0;
201 | }
202 |
203 | .button {
204 | display: inline-block;
205 | position: relative;
206 | color: #FFFFFF;
207 | top: 0;
208 | padding: 10px 0px;
209 | width: 39px;
210 | cursor: pointer;
211 | font-size: 11pt;
212 | text-align: center;
213 | text-shadow: 0px 1px 0px rgba(255,255,255,.3), 0px -1px 0px rgba(0,0,0,.7);
214 | }
215 |
216 | a.button:hover {
217 | top: 0;
218 | cursor: pointer;
219 | background: rgba(0, 0, 0, 0.3);
220 | border-radius: 5px;
221 | background-clip: padding-box;
222 | -webkit-border-radius: 5px;
223 | -webkit-background-clip: padding-box;
224 | }
225 |
226 | .no-fa-video-camera, .fa-microphone-slash {
227 | color: #636363;
228 | }
229 |
230 | .fade_line {
231 | height: 1px;
232 | background: black;
233 | background: -webkit-gradient(linear, 0 0, 100% 0, from(#e9e9e9), to(#e9e9e9), color-stop(50%, black));
234 | }
235 |
236 | .header_button_separator {
237 | display: inline-block;
238 | position:relative;
239 | top: 7;
240 | width: 1px;
241 | height: 25px;
242 | background: white;
243 | background: -webkit-gradient(linear, 0 0, 0 100%, from(#087dba), to(#087dba), color-stop(50%, white));
244 | }
245 |
246 | #right {
247 | display:block;
248 | position:absolute;
249 | right: 0px;
250 | top: 0px;
251 | background-image:url(../images/right1.png);
252 | background-repeat:no-repeat;
253 | margin:0;
254 | padding:0;
255 | width:100px;
256 | height:39px;
257 | }
258 |
259 | #rightlogo {
260 | position:absolute;
261 | top: 6px;
262 | right: 15px;
263 | background-image:url(../images/estoslogo.png);
264 | background-repeat:no-repeat;
265 | height: 25px;
266 | width: 62px;
267 | z-index:1;
268 | }
269 |
270 | input, textarea {
271 | border: 0px none;
272 | display: inline-block;
273 | font-size: 14px;
274 | padding: 5px;
275 | background: #f3f3f3;
276 | border-radius: 3px;
277 | font-weight: 100;
278 | line-height: 20px;
279 | height: 40px;
280 | color: #333;
281 | font-weight: bold;
282 | text-align: left;
283 | border:1px solid #ACD8F0;
284 | outline: none; /* removes the default outline */
285 | resize: none; /* prevents the user-resizing, adjust to taste */
286 | }
287 |
288 | input, textarea:focus {
289 | box-shadow: inset 0 0 3px 2px #ACD8F0; /* provides a more style-able
290 | replacement to the outline */
291 | }
292 |
293 | textarea {
294 | overflow: hidden;
295 | word-wrap: break-word;
296 | resize: horizontal;
297 | }
298 |
299 | button.no-icon {
300 | padding: 0 1em;
301 | }
302 |
303 | button {
304 | border: none;
305 | height: 35px;
306 | padding: 0 1em 0 2em;
307 | position: relative;
308 | border-radius: 3px;
309 | font-weight: bold;
310 | color: #fff;
311 | line-height: 35px;
312 | background: #2c8ad2;
313 | }
314 |
315 | button, input, select, textarea {
316 | font-size: 100%;
317 | margin: 0;
318 | vertical-align: baseline;
319 | }
320 |
321 | button, input[type="button"], input[type="reset"], input[type="submit"] {
322 | cursor: pointer;
323 | -webkit-appearance: button;
324 | }
325 |
326 | form {
327 | display: block;
328 | }
329 |
330 | /* Animated text area. */
331 | .animated {
332 | -webkit-transition: height 0.2s;
333 | -moz-transition: height 0.2s;
334 | transition: height 0.2s;
335 | }
336 |
--------------------------------------------------------------------------------
/css/modaldialog.css:
--------------------------------------------------------------------------------
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 | }
--------------------------------------------------------------------------------
/estos_log.js:
--------------------------------------------------------------------------------
1 | Strophe.addConnectionPlugin('logger', {
2 | // logs raw stanzas and makes them available for download as JSON
3 | connection: null,
4 | log: [],
5 | init: function (conn) {
6 | this.connection = conn;
7 | this.connection.rawInput = this.log_incoming.bind(this);;
8 | this.connection.rawOutput = this.log_outgoing.bind(this);;
9 | },
10 | log_incoming: function (stanza) {
11 | this.log.push([new Date().getTime(), 'incoming', stanza]);
12 | },
13 | log_outgoing: function (stanza) {
14 | this.log.push([new Date().getTime(), 'outgoing', stanza]);
15 | },
16 | });
17 |
--------------------------------------------------------------------------------
/images/estoslogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ESTOS/meet/b8098856dbf04382140f914ee1b6065193739d0e/images/estoslogo.png
--------------------------------------------------------------------------------
/images/jitsilogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ESTOS/meet/b8098856dbf04382140f914ee1b6065193739d0e/images/jitsilogo.png
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | WebRTC, meet the Jitsi Videobridge
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
36 |
37 |
Connection Settings
38 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | Enter a nickname in the box below
62 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/libs/colibri.js:
--------------------------------------------------------------------------------
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 self = 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 | self.peers.push(peer);
72 | self.channels.push([]);
73 | });
74 |
75 | this.peerconnection = new TraceablePeerConnection(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', self.peerconnection.iceConnectionState);
79 | /*
80 | if (self.peerconnection.signalingState == 'stable' && self.peerconnection.iceConnectionState == 'connected') {
81 | console.log('adding new remote SSRCs from iceconnectionstatechange');
82 | window.setTimeout(function() { self.modifySources(); }, 1000);
83 | }
84 | */
85 | };
86 | this.peerconnection.onsignalingstatechange = function (event) {
87 | console.warn(self.peerconnection.signalingState);
88 | /*
89 | if (self.peerconnection.signalingState == 'stable' && self.peerconnection.iceConnectionState == 'connected') {
90 | console.log('adding new remote SSRCs from signalingstatechange');
91 | window.setTimeout(function() { self.modifySources(); }, 1000);
92 | }
93 | */
94 | };
95 | this.peerconnection.onaddstream = function (event) {
96 | self.remoteStream = event.stream;
97 | // search the jid associated with this stream
98 | Object.keys(self.remotessrc).forEach(function (jid) {
99 | if (self.remotessrc[jid].join('\r\n').indexOf('mslabel:' + event.stream.id) != -1) {
100 | event.peerjid = jid;
101 | if (self.connection.jingle.jid2session[jid]) {
102 | self.connection.jingle.jid2session[jid].remotestream = event.stream;
103 | }
104 | }
105 | });
106 | $(document).trigger('remotestreamadded.jingle', [event, self.sid]);
107 | };
108 | this.peerconnection.onicecandidate = function (event) {
109 | self.sendIceCandidate(event.candidate);
110 | };
111 | this.peerconnection.createOffer(
112 | function (offer) {
113 | self.peerconnection.setLocalDescription(
114 | offer,
115 | function () {
116 | // success
117 | $(document).trigger('setLocalDescription.jingle', [self.sid]);
118 | // FIXME: could call _makeConference here and trickle candidates later
119 | },
120 | function (error) {
121 | console.log('setLocalDescription failed', error);
122 | }
123 | );
124 | },
125 | function (error) {
126 | console.warn(error);
127 | }
128 | );
129 | this.peerconnection.onicecandidate = function (event) {
130 | if (!event.candidate) {
131 | console.log('end of candidates');
132 | self._makeConference();
133 | return;
134 | }
135 | };
136 | };
137 |
138 | ColibriFocus.prototype._makeConference = function () {
139 | var self = this;
140 | var elem = $iq({to: this.bridgejid, type: 'get'});
141 | elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri'});
142 |
143 | var localSDP = new SDP(this.peerconnection.localDescription.sdp);
144 | localSDP.media.forEach(function (media, channel) {
145 | var name = SDPUtil.parse_mline(media.split('\r\n')[0]).media;
146 | elem.c('content', {name: name});
147 | elem.c('channel', {initiator: 'false', expire: '15'});
148 |
149 | // FIXME: should reuse code from .toJingle
150 | var mline = SDPUtil.parse_mline(media.split('\r\n')[0]);
151 | for (var j = 0; j < mline.fmt.length; j++) {
152 | var rtpmap = SDPUtil.find_line(media, 'a=rtpmap:' + mline.fmt[j]);
153 | elem.c('payload-type', SDPUtil.parse_rtpmap(rtpmap));
154 | elem.up();
155 | }
156 |
157 | localSDP.TransportToJingle(channel, elem);
158 |
159 | elem.up(); // end of channel
160 | for (j = 0; j < self.peers.length; j++) {
161 | elem.c('channel', {initiator: 'true', expire:'15' }).up();
162 | }
163 | elem.up(); // end of content
164 | });
165 |
166 | this.connection.sendIQ(elem,
167 | function (result) {
168 | self.createdConference(result);
169 | },
170 | function (error) {
171 | console.warn(error);
172 | }
173 | );
174 | };
175 |
176 | // callback when a conference was created
177 | ColibriFocus.prototype.createdConference = function (result) {
178 | console.log('created a conference on the bridge');
179 | var tmp;
180 |
181 | this.confid = $(result).find('>conference').attr('id');
182 | var remotecontents = $(result).find('>conference>content').get();
183 | var numparticipants = 0;
184 | for (var i = 0; i < remotecontents.length; i++) {
185 | tmp = $(remotecontents[i]).find('>channel').get();
186 | this.mychannel.push($(tmp.shift()));
187 | numparticipants = tmp.length;
188 | for (j = 0; j < tmp.length; j++) {
189 | if (this.channels[j] === undefined) {
190 | this.channels[j] = [];
191 | }
192 | this.channels[j].push(tmp[j]);
193 | }
194 | }
195 | console.log('remote channels', this.channels);
196 | var localSDP = new SDP(this.peerconnection.localDescription.sdp);
197 | localSDP.removeSessionLines('a=group:');
198 | localSDP.removeSessionLines('a=msid-semantic:');
199 |
200 | // establish our channel with the bridge
201 | // static answer taken from chrome M31, should be replaced by a
202 | // dynamic one that is based on our offer FIXME
203 | var bridgeSDP = new SDP("");
204 | // 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');
205 | // only do what's in the offer
206 | bridgeSDP.session = localSDP.session;
207 | bridgeSDP.media.length = localSDP.media.length;
208 | var channel;
209 | for (channel = 0; channel < bridgeSDP.media.length; channel++) {
210 | bridgeSDP.media[channel] = '';
211 | // unchanged lines
212 | bridgeSDP.media[channel] += SDPUtil.find_line(localSDP.media[channel], 'm=') + '\r\n';
213 | bridgeSDP.media[channel] += SDPUtil.find_line(localSDP.media[channel], 'c=') + '\r\n';
214 | if (SDPUtil.find_line(localSDP.media[channel], 'a=rtcp:')) {
215 | bridgeSDP.media[channel] += SDPUtil.find_line(localSDP.media[channel], 'a=rtcp:') + '\r\n';
216 | }
217 | if (SDPUtil.find_line(localSDP.media[channel], 'a=mid:')) {
218 | bridgeSDP.media[channel] += SDPUtil.find_line(localSDP.media[channel], 'a=mid:') + '\r\n';
219 | }
220 | if (SDPUtil.find_line(localSDP.media[channel], 'a=sendrecv')) {
221 | bridgeSDP.media[channel] += 'a=sendrecv\r\n';
222 | }
223 | if (SDPUtil.find_line(localSDP.media[channel], 'a=extmap:')) {
224 | bridgeSDP.media[channel] += SDPUtil.find_lines(localSDP.media[channel], 'a=extmap:').join('\r\n') + '\r\n';
225 | }
226 |
227 | // FIXME: should look at m-line and group the ids together
228 | if (SDPUtil.find_line(localSDP.media[channel], 'a=rtpmap:')) {
229 | bridgeSDP.media[channel] += SDPUtil.find_lines(localSDP.media[channel], 'a=rtpmap:').join('\r\n') + '\r\n';
230 | }
231 | if (SDPUtil.find_line(localSDP.media[channel], 'a=fmtp:')) {
232 | bridgeSDP.media[channel] += SDPUtil.find_lines(localSDP.media[channel], 'a=fmtp:').join('\r\n') + '\r\n';
233 | }
234 | if (SDPUtil.find_line(localSDP.media[channel], 'a=rtcp-fb:')) {
235 | bridgeSDP.media[channel] += SDPUtil.find_lines(localSDP.media[channel], 'a=rtcp-fb:').join('\r\n') + '\r\n';
236 | }
237 | // FIXME: changed lines -- a=sendrecv direction, a=setup direction
238 | }
239 | // get the mixed ssrc
240 | for (channel = 0; channel < bridgeSDP.media.length; channel++) {
241 | tmp = $(this.mychannel[channel]).find('>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
242 | // FIXME: check rtp-level-relay-type
243 | if (tmp.length) {
244 | bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'cname:mixed' + '\r\n';
245 | bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'label:mixedlabela0' + '\r\n';
246 | bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'msid:mixedmslabel mixedlabela0' + '\r\n';
247 | bridgeSDP.media[channel] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'mslabel:mixedmslabel' + '\r\n';
248 | } else {
249 | // make chrome happy... '3735928559' == 0xDEADBEEF
250 | // FIXME: this currently appears as two streams, should be one
251 | bridgeSDP.media[channel] += 'a=ssrc:' + '3735928559' + ' ' + 'cname:mixed' + '\r\n';
252 | bridgeSDP.media[channel] += 'a=ssrc:' + '3735928559' + ' ' + 'label:mixedlabelv0' + '\r\n';
253 | bridgeSDP.media[channel] += 'a=ssrc:' + '3735928559' + ' ' + 'msid:mixedmslabel mixedlabelv0' + '\r\n';
254 | bridgeSDP.media[channel] += 'a=ssrc:' + '3735928559' + ' ' + 'mslabel:mixedmslabel' + '\r\n';
255 | }
256 |
257 | // FIXME: should take code from .fromJingle
258 | tmp = $(this.mychannel[channel]).find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]');
259 | if (tmp.length) {
260 | bridgeSDP.media[channel] += 'a=ice-ufrag:' + tmp.attr('ufrag') + '\r\n';
261 | bridgeSDP.media[channel] += 'a=ice-pwd:' + tmp.attr('pwd') + '\r\n';
262 | tmp.find('>candidate').each(function () {
263 | bridgeSDP.media[channel] += SDPUtil.candidateFromJingle(this);
264 | });
265 | tmp = tmp.find('>fingerprint');
266 | if (tmp.length) {
267 | bridgeSDP.media[channel] += 'a=fingerprint:' + tmp.attr('hash') + ' ' + tmp.text() + '\r\n';
268 | if (tmp.attr('setup')) {
269 | bridgeSDP.media[channel] += 'a=setup:' + tmp.attr('setup') + '\r\n';
270 | } else {
271 | bridgeSDP.media[channel] += 'a=setup:active\r\n';
272 | }
273 | }
274 | }
275 | }
276 | bridgeSDP.raw = bridgeSDP.session + bridgeSDP.media.join('');
277 |
278 | var self = this;
279 | this.peerconnection.setRemoteDescription(
280 | new RTCSessionDescription({type: 'answer', sdp: bridgeSDP.raw}),
281 | function () {
282 | console.log('setRemoteDescription success');
283 | for (var i = 0; i < numparticipants; i++) {
284 | self.initiate(self.peers[i], true);
285 | }
286 | },
287 | function (error) {
288 | console.log('setRemoteDescription failed');
289 | }
290 | );
291 |
292 | };
293 |
294 | // send a session-initiate to a new participant
295 | ColibriFocus.prototype.initiate = function (peer, isInitiator) {
296 | var participant = this.peers.indexOf(peer);
297 | console.log('tell', peer, participant);
298 | var sdp;
299 | if (this.peerconnection !== null && this.peerconnection.signalingState == 'stable') {
300 | sdp = new SDP(this.peerconnection.remoteDescription.sdp);
301 | var localSDP = new SDP(this.peerconnection.localDescription.sdp);
302 | // throw away stuff we don't want
303 | // not needed with static offer
304 | sdp.removeSessionLines('a=group:');
305 | sdp.removeSessionLines('a=msid-semantic:'); // FIXME: not mapped over jingle anyway...
306 | for (var i = 0; i < sdp.media.length; i++) {
307 | sdp.removeMediaLines(i, 'a=rtcp-mux');
308 | sdp.removeMediaLines(i, 'a=ssrc:');
309 | sdp.removeMediaLines(i, 'a=crypto:');
310 | sdp.removeMediaLines(i, 'a=candidate:');
311 | sdp.removeMediaLines(i, 'a=ice-options:google-ice');
312 | sdp.removeMediaLines(i, 'a=ice-ufrag:');
313 | sdp.removeMediaLines(i, 'a=ice-pwd:');
314 | sdp.removeMediaLines(i, 'a=fingerprint:');
315 | sdp.removeMediaLines(i, 'a=setup:');
316 |
317 | if (1) { //i > 0) { // not for audio FIXME: does not work as intended
318 | // re-add all remote a=ssrcs
319 | for (var jid in this.remotessrc) {
320 | if (jid == peer) continue;
321 | sdp.media[i] += this.remotessrc[jid][i];
322 | }
323 | // and local a=ssrc lines
324 | sdp.media[i] += SDPUtil.find_lines(localSDP.media[i], 'a=ssrc').join('\r\n') + '\r\n';
325 | }
326 | }
327 | sdp.raw = sdp.session + sdp.media.join('');
328 | } else {
329 | console.error('can not initiate a new session without a stable peerconnection');
330 | return;
331 | }
332 |
333 | // add stuff we got from the bridge
334 | for (var j = 0; j < sdp.media.length; j++) {
335 | var chan = $(this.channels[participant][j]);
336 | console.log('channel id', chan.attr('id'));
337 |
338 | tmp = chan.find('>source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]');
339 | if (tmp.length) {
340 | sdp.media[j] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'cname:mixed' + '\r\n';
341 | sdp.media[j] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'label:mixedlabela0' + '\r\n';
342 | sdp.media[j] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'msid:mixedmslabel mixedlabela0' + '\r\n';
343 | sdp.media[j] += 'a=ssrc:' + tmp.attr('ssrc') + ' ' + 'mslabel:mixedmslabel' + '\r\n';
344 | } else {
345 | // make chrome happy... '3735928559' == 0xDEADBEEF
346 | sdp.media[j] += 'a=ssrc:' + '3735928559' + ' ' + 'cname:mixed' + '\r\n';
347 | sdp.media[j] += 'a=ssrc:' + '3735928559' + ' ' + 'label:mixedlabelv0' + '\r\n';
348 | sdp.media[j] += 'a=ssrc:' + '3735928559' + ' ' + 'msid:mixedmslabel mixedlabelv0' + '\r\n';
349 | sdp.media[j] += 'a=ssrc:' + '3735928559' + ' ' + 'mslabel:mixedmslabel' + '\r\n';
350 | }
351 |
352 | tmp = chan.find('>transport[xmlns="urn:xmpp:jingle:transports:ice-udp:1"]');
353 | if (tmp.length) {
354 | if (tmp.attr('ufrag'))
355 | sdp.media[j] += 'a=ice-ufrag:' + tmp.attr('ufrag') + '\r\n';
356 | if (tmp.attr('pwd'))
357 | sdp.media[j] += 'a=ice-pwd:' + tmp.attr('pwd') + '\r\n';
358 | // and the candidates...
359 | tmp.find('>candidate').each(function () {
360 | sdp.media[j] += SDPUtil.candidateFromJingle(this);
361 | });
362 | tmp = tmp.find('>fingerprint');
363 | if (tmp.length) {
364 | sdp.media[j] += 'a=fingerprint:' + tmp.attr('hash') + ' ' + tmp.text() + '\r\n';
365 | /*
366 | if (tmp.attr('direction')) {
367 | sdp.media[j] += 'a=setup:' + tmp.attr('direction') + '\r\n';
368 | }
369 | */
370 | sdp.media[j] += 'a=setup:actpass\r\n';
371 | }
372 | }
373 | }
374 | // make a new colibri session and configure it
375 | // FIXME: is it correct to use this.connection.jid when used in a MUC?
376 | var sess = new ColibriSession(this.connection.jid,
377 | Math.random().toString(36).substr(2, 12), // random string
378 | this.connection);
379 | sess.initiate(peer);
380 | sess.colibri = this;
381 | sess.localStream = this.connection.jingle.localStream;
382 | sess.media_constraints = this.connection.jingle.media_constraints;
383 | sess.pc_constraints = this.connection.jingle.pc_constraints;
384 | sess.ice_config = this.connection.jingle.ice_config;
385 |
386 | this.connection.jingle.sessions[sess.sid] = sess;
387 | this.connection.jingle.jid2session[sess.peerjid] = sess;
388 |
389 | // send a session-initiate
390 | var init = $iq({to: peer, type: 'set'})
391 | .c('jingle',
392 | {xmlns: 'urn:xmpp:jingle:1',
393 | action: 'session-initiate',
394 | initiator: sess.me,
395 | sid: sess.sid
396 | }
397 | );
398 | sdp.toJingle(init, 'initiator');
399 | this.connection.sendIQ(init,
400 | function (res) {
401 | console.log('got result');
402 | },
403 | function (err) {
404 | console.log('got error');
405 | }
406 | );
407 | };
408 |
409 | // pull in a new participant into the conference
410 | ColibriFocus.prototype.addNewParticipant = function (peer) {
411 | var self = this;
412 | if (this.confid === 0) {
413 | // bad state
414 | console.log('confid does not exist yet, postponing', peer);
415 | window.setTimeout(function () {
416 | self.addNewParticipant(peer);
417 | }, 250);
418 | return;
419 | }
420 | var index = this.channels.length;
421 | this.channels.push([]);
422 | this.peers.push(peer);
423 |
424 | var elem = $iq({to: this.bridgejid, type: 'get'});
425 | elem.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
426 | var localSDP = new SDP(this.peerconnection.localDescription.sdp);
427 | localSDP.media.forEach(function (media, channel) {
428 | var name = SDPUtil.parse_mline(media.split('\r\n')[0]).media;
429 | elem.c('content', {name: name});
430 | elem.c('channel', {initiator: 'true', expire:'15'});
431 | elem.up(); // end of channel
432 | elem.up(); // end of content
433 | });
434 |
435 | this.connection.sendIQ(elem,
436 | function (result) {
437 | var contents = $(result).find('>conference>content').get();
438 | for (var i = 0; i < contents.length; i++) {
439 | tmp = $(contents[i]).find('>channel').get();
440 | self.channels[index][i] = tmp[0];
441 | }
442 | self.initiate(peer, true);
443 | },
444 | function (error) {
445 | console.warn(error);
446 | }
447 | );
448 | };
449 |
450 | // update the channel description (payload-types + dtls fp) for a participant
451 | ColibriFocus.prototype.updateChannel = function (remoteSDP, participant) {
452 | console.log('change allocation for', this.confid);
453 | var change = $iq({to: this.bridgejid, type: 'set'});
454 | change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
455 | for (channel = 0; channel < this.channels[participant].length; channel++) {
456 | change.c('content', {name: channel === 0 ? 'audio' : 'video'});
457 | change.c('channel', {id: $(this.channels[participant][channel]).attr('id')});
458 |
459 | var rtpmap = SDPUtil.find_lines(remoteSDP.media[channel], 'a=rtpmap:');
460 | rtpmap.forEach(function (val) {
461 | // TODO: too much copy-paste
462 | var rtpmap = SDPUtil.parse_rtpmap(val);
463 | change.c('payload-type', rtpmap);
464 | //
465 | // put any 'a=fmtp:' + mline.fmt[j] lines into
466 | /*
467 | if (SDPUtil.find_line(remoteSDP.media[channel], 'a=fmtp:' + rtpmap.id)) {
468 | tmp = SDPUtil.parse_fmtp(SDPUtil.find_line(remoteSDP.media[channel], 'a=fmtp:' + rtpmap.id));
469 | for (var k = 0; k < tmp.length; k++) {
470 | change.c('parameter', tmp[k]).up();
471 | }
472 | }
473 | */
474 | change.up();
475 | });
476 | // now add transport
477 | remoteSDP.TransportToJingle(channel, change);
478 |
479 | change.up(); // end of channel
480 | change.up(); // end of content
481 | }
482 | this.connection.sendIQ(change,
483 | function (res) {
484 | console.log('got result');
485 | },
486 | function (err) {
487 | console.log('got error');
488 | }
489 | );
490 | };
491 |
492 | // tell everyone about a new participants a=ssrc lines (isadd is true)
493 | // or a leaving participants a=ssrc lines
494 | // FIXME: should not take an SDP, but rather the a=ssrc lines and probably a=mid
495 | ColibriFocus.prototype.sendSSRCUpdate = function (sdp, jid, isadd) {
496 | var self = this;
497 | this.peers.forEach(function (peerjid) {
498 | if (peerjid == jid) return;
499 | console.log('tell', peerjid, 'about ' + (isadd ? 'new' : 'removed') + ' ssrcs from', jid);
500 | if (!self.remotessrc[peerjid]) {
501 | // FIXME: this should only send to participants that are stable, i.e. who have sent a session-accept
502 | // possibly, this.remoteSSRC[session.peerjid] does not exist yet
503 | console.warn('do we really want to bother', peerjid, 'with updates yet?');
504 | }
505 | var channel;
506 | var peersess = self.connection.jingle.jid2session[peerjid];
507 | var modify = $iq({to: peerjid, type: 'set'})
508 | .c('jingle', {
509 | xmlns: 'urn:xmpp:jingle:1',
510 | action: isadd ? 'addsource' : 'removesource',
511 | initiator: peersess.initiator,
512 | sid: peersess.sid
513 | }
514 | );
515 | // FIXME: only announce video ssrcs since we mix audio and dont need
516 | // the audio ssrcs therefore
517 | var modified = false;
518 | for (channel = 0; channel < sdp.media.length; channel++) {
519 | modified = true;
520 | tmp = SDPUtil.find_lines(sdp.media[channel], 'a=ssrc:');
521 | modify.c('content', {name: SDPUtil.parse_mid(SDPUtil.find_line(sdp.media[channel], 'a=mid:'))});
522 | modify.c('source', { xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
523 | // FIXME: not completly sure this operates on blocks and / or handles different ssrcs correctly
524 | tmp.forEach(function (line) {
525 | var idx = line.indexOf(' ');
526 | var linessrc = line.substr(0, idx).substr(7);
527 | modify.attrs({ssrc: linessrc});
528 |
529 | var kv = line.substr(idx + 1);
530 | modify.c('parameter');
531 | if (kv.indexOf(':') == -1) {
532 | modify.attrs({ name: kv });
533 | } else {
534 | modify.attrs({ name: kv.split(':', 2)[0] });
535 | modify.attrs({ value: kv.split(':', 2)[1] });
536 | }
537 | modify.up();
538 | });
539 | modify.up(); // end of source
540 | modify.up(); // end of content
541 | }
542 | if (modified) {
543 | self.connection.sendIQ(modify,
544 | function (res) {
545 | console.warn('got modify result');
546 | },
547 | function (err) {
548 | console.warn('got modify error');
549 | }
550 | );
551 | } else {
552 | console.log('modification not necessary');
553 | }
554 | });
555 | };
556 |
557 | ColibriFocus.prototype.setRemoteDescription = function (session, elem, desctype) {
558 | var participant = this.peers.indexOf(session.peerjid);
559 | console.log('Colibri.setRemoteDescription from', session.peerjid, participant);
560 | var self = this;
561 | var remoteSDP = new SDP('');
562 | var tmp;
563 | var channel;
564 | remoteSDP.fromJingle(elem);
565 |
566 | // ACT 1: change allocation on bridge
567 | this.updateChannel(remoteSDP, participant);
568 |
569 | // ACT 2: tell anyone else about the new SSRCs
570 | this.sendSSRCUpdate(remoteSDP, session.peerjid, true);
571 |
572 | // ACT 3: note the SSRCs
573 | this.remotessrc[session.peerjid] = [];
574 | for (channel = 0; channel < this.channels[participant].length; channel++) {
575 | //if (channel == 0) continue; FIXME: does not work as intended
576 | this.remotessrc[session.peerjid][channel] = SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:').join('\r\n') + '\r\n';
577 | }
578 |
579 | // ACT 4: add new a=ssrc lines to local remotedescription
580 | for (channel = 0; channel < this.channels[participant].length; channel++) {
581 | //if (channel == 0) continue; FIXME: does not work as intended
582 | if (!this.addssrc[channel]) this.addssrc[channel] = '';
583 | this.addssrc[channel] += SDPUtil.find_lines(remoteSDP.media[channel], 'a=ssrc:').join('\r\n') + '\r\n';
584 | }
585 | this.modifySources();
586 | };
587 |
588 | // relay ice candidates to bridge using trickle
589 | ColibriFocus.prototype.addIceCandidate = function (session, elem) {
590 | var self = this;
591 | var participant = this.peers.indexOf(session.peerjid);
592 | //console.log('change transport allocation for', this.confid, session.peerjid, participant);
593 | var change = $iq({to: this.bridgejid, type: 'set'});
594 | change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
595 | $(elem).each(function () {
596 | var name = $(this).attr('name');
597 | var channel = name == 'audio' ? 0 : 1; // FIXME: search mlineindex in localdesc
598 |
599 | change.c('content', {name: name});
600 | change.c('channel', {id: $(self.channels[participant][channel]).attr('id')});
601 | $(this).find('>transport').each(function () {
602 | change.c('transport', {
603 | ufrag: $(this).attr('ufrag'),
604 | pwd: $(this).attr('pwd'),
605 | xmlns: $(this).attr('xmlns')
606 | });
607 |
608 | $(this).find('>candidate').each(function () {
609 | /* not yet
610 | if (this.getAttribute('protocol') == 'tcp' && this.getAttribute('port') == 0) {
611 | // chrome generates TCP candidates with port 0
612 | return;
613 | }
614 | */
615 | var line = SDPUtil.candidateFromJingle(this);
616 | change.c('candidate', SDPUtil.candidateToJingle(line)).up();
617 | });
618 | change.up(); // end of transport
619 | });
620 | change.up(); // end of channel
621 | change.up(); // end of content
622 | });
623 | // FIXME: need to check if there is at least one candidate when filtering TCP ones
624 | this.connection.sendIQ(change,
625 | function (res) {
626 | console.log('got result');
627 | },
628 | function (err) {
629 | console.warn('got error');
630 | }
631 | );
632 | };
633 |
634 | // send our own candidate to the bridge
635 | ColibriFocus.prototype.sendIceCandidate = function (candidate) {
636 | //console.log('candidate', candidate);
637 | if (!candidate) {
638 | console.log('end of candidates');
639 | return;
640 | }
641 | var mycands = $iq({to: this.bridgejid, type: 'set'});
642 | mycands.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
643 | mycands.c('content', {name: candidate.sdpMid });
644 | mycands.c('channel', {id: $(this.mychannel[candidate.sdpMLineIndex]).attr('id')});
645 | mycands.c('transport', {xmlns: 'urn:xmpp:jingle:transports:ice-udp:1'});
646 | tmp = SDPUtil.candidateToJingle(candidate.candidate);
647 | mycands.c('candidate', tmp).up();
648 | this.connection.sendIQ(mycands,
649 | function (res) {
650 | console.log('got result');
651 | },
652 | function (err) {
653 | console.warn('got error');
654 | }
655 | );
656 | };
657 |
658 | ColibriFocus.prototype.terminate = function (session, reason) {
659 | console.log('remote session terminated from', session.peerjid);
660 | var participant = this.peers.indexOf(session.peerjid);
661 | if (!this.remotessrc[session.peerjid] || participant == -1) {
662 | return;
663 | }
664 | var ssrcs = this.remotessrc[session.peerjid];
665 | for (var i = 0; i < ssrcs.length; i++) {
666 | if (!this.removessrc[i]) this.removessrc[i] = '';
667 | this.removessrc[i] += ssrcs[i];
668 | }
669 | // remove from this.peers
670 | this.peers.splice(participant, 1);
671 | // expire channel on bridge
672 | var change = $iq({to: this.bridgejid, type: 'set'});
673 | change.c('conference', {xmlns: 'http://jitsi.org/protocol/colibri', id: this.confid});
674 | for (var channel = 0; channel < this.channels[participant].length; channel++) {
675 | change.c('content', {name: channel === 0 ? 'audio' : 'video'});
676 | change.c('channel', {id: $(this.channels[participant][channel]).attr('id'), expire: '0'});
677 | change.up(); // end of channel
678 | change.up(); // end of content
679 | }
680 | this.connection.sendIQ(change,
681 | function (res) {
682 | console.log('got result');
683 | },
684 | function (err) {
685 | console.log('got error');
686 | }
687 | );
688 | // and remove from channels
689 | this.channels.splice(participant, 1);
690 |
691 | // tell everyone about the ssrcs to be removed
692 | var sdp = new SDP('');
693 | var localSDP = new SDP(this.peerconnection.localDescription.sdp);
694 | var contents = SDPUtil.find_lines(localSDP.raw, 'a=mid:').map(SDPUtil.parse_mid);
695 | for (var j = 0; j < ssrcs.length; j++) {
696 | sdp.media[j] = 'a=mid:' + contents[j] + '\r\n';
697 | sdp.media[j] += ssrcs[j];
698 | this.removessrc[j] += ssrcs[j];
699 | }
700 | this.sendSSRCUpdate(sdp, session.peerjid, false);
701 |
702 | delete this.remotessrc[session.peerjid];
703 | this.modifySources();
704 | };
705 |
706 | ColibriFocus.prototype.modifySources = function () {
707 | var self = this;
708 | if (!(this.addssrc.length || this.removessrc.length)) return;
709 | if (this.peerconnection.signalingState == 'closed') return;
710 |
711 | // FIXME: this is a big hack
712 | // https://code.google.com/p/webrtc/issues/detail?id=2688
713 | if (!(this.peerconnection.signalingState == 'stable' && this.peerconnection.iceConnectionState == 'connected')) {
714 | console.warn('modifySources not yet', this.peerconnection.signalingState, this.peerconnection.iceConnectionState);
715 | window.setTimeout(function () { self.modifySources(); }, 250);
716 | this.wait = true;
717 | return;
718 | }
719 | if (this.wait) {
720 | window.setTimeout(function () { self.modifySources(); }, 2500);
721 | this.wait = false;
722 | return;
723 | }
724 | var sdp = new SDP(this.peerconnection.remoteDescription.sdp);
725 |
726 | // add sources
727 | this.addssrc.forEach(function (lines, idx) {
728 | sdp.media[idx] += lines;
729 | });
730 | this.addssrc = [];
731 |
732 | // remove sources
733 | this.removessrc.forEach(function (lines, idx) {
734 | lines = lines.split('\r\n');
735 | lines.pop(); // remove empty last element;
736 | lines.forEach(function (line) {
737 | sdp.media[idx] = sdp.media[idx].replace(line + '\r\n', '');
738 | });
739 | });
740 | this.removessrc = [];
741 |
742 | sdp.raw = sdp.session + sdp.media.join('');
743 | /*
744 | * this seems to create a number of problems...
745 | this.peerconnection.setRemoteDescription(
746 | new RTCSessionDescription({type: 'offer', sdp: sdp.raw }),
747 | function () {
748 | console.log('setModifiedRemoteDescription ok');
749 | self.peerconnection.createAnswer(
750 | function (modifiedAnswer) {
751 | console.log('modifiedAnswer created', modifiedAnswer.sdp);
752 | // FIXME: pushing down an answer while ice connection state
753 | // is still checking is bad...
754 | console.log(self.peerconnection.iceConnectionState);
755 |
756 | // trying to work around another chrome bug
757 | //modifiedAnswer.sdp = modifiedAnswer.sdp.replace(/a=setup:active/g, 'a=setup:actpass');
758 | self.peerconnection.setLocalDescription(modifiedAnswer,
759 | function () {
760 | console.log('setModifiedLocalDescription ok');
761 | $(document).trigger('setLocalDescription.jingle', [self.sid]);
762 | },
763 | function (error) {
764 | console.log('setModifiedLocalDescription failed');
765 | }
766 | );
767 | },
768 | function (error) {
769 | console.log('createModifiedAnswer failed');
770 | }
771 | );
772 | },
773 | function (error) {
774 | console.log('setModifiedRemoteDescription failed');
775 | }
776 | );
777 | */
778 | this.peerconnection.createOffer(
779 | function (modifiedOffer) {
780 | console.log('created (un)modified offer');
781 | self.peerconnection.setLocalDescription(modifiedOffer,
782 | function () {
783 | console.log('setModifiedLocalDescription ok');
784 | self.peerconnection.setRemoteDescription(
785 | new RTCSessionDescription({type: 'answer', sdp: sdp.raw }),
786 | function () {
787 | console.log('setModifiedRemoteDescription ok');
788 | },
789 | function (error) {
790 | console.log('setModifiedRemoteDescription failed');
791 | }
792 | );
793 | $(document).trigger('setLocalDescription.jingle', [self.sid]);
794 | },
795 | function (error) {
796 | console.log('setModifiedLocalDescription failed');
797 | }
798 | );
799 | },
800 | function (error) {
801 | console.log('creating (un)modified offerfailed');
802 | }
803 | );
804 | };
805 |
806 |
807 | // A colibri session is similar to a jingle session, it just implements some things differently
808 | // FIXME: inherit jinglesession, see https://github.com/legastero/Jingle-RTCPeerConnection/blob/master/index.js
809 | function ColibriSession(me, sid, connection) {
810 | this.me = me;
811 | this.sid = sid;
812 | this.connection = connection;
813 | //this.peerconnection = null;
814 | //this.mychannel = null;
815 | //this.channels = null;
816 | this.peerjid = null;
817 |
818 | this.colibri = null;
819 | }
820 |
821 | // implementation of JingleSession interface
822 | ColibriSession.prototype.initiate = function (peerjid, isInitiator) {
823 | this.peerjid = peerjid;
824 | };
825 |
826 | ColibriSession.prototype.sendOffer = function (offer) {
827 | console.log('ColibriSession.sendOffer');
828 | };
829 |
830 |
831 | ColibriSession.prototype.accept = function () {
832 | console.log('ColibriSession.accept');
833 | };
834 |
835 | ColibriSession.prototype.terminate = function (reason) {
836 | this.colibri.terminate(this, reason);
837 | };
838 |
839 | ColibriSession.prototype.active = function () {
840 | console.log('ColibriSession.active');
841 | };
842 |
843 | ColibriSession.prototype.setRemoteDescription = function (elem, desctype) {
844 | this.colibri.setRemoteDescription(this, elem, desctype);
845 | };
846 |
847 | ColibriSession.prototype.addIceCandidate = function (elem) {
848 | this.colibri.addIceCandidate(this, elem);
849 | };
850 |
851 | ColibriSession.prototype.sendAnswer = function (sdp, provisional) {
852 | console.log('ColibriSession.sendAnswer');
853 | };
854 |
855 | ColibriSession.prototype.sendTerminate = function (reason, text) {
856 | console.log('ColibriSession.sendTerminate');
857 | };
858 |
--------------------------------------------------------------------------------
/libs/jquery-impromptu.js:
--------------------------------------------------------------------------------
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 = '';
34 | if(opts.useiframe && ($('object, applet').length > 0)) {
35 | msgbox += '
';
36 | } else {
37 | msgbox +='
';
38 | }
39 | msgbox += '
'+
45 | '
';
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: '×',
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 = '';
430 | }
431 | if(stateobj.title && stateobj.title !== ''){
432 | title = ''+ stateobj.title +'
';
433 | }
434 | state += ''+
435 | arrow + title +
436 | '
' + stateobj.html +'
'+
437 | '
';
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 += '';
451 |
452 | } else {
453 | state += '';
454 |
455 | }
456 | i++;
457 | }
458 | state += '
';
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);
667 |
--------------------------------------------------------------------------------
/libs/jquery.autosize.js:
--------------------------------------------------------------------------------
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 = '',
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
--------------------------------------------------------------------------------
/muc.js:
--------------------------------------------------------------------------------
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 | joined: false,
11 | isOwner: false,
12 | init: function (conn) {
13 | this.connection = conn;
14 | },
15 | doJoin: function (jid, password) {
16 | this.myroomjid = jid;
17 | if (!this.roomjid) {
18 | this.roomjid = Strophe.getBareJidFromJid(jid);
19 | // add handlers (just once)
20 | this.connection.addHandler(this.onPresence.bind(this), null, 'presence', null, null, this.roomjid, {matchBare: true});
21 | this.connection.addHandler(this.onPresenceUnavailable.bind(this), null, 'presence', 'unavailable', null, this.roomjid, {matchBare: true});
22 | this.connection.addHandler(this.onPresenceError.bind(this), null, 'presence', 'error', null, this.roomjid, {matchBare: true});
23 | this.connection.addHandler(this.onMessage.bind(this), null, 'message', null, null, this.roomjid, {matchBare: true});
24 | }
25 |
26 | var join = $pres({to: this.myroomjid }).c('x', {xmlns: 'http://jabber.org/protocol/muc'});
27 | if (password !== undefined) {
28 | join.c('password').t(password);
29 | }
30 | this.connection.send(join);
31 | },
32 | onPresence: function (pres) {
33 | var from = pres.getAttribute('from');
34 | var type = pres.getAttribute('type');
35 | if (type != null) {
36 | return true;
37 | }
38 | if ($(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>status[code="201"]').length) {
39 | // http://xmpp.org/extensions/xep-0045.html#createroom-instant
40 | this.isOwner = true;
41 | var create = $iq({type: 'set', to: this.roomjid})
42 | .c('query', {xmlns: 'http://jabber.org/protocol/muc#owner'})
43 | .c('x', {xmlns: 'jabber:x:data', type: 'submit'});
44 | this.connection.send(create); // fire away
45 | }
46 |
47 | var member = {};
48 | member.show = $(pres).find('>show').text();
49 | member.status = $(pres).find('>status').text();
50 | var tmp = $(pres).find('>x[xmlns="http://jabber.org/protocol/muc#user"]>item');
51 | member.affiliation = tmp.attr('affiliation');
52 | member.role = tmp.attr('role');
53 | if (from == this.myroomjid) {
54 | if (member.affiliation == 'owner') this.isOwner = true;
55 | if (!this.joined) {
56 | this.joined = true;
57 | $(document).trigger('joined.muc', [from, member]);
58 | }
59 | } else if (this.members[from] === undefined) {
60 | // new participant
61 | this.members[from] = member;
62 | $(document).trigger('entered.muc', [from, member, pres]);
63 | } else {
64 | console.log('presence change from', from);
65 | $(document).trigger('presence.muc', [from, member, pres]);
66 | }
67 | return true;
68 | },
69 | onPresenceUnavailable: function (pres) {
70 | var from = pres.getAttribute('from');
71 | delete this.members[from];
72 | $(document).trigger('left.muc', [from]);
73 | return true;
74 | },
75 | onPresenceError: function (pres) {
76 | var from = pres.getAttribute('from');
77 | if ($(pres).find('>error[type="auth"]>not-authorized[xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"]').length) {
78 | $(document).trigger('passwordrequired.muc', [from]);
79 |
80 | // FIXME: remove once moved to passwordrequired which should reuse dojoin
81 | var ob = this;
82 | window.setTimeout(function () {
83 | var given = window.prompt('Password required');
84 | if (given != null) {
85 | // FIXME: reuse doJoin?
86 | ob.connection.send($pres({to: ob.myroomjid }).c('x', {xmlns: 'http://jabber.org/protocol/muc'}).c('password').t(given));
87 | } else {
88 | // user aborted
89 | }
90 | }, 50);
91 | } else {
92 | console.warn('onPresError ', pres);
93 | }
94 | return true;
95 | },
96 | sendMessage: function (body, nickname) {
97 | var msg = $msg({to: this.roomjid, type: 'groupchat'});
98 | msg.c('body', body).up();
99 | if (nickname) {
100 | msg.c('nick', {xmlns: 'http://jabber.org/protocol/nick'}).t(nickname).up().up();
101 | }
102 | this.connection.send(msg);
103 | },
104 | onMessage: function (msg) {
105 | var txt = $(msg).find('>body').text();
106 | // TODO:
107 | // FIXME: this is a hack. but jingle on muc makes nickchanges hard
108 | var nick = $(msg).find('>nick[xmlns="http://jabber.org/protocol/nick"]').text() || Strophe.getResourceFromJid(msg.getAttribute('from'));
109 | if (txt) {
110 | console.log('chat', nick, txt);
111 |
112 | updateChatConversation(nick, txt);
113 | }
114 | return true;
115 | },
116 | lockRoom: function (key) {
117 | //http://xmpp.org/extensions/xep-0045.html#roomconfig
118 | var ob = this;
119 | this.connection.sendIQ($iq({to: this.roomjid, type: 'get'}).c('query', {xmlns: 'http://jabber.org/protocol/muc#owner'}),
120 | function (res) {
121 | if ($(res).find('>query>x[xmlns="jabber:x:data"]>field[var="muc#roomconfig_roomsecret"]').length) {
122 | var formsubmit = $iq({to: ob.roomjid, type: 'set'}).c('query', {xmlns: 'http://jabber.org/protocol/muc#owner'});
123 | formsubmit.c('x', {xmlns: 'jabber:x:data', type: 'submit'});
124 | formsubmit.c('field', {'var': 'FORM_TYPE'}).c('value').t('http://jabber.org/protocol/muc#roomconfig').up().up();
125 | formsubmit.c('field', {'var': 'muc#roomconfig_roomsecret'}).c('value').t(key).up().up();
126 | // FIXME: is muc#roomconfig_passwordprotectedroom required?
127 | this.connection.sendIQ(formsubmit,
128 | function (res) {
129 | console.log('set room password');
130 | },
131 | function (err) {
132 | console.warn('setting password failed', err);
133 | }
134 | );
135 | } else {
136 | console.warn('room passwords not supported');
137 | }
138 | },
139 | function (err) {
140 | console.warn('setting password failed', err);
141 | }
142 | );
143 | }
144 | });
145 |
146 |
--------------------------------------------------------------------------------
/webrtcrequired.html:
--------------------------------------------------------------------------------
1 | Sorry, webrtc is required for this and your browser does not seem to support it.
2 |
--------------------------------------------------------------------------------