├── 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 |
39 | 40 | 41 | 42 | 43 |
44 |
45 | 46 |
47 |
48 |
49 | 50 |
51 |
52 |
53 | 54 | 55 | 57 |
58 |
59 |
60 |
61 | Enter a nickname in the box below 62 |
63 | 64 |
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 += '
'+ 40 | '
'+ 41 | '
'+ opts.closeText +'
'+ 42 | '
'+ 43 | '
'+ 44 | '
'+ 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 += ''; 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 = '