├── .gitignore ├── Gemfile ├── Gemfile.lock ├── README.md ├── public ├── RTCMultiConnection.js ├── RecordRTC.js ├── app.js ├── jquery-2.0.3.min.js ├── jquery-2.0.3.min.map ├── style.css └── videos │ └── .gitkeep ├── server.rb ├── uploads └── .gitkeep └── views ├── index.erb └── video.erb /.gitignore: -------------------------------------------------------------------------------- 1 | uploads/* 2 | public/videos/* -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "sinatra" 4 | gem "uuid" -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | macaddr (1.6.1) 5 | systemu (~> 2.5.0) 6 | rack (1.5.2) 7 | rack-protection (1.5.1) 8 | rack 9 | sinatra (1.4.4) 10 | rack (~> 1.4) 11 | rack-protection (~> 1.4) 12 | tilt (~> 1.3, >= 1.3.4) 13 | systemu (2.5.2) 14 | tilt (1.4.1) 15 | uuid (2.3.7) 16 | macaddr (~> 1.0) 17 | 18 | PLATFORMS 19 | ruby 20 | 21 | DEPENDENCIES 22 | sinatra 23 | uuid 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RTC Recording Experiment 2 | 3 | An experiment to record audio+video using WebRTC and then send it to a server. 4 | 5 | # Uses 6 | 7 | * [RecordRTC](https://github.com/muaz-khan/WebRTC-Experiment/tree/master/RecordRTC) 8 | * [FFmpeg](http://www.ffmpeg.org/) 9 | 10 | # Requires 11 | 12 | * Ruby 13 | * A modern web browser 14 | * FFmpeg 15 | 16 | # Running on a Mac 17 | 18 | ```sh 19 | # install dependencies 20 | brew install ffmpeg 21 | # run app 22 | ruby server.rb 23 | ``` 24 | 25 | 26 | -------------------------------------------------------------------------------- /public/RTCMultiConnection.js: -------------------------------------------------------------------------------- 1 | // Muaz Khan - https://github.com/muaz-khan 2 | // MIT License - https://www.WebRTC-Experiment.com/licence/ 3 | // Documentation - https://github.com/muaz-khan/WebRTC-Experiment/tree/master/RTCMultiConnection 4 | // ======================= 5 | // RTCMultiConnection-v1.4 6 | 7 | (function() { 8 | window.RTCMultiConnection = function(channel) { 9 | this.channel = channel || location.href.replace( /\/|:|#|%|\.|\[|\]/g , ''); 10 | 11 | this.open = function(_channel) { 12 | self.joinedARoom = true; 13 | 14 | if (_channel) 15 | self.channel = _channel; 16 | 17 | // if firebase && if session initiator 18 | if (self.socket && self.socket.onDisconnect) 19 | self.socket.onDisconnect().remove(); 20 | 21 | self.isInitiator = true; 22 | 23 | prepareInit(function() { 24 | init(); 25 | captureUserMedia(rtcSession.initSession); 26 | }); 27 | }; 28 | 29 | // check pre-opened connections 30 | this.connect = function(_channel) { 31 | if (_channel) 32 | self.channel = _channel; 33 | 34 | prepareInit(init); 35 | }; 36 | 37 | // join a session 38 | this.join = joinSession; 39 | 40 | // send file/data or /text 41 | this.send = function(data, _channel) { 42 | if (!data) 43 | throw 'No file, data or text message to share.'; 44 | 45 | if (data.size) { 46 | FileSender.send({ 47 | file: data, 48 | channel: rtcSession, 49 | onFileSent: self.onFileSent, 50 | onFileProgress: self.onFileProgress, 51 | _channel: _channel 52 | }); 53 | } else 54 | TextSender.send({ 55 | text: data, 56 | channel: rtcSession, 57 | _channel: _channel 58 | }); 59 | }; 60 | 61 | var self = this, 62 | rtcSession, fileReceiver, textReceiver; 63 | 64 | // verify openSignalingChannel method's presence 65 | 66 | function prepareInit(callback) { 67 | if (!self.openSignalingChannel) { 68 | if (typeof self.transmitRoomOnce == 'undefined') 69 | self.transmitRoomOnce = true; 70 | 71 | // for custom socket.io over node.js implementation - visit - https://github.com/muaz-khan/WebRTC-Experiment/blob/master/socketio-over-nodejs 72 | self.openSignalingChannel = function(config) { 73 | var channel = config.channel || self.channel || 'default-channel'; 74 | var firebase = new window.Firebase('https://' + (self.firebase || 'chat') + '.firebaseIO.com/' + channel); 75 | firebase.channel = channel; 76 | firebase.on('child_added', function(data) { 77 | config.onmessage(data.val()); 78 | }); 79 | 80 | firebase.send = function(data) { 81 | this.push(data); 82 | }; 83 | 84 | if (!self.socket) 85 | self.socket = firebase; 86 | 87 | if (channel != self.channel || (self.isInitiator && channel == self.channel)) 88 | firebase.onDisconnect().remove(); 89 | 90 | if (config.onopen) 91 | setTimeout(config.onopen, 1); 92 | 93 | return firebase; 94 | }; 95 | 96 | if (!window.Firebase) { 97 | loadScript('https://cdn.firebase.com/v0/firebase.js', callback); 98 | } else 99 | callback(); 100 | } else 101 | callback(); 102 | } 103 | 104 | // set config passed over RTCMultiSession 105 | 106 | function init() { 107 | if (self.config) 108 | return; 109 | 110 | self.config = { 111 | onNewSession: function(session) { 112 | if (!rtcSession) { 113 | self._session = session; 114 | return; 115 | } 116 | 117 | if (self.onNewSession) 118 | return self.onNewSession(session); 119 | 120 | if (self.joinedARoom) 121 | return false; 122 | self.joinedARoom = true; 123 | 124 | return joinSession(session); 125 | }, 126 | onmessage: function(e) { 127 | if (!e.data.size) 128 | e.data = JSON.parse(e.data); 129 | 130 | if (e.data.type === 'text') 131 | textReceiver.receive(e.data, self.onmessage, e.userid, e.extra); 132 | 133 | else if (e.data.size || e.data.type === 'file') 134 | fileReceiver.receive(e.data, self); 135 | else 136 | self.onmessage(e); 137 | } 138 | }; 139 | rtcSession = new RTCMultiSession(self); 140 | fileReceiver = new FileReceiver(); 141 | textReceiver = new TextReceiver(); 142 | 143 | if (self._session) 144 | self.config.onNewSession(self._session); 145 | } 146 | 147 | function joinSession(session) { 148 | if (!session || !session.userid || !session.sessionid) 149 | throw 'invalid data passed.'; 150 | 151 | self.session = session.session; 152 | 153 | extra = self.extra || session.extra || { }; 154 | 155 | if (session.oneway || session.data) 156 | rtcSession.joinSession(session, extra); 157 | else 158 | captureUserMedia(function() { 159 | rtcSession.joinSession(session, extra); 160 | }); 161 | } 162 | 163 | // capture user's media resources 164 | 165 | function captureUserMedia(callback, _session) { 166 | var session = _session || self.session; 167 | 168 | if (self.dontAttachStream) 169 | return callback(); 170 | 171 | if (isData(session) || (!self.isInitiator && session.oneway)) { 172 | self.attachStreams = []; 173 | return callback(); 174 | } 175 | 176 | var constraints = { 177 | audio: !!session.audio, 178 | video: !!session.video 179 | }; 180 | var screen_constraints = { 181 | audio: false, 182 | video: { 183 | mandatory: { 184 | chromeMediaSource: 'screen' 185 | }, 186 | optional: [] 187 | } 188 | }; 189 | 190 | if (session.screen) { 191 | _captureUserMedia(screen_constraints, constraints.audio || constraints.video ? function() { 192 | _captureUserMedia(constraints, callback); 193 | } : callback); 194 | } else _captureUserMedia(constraints, callback, session.audio && !session.video); 195 | 196 | function _captureUserMedia(forcedConstraints, forcedCallback, isRemoveVideoTracks) { 197 | var mediaConfig = { 198 | onsuccess: function(stream, returnBack) { 199 | if (returnBack) return forcedCallback && forcedCallback(stream); 200 | 201 | if (isRemoveVideoTracks && !moz) { 202 | stream = new window.webkitMediaStream(stream.getAudioTracks()); 203 | } 204 | 205 | var mediaElement = getMediaElement(stream, session); 206 | mediaElement.muted = true; 207 | 208 | stream.onended = function() { 209 | if (self.onstreamended) 210 | self.onstreamended(streamedObject); 211 | }; 212 | 213 | self.attachStreams.push(stream); 214 | 215 | var streamedObject = { 216 | stream: stream, 217 | streamid: stream.label, 218 | mediaElement: mediaElement, 219 | blobURL: mediaElement.mozSrcObject || mediaElement.src, 220 | type: 'local', 221 | userid: self.userid || 'self', 222 | extra: self.extra 223 | }; 224 | 225 | self.streams[stream.label] = self._getStream({ 226 | stream: stream, 227 | userid: self.userid, 228 | type: 'local', 229 | streamObject: streamedObject 230 | }); 231 | 232 | self.onstream(streamedObject); 233 | if (forcedCallback) forcedCallback(stream); 234 | }, 235 | onerror: function(e) { 236 | console.trace('media error', e); 237 | 238 | if (session.audio && !session.video) 239 | throw 'Microphone access is denied.'; 240 | else if (session.screen) { 241 | if (location.protocol === 'http:') 242 | throw ' is mandatory to capture screen.'; 243 | else 244 | throw 'Multi-capturing of screen is not allowed. Capturing process is denied. Are you enabled flag: "Enable screen capture support in getUserMedia"?'; 245 | } else 246 | throw 'Webcam access is denied.'; 247 | }, 248 | mediaConstraints: self.mediaConstraints || { } 249 | }; 250 | 251 | mediaConfig.constraints = forcedConstraints || constraints; 252 | mediaConfig.media = self.media; 253 | getUserMedia(mediaConfig); 254 | } 255 | } 256 | 257 | this.captureUserMedia = captureUserMedia; 258 | 259 | // eject a user; or leave the session 260 | this.leave = this.eject = function(userid) { 261 | rtcSession.leave(userid); 262 | 263 | if (!userid) { 264 | var streams = self.attachStreams; 265 | for (var i = 0; i < streams.length; i++) { 266 | streams[i].stop(); 267 | } 268 | currentUserMediaRequest.streams = []; 269 | self.attachStreams = []; 270 | } 271 | 272 | // if firebase; remove data from firebase servers 273 | if (self.isInitiator && !!self.socket && !!self.socket.remove) { 274 | self.socket.remove(); 275 | } 276 | }; 277 | 278 | // close entire session 279 | this.close = function() { 280 | self.autoCloseEntireSession = true; 281 | rtcSession.leave(); 282 | }; 283 | 284 | // renegotiate new media stream 285 | this.addStream = function(session, socket) { 286 | captureUserMedia(function(stream) { 287 | rtcSession.addStream({ 288 | stream: stream, 289 | renegotiate: session, 290 | socket: socket 291 | }); 292 | }, session); 293 | }; 294 | 295 | // detach pre-attached streams 296 | this.removeStream = function(streamid) { 297 | if (!this.streams[streamid]) return console.warn('No such stream exists. Stream-id:', streamid); 298 | this.detachStreams.push(streamid); 299 | }; 300 | 301 | // set RTCMultiConnection defaults on constructor invocation 302 | this.setDefaults(); 303 | }; 304 | 305 | function RTCMultiSession(root) { 306 | var config = root.config; 307 | var session = root.session; 308 | 309 | var self = { }; 310 | var socketObjects = { }; 311 | var sockets = []; 312 | 313 | self.userid = root.userid = root.userid || root.token(); 314 | self.sessionid = root.channel; 315 | 316 | var participants = { }, 317 | isbroadcaster, 318 | isAcceptNewSession = true; 319 | 320 | function newPrivateSocket(_config) { 321 | var socketConfig = { 322 | channel: _config.channel, 323 | onmessage: socketResponse, 324 | onopen: function() { 325 | if (isofferer && !peer) 326 | initPeer(); 327 | 328 | _config.socketIndex = socket.index = sockets.length; 329 | socketObjects[socketConfig.channel] = socket; 330 | sockets[_config.socketIndex] = socket; 331 | } 332 | }; 333 | 334 | socketConfig.callback = function(_socket) { 335 | socket = _socket; 336 | socketConfig.onopen(); 337 | }; 338 | 339 | var socket = root.openSignalingChannel(socketConfig), 340 | isofferer = _config.isofferer, 341 | peer; 342 | 343 | var peerConfig = { 344 | onopen: onChannelOpened, 345 | onICE: function(candidate) { 346 | if (!root.candidates) throw 'ICE candidates are mandatory.'; 347 | if (!root.candidates.host && candidate.candidate.indexOf('typ host') != -1) return; 348 | if (!root.candidates.relay && candidate.candidate.indexOf('relay') != -1) return; 349 | if (!root.candidates.reflexive && candidate.candidate.indexOf('srflx') != -1) return; 350 | 351 | log(candidate.candidate); 352 | socket && socket.send({ 353 | userid: self.userid, 354 | candidate: { 355 | sdpMLineIndex: candidate.sdpMLineIndex, 356 | candidate: JSON.stringify(candidate.candidate) 357 | } 358 | }); 359 | }, 360 | onmessage: function(event) { 361 | config.onmessage({ 362 | data: event.data, 363 | userid: _config.userid, 364 | extra: _config.extra 365 | }); 366 | }, 367 | onstream: function(stream) { 368 | var mediaElement = getMediaElement(stream, session); 369 | 370 | _config.stream = stream; 371 | if (mediaElement.tagName.toLowerCase() == 'audio') 372 | mediaElement.addEventListener('play', function() { 373 | setTimeout(function() { 374 | mediaElement.muted = false; 375 | afterRemoteStreamStartedFlowing(mediaElement); 376 | }, 3000); 377 | }, false); 378 | else 379 | waitUntilRemoteStreamStartsFlowing(mediaElement); 380 | }, 381 | 382 | onclose: function(e) { 383 | e.extra = _config.extra; 384 | e.userid = _config.userid; 385 | root.onclose(e); 386 | 387 | // suggested in #71 by "efaj" 388 | if (root.channels[e.userid]) 389 | delete root.channels[e.userid]; 390 | }, 391 | onerror: function(e) { 392 | e.extra = _config.extra; 393 | e.userid = _config.userid; 394 | root.onerror(e); 395 | }, 396 | 397 | attachStreams: root.attachStreams, 398 | iceServers: root.iceServers, 399 | bandwidth: root.bandwidth, 400 | sdpConstraints: root.sdpConstraints || { }, 401 | disableDtlsSrtp: root.disableDtlsSrtp, 402 | reliable: !!root.reliable 403 | }; 404 | 405 | function initPeer(offerSDP) { 406 | if (!offerSDP) 407 | peerConfig.onOfferSDP = function(sdp) { 408 | sendsdp({ 409 | sdp: sdp, 410 | socket: socket 411 | }); 412 | }; 413 | else { 414 | peerConfig.offerSDP = offerSDP; 415 | peerConfig.onAnswerSDP = function(sdp) { 416 | sendsdp({ 417 | sdp: sdp, 418 | socket: socket 419 | }); 420 | }; 421 | } 422 | 423 | if (!session.data) peerConfig.onmessage = null; 424 | peerConfig.session = session; 425 | peer = new RTCPeerConnection(peerConfig); 426 | } 427 | 428 | function waitUntilRemoteStreamStartsFlowing(mediaElement) { 429 | if (!(mediaElement.readyState <= HTMLMediaElement.HAVE_CURRENT_DATA 430 | || mediaElement.paused || mediaElement.currentTime <= 0)) { 431 | afterRemoteStreamStartedFlowing(mediaElement); 432 | } else 433 | setTimeout(function() { 434 | waitUntilRemoteStreamStartsFlowing(mediaElement); 435 | }, 50); 436 | } 437 | 438 | function afterRemoteStreamStartedFlowing(mediaElement) { 439 | (function setVolume() { 440 | mediaElement.volume += .1; 441 | if (mediaElement.volume < .9) setTimeout(setVolume, 600); 442 | else mediaElement.volume = 1; 443 | })(); 444 | 445 | var stream = _config.stream; 446 | stream.onended = function() { 447 | root.onstreamended(streamedObject); 448 | }; 449 | 450 | stream.onended = function() { 451 | if (root.onstreamended) 452 | root.onstreamended(streamedObject); 453 | }; 454 | 455 | var streamedObject = { 456 | mediaElement: mediaElement, 457 | 458 | stream: stream, 459 | streamid: stream.label, 460 | session: session, 461 | 462 | blobURL: mediaElement.mozSrcObject || mediaElement.src, 463 | type: 'remote', 464 | 465 | extra: _config.extra, 466 | userid: _config.userid 467 | }; 468 | 469 | // connection.streams['stream-id'].mute({audio:true}) 470 | root.streams[stream.label] = root._getStream({ 471 | stream: stream, 472 | userid: _config.userid, 473 | socket: socket, 474 | type: 'remote', 475 | streamObject: streamedObject 476 | }); 477 | 478 | root.onstream(streamedObject); 479 | 480 | onSessionOpened(); 481 | 482 | // mic/speaker activity detection 483 | // voiceActivityDetection(peer.connection); 484 | } 485 | 486 | function onChannelOpened(channel) { 487 | _config.channel = channel; 488 | 489 | // connection.channels['user-id'].send(data); 490 | root.channels[_config.userid] = { 491 | channel: _config.channel, 492 | send: function(data) { 493 | root.send(data, this.channel); 494 | } 495 | }; 496 | 497 | root.onopen({ 498 | extra: _config.extra, 499 | userid: _config.userid 500 | }); 501 | 502 | if (isData(session)) onSessionOpened(); 503 | } 504 | 505 | function updateSocket() { 506 | if (socket.userid == _config.userid) 507 | return; 508 | 509 | socket.userid = _config.userid; 510 | sockets[_config.socketIndex] = socket; 511 | 512 | // connection.peers['user-id'].addStream({audio:true}) 513 | root.peers[_config.userid] = { 514 | socket: socket, 515 | peer: peer, 516 | userid: _config.userid, 517 | addStream: function(session) { 518 | root.addStream(session, this.socket); 519 | } 520 | }; 521 | } 522 | 523 | function onSessionOpened() { 524 | // admin/guest is one-to-one relationship 525 | if (root.userType && !root.session['many-to-many']) return; 526 | 527 | // original conferencing infrastructure! 528 | if (!session.oneway && !session.broadcast && isbroadcaster && getLength(participants) > 1 && getLength(participants) <= root.maxParticipantsAllowed) { 529 | defaultSocket.send({ 530 | newParticipant: _config.userid || socket.channel, 531 | userid: self.userid, 532 | extra: _config.extra || { } 533 | }); 534 | } 535 | } 536 | 537 | function socketResponse(response) { 538 | if (response.userid == self.userid) 539 | return; 540 | 541 | if (response.sdp) { 542 | _config.userid = response.userid; 543 | _config.extra = response.extra; 544 | _config.renegotiate = response.renegotiate; 545 | 546 | // to make sure user-id for socket object is set 547 | // even if one-way streaming 548 | updateSocket(); 549 | 550 | sdpInvoker(response.sdp, response.labels); 551 | } 552 | 553 | if (response.candidate) { 554 | peer && peer.addICE({ 555 | sdpMLineIndex: response.candidate.sdpMLineIndex, 556 | candidate: JSON.parse(response.candidate.candidate) 557 | }); 558 | } 559 | 560 | if (response.mute || response.unmute) { 561 | log(response); 562 | } 563 | 564 | if (response.left) { 565 | if (peer && peer.connection) { 566 | peer.connection.close(); 567 | peer.connection = null; 568 | 569 | // firefox is unable to stop remote streams 570 | // firefox doesn't auto stop streams when peer.close() is called. 571 | if(moz) { 572 | var userLeft = response.userid; 573 | for(var stream in root.streams) { 574 | stream = root.streams[stream]; 575 | if(stream.userid == userLeft) { 576 | stream.stop(); 577 | stream.stream.onended(stream.streamObject); 578 | } 579 | } 580 | } 581 | } 582 | 583 | if (response.closeEntireSession) 584 | clearSession(); 585 | else if (socket) { 586 | socket.send({ 587 | left: true, 588 | extra: root.extra, 589 | userid: self.userid 590 | }); 591 | 592 | if (sockets[_config.socketIndex]) 593 | delete sockets[_config.socketIndex]; 594 | if (socketObjects[socket.channel]) 595 | delete socketObjects[socket.channel]; 596 | 597 | socket = null; 598 | } 599 | 600 | if (participants[response.userid]) delete participants[response.userid]; 601 | 602 | root.onleave({ 603 | userid: response.userid, 604 | extra: response.extra 605 | }); 606 | 607 | if (root.userType) root.busy = false; 608 | } 609 | 610 | // keeping session active even if initiator leaves 611 | if (response.playRoleOfBroadcaster) 612 | setTimeout(function() { 613 | root.dontAttachStream = true; 614 | self.userid = response.userid; 615 | root.open({ 616 | extra: root.extra 617 | }); 618 | sockets = swap(sockets); 619 | root.dontAttachStream = false; 620 | }, 600); 621 | 622 | // if renegotiation process initiated by answerer 623 | if (response.suggestRenegotiation) { 624 | renegotiate = response.renegotiate; 625 | 626 | // detaching old streams 627 | detachMediaStream(root.detachStreams, peer.connection); 628 | 629 | if (isData(renegotiate)) 630 | createOffer(); 631 | else 632 | root.captureUserMedia(function(stream) { 633 | peer.connection.addStream(stream); 634 | createOffer(); 635 | }, renegotiate); 636 | 637 | function createOffer() { 638 | peer.recreateOffer(renegotiate, function(sdp) { 639 | sendsdp({ 640 | sdp: sdp, 641 | socket: socket, 642 | renegotiate: response.renegotiate, 643 | labels: root.detachStreams 644 | }); 645 | root.detachStreams = []; 646 | }); 647 | } 648 | } 649 | } 650 | 651 | function sdpInvoker(sdp, labels) { 652 | log(sdp.sdp); 653 | 654 | if (isofferer) 655 | return peer.addAnswerSDP(sdp); 656 | if (!_config.renegotiate) 657 | return initPeer(sdp); 658 | 659 | session = _config.renegotiate; 660 | // detach streams 661 | detachMediaStream(labels, peer.connection); 662 | 663 | if (session.oneway || isData(session)) { 664 | createAnswer(); 665 | } else { 666 | if (_config.capturing) 667 | return; 668 | 669 | _config.capturing = true; 670 | 671 | root.captureUserMedia(function(stream) { 672 | _config.capturing = false; 673 | 674 | peer.connection.addStream(stream); 675 | createAnswer(); 676 | }, _config.renegotiate); 677 | } 678 | 679 | delete _config.renegotiate; 680 | 681 | function createAnswer() { 682 | peer.recreateAnswer(sdp, session, function(_sdp) { 683 | sendsdp({ 684 | sdp: _sdp, 685 | socket: socket 686 | }); 687 | }); 688 | } 689 | } 690 | } 691 | 692 | function detachMediaStream(labels, peer) { 693 | for (var i = 0; i < labels.length; i++) { 694 | var label = labels[i]; 695 | if (root.streams[label]) { 696 | var stream = root.streams[label].stream; 697 | stream.stop(); 698 | peer.removeStream(stream); 699 | } 700 | } 701 | } 702 | 703 | // for PHP-based socket.io; split SDP in parts here 704 | 705 | function sendsdp(e) { 706 | e.socket.send({ 707 | userid: self.userid, 708 | sdp: e.sdp, 709 | extra: root.extra, 710 | renegotiate: e.renegotiate ? e.renegotiate : false, 711 | labels: e.labels || [] 712 | }); 713 | } 714 | 715 | // sharing new user with existing participants 716 | 717 | function onNewParticipant(channel, extra) { 718 | if (!channel || !!participants[channel] || channel == self.userid) 719 | return; 720 | 721 | participants[channel] = channel; 722 | 723 | var new_channel = root.token(); 724 | newPrivateSocket({ 725 | channel: new_channel, 726 | extra: extra || { } 727 | }); 728 | 729 | defaultSocket.send({ 730 | participant: true, 731 | userid: self.userid, 732 | targetUser: channel, 733 | channel: new_channel, 734 | extra: root.extra 735 | }); 736 | } 737 | 738 | // if a user leaves 739 | 740 | function clearSession(channel) { 741 | var alert = { 742 | left: true, 743 | extra: root.extra, 744 | userid: self.userid 745 | }; 746 | 747 | if (isbroadcaster) { 748 | if (root.autoCloseEntireSession) { 749 | alert.closeEntireSession = true; 750 | } else if (sockets[0]) { 751 | sockets[0].send({ 752 | playRoleOfBroadcaster: true, 753 | userid: self.userid 754 | }); 755 | } 756 | } 757 | 758 | if (!channel) { 759 | var length = sockets.length; 760 | for (var i = 0; i < length; i++) { 761 | socket = sockets[i]; 762 | if (socket) { 763 | socket.send(alert); 764 | if (socketObjects[socket.channel]) 765 | delete socketObjects[socket.channel]; 766 | delete sockets[i]; 767 | } 768 | } 769 | } 770 | 771 | // eject a specific user! 772 | if (channel) { 773 | socket = socketObjects[channel]; 774 | if (socket) { 775 | socket.send(alert); 776 | if (sockets[socket.index]) 777 | delete sockets[socket.index]; 778 | delete socketObjects[channel]; 779 | } 780 | } 781 | 782 | sockets = swap(sockets); 783 | } 784 | 785 | window.onbeforeunload = function() { 786 | clearSession(); 787 | }; 788 | 789 | window.onkeyup = function(e) { 790 | if (e.keyCode == 116) 791 | clearSession(); 792 | }; 793 | 794 | function initDefaultSocket() { 795 | defaultSocket = root.openSignalingChannel({ 796 | onmessage: function(response) { 797 | if (response.userid == self.userid) 798 | return; 799 | if (isAcceptNewSession && response.sessionid && response.userid) { 800 | root.session = session = response.session; 801 | config.onNewSession(response); 802 | } 803 | if (response.newParticipant && self.joinedARoom && self.broadcasterid === response.userid) 804 | onNewParticipant(response.newParticipant, response.extra); 805 | 806 | if (getLength(participants) < root.maxParticipantsAllowed && response.userid && response.targetUser == self.userid && response.participant && !participants[response.userid]) { 807 | acceptRequest(response.channel || response.userid, response.extra, response.userid); 808 | } 809 | 810 | if (response.userType && response.userType != root.userType) { 811 | if (!root.busy) { 812 | if (response.userType == 'admin') { 813 | if (root.onAdmin) root.onAdmin(response); 814 | else root.accept(response.userid); 815 | } 816 | if (response.userType == 'guest') { 817 | if (root.onGuest) root.onGuest(response); 818 | else root.accept(response.userid); 819 | } 820 | } else { 821 | if (response.userType != root.userType) { 822 | defaultSocket.send({ 823 | rejectedRequestOf: response.userid, 824 | userid: self.userid, 825 | extra: root.extra || { } 826 | }); 827 | } 828 | } 829 | } 830 | 831 | if (response.acceptedRequestOf == self.userid) { 832 | if (root.onstats) root.onstats('accepted', response); 833 | } 834 | 835 | if (response.rejectedRequestOf == self.userid) { 836 | if (root.onstats) root.onstats('busy', response); 837 | sendRequest(); 838 | } 839 | }, 840 | callback: function(socket) { 841 | defaultSocket = socket; 842 | if (root.userType) sendRequest(); 843 | } 844 | }); 845 | } 846 | 847 | var that = this, defaultSocket; 848 | 849 | initDefaultSocket(); 850 | 851 | function sendRequest() { 852 | defaultSocket.send({ 853 | userType: root.userType, 854 | userid: root.userid, 855 | extra: root.extra || { } 856 | }); 857 | } 858 | 859 | function setDirections() { 860 | if (root.direction == 'one-way') root.session.oneway = true; 861 | if (root.direction == 'one-to-one') root.maxParticipantsAllowed = 1; 862 | if (root.direction == 'one-to-many') root.session.broadcast = true; 863 | if (root.direction == 'many-to-many') { 864 | root.session.oneway = false; 865 | root.session.broadcast = false; 866 | root.maxParticipantsAllowed = 256; 867 | } 868 | } 869 | 870 | // open new session 871 | this.initSession = function() { 872 | setDirections(); 873 | session = root.session; 874 | 875 | isbroadcaster = true; 876 | participants = { }; 877 | 878 | self.sessionid = root.sessionid || root.channel; 879 | 880 | this.isOwnerLeaving = isAcceptNewSession = false; 881 | 882 | (function transmit() { 883 | if (getLength(participants) < root.maxParticipantsAllowed) { 884 | defaultSocket && defaultSocket.send({ 885 | sessionid: self.sessionid, 886 | userid: root.userid, 887 | session: session, 888 | extra: root.extra 889 | }); 890 | } 891 | 892 | if (!root.transmitRoomOnce && !that.isOwnerLeaving) 893 | setTimeout(transmit, root.interval || 3000); 894 | })(); 895 | }; 896 | 897 | // join existing session 898 | this.joinSession = function(_config) { 899 | _config = _config || { }; 900 | 901 | participants = { }; 902 | 903 | session = _config.session; 904 | 905 | self.joinedARoom = true; 906 | self.broadcasterid = _config.userid; 907 | 908 | if (_config.sessionid) 909 | self.sessionid = _config.sessionid; 910 | 911 | isAcceptNewSession = false; 912 | 913 | var channel = getRandomString(); 914 | newPrivateSocket({ 915 | channel: channel, 916 | extra: root.extra 917 | }); 918 | 919 | defaultSocket.send({ 920 | participant: true, 921 | userid: self.userid, 922 | channel: channel, 923 | targetUser: _config.userid, 924 | extra: root.extra 925 | }); 926 | }; 927 | 928 | // send file/data or text message 929 | this.send = function(message, _channel) { 930 | message = JSON.stringify(message); 931 | 932 | if (_channel) { 933 | if (_channel.readyState == 'open') { 934 | _channel.send(message); 935 | } 936 | return; 937 | } 938 | 939 | for (var dataChannel in root.channels) { 940 | var channel = root.channels[dataChannel].channel; 941 | if (channel.readyState == 'open') { 942 | channel.send(message); 943 | } 944 | } 945 | }; 946 | 947 | // leave session 948 | this.leave = function(userid) { 949 | clearSession(userid); 950 | 951 | if (!userid) { 952 | // self.userid = root.userid = root.token(); 953 | root.joinedARoom = self.joinedARoom = isbroadcaster = false; 954 | isAcceptNewSession = true; 955 | } 956 | 957 | if (isbroadcaster) { 958 | this.isOwnerLeaving = true; 959 | root.isInitiator = false; 960 | } 961 | 962 | root.busy = false; 963 | }; 964 | 965 | // renegotiate new stream 966 | this.addStream = function(e) { 967 | session = e.renegotiate; 968 | 969 | if (e.socket) 970 | addStream(e.socket); 971 | else 972 | for (var i = 0; i < sockets.length; i++) 973 | addStream(sockets[i]); 974 | 975 | function addStream(socket) { 976 | peer = root.peers[socket.userid]; 977 | 978 | if (!peer) 979 | throw 'No such peer exists.'; 980 | 981 | peer = peer.peer; 982 | 983 | // if offerer; renegotiate 984 | if (peer && peer.connection.localDescription.type == 'offer') { 985 | // detaching old streams 986 | detachMediaStream(root.detachStreams, peer.connection); 987 | 988 | if (session.audio || session.video || session.screen) 989 | peer.connection.addStream(e.stream); 990 | 991 | peer.recreateOffer(session, function(sdp) { 992 | sendsdp({ 993 | sdp: sdp, 994 | socket: socket, 995 | renegotiate: session, 996 | labels: root.detachStreams 997 | }); 998 | root.detachStreams = []; 999 | }); 1000 | } else { 1001 | // otherwise; suggest other user to play role of renegotiator 1002 | socket.send({ 1003 | userid: self.userid, 1004 | renegotiate: session, 1005 | suggestRenegotiation: true 1006 | }); 1007 | } 1008 | } 1009 | }; 1010 | 1011 | root.request = function(userid) { 1012 | if (!root.session['many-to-many']) root.busy = true; 1013 | 1014 | root.captureUserMedia(function() { 1015 | // open private socket that will be used to receive offer-sdp 1016 | newPrivateSocket({ 1017 | channel: self.userid, 1018 | extra: root.extra || { } 1019 | }); 1020 | 1021 | // ask other user to create offer-sdp 1022 | defaultSocket.send({ 1023 | participant: true, 1024 | userid: self.userid, 1025 | extra: root.extra || { }, 1026 | targetUser: userid 1027 | }); 1028 | }); 1029 | }; 1030 | 1031 | function acceptRequest(channel, extra, userid) { 1032 | if (root.userType && !root.busy) { 1033 | if (root.onRequest) root.onRequest(channel, extra, userid); 1034 | else _accept(channel, extra, userid); 1035 | } 1036 | 1037 | if (!root.userType) _accept(channel, extra, userid); 1038 | } 1039 | 1040 | function _accept(channel, extra, userid) { 1041 | if (root.userType) { 1042 | if (!root.session['many-to-many']) root.busy = true; 1043 | defaultSocket.send({ 1044 | acceptedRequestOf: userid, 1045 | userid: self.userid, 1046 | extra: root.extra || { } 1047 | }); 1048 | } 1049 | 1050 | participants[userid] = userid; 1051 | newPrivateSocket({ 1052 | isofferer: true, 1053 | userid: userid, 1054 | channel: channel, 1055 | extra: extra || { } 1056 | }); 1057 | } 1058 | 1059 | root.accept = function(userid, extra) { 1060 | root.captureUserMedia(function() { 1061 | _accept(userid, extra); 1062 | }); 1063 | }; 1064 | } 1065 | 1066 | function getRandomString() { 1067 | return (Math.random() * new Date().getTime()).toString(36).toUpperCase().replace( /\./g , '-'); 1068 | } 1069 | 1070 | var FileSender = { 1071 | send: function(config) { 1072 | var channel = config.channel, 1073 | _channel = config._channel, 1074 | file = config.file; 1075 | 1076 | var packetSize = 1000, 1077 | textToTransfer = '', 1078 | numberOfPackets = 0, 1079 | packets = 0; 1080 | 1081 | // uuid to uniquely identify sending instance 1082 | file.uuid = getRandomString(); 1083 | 1084 | var reader = new window.FileReader(); 1085 | reader.readAsDataURL(file); 1086 | reader.onload = onReadAsDataURL; 1087 | 1088 | function onReadAsDataURL(event, text) { 1089 | var data = { 1090 | type: 'file', 1091 | uuid: file.uuid 1092 | }; 1093 | 1094 | if (event) { 1095 | text = event.target.result; 1096 | numberOfPackets = packets = data.packets = parseInt(text.length / packetSize); 1097 | } 1098 | 1099 | if (config.onFileProgress) 1100 | config.onFileProgress({ 1101 | remaining: packets--, 1102 | length: numberOfPackets, 1103 | sent: numberOfPackets - packets 1104 | }, file.uuid); 1105 | 1106 | if (text.length > packetSize) data.message = text.slice(0, packetSize); 1107 | else { 1108 | data.message = text; 1109 | data.last = true; 1110 | data.name = file.name; 1111 | 1112 | if (config.onFileSent) config.onFileSent(file, file.uuid); 1113 | } 1114 | 1115 | // WebRTC-DataChannels.send(data, privateDataChannel) 1116 | channel.send(data, _channel); 1117 | 1118 | textToTransfer = text.slice(data.message.length); 1119 | if (textToTransfer.length) { 1120 | setTimeout(function() { 1121 | onReadAsDataURL(null, textToTransfer); 1122 | }, moz ? 1 : 500); 1123 | // bug: 1124 | // what's the best method to speedup data transferring on chrome? 1125 | // what if SCTP data channels flag enabled? 1126 | } 1127 | } 1128 | } 1129 | }; 1130 | 1131 | function FileReceiver() { 1132 | var content = { }, 1133 | packets = { }, 1134 | numberOfPackets = { }; 1135 | 1136 | // "root" is RTCMultiConnection object 1137 | // "data" is object passed using WebRTC DataChannels 1138 | 1139 | function receive(data, root) { 1140 | // uuid is used to uniquely identify sending instance 1141 | var uuid = data.uuid; 1142 | 1143 | if (data.packets) numberOfPackets[uuid] = packets[uuid] = parseInt(data.packets); 1144 | 1145 | if (root.onFileProgress) 1146 | root.onFileProgress({ 1147 | remaining: packets[uuid]--, 1148 | length: numberOfPackets[uuid], 1149 | received: numberOfPackets[uuid] - packets[uuid] 1150 | }, uuid); 1151 | 1152 | if (!content[uuid]) content[uuid] = []; 1153 | 1154 | content[uuid].push(data.message); 1155 | 1156 | // if it is last packet 1157 | if (data.last) { 1158 | var dataURL = content[uuid].join(''); 1159 | var blob = FileConverter.DataUrlToBlob(dataURL); 1160 | var virtualURL = (window.URL || window.webkitURL).createObjectURL(blob); 1161 | 1162 | // if you don't want to auto-save to disk: 1163 | // connection.autoSaveToDisk=false; 1164 | if (root.autoSaveToDisk) 1165 | FileSaver.SaveToDisk(dataURL, data.name); 1166 | 1167 | // connection.onFileReceived = function(fileName, file) {} 1168 | // file.blob || file.dataURL || file.url || file.uuid 1169 | if (root.onFileReceived) 1170 | root.onFileReceived(data.name, { 1171 | blob: blob, 1172 | dataURL: dataURL, 1173 | url: virtualURL, 1174 | uuid: uuid 1175 | }); 1176 | 1177 | delete content[uuid]; 1178 | } 1179 | } 1180 | 1181 | return { 1182 | receive: receive 1183 | }; 1184 | } 1185 | 1186 | var FileSaver = { 1187 | SaveToDisk: function(fileUrl, fileName) { 1188 | var hyperlink = document.createElement('a'); 1189 | hyperlink.href = fileUrl; 1190 | hyperlink.target = '_blank'; 1191 | hyperlink.download = fileName || fileUrl; 1192 | 1193 | var mouseEvent = new MouseEvent('click', { 1194 | view: window, 1195 | bubbles: true, 1196 | cancelable: true 1197 | }); 1198 | 1199 | hyperlink.dispatchEvent(mouseEvent); 1200 | (window.URL || window.webkitURL).revokeObjectURL(hyperlink.href); 1201 | } 1202 | }; 1203 | 1204 | var FileConverter = { 1205 | DataUrlToBlob: function(dataURL) { 1206 | var binary = atob(dataURL.substr(dataURL.indexOf(',') + 1)); 1207 | var array = []; 1208 | for (var i = 0; i < binary.length; i++) { 1209 | array.push(binary.charCodeAt(i)); 1210 | } 1211 | 1212 | var type; 1213 | 1214 | try { 1215 | type = dataURL.substr(dataURL.indexOf(':') + 1).split(';')[0]; 1216 | } catch(e) { 1217 | type = 'text/plain'; 1218 | } 1219 | 1220 | var uint8Array = new Uint8Array(array); 1221 | // bug: must recheck FileConverter 1222 | return new Blob([new DataView(uint8Array.buffer)], { type: type }); 1223 | }, 1224 | BinaryStringToBlob: function(binaryString, type) { 1225 | var byteArray = new Uint8Array(binaryString.length); 1226 | for (var i = 0; i < binaryString.length; i++) { 1227 | byteArray[i] = binaryString.charCodeAt(i) & 0xff; 1228 | } 1229 | 1230 | return new Blob([new DataView(byteArray.buffer)], { type: type }); 1231 | } 1232 | }; 1233 | 1234 | var TextSender = { 1235 | send: function(config) { 1236 | var channel = config.channel, 1237 | _channel = config._channel, 1238 | initialText = config.text, 1239 | packetSize = 1000, 1240 | textToTransfer = '', 1241 | isobject = false; 1242 | 1243 | if (typeof initialText !== 'string') { 1244 | isobject = true; 1245 | initialText = JSON.stringify(initialText); 1246 | } 1247 | 1248 | // uuid is used to uniquely identify sending instance 1249 | var uuid = getRandomString(); 1250 | var sendingTime = new Date().getTime(); 1251 | 1252 | sendText(initialText); 1253 | 1254 | function sendText(textMessage, text) { 1255 | var data = { 1256 | type: 'text', 1257 | uuid: uuid, 1258 | sendingTime: sendingTime 1259 | }; 1260 | 1261 | if (textMessage) { 1262 | text = textMessage; 1263 | data.packets = parseInt(text.length / packetSize); 1264 | } 1265 | 1266 | if (text.length > packetSize) 1267 | data.message = text.slice(0, packetSize); 1268 | else { 1269 | data.message = text; 1270 | data.last = true; 1271 | data.isobject = isobject; 1272 | } 1273 | 1274 | channel.send(data, _channel); 1275 | 1276 | textToTransfer = text.slice(data.message.length); 1277 | 1278 | if (textToTransfer.length) 1279 | setTimeout(function() { 1280 | sendText(null, textToTransfer); 1281 | }, moz ? 1 : 500); 1282 | } 1283 | } 1284 | }; 1285 | 1286 | function TextReceiver() { 1287 | var content = { }; 1288 | 1289 | function receive(data, onmessage, userid, extra) { 1290 | // uuid is used to uniquely identify sending instance 1291 | var uuid = data.uuid; 1292 | if (!content[uuid]) content[uuid] = []; 1293 | 1294 | content[uuid].push(data.message); 1295 | if (data.last) { 1296 | var message = content[uuid].join(''); 1297 | if (data.isobject) message = JSON.parse(message); 1298 | 1299 | // latency detection 1300 | var receivingTime = new Date().getTime(); 1301 | var latency = receivingTime - data.sendingTime; 1302 | 1303 | if (onmessage) 1304 | onmessage({ 1305 | data: message, 1306 | userid: userid, 1307 | extra: extra, 1308 | latency: latency 1309 | }); 1310 | 1311 | delete content[uuid]; 1312 | } 1313 | } 1314 | 1315 | return { 1316 | receive: receive 1317 | }; 1318 | } 1319 | 1320 | window.MediaStream = window.MediaStream || window.webkitMediaStream; 1321 | 1322 | window.moz = !!navigator.mozGetUserMedia; 1323 | var RTCPeerConnection = function(options) { 1324 | var w = window, 1325 | PeerConnection = w.mozRTCPeerConnection || w.webkitRTCPeerConnection, 1326 | SessionDescription = w.mozRTCSessionDescription || w.RTCSessionDescription, 1327 | IceCandidate = w.mozRTCIceCandidate || w.RTCIceCandidate; 1328 | 1329 | if (moz) console.warn('Should we use "stun:stun.services.mozilla.com"?'); 1330 | 1331 | var STUN = { 1332 | url: !moz ? 'stun:stun.l.google.com:19302' : 'stun:23.21.150.121' 1333 | }; 1334 | 1335 | var TURN = { 1336 | url: 'turn:homeo@turn.bistri.com:80', 1337 | credential: 'homeo' 1338 | }; 1339 | 1340 | var iceServers = { 1341 | iceServers: options.iceServers || [STUN] 1342 | }; 1343 | 1344 | if (!moz && !options.iceServers) { 1345 | if (parseInt(navigator.userAgent.match( /Chrom(e|ium)\/([0-9]+)\./ )[2]) >= 28) 1346 | TURN = { 1347 | url: 'turn:turn.bistri.com:80', 1348 | credential: 'homeo', 1349 | username: 'homeo' 1350 | }; 1351 | 1352 | iceServers.iceServers = [STUN, TURN]; 1353 | } 1354 | 1355 | var optional = { 1356 | optional: [] 1357 | }; 1358 | 1359 | if (!moz) { 1360 | optional.optional = [{ 1361 | DtlsSrtpKeyAgreement: true 1362 | }]; 1363 | 1364 | if (options.disableDtlsSrtp) 1365 | optional = { 1366 | optional: [] 1367 | }; 1368 | 1369 | if (options.onmessage) 1370 | optional.optional = [{ 1371 | RtpDataChannels: true 1372 | }]; 1373 | } 1374 | 1375 | // local/host candidates can also be used for peer connection 1376 | if (!navigator.onLine) { 1377 | iceServers = null; 1378 | console.warn('No internet connection detected. No STUN/TURN server is used to make sure local/host candidates are used for peers connection.'); 1379 | } else log('iceServers', JSON.stringify(iceServers, null, '\t')); 1380 | 1381 | var peer = new PeerConnection(iceServers, optional); 1382 | 1383 | openOffererChannel(); 1384 | 1385 | peer.onicecandidate = function(event) { 1386 | if (event.candidate) 1387 | options.onICE(event.candidate); 1388 | }; 1389 | 1390 | // adding media streams to the PeerConnection 1391 | if (options.attachStreams && options.attachStreams.length) { 1392 | var streams = options.attachStreams; 1393 | for (var i = 0; i < streams.length; i++) { 1394 | peer.addStream(streams[i]); 1395 | } 1396 | } 1397 | 1398 | peer.onaddstream = function(event) { 1399 | log('on:add:stream', event.stream); 1400 | 1401 | if (!event || !options.onstream) return; 1402 | 1403 | options.onstream(event.stream); 1404 | options.renegotiate = false; 1405 | }; 1406 | 1407 | peer.onsignalingstatechange = function() { 1408 | log('onsignalingstatechange:', toStr({ 1409 | iceGatheringState: peer.iceGatheringState, 1410 | signalingState: peer.signalingState 1411 | })); 1412 | }; 1413 | peer.oniceconnectionstatechange = function() { 1414 | log('oniceconnectionstatechange:', toStr({ 1415 | iceGatheringState: peer.iceGatheringState, 1416 | signalingState: peer.signalingState 1417 | })); 1418 | }; 1419 | 1420 | peer.onremoveStream = function(event) { 1421 | log('on:remove:stream', event.stream); 1422 | }; 1423 | 1424 | peer.onconnecting = function(event) { 1425 | log('on:connecting', event); 1426 | }; 1427 | 1428 | peer.onnegotiationneeded = function(event) { 1429 | log('on:negotiation:needed', event); 1430 | }; 1431 | 1432 | var constraints; 1433 | 1434 | function setConstraints() { 1435 | var session = options.session; 1436 | 1437 | var sdpConstraints = options.sdpConstraints; 1438 | constraints = options.constraints || { 1439 | optional: [], 1440 | mandatory: { 1441 | OfferToReceiveAudio: !!session.audio, 1442 | OfferToReceiveVideo: !!session.video || !!session.screen 1443 | } 1444 | }; 1445 | 1446 | if (sdpConstraints.mandatory) 1447 | constraints.mandatory = merge(constraints.mandatory, sdpConstraints.mandatory); 1448 | 1449 | if (sdpConstraints.optional) 1450 | constraints.optional[0] = merge({ }, sdpConstraints.optional); 1451 | 1452 | log('sdp constraints', JSON.stringify(constraints, null, '\t')); 1453 | } 1454 | 1455 | setConstraints(); 1456 | 1457 | function createOffer() { 1458 | if (!options.onOfferSDP) 1459 | return; 1460 | 1461 | peer.createOffer(function(sessionDescription) { 1462 | sessionDescription.sdp = serializeSdp(sessionDescription.sdp); 1463 | peer.setLocalDescription(sessionDescription); 1464 | options.onOfferSDP(sessionDescription); 1465 | }, onSdpError, constraints); 1466 | } 1467 | 1468 | function createAnswer() { 1469 | if (!options.onAnswerSDP) 1470 | return; 1471 | 1472 | //options.offerSDP.sdp = addStereo(options.offerSDP.sdp); 1473 | options.offerSDP = new SessionDescription(options.offerSDP, onSdpSuccess, onSdpError); 1474 | peer.setRemoteDescription(options.offerSDP); 1475 | 1476 | peer.createAnswer(function(sessionDescription) { 1477 | sessionDescription.sdp = serializeSdp(sessionDescription.sdp); 1478 | peer.setLocalDescription(sessionDescription); 1479 | options.onAnswerSDP(sessionDescription); 1480 | }, onSdpError, constraints); 1481 | } 1482 | 1483 | if ((options.onmessage && !moz) || !options.onmessage) { 1484 | createOffer(); 1485 | createAnswer(); 1486 | } 1487 | 1488 | var bandwidth = options.bandwidth; 1489 | 1490 | function setBandwidth(sdp) { 1491 | if (!bandwidth) return; 1492 | 1493 | // remove existing bandwidth lines 1494 | sdp = sdp.replace( /b=AS([^\r\n]+\r\n)/g , ''); 1495 | 1496 | if (bandwidth.audio) { 1497 | sdp = sdp.replace( /a=mid:audio\r\n/g , 'a=mid:audio\r\nb=AS:' + bandwidth.audio + '\r\n'); 1498 | } 1499 | 1500 | if (bandwidth.video) { 1501 | sdp = sdp.replace( /a=mid:video\r\n/g , 'a=mid:video\r\nb=AS:' + bandwidth.video + '\r\n'); 1502 | } 1503 | 1504 | // According to http://goo.gl/YUtdNk 1505 | // "...We should always use the default bandwidth for RTP-based data 1506 | // channels. Don't allow SDP to set the bandwidth, because that 1507 | // would give JS the opportunity to 'break the Internet'..." 1508 | // bug: is that matters? 1509 | if (bandwidth.data) { 1510 | // sdp = sdp.replace( /a=mid:data\r\n/g , 'a=mid:data\r\nb=AS:' + bandwidth.data + '\r\n'); 1511 | } 1512 | 1513 | return sdp; 1514 | } 1515 | 1516 | // var bitrate = options.bitrate || {}; 1517 | 1518 | function setBitrate(sdp) { 1519 | // sdp = sdp.replace( /a=mid:video\r\n/g , 'a=mid:video\r\na=rtpmap:120 VP8/90000\r\na=fmtp:120 x-google-min-bitrate=' + (bitrate || 10) + '\r\n'); 1520 | return sdp; 1521 | } 1522 | 1523 | var framerate = options.framerate || { }; 1524 | 1525 | function setFramerate(sdp) { 1526 | // bug: need to make sure whether following lines causes crash or failures 1527 | // sdp = sdp.replace('a=fmtp:111 minptime=10', 'a=fmtp:111 minptime=' + (framerate.minptime || 10) + '; maxaveragebitrate=128000'); 1528 | // sdp = sdp.replace('a=maxptime:60', 'a=maxptime:' + (framerate.maxptime || 60)); 1529 | return sdp; 1530 | } 1531 | 1532 | function getInteropSDP(sdp) { 1533 | var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''), 1534 | extractedChars = ''; 1535 | 1536 | function getChars() { 1537 | extractedChars += chars[parseInt(Math.random() * 40)] || ''; 1538 | if (extractedChars.length < 40) 1539 | getChars(); 1540 | 1541 | return extractedChars; 1542 | } 1543 | 1544 | // for audio-only streaming: multiple-crypto lines are not allowed 1545 | if (options.onAnswerSDP) 1546 | sdp = sdp.replace( /(a=crypto:0 AES_CM_128_HMAC_SHA1_32)(.*?)(\r\n)/g , ''); 1547 | 1548 | var inline = getChars() + '\r\n' + (extractedChars = ''); 1549 | sdp = sdp.indexOf('a=crypto') == -1 ? sdp.replace( /c=IN/g , 1550 | 'a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:' + inline + 1551 | 'c=IN') : sdp; 1552 | 1553 | return sdp; 1554 | } 1555 | 1556 | function serializeSdp(sdp) { 1557 | if (moz) return sdp; 1558 | sdp = setBandwidth(sdp); 1559 | // sdp = setFramerate(sdp); 1560 | // sdp = setBitrate(sdp); 1561 | // sdp = getInteropSDP(sdp); 1562 | return sdp; 1563 | } 1564 | 1565 | var channel; 1566 | 1567 | function openOffererChannel() { 1568 | if (!options.onmessage || (moz && !options.onOfferSDP)) 1569 | return; 1570 | 1571 | _openOffererChannel(); 1572 | 1573 | if (!moz) return; 1574 | navigator.mozGetUserMedia({ 1575 | audio: true, 1576 | fake: true 1577 | }, function(stream) { 1578 | peer.addStream(stream); 1579 | createOffer(); 1580 | }, useless); 1581 | } 1582 | 1583 | function _openOffererChannel() { 1584 | var reliable = { 1585 | reliable: false 1586 | }; 1587 | if (moz || options.reliable) { 1588 | console.warn('Reliable sctp-based channels "seems" (still) buggy on windows.'); 1589 | reliable = { }; 1590 | } 1591 | 1592 | channel = peer.createDataChannel(options.channel || 'RTCDataChannel', reliable); 1593 | 1594 | if (moz) channel.binaryType = 'blob'; 1595 | if (options.binaryType) 1596 | channel.binaryType = options.binaryType; 1597 | 1598 | setChannelEvents(); 1599 | } 1600 | 1601 | function setChannelEvents() { 1602 | channel.onmessage = options.onmessage; 1603 | channel.onopen = function() { 1604 | options.onopen(channel); 1605 | }; 1606 | channel.onclose = options.onclose; 1607 | channel.onerror = options.onerror; 1608 | } 1609 | 1610 | if (options.onAnswerSDP && options.onmessage && moz) 1611 | openAnswererChannel(); 1612 | 1613 | function openAnswererChannel() { 1614 | peer.ondatachannel = function(event) { 1615 | channel = event.channel; 1616 | channel.binaryType = 'blob'; 1617 | if (options.binaryType) 1618 | channel.binaryType = options.binaryType; 1619 | setChannelEvents(); 1620 | }; 1621 | 1622 | navigator.mozGetUserMedia({ 1623 | audio: true, 1624 | fake: true 1625 | }, function(stream) { 1626 | peer.addStream(stream); 1627 | createAnswer(); 1628 | }, useless); 1629 | } 1630 | 1631 | // fake:true is also available on chrome under a flag! 1632 | 1633 | function useless() { 1634 | log('Error in fake:true'); 1635 | } 1636 | 1637 | function onSdpSuccess() { 1638 | } 1639 | 1640 | function onSdpError(e) { 1641 | console.error('sdp error:', e.name, e.message); 1642 | } 1643 | 1644 | return { 1645 | connection: peer, 1646 | addAnswerSDP: function(sdp) { 1647 | peer.setRemoteDescription(new SessionDescription(sdp), onSdpSuccess, onSdpError); 1648 | }, 1649 | addICE: function(candidate) { 1650 | peer.addIceCandidate(new IceCandidate({ 1651 | sdpMLineIndex: candidate.sdpMLineIndex, 1652 | candidate: candidate.candidate 1653 | })); 1654 | }, 1655 | recreateAnswer: function(sdp, session, callback) { 1656 | options.renegotiate = true; 1657 | 1658 | options.session = session; 1659 | setConstraints(); 1660 | 1661 | options.onAnswerSDP = callback; 1662 | options.offerSDP = sdp; 1663 | createAnswer(); 1664 | }, 1665 | recreateOffer: function(session, callback) { 1666 | options.renegotiate = true; 1667 | 1668 | options.session = session; 1669 | setConstraints(); 1670 | 1671 | options.onOfferSDP = callback; 1672 | createOffer(); 1673 | } 1674 | }; 1675 | }; 1676 | 1677 | var video_constraints = { 1678 | mandatory: { }, 1679 | optional: [] 1680 | }; 1681 | 1682 | /* by @FreCap pull request #41 */ 1683 | var currentUserMediaRequest = { 1684 | streams: [], 1685 | mutex: false, 1686 | queueRequests: [] 1687 | }; 1688 | 1689 | function getUserMedia(options) { 1690 | if (currentUserMediaRequest.mutex === true) { 1691 | currentUserMediaRequest.queueRequests.push(options); 1692 | return; 1693 | } 1694 | currentUserMediaRequest.mutex = true; 1695 | 1696 | // http://tools.ietf.org/html/draft-alvestrand-constraints-resolution-00 1697 | var mediaConstraints = options.mediaConstraints || { }; 1698 | var n = navigator, 1699 | hints = options.constraints || { 1700 | audio: true, 1701 | video: video_constraints 1702 | }; 1703 | 1704 | if (hints.video == true) hints.video = video_constraints; 1705 | 1706 | // connection.mediaConstraints.audio = false; 1707 | if (typeof mediaConstraints.audio != 'undefined') 1708 | hints.audio = mediaConstraints.audio; 1709 | 1710 | // connection.media.min(320,180); 1711 | // connection.media.max(1920,1080); 1712 | var media = options.media; 1713 | if(!moz) { 1714 | var mandatory = { 1715 | minWidth: media.minWidth, 1716 | minHeight: media.minHeight, 1717 | maxWidth: media.maxWidth, 1718 | maxHeight: media.maxHeight, 1719 | minAspectRatio: media.minAspectRatio 1720 | }; 1721 | 1722 | // https://code.google.com/p/chromium/issues/detail?id=143631#c9 1723 | var allowed = ['1920:1080', '1280:720', '960:720', '640:360', '640:480', '320:240', '320:180']; 1724 | 1725 | if (allowed.indexOf(mandatory.minWidth + ':' + mandatory.minHeight) == -1 || 1726 | allowed.indexOf(mandatory.maxWidth + ':' + mandatory.maxHeight) == -1) { 1727 | console.error('The min/max width/height constraints you passed "seems" NOT supported.', toStr(mandatory)); 1728 | } 1729 | 1730 | if (mandatory.minWidth > mandatory.maxWidth || mandatory.minHeight > mandatory.maxHeight) { 1731 | console.error('Minimum value must not exceed maximum value.', toStr(mandatory)); 1732 | } 1733 | 1734 | if (mandatory.minWidth >= 1280 && mandatory.minHeight >= 720) { 1735 | console.info('Enjoy HD video! min/' + mandatory.minWidth + ':' + mandatory.minHeight + ', max/' + mandatory.maxWidth + ':' + mandatory.maxHeight); 1736 | } 1737 | 1738 | hints.video.mandatory = merge(hints.video.mandatory, mandatory); 1739 | } 1740 | 1741 | if (mediaConstraints.mandatory) 1742 | hints.video.mandatory = merge(hints.video.mandatory, mediaConstraints.mandatory); 1743 | 1744 | // mediaConstraints.optional.bandwidth = 1638400; 1745 | if (mediaConstraints.optional) 1746 | hints.video.optional[0] = merge({ }, mediaConstraints.optional); 1747 | 1748 | log('media hints:', toStr(hints)); 1749 | 1750 | // easy way to match 1751 | var idInstance = JSON.stringify(hints); 1752 | 1753 | function streaming(stream, returnBack) { 1754 | var video = options.video; 1755 | if (video) { 1756 | video[moz ? 'mozSrcObject' : 'src'] = moz ? stream : window.webkitURL.createObjectURL(stream); 1757 | video.play(); 1758 | } 1759 | 1760 | options.onsuccess(stream, returnBack); 1761 | currentUserMediaRequest.streams[idInstance] = stream; 1762 | currentUserMediaRequest.mutex = false; 1763 | if (currentUserMediaRequest.queueRequests.length) 1764 | getUserMedia(currentUserMediaRequest.queueRequests.shift()); 1765 | } 1766 | 1767 | if (currentUserMediaRequest.streams[idInstance]) { 1768 | streaming(currentUserMediaRequest.streams[idInstance], true); 1769 | } else { 1770 | n.getMedia = n.webkitGetUserMedia || n.mozGetUserMedia; 1771 | n.getMedia(hints, streaming, options.onerror || function(e) { 1772 | console.error(e); 1773 | }); 1774 | } 1775 | } 1776 | 1777 | function isData(session) { 1778 | return !session.audio && !session.video && !session.screen && session.data; 1779 | } 1780 | 1781 | function swap(arr) { 1782 | var swapped = [], 1783 | length = arr.length; 1784 | for (var i = 0; i < length; i++) 1785 | if (arr[i] && arr[i] !== true) 1786 | swapped.push(arr[i]); 1787 | return swapped; 1788 | } 1789 | 1790 | function log(a, b, c, d, e, f) { 1791 | if (window.skipRTCMultiConnectionLogs) return; 1792 | if (f) 1793 | console.log(a, b, c, d, e, f); 1794 | else if (e) 1795 | console.log(a, b, c, d, e); 1796 | else if (d) 1797 | console.log(a, b, c, d); 1798 | else if (c) 1799 | console.log(a, b, c); 1800 | else if (b) 1801 | console.log(a, b); 1802 | else if (a) 1803 | console.log(a); 1804 | } 1805 | 1806 | function toStr(obj) { 1807 | return JSON.stringify(obj, function(key, value) { 1808 | if (value && value.sdp) { 1809 | console.log(value.sdp.type, '---', value.sdp.sdp); 1810 | return ''; 1811 | } else return value; 1812 | }, '---'); 1813 | } 1814 | 1815 | function getLength(obj) { 1816 | var length = 0; 1817 | for (var o in obj) 1818 | if (o) length++; 1819 | return length; 1820 | } 1821 | 1822 | // Get HTMLAudioElement/HTMLVideoElement accordingly 1823 | 1824 | function getMediaElement(stream, session) { 1825 | var isAudio = session.audio && !session.video && !session.screen; 1826 | if (!moz && stream.getAudioTracks && stream.getVideoTracks) { 1827 | isAudio = stream.getAudioTracks().length && !stream.getVideoTracks().length; 1828 | } 1829 | 1830 | var mediaElement = document.createElement(isAudio ? 'audio' : 'video'); 1831 | mediaElement[moz ? 'mozSrcObject' : 'src'] = moz ? stream : window.webkitURL.createObjectURL(stream); 1832 | mediaElement.autoplay = true; 1833 | mediaElement.controls = true; 1834 | mediaElement.volume = .1; 1835 | mediaElement.play(); 1836 | return mediaElement; 1837 | } 1838 | 1839 | function merge(mergein, mergeto) { 1840 | if (!mergein) mergein = { }; 1841 | if (!mergeto) return mergein; 1842 | 1843 | for (var item in mergeto) { 1844 | mergein[item] = mergeto[item]; 1845 | } 1846 | return mergein; 1847 | } 1848 | 1849 | // the purpose of this method is to detect mic/speaker activity 1850 | 1851 | function voiceActivityDetection(peer) { 1852 | if (moz) return; 1853 | 1854 | peer.getStats(function(stats) { 1855 | var output = { }; 1856 | var sr = stats.result(); 1857 | for (var i = 0; i < sr.length; i++) { 1858 | var obj = sr[i].remote; 1859 | if (obj) { 1860 | var nspk = 0.0; 1861 | var nmic = 0.0; 1862 | if (obj.stat('audioInputLevel')) { 1863 | nmic = obj.stat('audioInputLevel'); 1864 | } 1865 | if (nmic > 0.0) { 1866 | output.mic = Math.floor(Math.max((Math.LOG2E * Math.log(nmic) - 4.0), 0.0)); 1867 | } 1868 | if (obj.stat('audioOutputLevel')) { 1869 | nspk = obj.stat('audioOutputLevel'); 1870 | } 1871 | if (nspk > 0.0) { 1872 | output.speaker = Math.floor(Math.max((Math.LOG2E * Math.log(nspk) - 4.0), 0.0)); 1873 | } 1874 | } 1875 | } 1876 | log('mic intensity:', output.mic); 1877 | log('speaker intensity:', output.speaker); 1878 | log('Type to stop this logger.'); 1879 | }); 1880 | 1881 | if (!window.skipRTCMultiConnectionLogs) 1882 | setTimeout(function() { 1883 | voiceActivityDetection(peer); 1884 | }, 2000); 1885 | } 1886 | 1887 | function loadScript(src, onload) { 1888 | var script = document.createElement('script'); 1889 | script.src = src; 1890 | if (onload) script.onload = onload; 1891 | document.documentElement.appendChild(script); 1892 | } 1893 | 1894 | function muteOrUnmute(e) { 1895 | var stream = e.stream, 1896 | root = e.root, 1897 | session = e.session || { }, 1898 | enabled = e.enabled; 1899 | 1900 | if (!session.audio && !session.video) { 1901 | session = merge(session, { 1902 | audio: true, 1903 | video: true 1904 | }); 1905 | } 1906 | 1907 | // implementation from #68 1908 | if (session.type) { 1909 | if (session.type == 'remote' && root.type != 'remote') return; 1910 | if (session.type == 'local' && root.type != 'local') return; 1911 | } 1912 | 1913 | log('session', JSON.stringify(session, null, '\t')); 1914 | 1915 | // enable/disable audio/video tracks 1916 | 1917 | if (session.audio) { 1918 | var audioTracks = stream.getAudioTracks()[0]; 1919 | if (audioTracks) 1920 | audioTracks.enabled = !enabled; 1921 | } 1922 | 1923 | if (session.video) { 1924 | var videoTracks = stream.getVideoTracks()[0]; 1925 | if (videoTracks) 1926 | videoTracks.enabled = !enabled; 1927 | } 1928 | 1929 | // socket message to change media element look 1930 | if (root.socket) 1931 | root.socket.send({ 1932 | userid: root.userid, 1933 | mute: !!enabled, 1934 | unmute: !enabled 1935 | }); 1936 | } 1937 | 1938 | RTCMultiConnection.prototype.setDefaults = DefaultSettings; 1939 | 1940 | function DefaultSettings() { 1941 | this.onmessage = function(e) { 1942 | log(e.userid, 'posted', e.data); 1943 | }; 1944 | 1945 | this.onopen = function(e) { 1946 | log('Data connection is opened between you and', e.userid); 1947 | }; 1948 | 1949 | this.onerror = function(e) { 1950 | console.error('Error in data connection between you and', e.userid, e); 1951 | }; 1952 | 1953 | this.onclose = function(e) { 1954 | console.warn('Data connection between you and', e.userid, 'is closed.', e); 1955 | }; 1956 | 1957 | this.onFileReceived = function(fileName) { 1958 | log('File <', fileName, '> received successfully.'); 1959 | }; 1960 | 1961 | this.onFileSent = function(file) { 1962 | log('File <', file.name, '> sent successfully.'); 1963 | }; 1964 | 1965 | this.onFileProgress = function(packets) { 1966 | log('<', packets.remaining, '> items remaining.'); 1967 | }; 1968 | 1969 | this.onstream = function(e) { 1970 | log('on:add:stream', e.stream); 1971 | }; 1972 | 1973 | this.onleave = function(e) { 1974 | log(e.userid, 'left!'); 1975 | }; 1976 | 1977 | this.onstreamended = function(e) { 1978 | log('on:stream:ended', e.stream); 1979 | }; 1980 | 1981 | this.peers = { }; 1982 | 1983 | this.streams = { 1984 | mute: function(session) { 1985 | this._private(session, true); 1986 | }, 1987 | unmute: function(session) { 1988 | this._private(session, false); 1989 | }, 1990 | _private: function(session, enabled) { 1991 | // implementation from #68 1992 | for (var stream in this) { 1993 | if (stream != 'mute' && stream != 'unmute' && stream != '_private') { 1994 | var root = this[stream]; 1995 | muteOrUnmute({ 1996 | root: root, 1997 | session: session, 1998 | stream: root.stream, 1999 | enabled: enabled 2000 | }); 2001 | } 2002 | } 2003 | } 2004 | }; 2005 | this.channels = { }; 2006 | this.extra = { }; 2007 | 2008 | this.session = { 2009 | audio: true, 2010 | video: true, 2011 | data: true 2012 | }; 2013 | 2014 | this.bandwidth = { 2015 | //audio: 50, 2016 | //video: 256, 2017 | data: 1638400 2018 | }; 2019 | 2020 | this.media = { 2021 | min: function(width, height) { 2022 | this.minWidth = width; 2023 | this.minHeight = height; 2024 | }, 2025 | minWidth: 640, // 1920 2026 | minHeight: 360, // 1080 2027 | max: function(width, height) { 2028 | this.maxWidth = width; 2029 | this.maxHeight = height; 2030 | }, 2031 | maxWidth: 1920, 2032 | maxHeight: 1080, 2033 | bandwidth: 256, 2034 | minFrameRate: 32, 2035 | minAspectRatio: 1.77 2036 | }; 2037 | 2038 | this.candidates = { 2039 | host: true, 2040 | relay: true, 2041 | reflexive: true 2042 | }; 2043 | 2044 | this.mediaConstraints = { }; 2045 | this.sdpConstraints = { }; 2046 | 2047 | this.attachStreams = []; 2048 | this.detachStreams = []; 2049 | 2050 | this.maxParticipantsAllowed = 256; 2051 | this.autoSaveToDisk = true; 2052 | 2053 | // 'many-to-many' / 'one-to-many' / 'one-to-one' / 'one-way' 2054 | this.direction = 'many-to-many'; 2055 | 2056 | this._getStream = function(e) { 2057 | return { 2058 | stream: e.stream, 2059 | userid: e.userid, 2060 | socket: e.socket, 2061 | type: e.type, 2062 | stop: function() { 2063 | var stream = this.stream; 2064 | if (stream && stream.stop) 2065 | stream.stop(); 2066 | }, 2067 | mute: function(session) { 2068 | this._private(session, true); 2069 | }, 2070 | unmute: function(session) { 2071 | this._private(session, false); 2072 | }, 2073 | _private: function(session, enabled) { 2074 | muteOrUnmute({ 2075 | root: this, 2076 | session: session, 2077 | enabled: enabled, 2078 | stream: this.stream 2079 | }); 2080 | }, 2081 | startRecording: function(session) { 2082 | if (!session) session = { audio: true, video: true }; 2083 | if (!window.RecordRTC) { 2084 | var self = this; 2085 | return loadScript('https://www.webrtc-experiment.com/RecordRTC.js', function() { 2086 | self.startRecording(session); 2087 | }); 2088 | } 2089 | 2090 | var stream = this.stream; 2091 | if (session.audio) { 2092 | this.recordAudio = RecordRTC(stream, session); 2093 | this.recordAudio.startRecording(); 2094 | } 2095 | 2096 | // video recording on firefox has some issues 2097 | if (!moz && session.video) { 2098 | this.recordVideo = RecordRTC(stream, merge(session, { 2099 | type: 'video' 2100 | })); 2101 | this.recordVideo.startRecording(); 2102 | } 2103 | }, 2104 | stopRecording: function(onBlob, session) { 2105 | if (!session) session = { audio: true, video: true }; 2106 | else 2107 | session = { 2108 | audio: session == 'audio', 2109 | video: session == 'video' 2110 | }; 2111 | 2112 | if (session.audio && this.recordAudio) { 2113 | this.recordAudio.stopRecording(); 2114 | 2115 | var blob = this.recordAudio.getBlob(); 2116 | blob.recordingType = 'audio'; 2117 | if (onBlob) onBlob(blob); 2118 | } 2119 | 2120 | if (!moz && session.video && this.recordVideo) { 2121 | this.recordVideo.stopRecording(); 2122 | 2123 | blob = this.recordVideo.getBlob(); 2124 | blob.recordingType = 'video'; 2125 | if (onBlob) onBlob(blob); 2126 | } 2127 | } 2128 | }; 2129 | }; 2130 | 2131 | this.token = function() { 2132 | return (Math.random() * new Date().getTime()).toString(36).replace( /\./g , ''); 2133 | }; 2134 | } 2135 | })(); -------------------------------------------------------------------------------- /public/RecordRTC.js: -------------------------------------------------------------------------------- 1 | // Muaz Khan - https://github.com/muaz-khan 2 | // MIT License - https://www.webrtc-experiment.com/licence/ 3 | // Documentation - https://github.com/muaz-khan/WebRTC-Experiment/tree/master/RecordRTC 4 | // ____________ 5 | // RecordRTC.js 6 | 7 | /* 8 | need to fix: 9 | 1. chrome tabCapture and audio/video recording 10 | 2. ffmpeg/avconv to merge webm/wav 11 | 3. longer video issues 12 | 4. longer audio issues 13 | */ 14 | 15 | function RecordRTC(mediaStream, config) { 16 | config = config || { }; 17 | 18 | if (!mediaStream) throw 'MediaStream is mandatory.'; 19 | if (!config.type) config.type = 'audio'; 20 | 21 | function startRecording() { 22 | console.debug('started recording stream.'); 23 | 24 | // Media Stream Recording API has not been implemented in chrome yet; 25 | // That's why using WebAudio API to record stereo audio in WAV format 26 | var Recorder = IsChrome ? window.StereoRecorder : window.MediaStreamRecorder; 27 | 28 | // video recorder (in WebM format) 29 | if (config.type == 'video') Recorder = window.WhammyRecorder; 30 | 31 | // video recorder (in Gif format) 32 | if (config.type == 'gif') Recorder = window.GifRecorder; 33 | 34 | mediaRecorder = new Recorder(mediaStream); 35 | 36 | // Merge all data-types except "function" 37 | mediaRecorder = mergeProps(mediaRecorder, config); 38 | 39 | mediaRecorder.record(); 40 | } 41 | 42 | function stopRecording(callback) { 43 | console.warn('stopped recording stream.'); 44 | mediaRecorder.stop(); 45 | if (callback && mediaRecorder) { 46 | var url = URL.createObjectURL(mediaRecorder.recordedBlob); 47 | callback(url); 48 | } 49 | } 50 | 51 | var mediaRecorder; 52 | 53 | return { 54 | startRecording: startRecording, 55 | stopRecording: stopRecording, 56 | getBlob: function() { 57 | if (!mediaRecorder) return console.warn('RecordRTC is idle.'); 58 | return mediaRecorder.recordedBlob; 59 | }, 60 | getDataURL: function(callback) { 61 | if (!mediaRecorder) return console.warn('RecordRTC is idle.'); 62 | 63 | var reader = new FileReader(); 64 | reader.readAsDataURL(mediaRecorder.recordedBlob); 65 | reader.onload = function(event) { 66 | if (callback) callback(event.target.result); 67 | }; 68 | }, 69 | toURL: function() { 70 | if (!mediaRecorder) return console.warn('RecordRTC is idle.'); 71 | return URL.createObjectURL(mediaRecorder.recordedBlob); 72 | }, 73 | save: function() { 74 | if (!mediaRecorder) return console.warn('RecordRTC is idle.'); 75 | console.log('saving recorded stream to disk!'); 76 | // bug: should we use "getBlob" instead; to handle aww-snaps! 77 | this.getDataURL(function(dataURL) { 78 | var hyperlink = document.createElement('a'); 79 | hyperlink.href = dataURL; 80 | hyperlink.target = '_blank'; 81 | hyperlink.download = (Math.round(Math.random() * 9999999999) + 888888888) + '.' + mediaRecorder.recordedBlob.type.split('/')[1]; 82 | 83 | var evt = new MouseEvent('click', { 84 | view: window, 85 | bubbles: true, 86 | cancelable: true 87 | }); 88 | 89 | hyperlink.dispatchEvent(evt); 90 | 91 | (window.URL || window.webkitURL).revokeObjectURL(hyperlink.href); 92 | }); 93 | } 94 | }; 95 | } 96 | 97 | // __________________________ 98 | // Cross-Browser Declarations 99 | 100 | // animation-frame used in WebM recording 101 | requestAnimationFrame = window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame; 102 | cancelAnimationFrame = window.webkitCancelAnimationFrame || window.mozCancelAnimationFrame; 103 | 104 | // WebAudio API representer 105 | AudioContext = window.webkitAudioContext || window.mozAudioContext; 106 | 107 | URL = window.URL || window.webkitURL; 108 | navigator.getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia; 109 | 110 | if (window.webkitMediaStream) window.MediaStream = window.webkitMediaStream; 111 | 112 | IsChrome = !!navigator.webkitGetUserMedia; 113 | 114 | // Merge all other data-types except "function" 115 | 116 | function mergeProps(mergein, mergeto) { 117 | mergeto = reformatProps(mergeto); 118 | for (var t in mergeto) { 119 | if (typeof mergeto[t] !== 'function') { 120 | mergein[t] = mergeto[t]; 121 | } 122 | } 123 | return mergein; 124 | } 125 | 126 | function reformatProps(obj) { 127 | var output = { }; 128 | for (var o in obj) { 129 | if (o.indexOf('-') != -1) { 130 | var splitted = o.split('-'); 131 | var name = splitted[0] + splitted[1].split('')[0].toUpperCase() + splitted[1].substr(1); 132 | output[name] = obj[o]; 133 | } else output[o] = obj[o]; 134 | } 135 | return output; 136 | } 137 | 138 | // ______________________ 139 | // MediaStreamRecorder.js 140 | 141 | // encoder only supports 48k/16k mono audio channel 142 | 143 | function MediaStreamRecorder(mediaStream) { 144 | var self = this; 145 | 146 | this.record = function() { 147 | // http://dxr.mozilla.org/mozilla-central/source/content/media/MediaRecorder.cpp 148 | // https://wiki.mozilla.org/Gecko:MediaRecorder 149 | mediaRecorder = new MediaRecorder(mediaStream); 150 | mediaRecorder.ondataavailable = function(e) { 151 | self.recordedBlob = new Blob([self.recordedBlob, e.data], { type: 'audio/ogg' }); 152 | }; 153 | 154 | mediaRecorder.start(0); 155 | }; 156 | 157 | this.stop = function() { 158 | if (mediaRecorder.state == 'recording') { 159 | mediaRecorder.requestData(); 160 | mediaRecorder.stop(); 161 | } 162 | }; 163 | 164 | // Reference to "MediaRecorder" object 165 | var mediaRecorder; 166 | } 167 | 168 | 169 | // _________________ 170 | // StereoRecorder.js 171 | 172 | function StereoRecorder(mediaStream) { 173 | this.record = function() { 174 | mediaRecorder = new StereoAudioRecorder(mediaStream, this); 175 | mediaRecorder.record(); 176 | }; 177 | 178 | this.stop = function() { 179 | if (mediaRecorder) mediaRecorder.stop(); 180 | this.recordedBlob = mediaRecorder.recordedBlob; 181 | }; 182 | 183 | // Reference to "StereoAudioRecorder" object 184 | var mediaRecorder; 185 | } 186 | 187 | // source code from: http://typedarray.org/wp-content/projects/WebAudioRecorder/script.js 188 | // ______________________ 189 | // StereoAudioRecorder.js 190 | 191 | function StereoAudioRecorder(mediaStream, root) { 192 | // variables 193 | var leftchannel = []; 194 | var rightchannel = []; 195 | var recorder; 196 | var recording = false; 197 | var recordingLength = 0; 198 | var volume; 199 | var audioInput; 200 | var audioContext; 201 | var context; 202 | 203 | this.record = function() { 204 | recording = true; 205 | // reset the buffers for the new recording 206 | leftchannel.length = rightchannel.length = 0; 207 | recordingLength = 0; 208 | }; 209 | 210 | this.stop = function() { 211 | // we stop recording 212 | recording = false; 213 | 214 | // we flat the left and right channels down 215 | var leftBuffer = mergeBuffers(leftchannel, recordingLength); 216 | var rightBuffer = mergeBuffers(rightchannel, recordingLength); 217 | // we interleave both channels together 218 | var interleaved = interleave(leftBuffer, rightBuffer); 219 | 220 | // we create our wav file 221 | var buffer = new ArrayBuffer(44 + interleaved.length * 2); 222 | var view = new DataView(buffer); 223 | 224 | // RIFF chunk descriptor 225 | writeUTFBytes(view, 0, 'RIFF'); 226 | view.setUint32(4, 44 + interleaved.length * 2, true); 227 | writeUTFBytes(view, 8, 'WAVE'); 228 | // FMT sub-chunk 229 | writeUTFBytes(view, 12, 'fmt '); 230 | view.setUint32(16, 16, true); 231 | view.setUint16(20, 1, true); 232 | // stereo (2 channels) 233 | view.setUint16(22, 2, true); 234 | view.setUint32(24, sampleRate, true); 235 | view.setUint32(28, sampleRate * 4, true); 236 | view.setUint16(32, 4, true); 237 | view.setUint16(34, 16, true); 238 | // data sub-chunk 239 | writeUTFBytes(view, 36, 'data'); 240 | view.setUint32(40, interleaved.length * 2, true); 241 | 242 | // write the PCM samples 243 | var lng = interleaved.length; 244 | var index = 44; 245 | volume = 1; 246 | for (var i = 0; i < lng; i++) { 247 | view.setInt16(index, interleaved[i] * (0x7FFF * volume), true); 248 | index += 2; 249 | } 250 | 251 | // final binary blob 252 | this.recordedBlob = new Blob([view], { type: 'audio/wav' }); 253 | 254 | // recorded audio length 255 | this.length = recordingLength; 256 | }; 257 | 258 | function interleave(leftChannel, rightChannel) { 259 | var length = leftChannel.length + rightChannel.length; 260 | var result = new Float32Array(length); 261 | 262 | var inputIndex = 0; 263 | 264 | for (var index = 0; index < length;) { 265 | result[index++] = leftChannel[inputIndex]; 266 | result[index++] = rightChannel[inputIndex]; 267 | inputIndex++; 268 | } 269 | return result; 270 | } 271 | 272 | function mergeBuffers(channelBuffer, rLength) { 273 | var result = new Float32Array(rLength); 274 | var offset = 0; 275 | var lng = channelBuffer.length; 276 | for (var i = 0; i < lng; i++) { 277 | var buffer = channelBuffer[i]; 278 | result.set(buffer, offset); 279 | offset += buffer.length; 280 | } 281 | return result; 282 | } 283 | 284 | function writeUTFBytes(view, offset, string) { 285 | var lng = string.length; 286 | for (var i = 0; i < lng; i++) { 287 | view.setUint8(offset + i, string.charCodeAt(i)); 288 | } 289 | } 290 | 291 | // creates the audio context 292 | audioContext = window.AudioContext || window.webkitAudioContext; 293 | context = new audioContext(); 294 | 295 | // creates a gain node 296 | volume = context.createGain(); 297 | 298 | // creates an audio node from the microphone incoming stream 299 | audioInput = context.createMediaStreamSource(mediaStream); 300 | 301 | // connect the stream to the gain node 302 | audioInput.connect(volume); 303 | 304 | // From the spec: This value controls how frequently the audioprocess event is 305 | // dispatched and how many sample-frames need to be processed each call. 306 | // Lower values for buffer size will result in a lower (better) latency. 307 | // Higher values will be necessary to avoid audio breakup and glitches 308 | 309 | // bug: how to minimize wav size? 310 | 311 | // The size of the buffer (in sample-frames) which needs to 312 | // be processed each time onprocessaudio is called. 313 | // Legal values are (256, 512, 1024, 2048, 4096, 8192, 16384). 314 | var legalBufferValues = [256, 512, 1024, 2048, 4096, 8192, 16384]; 315 | var bufferSize = root.bufferSize || 2048; 316 | 317 | if (legalBufferValues.indexOf(bufferSize) == -1) { 318 | throw 'Legal values for buffer-size are ' + JSON.stringify(legalBufferValues, null, '\t'); 319 | } 320 | 321 | // The sample rate (in sample-frames per second) at which the 322 | // AudioContext handles audio. It is assumed that all AudioNodes 323 | // in the context run at this rate. In making this assumption, 324 | // sample-rate converters or "varispeed" processors are not supported 325 | // in real-time processing. 326 | 327 | // The sampleRate parameter describes the sample-rate of the 328 | // linear PCM audio data in the buffer in sample-frames per second. 329 | // An implementation must support sample-rates in at least 330 | // the range 22050 to 96000. 331 | var sampleRate = root.sampleRate || context.sampleRate || 44100; 332 | 333 | if (sampleRate < 22050 || sampleRate > 96000) { 334 | throw 'sample-rate must be under range 22050 and 96000.'; 335 | } 336 | 337 | console.log('sample-rate', sampleRate); 338 | console.log('buffer-size', bufferSize); 339 | 340 | recorder = context.createScriptProcessor(bufferSize, 2, 2); 341 | 342 | recorder.onaudioprocess = function(e) { 343 | if (!recording) return; 344 | var left = e.inputBuffer.getChannelData(0); 345 | var right = e.inputBuffer.getChannelData(1); 346 | // we clone the samples 347 | leftchannel.push(new Float32Array(left)); 348 | rightchannel.push(new Float32Array(right)); 349 | recordingLength += bufferSize; 350 | }; 351 | 352 | // we connect the recorder 353 | volume.connect(recorder); 354 | recorder.connect(context.destination); 355 | } 356 | 357 | // _________________ 358 | // WhammyRecorder.js 359 | 360 | function WhammyRecorder(mediaStream) { 361 | this.record = function() { 362 | if (!this.width) this.width = video.offsetWidth || 320; 363 | if (!this.height) this.height = video.offsetHeight || 240; 364 | 365 | if (!this.video) { 366 | this.video = { 367 | width: this.width, 368 | height: this.height 369 | }; 370 | } 371 | 372 | if (!this.canvas) { 373 | this.canvas = { 374 | width: this.width, 375 | height: this.height 376 | }; 377 | } 378 | 379 | canvas.width = this.canvas.width; 380 | canvas.height = this.canvas.height; 381 | 382 | video.width = this.video.width; 383 | video.height = this.video.height; 384 | 385 | startTime = Date.now(); 386 | 387 | function drawVideoFrame(time) { 388 | lastAnimationFrame = requestAnimationFrame(drawVideoFrame); 389 | 390 | if (typeof lastFrameTime === undefined) { 391 | lastFrameTime = time; 392 | } 393 | 394 | // ~10 fps 395 | if (time - lastFrameTime < 90) return; 396 | 397 | context.drawImage(video, 0, 0, canvas.width, canvas.height); 398 | 399 | // whammy.add(canvas, time - lastFrameTime); 400 | whammy.add(canvas); 401 | 402 | lastFrameTime = time; 403 | } 404 | 405 | setTimeout(function() { 406 | lastAnimationFrame = requestAnimationFrame(drawVideoFrame); 407 | }, 500); 408 | }; 409 | 410 | this.stop = function() { 411 | if (lastAnimationFrame) cancelAnimationFrame(lastAnimationFrame); 412 | endTime = Date.now(); 413 | this.recordedBlob = whammy.compile(); 414 | whammy.frames = []; 415 | }; 416 | 417 | var canvas = document.createElement('canvas'); 418 | var context = canvas.getContext('2d'); 419 | 420 | var video = document.createElement('video'); 421 | video.muted = true; 422 | video.volume = 0; 423 | video.autoplay = true; 424 | video.src = URL.createObjectURL(mediaStream); 425 | video.play(); 426 | 427 | var lastAnimationFrame = null; 428 | var startTime, endTime, lastFrameTime; 429 | 430 | // Whammy.Video(speed, quality); 431 | var whammy = new Whammy.Video(10, 1); 432 | } 433 | 434 | // ______________ 435 | // GifRecorder.js 436 | 437 | function GifRecorder(mediaStream) { 438 | this.record = function() { 439 | if (!this.width) this.width = video.offsetWidth || 320; 440 | if (!this.height) this.height = video.offsetHeight || 240; 441 | 442 | if (!this.video) { 443 | this.video = { 444 | width: this.width, 445 | height: this.height 446 | }; 447 | } 448 | 449 | if (!this.canvas) { 450 | this.canvas = { 451 | width: this.width, 452 | height: this.height 453 | }; 454 | } 455 | 456 | canvas.width = this.canvas.width; 457 | canvas.height = this.canvas.height; 458 | 459 | video.width = this.video.width; 460 | video.height = this.video.height; 461 | 462 | // external library to record as GIF images 463 | gifEncoder = new GIFEncoder(); 464 | 465 | // void setRepeat(int iter) 466 | // Sets the number of times the set of GIF frames should be played. 467 | // Default is 1; 0 means play indefinitely. 468 | gifEncoder.setRepeat(0); 469 | 470 | // void setFrameRate(Number fps) 471 | // Sets frame rate in frames per second. 472 | // Equivalent to setDelay(1000/fps). 473 | // Using "setDelay" instead of "setFrameRate" 474 | gifEncoder.setDelay(this.frameRate || 200); 475 | 476 | // void setQuality(int quality) 477 | // Sets quality of color quantization (conversion of images to the 478 | // maximum 256 colors allowed by the GIF specification). 479 | // Lower values (minimum = 1) produce better colors, 480 | // but slow processing significantly. 10 is the default, 481 | // and produces good color mapping at reasonable speeds. 482 | // Values greater than 20 do not yield significant improvements in speed. 483 | gifEncoder.setQuality(this.quality || 10); 484 | 485 | // Boolean start() 486 | // This writes the GIF Header and returns false if it fails. 487 | gifEncoder.start(); 488 | 489 | startTime = Date.now(); 490 | 491 | function drawVideoFrame(time) { 492 | lastAnimationFrame = requestAnimationFrame(drawVideoFrame); 493 | 494 | if (typeof lastFrameTime === undefined) { 495 | lastFrameTime = time; 496 | } 497 | 498 | // ~10 fps 499 | if (time - lastFrameTime < 90) return; 500 | 501 | context.drawImage(video, 0, 0, canvas.width, canvas.height); 502 | gifEncoder.addFrame(context); 503 | lastFrameTime = time; 504 | } 505 | 506 | lastAnimationFrame = requestAnimationFrame(drawVideoFrame); 507 | }; 508 | 509 | this.stop = function() { 510 | if (lastAnimationFrame) cancelAnimationFrame(lastAnimationFrame); 511 | 512 | endTime = Date.now(); 513 | 514 | this.recordedBlob = new Blob([new Uint8Array(gifEncoder.stream().bin)], { 515 | type: 'image/gif' 516 | }); 517 | 518 | // bug: find a way to clear old recorded blobs 519 | gifEncoder.stream().bin = []; 520 | }; 521 | 522 | var canvas = document.createElement('canvas'); 523 | var context = canvas.getContext('2d'); 524 | 525 | var video = document.createElement('video'); 526 | video.muted = true; 527 | video.autoplay = true; 528 | video.src = URL.createObjectURL(mediaStream); 529 | video.play(); 530 | 531 | var lastAnimationFrame = null; 532 | var startTime, endTime, lastFrameTime; 533 | 534 | var gifEncoder; 535 | } 536 | 537 | // whammy.js 538 | 539 | var Whammy = (function() { 540 | // in this case, frames has a very specific meaning, which will be 541 | // detailed once i finish writing the code 542 | 543 | function toWebM(frames) { 544 | var info = checkFrames(frames); 545 | var counter = 0; 546 | var EBML = [ 547 | { 548 | "id": 0x1a45dfa3, // EBML 549 | "data": [ 550 | { 551 | "data": 1, 552 | "id": 0x4286 // EBMLVersion 553 | }, 554 | { 555 | "data": 1, 556 | "id": 0x42f7 // EBMLReadVersion 557 | }, 558 | { 559 | "data": 4, 560 | "id": 0x42f2 // EBMLMaxIDLength 561 | }, 562 | { 563 | "data": 8, 564 | "id": 0x42f3 // EBMLMaxSizeLength 565 | }, 566 | { 567 | "data": "webm", 568 | "id": 0x4282 // DocType 569 | }, 570 | { 571 | "data": 2, 572 | "id": 0x4287 // DocTypeVersion 573 | }, 574 | { 575 | "data": 2, 576 | "id": 0x4285 // DocTypeReadVersion 577 | } 578 | ] 579 | }, 580 | { 581 | "id": 0x18538067, // Segment 582 | "data": [ 583 | { 584 | "id": 0x1549a966, // Info 585 | "data": [ 586 | { 587 | "data": 1e6, //do things in millisecs (num of nanosecs for duration scale) 588 | "id": 0x2ad7b1 // TimecodeScale 589 | }, 590 | { 591 | "data": "whammy", 592 | "id": 0x4d80 // MuxingApp 593 | }, 594 | { 595 | "data": "whammy", 596 | "id": 0x5741 // WritingApp 597 | }, 598 | { 599 | "data": doubleToString(info.duration), 600 | "id": 0x4489 // Duration 601 | } 602 | ] 603 | }, 604 | { 605 | "id": 0x1654ae6b, // Tracks 606 | "data": [ 607 | { 608 | "id": 0xae, // TrackEntry 609 | "data": [ 610 | { 611 | "data": 1, 612 | "id": 0xd7 // TrackNumber 613 | }, 614 | { 615 | "data": 1, 616 | "id": 0x63c5 // TrackUID 617 | }, 618 | { 619 | "data": 0, 620 | "id": 0x9c // FlagLacing 621 | }, 622 | { 623 | "data": "und", 624 | "id": 0x22b59c // Language 625 | }, 626 | { 627 | "data": "V_VP8", 628 | "id": 0x86 // CodecID 629 | }, 630 | { 631 | "data": "VP8", 632 | "id": 0x258688 // CodecName 633 | }, 634 | { 635 | "data": 1, 636 | "id": 0x83 // TrackType 637 | }, 638 | { 639 | "id": 0xe0, // Video 640 | "data": [ 641 | { 642 | "data": info.width, 643 | "id": 0xb0 // PixelWidth 644 | }, 645 | { 646 | "data": info.height, 647 | "id": 0xba // PixelHeight 648 | } 649 | ] 650 | } 651 | ] 652 | } 653 | ] 654 | }, 655 | { 656 | "id": 0x1f43b675, // Cluster 657 | "data": [ 658 | { 659 | "data": 0, 660 | "id": 0xe7 // Timecode 661 | } 662 | ].concat(frames.map(function(webp) { 663 | var block = makeSimpleBlock({ 664 | discardable: 0, 665 | frame: webp.data.slice(4), 666 | invisible: 0, 667 | keyframe: 1, 668 | lacing: 0, 669 | trackNum: 1, 670 | timecode: Math.round(counter) 671 | }); 672 | counter += webp.duration; 673 | return { 674 | data: block, 675 | id: 0xa3 676 | }; 677 | })) 678 | } 679 | ] 680 | } 681 | ]; 682 | return generateEBML(EBML); 683 | } 684 | 685 | // sums the lengths of all the frames and gets the duration, woo 686 | 687 | function checkFrames(frames) { 688 | var width = frames[0].width, 689 | height = frames[0].height, 690 | duration = frames[0].duration; 691 | for (var i = 1; i < frames.length; i++) { 692 | if (frames[i].width != width) throw "Frame " + (i + 1) + " has a different width"; 693 | if (frames[i].height != height) throw "Frame " + (i + 1) + " has a different height"; 694 | if (frames[i].duration < 0) throw "Frame " + (i + 1) + " has a weird duration"; 695 | duration += frames[i].duration; 696 | } 697 | return { 698 | duration: duration, 699 | width: width, 700 | height: height 701 | }; 702 | } 703 | 704 | 705 | function numToBuffer(num) { 706 | var parts = []; 707 | while (num > 0) { 708 | parts.push(num & 0xff); 709 | num = num >> 8; 710 | } 711 | return new Uint8Array(parts.reverse()); 712 | } 713 | 714 | function strToBuffer(str) { 715 | // return new Blob([str]); 716 | 717 | var arr = new Uint8Array(str.length); 718 | for (var i = 0; i < str.length; i++) { 719 | arr[i] = str.charCodeAt(i); 720 | } 721 | return arr; 722 | 723 | // this is slower 724 | 725 | /* 726 | return new Uint8Array(str.split('').map(function(e){ 727 | return e.charCodeAt(0) 728 | })) 729 | */ 730 | } 731 | 732 | 733 | // sorry this is ugly, and sort of hard to understand exactly why this was done 734 | // at all really, but the reason is that there's some code below that i dont really 735 | // feel like understanding, and this is easier than using my brain. 736 | 737 | function bitsToBuffer(bits) { 738 | var data = []; 739 | var pad = (bits.length % 8) ? (new Array(1 + 8 - (bits.length % 8))).join('0') : ''; 740 | bits = pad + bits; 741 | for (var i = 0; i < bits.length; i += 8) { 742 | data.push(parseInt(bits.substr(i, 8), 2)); 743 | } 744 | return new Uint8Array(data); 745 | } 746 | 747 | function generateEBML(json) { 748 | var ebml = []; 749 | for (var i = 0; i < json.length; i++) { 750 | var data = json[i].data; 751 | 752 | // console.log(data); 753 | 754 | if (typeof data == 'object') data = generateEBML(data); 755 | if (typeof data == 'number') data = bitsToBuffer(data.toString(2)); 756 | if (typeof data == 'string') data = strToBuffer(data); 757 | 758 | // console.log(data) 759 | 760 | var len = data.size || data.byteLength; 761 | var zeroes = Math.ceil(Math.ceil(Math.log(len) / Math.log(2)) / 8); 762 | var size_str = len.toString(2); 763 | var padded = (new Array((zeroes * 7 + 7 + 1) - size_str.length)).join('0') + size_str; 764 | var size = (new Array(zeroes)).join('0') + '1' + padded; 765 | 766 | // i actually dont quite understand what went on up there, so I'm not really 767 | // going to fix this, i'm probably just going to write some hacky thing which 768 | // converts that string into a buffer-esque thing 769 | 770 | ebml.push(numToBuffer(json[i].id)); 771 | ebml.push(bitsToBuffer(size)); 772 | ebml.push(data); 773 | } 774 | return new Blob(ebml, { 775 | type: "video/webm" 776 | }); 777 | } 778 | 779 | // OKAY, so the following two functions are the string-based old stuff, the reason they're 780 | // still sort of in here, is that they're actually faster than the new blob stuff because 781 | // getAsFile isn't widely implemented, or at least, it doesn't work in chrome, which is the 782 | // only browser which supports get as webp 783 | 784 | // Converting between a string of 0010101001's and binary back and forth is probably inefficient 785 | // TODO: get rid of this function 786 | 787 | function toBinStr_old(bits) { 788 | var data = ''; 789 | var pad = (bits.length % 8) ? (new Array(1 + 8 - (bits.length % 8))).join('0') : ''; 790 | bits = pad + bits; 791 | for (var i = 0; i < bits.length; i += 8) { 792 | data += String.fromCharCode(parseInt(bits.substr(i, 8), 2)); 793 | } 794 | return data; 795 | } 796 | 797 | function generateEBML_old(json) { 798 | var ebml = ''; 799 | for (var i = 0; i < json.length; i++) { 800 | var data = json[i].data; 801 | if (typeof data == 'object') data = generateEBML_old(data); 802 | if (typeof data == 'number') data = toBinStr_old(data.toString(2)); 803 | 804 | var len = data.length; 805 | var zeroes = Math.ceil(Math.ceil(Math.log(len) / Math.log(2)) / 8); 806 | var size_str = len.toString(2); 807 | var padded = (new Array((zeroes * 7 + 7 + 1) - size_str.length)).join('0') + size_str; 808 | var size = (new Array(zeroes)).join('0') + '1' + padded; 809 | 810 | ebml += toBinStr_old(json[i].id.toString(2)) + toBinStr_old(size) + data; 811 | 812 | } 813 | return ebml; 814 | } 815 | 816 | // woot, a function that's actually written for this project! 817 | // this parses some json markup and makes it into that binary magic 818 | // which can then get shoved into the matroska comtainer (peaceably) 819 | 820 | function makeSimpleBlock(data) { 821 | var flags = 0; 822 | if (data.keyframe) flags |= 128; 823 | if (data.invisible) flags |= 8; 824 | if (data.lacing) flags |= (data.lacing << 1); 825 | if (data.discardable) flags |= 1; 826 | if (data.trackNum > 127) { 827 | throw "TrackNumber > 127 not supported"; 828 | } 829 | var out = [data.trackNum | 0x80, data.timecode >> 8, data.timecode & 0xff, flags].map(function(e) { 830 | return String.fromCharCode(e); 831 | }).join('') + data.frame; 832 | 833 | return out; 834 | } 835 | 836 | // here's something else taken verbatim from weppy, awesome rite? 837 | 838 | function parseWebP(riff) { 839 | var VP8 = riff.RIFF[0].WEBP[0]; 840 | 841 | var frame_start = VP8.indexOf('\x9d\x01\x2a'); // A VP8 keyframe starts with the 0x9d012a header 842 | for (var i = 0, c = []; i < 4; i++) c[i] = VP8.charCodeAt(frame_start + 3 + i); 843 | 844 | var width, horizontal_scale, height, vertical_scale, tmp; 845 | 846 | //the code below is literally copied verbatim from the bitstream spec 847 | tmp = (c[1] << 8) | c[0]; 848 | width = tmp & 0x3FFF; 849 | horizontal_scale = tmp >> 14; 850 | tmp = (c[3] << 8) | c[2]; 851 | height = tmp & 0x3FFF; 852 | vertical_scale = tmp >> 14; 853 | return { 854 | width: width, 855 | height: height, 856 | data: VP8, 857 | riff: riff 858 | }; 859 | } 860 | 861 | // i think i'm going off on a riff by pretending this is some known 862 | // idiom which i'm making a casual and brilliant pun about, but since 863 | // i can't find anything on google which conforms to this idiomatic 864 | // usage, I'm assuming this is just a consequence of some psychotic 865 | // break which makes me make up puns. well, enough riff-raff (aha a 866 | // rescue of sorts), this function was ripped wholesale from weppy 867 | 868 | function parseRIFF(string) { 869 | var offset = 0; 870 | var chunks = { }; 871 | 872 | while (offset < string.length) { 873 | var id = string.substr(offset, 4); 874 | var len = parseInt(string.substr(offset + 4, 4).split('').map(function(i) { 875 | var unpadded = i.charCodeAt(0).toString(2); 876 | return (new Array(8 - unpadded.length + 1)).join('0') + unpadded; 877 | }).join(''), 2); 878 | var data = string.substr(offset + 4 + 4, len); 879 | offset += 4 + 4 + len; 880 | chunks[id] = chunks[id] || []; 881 | 882 | if (id == 'RIFF' || id == 'LIST') { 883 | chunks[id].push(parseRIFF(data)); 884 | } else { 885 | chunks[id].push(data); 886 | } 887 | } 888 | return chunks; 889 | } 890 | 891 | // here's a little utility function that acts as a utility for other functions 892 | // basically, the only purpose is for encoding "Duration", which is encoded as 893 | // a double (considerably more difficult to encode than an integer) 894 | 895 | function doubleToString(num) { 896 | return [].slice.call( 897 | new Uint8Array( 898 | ( 899 | new Float64Array([num]) // create a float64 array 900 | // extract the array buffer 901 | ).buffer), 0) // convert the Uint8Array into a regular array 902 | .map(function(e) { // since it's a regular array, we can now use map 903 | return String.fromCharCode(e); // encode all the bytes individually 904 | }) 905 | .reverse() // correct the byte endianness (assume it's little endian for now) 906 | .join(''); // join the bytes in holy matrimony as a string 907 | } 908 | 909 | function WhammyVideo(speed, quality) { // a more abstract-ish API 910 | this.frames = []; 911 | this.duration = 1000 / speed; 912 | this.quality = quality || 0.8; 913 | } 914 | 915 | WhammyVideo.prototype.add = function(frame, duration) { 916 | if (typeof duration != 'undefined' && this.duration) throw "you can't pass a duration if the fps is set"; 917 | if ('canvas' in frame) { //CanvasRenderingContext2D 918 | frame = frame.canvas; 919 | } 920 | if ('toDataURL' in frame) { 921 | frame = frame.toDataURL('image/webp', this.quality); 922 | } else if (typeof frame != "string") { 923 | throw "frame must be a a HTMLCanvasElement, a CanvasRenderingContext2D or a DataURI formatted string"; 924 | } 925 | if (!( /^data:image\/webp;base64,/ig ).test(frame)) { 926 | throw "Input must be formatted properly as a base64 encoded DataURI of type image/webp"; 927 | } 928 | this.frames.push({ 929 | image: frame, 930 | duration: duration || this.duration 931 | }); 932 | }; 933 | WhammyVideo.prototype.compile = function() { 934 | return new toWebM(this.frames.map(function(frame) { 935 | var webp = parseWebP(parseRIFF(atob(frame.image.slice(23)))); 936 | webp.duration = frame.duration; 937 | return webp; 938 | })); 939 | }; 940 | return { 941 | Video: WhammyVideo, 942 | fromImageArray: function(images, fps) { 943 | return toWebM(images.map(function(image) { 944 | var webp = parseWebP(parseRIFF(atob(image.slice(23)))); 945 | webp.duration = 1000 / fps; 946 | return webp; 947 | })); 948 | }, 949 | toWebM: toWebM 950 | // expose methods of madness 951 | }; 952 | })(); 953 | 954 | // gifEncoder 955 | 956 | function encode64(n){for(var o="",f=0,l=n.length,u="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",s,i,r,c,h,e,t;f>2,h=(s&3)<<4|i>>4,e=(i&15)<<2|r>>6,t=r&63,isNaN(i)?e=t=64:isNaN(r)&&(t=64),o=o+u.charAt(c)+u.charAt(h)+u.charAt(e)+u.charAt(t);return o}LZWEncoder=function(){var c={},it=-1,st,ht,rt,l,w,et,ut=12,ct=5003,t,ft=ut,o,ot=1<=254&&k(t)},at=function(n){tt(a),s=f+2,h=!0,e(f,n)},tt=function(n){for(var t=0;t=0){rt=g-c,c==0&&(rt=1);do if((c-=rt)<0&&(c+=g),u[c]==w){l=y[c];continue n}while(u[c]>=0)}e(l,i),l=nt,s0&&(n.writeByte(r),n.writeBytes(g,0,r),r=0)},b=function(n){return(1<0?i|=r<=8;)nt(i&255,u),i>>=8,n-=8;if((s>o||h)&&(h?(o=b(t=v),h=!1):(++t,o=t==ft?ot:b(t))),r==p){while(n>0)nt(i&255,u),i>>=8,n-=8;k(u)}};return lt.apply(this,arguments),c},NeuQuant=function(){var c={},t=256,tt=499,nt=491,rt=487,it=503,g=3*it,b=t-1,r=4,pt=100,ft=16,y=1<>a,dt=y<>3,l=6,ti=1<>1,i=o+1;i>1,i=o+1;i<256;i++)f[i]=b},vt=function(){var t,u,k,b,p,c,n,s,o,y,ut,a,f,ft;for(i>l,n<=1&&(n=0),t=0;t=ft&&(f-=i),t++,y==0&&(y=1),t%y==0)for(s-=s/et,c-=c/kt,n=c>>l,n<=1&&(n=0),u=0;u=0;)c=h?c=t:(c++,e<0&&(e=-e),o=s[0]-i,o<0&&(o=-o),e+=o,e=0&&(s=n[l],e=r-s[1],e>=h?l=-1:(l--,e<0&&(e=-e),o=s[0]-i,o<0&&(o=-o),e+=o,e>=r,n[i][1]>>=r,n[i][2]>>=r,n[i][3]=i},lt=function(i,r,f,e,o){var a,y,l,c,h,p,s;for(l=r-i,l<-1&&(l=-1),c=r+i,c>t&&(c=t),a=r+1,y=r-1,p=1;al;){if(h=v[p++],al){s=n[y--];try{s[0]-=h*(s[0]-f)/u,s[1]-=h*(s[1]-e)/u,s[2]-=h*(s[2]-o)/u}catch(w){}}}},at=function(t,i,r,u,f){var o=n[i];o[0]-=t*(o[0]-r)/e,o[1]-=t*(o[1]-u)/e,o[2]-=t*(o[2]-f)/e},yt=function(i,u,f){var h,c,e,b,d,l,k,v,w,y;for(v=2147483647,w=v,l=-1,k=l,h=0;h>ft-r),b>a,s[h]-=d,o[h]+=d<=0&&(y=n)},dt=t.setRepeat=function(n){n>=0&&(k=n)},bt=t.setTransparent=function(n){v=n},kt=t.addFrame=function(t,i){if(t==null||!f||n==null){throw new Error("Please call start method before calling addFrame");return!1}var r=!0;try{i?a=t:(a=t.getImageData(0,0,t.canvas.width,t.canvas.height).data,ft||et(t.canvas.width,t.canvas.height)),ct(),ht(),e&&(vt(),tt(),k>=0&<()),st(),ot(),e||tt(),at(),e=!1}catch(u){r=!1}return r},ui=t.finish=function(){if(!f)return!1;var t=!0;f=!1;try{n.writeByte(59)}catch(i){t=!1}return t},nt=function(){g=0,a=null,i=null,l=null,r=null,b=!1,e=!0},fi=t.setFrameRate=function(n){n!=15&&(d=Math.round(100/n))},ri=t.setQuality=function(n){n<1&&(n=1),it=n},et=t.setSize=function et(n,t){(!f||e)&&(o=n,s=t,o<1&&(o=320),s<1&&(s=240),ft=!0)},ti=t.start=function(){nt();var t=!0;b=!1,n=new h;try{n.writeUTFBytes("GIF89a")}catch(i){t=!1}return f=t},ii=t.cont=function(){nt();var t=!0;return b=!1,n=new h,f=t},ht=function(){var e=i.length,o=e/3,f,n,t,u;for(l=[],f=new NeuQuant(i,e,it),r=f.process(),n=0,t=0;t>16,v=(n&65280)>>8,a=n&255,s=0,h=16777216,l=r.length;for(t=0;t=0&&(t=y&7),t<<=2,n.writeByte(0|t|0|i),u(d),n.writeByte(g),n.writeByte(0)},ot=function(){n.writeByte(44),u(0),u(0),u(o),u(s),e?n.writeByte(0):n.writeByte(128|p)},vt=function(){u(o),u(s),n.writeByte(240|p),n.writeByte(0),n.writeByte(0)},lt=function(){n.writeByte(33),n.writeByte(255),n.writeByte(11),n.writeUTFBytes("NETSCAPE2.0"),n.writeByte(3),n.writeByte(1),u(k),n.writeByte(0)},tt=function(){var i,t;for(n.writeBytes(r),i=768-r.length,t=0;t>8&255)},at=function(){var t=new LZWEncoder(o,s,l,rt);t.encode(n)},wt=t.stream=function(){return n},pt=t.setProperties=function(n,t){f=n,e=t};return t} -------------------------------------------------------------------------------- /public/app.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function(){ 2 | // config 3 | var videoWidth = 640; 4 | var videoHeight = 480; 5 | 6 | // setup 7 | var stream; 8 | var audio_recorder = null; 9 | var video_recorder = null; 10 | var recording = false; 11 | var playing = false; 12 | var formData = null; 13 | 14 | // set the video options 15 | var videoOptions = { 16 | type: "video", 17 | video: { 18 | width: videoWidth, 19 | height: videoHeight 20 | }, 21 | canvas: { 22 | width: videoWidth, 23 | height: videoHeight 24 | } 25 | }; 26 | 27 | // record the video and audio 28 | navigator.getUserMedia({audio: true, video: { mandatory: {}, optional: []}}, function(pStream) { 29 | 30 | stream = pStream; 31 | // setup video 32 | video = $("video.recorder")[0]; 33 | 34 | video.src = window.URL.createObjectURL(stream); 35 | video.width = videoWidth; 36 | video.height = videoHeight; 37 | 38 | // init recorders 39 | audio_recorder = RecordRTC(stream, { type: "audio", bufferSize: 16384 }); 40 | video_recorder = RecordRTC(stream, videoOptions); 41 | 42 | // update UI 43 | $("#record_button").show(); 44 | }, function(){}); 45 | 46 | // start recording 47 | var startRecording = function() { 48 | // record the audio and video 49 | video_recorder.startRecording(); 50 | audio_recorder.startRecording(); 51 | 52 | // update the UI 53 | $("#play_button").hide(); 54 | $("#upload_button").hide(); 55 | $("video.recorder").show(); 56 | $("#video-player").remove(); 57 | $("#audio-player").remove(); 58 | $("#record_button").text("Stop recording"); 59 | 60 | // toggle boolean 61 | recording = true; 62 | } 63 | 64 | // stop recording 65 | var stopRecording = function() { 66 | // stop recorders 67 | audio_recorder.stopRecording(); 68 | video_recorder.stopRecording(); 69 | 70 | // set form data 71 | formData = new FormData(); 72 | 73 | var audio_blob = audio_recorder.getBlob(); 74 | formData.append("audio", audio_blob); 75 | 76 | var video_blob = video_recorder.getBlob(); 77 | formData.append("video", video_blob); 78 | 79 | // add players 80 | var audio_player = document.createElement("audio"); 81 | audio_player.id = "audio-player"; 82 | audio_player.src = URL.createObjectURL(audio_blob); 83 | $("#players").append(audio_player); 84 | 85 | var video_payer = document.createElement("video"); 86 | video_payer.id = "video-player"; 87 | video_payer.src = URL.createObjectURL(video_blob); 88 | $("#players").append(video_payer); 89 | 90 | // update UI 91 | $("video.recorder").hide(); 92 | $("#play_button").show(); 93 | $("#upload_button").show(); 94 | $("#record_button").text("Start recording"); 95 | 96 | // toggle boolean 97 | recording = false; 98 | } 99 | 100 | // handle recording 101 | $("#record_button").click(function(){ 102 | if (recording) { 103 | stopRecording(); 104 | } else { 105 | startRecording(); 106 | } 107 | }); 108 | 109 | // stop playback 110 | var stopPlayback = function() { 111 | // controlling 112 | video = $("#video-player")[0]; 113 | video.pause(); 114 | video.currentTime = 0; 115 | audio = $("#audio-player")[0]; 116 | audio.pause(); 117 | audio.currentTime = 0; 118 | 119 | // update ui 120 | $("#play_button").text("Play"); 121 | 122 | // toggle boolean 123 | playing = false; 124 | } 125 | 126 | // start playback 127 | var startPlayback = function() { 128 | // video controlling 129 | video = $("#video-player")[0]; 130 | video.play(); 131 | audio = $("#audio-player")[0]; 132 | audio.play(); 133 | $("#video-player").bind("ended", stopPlayback); 134 | 135 | // Update UI 136 | $("#play_button").text("Stop"); 137 | 138 | // toggle boolean 139 | playing = true; 140 | } 141 | 142 | // handle playback 143 | $("#play_button").click(function(){ 144 | if (playing) { 145 | stopPlayback(); 146 | } else { 147 | startPlayback(); 148 | } 149 | }); 150 | 151 | // Upload button 152 | $("#upload_button").click(function(){ 153 | var request = new XMLHttpRequest(); 154 | 155 | request.onreadystatechange = function () { 156 | if (request.readyState == 4 && request.status == 200) { 157 | window.location.href = "/video/"+request.responseText; 158 | } 159 | }; 160 | 161 | request.open('POST', "/upload"); 162 | request.send(formData); 163 | }); 164 | }); -------------------------------------------------------------------------------- /public/jquery-2.0.3.min.js: -------------------------------------------------------------------------------- 1 | /*! jQuery v2.0.3 | (c) 2005, 2013 jQuery Foundation, Inc. | jquery.org/license 2 | //@ sourceMappingURL=jquery-2.0.3.min.map 3 | */ 4 | (function(e,undefined){var t,n,r=typeof undefined,i=e.location,o=e.document,s=o.documentElement,a=e.jQuery,u=e.$,l={},c=[],p="2.0.3",f=c.concat,h=c.push,d=c.slice,g=c.indexOf,m=l.toString,y=l.hasOwnProperty,v=p.trim,x=function(e,n){return new x.fn.init(e,n,t)},b=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,w=/\S+/g,T=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,C=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,k=/^-ms-/,N=/-([\da-z])/gi,E=function(e,t){return t.toUpperCase()},S=function(){o.removeEventListener("DOMContentLoaded",S,!1),e.removeEventListener("load",S,!1),x.ready()};x.fn=x.prototype={jquery:p,constructor:x,init:function(e,t,n){var r,i;if(!e)return this;if("string"==typeof e){if(r="<"===e.charAt(0)&&">"===e.charAt(e.length-1)&&e.length>=3?[null,e,null]:T.exec(e),!r||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof x?t[0]:t,x.merge(this,x.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:o,!0)),C.test(r[1])&&x.isPlainObject(t))for(r in t)x.isFunction(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return i=o.getElementById(r[2]),i&&i.parentNode&&(this.length=1,this[0]=i),this.context=o,this.selector=e,this}return e.nodeType?(this.context=this[0]=e,this.length=1,this):x.isFunction(e)?n.ready(e):(e.selector!==undefined&&(this.selector=e.selector,this.context=e.context),x.makeArray(e,this))},selector:"",length:0,toArray:function(){return d.call(this)},get:function(e){return null==e?this.toArray():0>e?this[this.length+e]:this[e]},pushStack:function(e){var t=x.merge(this.constructor(),e);return t.prevObject=this,t.context=this.context,t},each:function(e,t){return x.each(this,e,t)},ready:function(e){return x.ready.promise().done(e),this},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(0>e?t:0);return this.pushStack(n>=0&&t>n?[this[n]]:[])},map:function(e){return this.pushStack(x.map(this,function(t,n){return e.call(t,n,t)}))},end:function(){return this.prevObject||this.constructor(null)},push:h,sort:[].sort,splice:[].splice},x.fn.init.prototype=x.fn,x.extend=x.fn.extend=function(){var e,t,n,r,i,o,s=arguments[0]||{},a=1,u=arguments.length,l=!1;for("boolean"==typeof s&&(l=s,s=arguments[1]||{},a=2),"object"==typeof s||x.isFunction(s)||(s={}),u===a&&(s=this,--a);u>a;a++)if(null!=(e=arguments[a]))for(t in e)n=s[t],r=e[t],s!==r&&(l&&r&&(x.isPlainObject(r)||(i=x.isArray(r)))?(i?(i=!1,o=n&&x.isArray(n)?n:[]):o=n&&x.isPlainObject(n)?n:{},s[t]=x.extend(l,o,r)):r!==undefined&&(s[t]=r));return s},x.extend({expando:"jQuery"+(p+Math.random()).replace(/\D/g,""),noConflict:function(t){return e.$===x&&(e.$=u),t&&e.jQuery===x&&(e.jQuery=a),x},isReady:!1,readyWait:1,holdReady:function(e){e?x.readyWait++:x.ready(!0)},ready:function(e){(e===!0?--x.readyWait:x.isReady)||(x.isReady=!0,e!==!0&&--x.readyWait>0||(n.resolveWith(o,[x]),x.fn.trigger&&x(o).trigger("ready").off("ready")))},isFunction:function(e){return"function"===x.type(e)},isArray:Array.isArray,isWindow:function(e){return null!=e&&e===e.window},isNumeric:function(e){return!isNaN(parseFloat(e))&&isFinite(e)},type:function(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?l[m.call(e)]||"object":typeof e},isPlainObject:function(e){if("object"!==x.type(e)||e.nodeType||x.isWindow(e))return!1;try{if(e.constructor&&!y.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(t){return!1}return!0},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},error:function(e){throw Error(e)},parseHTML:function(e,t,n){if(!e||"string"!=typeof e)return null;"boolean"==typeof t&&(n=t,t=!1),t=t||o;var r=C.exec(e),i=!n&&[];return r?[t.createElement(r[1])]:(r=x.buildFragment([e],t,i),i&&x(i).remove(),x.merge([],r.childNodes))},parseJSON:JSON.parse,parseXML:function(e){var t,n;if(!e||"string"!=typeof e)return null;try{n=new DOMParser,t=n.parseFromString(e,"text/xml")}catch(r){t=undefined}return(!t||t.getElementsByTagName("parsererror").length)&&x.error("Invalid XML: "+e),t},noop:function(){},globalEval:function(e){var t,n=eval;e=x.trim(e),e&&(1===e.indexOf("use strict")?(t=o.createElement("script"),t.text=e,o.head.appendChild(t).parentNode.removeChild(t)):n(e))},camelCase:function(e){return e.replace(k,"ms-").replace(N,E)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,t,n){var r,i=0,o=e.length,s=j(e);if(n){if(s){for(;o>i;i++)if(r=t.apply(e[i],n),r===!1)break}else for(i in e)if(r=t.apply(e[i],n),r===!1)break}else if(s){for(;o>i;i++)if(r=t.call(e[i],i,e[i]),r===!1)break}else for(i in e)if(r=t.call(e[i],i,e[i]),r===!1)break;return e},trim:function(e){return null==e?"":v.call(e)},makeArray:function(e,t){var n=t||[];return null!=e&&(j(Object(e))?x.merge(n,"string"==typeof e?[e]:e):h.call(n,e)),n},inArray:function(e,t,n){return null==t?-1:g.call(t,e,n)},merge:function(e,t){var n=t.length,r=e.length,i=0;if("number"==typeof n)for(;n>i;i++)e[r++]=t[i];else while(t[i]!==undefined)e[r++]=t[i++];return e.length=r,e},grep:function(e,t,n){var r,i=[],o=0,s=e.length;for(n=!!n;s>o;o++)r=!!t(e[o],o),n!==r&&i.push(e[o]);return i},map:function(e,t,n){var r,i=0,o=e.length,s=j(e),a=[];if(s)for(;o>i;i++)r=t(e[i],i,n),null!=r&&(a[a.length]=r);else for(i in e)r=t(e[i],i,n),null!=r&&(a[a.length]=r);return f.apply([],a)},guid:1,proxy:function(e,t){var n,r,i;return"string"==typeof t&&(n=e[t],t=e,e=n),x.isFunction(e)?(r=d.call(arguments,2),i=function(){return e.apply(t||this,r.concat(d.call(arguments)))},i.guid=e.guid=e.guid||x.guid++,i):undefined},access:function(e,t,n,r,i,o,s){var a=0,u=e.length,l=null==n;if("object"===x.type(n)){i=!0;for(a in n)x.access(e,t,a,n[a],!0,o,s)}else if(r!==undefined&&(i=!0,x.isFunction(r)||(s=!0),l&&(s?(t.call(e,r),t=null):(l=t,t=function(e,t,n){return l.call(x(e),n)})),t))for(;u>a;a++)t(e[a],n,s?r:r.call(e[a],a,t(e[a],n)));return i?e:l?t.call(e):u?t(e[0],n):o},now:Date.now,swap:function(e,t,n,r){var i,o,s={};for(o in t)s[o]=e.style[o],e.style[o]=t[o];i=n.apply(e,r||[]);for(o in t)e.style[o]=s[o];return i}}),x.ready.promise=function(t){return n||(n=x.Deferred(),"complete"===o.readyState?setTimeout(x.ready):(o.addEventListener("DOMContentLoaded",S,!1),e.addEventListener("load",S,!1))),n.promise(t)},x.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(e,t){l["[object "+t+"]"]=t.toLowerCase()});function j(e){var t=e.length,n=x.type(e);return x.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===n||"function"!==n&&(0===t||"number"==typeof t&&t>0&&t-1 in e)}t=x(o),function(e,undefined){var t,n,r,i,o,s,a,u,l,c,p,f,h,d,g,m,y,v="sizzle"+-new Date,b=e.document,w=0,T=0,C=st(),k=st(),N=st(),E=!1,S=function(e,t){return e===t?(E=!0,0):0},j=typeof undefined,D=1<<31,A={}.hasOwnProperty,L=[],q=L.pop,H=L.push,O=L.push,F=L.slice,P=L.indexOf||function(e){var t=0,n=this.length;for(;n>t;t++)if(this[t]===e)return t;return-1},R="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",M="[\\x20\\t\\r\\n\\f]",W="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",$=W.replace("w","w#"),B="\\["+M+"*("+W+")"+M+"*(?:([*^$|!~]?=)"+M+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+$+")|)|)"+M+"*\\]",I=":("+W+")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|"+B.replace(3,8)+")*)|.*)\\)|)",z=RegExp("^"+M+"+|((?:^|[^\\\\])(?:\\\\.)*)"+M+"+$","g"),_=RegExp("^"+M+"*,"+M+"*"),X=RegExp("^"+M+"*([>+~]|"+M+")"+M+"*"),U=RegExp(M+"*[+~]"),Y=RegExp("="+M+"*([^\\]'\"]*)"+M+"*\\]","g"),V=RegExp(I),G=RegExp("^"+$+"$"),J={ID:RegExp("^#("+W+")"),CLASS:RegExp("^\\.("+W+")"),TAG:RegExp("^("+W.replace("w","w*")+")"),ATTR:RegExp("^"+B),PSEUDO:RegExp("^"+I),CHILD:RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:RegExp("^(?:"+R+")$","i"),needsContext:RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Q=/^[^{]+\{\s*\[native \w/,K=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,Z=/^(?:input|select|textarea|button)$/i,et=/^h\d$/i,tt=/'|\\/g,nt=RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),rt=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:0>r?String.fromCharCode(r+65536):String.fromCharCode(55296|r>>10,56320|1023&r)};try{O.apply(L=F.call(b.childNodes),b.childNodes),L[b.childNodes.length].nodeType}catch(it){O={apply:L.length?function(e,t){H.apply(e,F.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function ot(e,t,r,i){var o,s,a,u,l,f,g,m,x,w;if((t?t.ownerDocument||t:b)!==p&&c(t),t=t||p,r=r||[],!e||"string"!=typeof e)return r;if(1!==(u=t.nodeType)&&9!==u)return[];if(h&&!i){if(o=K.exec(e))if(a=o[1]){if(9===u){if(s=t.getElementById(a),!s||!s.parentNode)return r;if(s.id===a)return r.push(s),r}else if(t.ownerDocument&&(s=t.ownerDocument.getElementById(a))&&y(t,s)&&s.id===a)return r.push(s),r}else{if(o[2])return O.apply(r,t.getElementsByTagName(e)),r;if((a=o[3])&&n.getElementsByClassName&&t.getElementsByClassName)return O.apply(r,t.getElementsByClassName(a)),r}if(n.qsa&&(!d||!d.test(e))){if(m=g=v,x=t,w=9===u&&e,1===u&&"object"!==t.nodeName.toLowerCase()){f=gt(e),(g=t.getAttribute("id"))?m=g.replace(tt,"\\$&"):t.setAttribute("id",m),m="[id='"+m+"'] ",l=f.length;while(l--)f[l]=m+mt(f[l]);x=U.test(e)&&t.parentNode||t,w=f.join(",")}if(w)try{return O.apply(r,x.querySelectorAll(w)),r}catch(T){}finally{g||t.removeAttribute("id")}}}return kt(e.replace(z,"$1"),t,r,i)}function st(){var e=[];function t(n,r){return e.push(n+=" ")>i.cacheLength&&delete t[e.shift()],t[n]=r}return t}function at(e){return e[v]=!0,e}function ut(e){var t=p.createElement("div");try{return!!e(t)}catch(n){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function lt(e,t){var n=e.split("|"),r=e.length;while(r--)i.attrHandle[n[r]]=t}function ct(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&(~t.sourceIndex||D)-(~e.sourceIndex||D);if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function pt(e){return function(t){var n=t.nodeName.toLowerCase();return"input"===n&&t.type===e}}function ft(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function ht(e){return at(function(t){return t=+t,at(function(n,r){var i,o=e([],n.length,t),s=o.length;while(s--)n[i=o[s]]&&(n[i]=!(r[i]=n[i]))})})}s=ot.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return t?"HTML"!==t.nodeName:!1},n=ot.support={},c=ot.setDocument=function(e){var t=e?e.ownerDocument||e:b,r=t.defaultView;return t!==p&&9===t.nodeType&&t.documentElement?(p=t,f=t.documentElement,h=!s(t),r&&r.attachEvent&&r!==r.top&&r.attachEvent("onbeforeunload",function(){c()}),n.attributes=ut(function(e){return e.className="i",!e.getAttribute("className")}),n.getElementsByTagName=ut(function(e){return e.appendChild(t.createComment("")),!e.getElementsByTagName("*").length}),n.getElementsByClassName=ut(function(e){return e.innerHTML="
",e.firstChild.className="i",2===e.getElementsByClassName("i").length}),n.getById=ut(function(e){return f.appendChild(e).id=v,!t.getElementsByName||!t.getElementsByName(v).length}),n.getById?(i.find.ID=function(e,t){if(typeof t.getElementById!==j&&h){var n=t.getElementById(e);return n&&n.parentNode?[n]:[]}},i.filter.ID=function(e){var t=e.replace(nt,rt);return function(e){return e.getAttribute("id")===t}}):(delete i.find.ID,i.filter.ID=function(e){var t=e.replace(nt,rt);return function(e){var n=typeof e.getAttributeNode!==j&&e.getAttributeNode("id");return n&&n.value===t}}),i.find.TAG=n.getElementsByTagName?function(e,t){return typeof t.getElementsByTagName!==j?t.getElementsByTagName(e):undefined}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},i.find.CLASS=n.getElementsByClassName&&function(e,t){return typeof t.getElementsByClassName!==j&&h?t.getElementsByClassName(e):undefined},g=[],d=[],(n.qsa=Q.test(t.querySelectorAll))&&(ut(function(e){e.innerHTML="",e.querySelectorAll("[selected]").length||d.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll(":checked").length||d.push(":checked")}),ut(function(e){var n=t.createElement("input");n.setAttribute("type","hidden"),e.appendChild(n).setAttribute("t",""),e.querySelectorAll("[t^='']").length&&d.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll(":enabled").length||d.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),d.push(",.*:")})),(n.matchesSelector=Q.test(m=f.webkitMatchesSelector||f.mozMatchesSelector||f.oMatchesSelector||f.msMatchesSelector))&&ut(function(e){n.disconnectedMatch=m.call(e,"div"),m.call(e,"[s!='']:x"),g.push("!=",I)}),d=d.length&&RegExp(d.join("|")),g=g.length&&RegExp(g.join("|")),y=Q.test(f.contains)||f.compareDocumentPosition?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},S=f.compareDocumentPosition?function(e,r){if(e===r)return E=!0,0;var i=r.compareDocumentPosition&&e.compareDocumentPosition&&e.compareDocumentPosition(r);return i?1&i||!n.sortDetached&&r.compareDocumentPosition(e)===i?e===t||y(b,e)?-1:r===t||y(b,r)?1:l?P.call(l,e)-P.call(l,r):0:4&i?-1:1:e.compareDocumentPosition?-1:1}:function(e,n){var r,i=0,o=e.parentNode,s=n.parentNode,a=[e],u=[n];if(e===n)return E=!0,0;if(!o||!s)return e===t?-1:n===t?1:o?-1:s?1:l?P.call(l,e)-P.call(l,n):0;if(o===s)return ct(e,n);r=e;while(r=r.parentNode)a.unshift(r);r=n;while(r=r.parentNode)u.unshift(r);while(a[i]===u[i])i++;return i?ct(a[i],u[i]):a[i]===b?-1:u[i]===b?1:0},t):p},ot.matches=function(e,t){return ot(e,null,null,t)},ot.matchesSelector=function(e,t){if((e.ownerDocument||e)!==p&&c(e),t=t.replace(Y,"='$1']"),!(!n.matchesSelector||!h||g&&g.test(t)||d&&d.test(t)))try{var r=m.call(e,t);if(r||n.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(i){}return ot(t,p,null,[e]).length>0},ot.contains=function(e,t){return(e.ownerDocument||e)!==p&&c(e),y(e,t)},ot.attr=function(e,t){(e.ownerDocument||e)!==p&&c(e);var r=i.attrHandle[t.toLowerCase()],o=r&&A.call(i.attrHandle,t.toLowerCase())?r(e,t,!h):undefined;return o===undefined?n.attributes||!h?e.getAttribute(t):(o=e.getAttributeNode(t))&&o.specified?o.value:null:o},ot.error=function(e){throw Error("Syntax error, unrecognized expression: "+e)},ot.uniqueSort=function(e){var t,r=[],i=0,o=0;if(E=!n.detectDuplicates,l=!n.sortStable&&e.slice(0),e.sort(S),E){while(t=e[o++])t===e[o]&&(i=r.push(o));while(i--)e.splice(r[i],1)}return e},o=ot.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(1===i||9===i||11===i){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=o(e)}else if(3===i||4===i)return e.nodeValue}else for(;t=e[r];r++)n+=o(t);return n},i=ot.selectors={cacheLength:50,createPseudo:at,match:J,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(nt,rt),e[3]=(e[4]||e[5]||"").replace(nt,rt),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||ot.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&ot.error(e[0]),e},PSEUDO:function(e){var t,n=!e[5]&&e[2];return J.CHILD.test(e[0])?null:(e[3]&&e[4]!==undefined?e[2]=e[4]:n&&V.test(n)&&(t=gt(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(nt,rt).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=C[e+" "];return t||(t=RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&C(e,function(e){return t.test("string"==typeof e.className&&e.className||typeof e.getAttribute!==j&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=ot.attr(r,e);return null==i?"!="===t:t?(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i+" ").indexOf(n)>-1:"|="===t?i===n||i.slice(0,n.length+1)===n+"-":!1):!0}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),s="last"!==e.slice(-4),a="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,u){var l,c,p,f,h,d,g=o!==s?"nextSibling":"previousSibling",m=t.parentNode,y=a&&t.nodeName.toLowerCase(),x=!u&&!a;if(m){if(o){while(g){p=t;while(p=p[g])if(a?p.nodeName.toLowerCase()===y:1===p.nodeType)return!1;d=g="only"===e&&!d&&"nextSibling"}return!0}if(d=[s?m.firstChild:m.lastChild],s&&x){c=m[v]||(m[v]={}),l=c[e]||[],h=l[0]===w&&l[1],f=l[0]===w&&l[2],p=h&&m.childNodes[h];while(p=++h&&p&&p[g]||(f=h=0)||d.pop())if(1===p.nodeType&&++f&&p===t){c[e]=[w,h,f];break}}else if(x&&(l=(t[v]||(t[v]={}))[e])&&l[0]===w)f=l[1];else while(p=++h&&p&&p[g]||(f=h=0)||d.pop())if((a?p.nodeName.toLowerCase()===y:1===p.nodeType)&&++f&&(x&&((p[v]||(p[v]={}))[e]=[w,f]),p===t))break;return f-=i,f===r||0===f%r&&f/r>=0}}},PSEUDO:function(e,t){var n,r=i.pseudos[e]||i.setFilters[e.toLowerCase()]||ot.error("unsupported pseudo: "+e);return r[v]?r(t):r.length>1?(n=[e,e,"",t],i.setFilters.hasOwnProperty(e.toLowerCase())?at(function(e,n){var i,o=r(e,t),s=o.length;while(s--)i=P.call(e,o[s]),e[i]=!(n[i]=o[s])}):function(e){return r(e,0,n)}):r}},pseudos:{not:at(function(e){var t=[],n=[],r=a(e.replace(z,"$1"));return r[v]?at(function(e,t,n,i){var o,s=r(e,null,i,[]),a=e.length;while(a--)(o=s[a])&&(e[a]=!(t[a]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),!n.pop()}}),has:at(function(e){return function(t){return ot(e,t).length>0}}),contains:at(function(e){return function(t){return(t.textContent||t.innerText||o(t)).indexOf(e)>-1}}),lang:at(function(e){return G.test(e||"")||ot.error("unsupported lang: "+e),e=e.replace(nt,rt).toLowerCase(),function(t){var n;do if(n=h?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return n=n.toLowerCase(),n===e||0===n.indexOf(e+"-");while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===f},focus:function(e){return e===p.activeElement&&(!p.hasFocus||p.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:function(e){return e.disabled===!1},disabled:function(e){return e.disabled===!0},checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,e.selected===!0},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeName>"@"||3===e.nodeType||4===e.nodeType)return!1;return!0},parent:function(e){return!i.pseudos.empty(e)},header:function(e){return et.test(e.nodeName)},input:function(e){return Z.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||t.toLowerCase()===e.type)},first:ht(function(){return[0]}),last:ht(function(e,t){return[t-1]}),eq:ht(function(e,t,n){return[0>n?n+t:n]}),even:ht(function(e,t){var n=0;for(;t>n;n+=2)e.push(n);return e}),odd:ht(function(e,t){var n=1;for(;t>n;n+=2)e.push(n);return e}),lt:ht(function(e,t,n){var r=0>n?n+t:n;for(;--r>=0;)e.push(r);return e}),gt:ht(function(e,t,n){var r=0>n?n+t:n;for(;t>++r;)e.push(r);return e})}},i.pseudos.nth=i.pseudos.eq;for(t in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})i.pseudos[t]=pt(t);for(t in{submit:!0,reset:!0})i.pseudos[t]=ft(t);function dt(){}dt.prototype=i.filters=i.pseudos,i.setFilters=new dt;function gt(e,t){var n,r,o,s,a,u,l,c=k[e+" "];if(c)return t?0:c.slice(0);a=e,u=[],l=i.preFilter;while(a){(!n||(r=_.exec(a)))&&(r&&(a=a.slice(r[0].length)||a),u.push(o=[])),n=!1,(r=X.exec(a))&&(n=r.shift(),o.push({value:n,type:r[0].replace(z," ")}),a=a.slice(n.length));for(s in i.filter)!(r=J[s].exec(a))||l[s]&&!(r=l[s](r))||(n=r.shift(),o.push({value:n,type:s,matches:r}),a=a.slice(n.length));if(!n)break}return t?a.length:a?ot.error(e):k(e,u).slice(0)}function mt(e){var t=0,n=e.length,r="";for(;n>t;t++)r+=e[t].value;return r}function yt(e,t,n){var i=t.dir,o=n&&"parentNode"===i,s=T++;return t.first?function(t,n,r){while(t=t[i])if(1===t.nodeType||o)return e(t,n,r)}:function(t,n,a){var u,l,c,p=w+" "+s;if(a){while(t=t[i])if((1===t.nodeType||o)&&e(t,n,a))return!0}else while(t=t[i])if(1===t.nodeType||o)if(c=t[v]||(t[v]={}),(l=c[i])&&l[0]===p){if((u=l[1])===!0||u===r)return u===!0}else if(l=c[i]=[p],l[1]=e(t,n,a)||r,l[1]===!0)return!0}}function vt(e){return e.length>1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function xt(e,t,n,r,i){var o,s=[],a=0,u=e.length,l=null!=t;for(;u>a;a++)(o=e[a])&&(!n||n(o,r,i))&&(s.push(o),l&&t.push(a));return s}function bt(e,t,n,r,i,o){return r&&!r[v]&&(r=bt(r)),i&&!i[v]&&(i=bt(i,o)),at(function(o,s,a,u){var l,c,p,f=[],h=[],d=s.length,g=o||Ct(t||"*",a.nodeType?[a]:a,[]),m=!e||!o&&t?g:xt(g,f,e,a,u),y=n?i||(o?e:d||r)?[]:s:m;if(n&&n(m,y,a,u),r){l=xt(y,h),r(l,[],a,u),c=l.length;while(c--)(p=l[c])&&(y[h[c]]=!(m[h[c]]=p))}if(o){if(i||e){if(i){l=[],c=y.length;while(c--)(p=y[c])&&l.push(m[c]=p);i(null,y=[],l,u)}c=y.length;while(c--)(p=y[c])&&(l=i?P.call(o,p):f[c])>-1&&(o[l]=!(s[l]=p))}}else y=xt(y===s?y.splice(d,y.length):y),i?i(null,s,y,u):O.apply(s,y)})}function wt(e){var t,n,r,o=e.length,s=i.relative[e[0].type],a=s||i.relative[" "],l=s?1:0,c=yt(function(e){return e===t},a,!0),p=yt(function(e){return P.call(t,e)>-1},a,!0),f=[function(e,n,r){return!s&&(r||n!==u)||((t=n).nodeType?c(e,n,r):p(e,n,r))}];for(;o>l;l++)if(n=i.relative[e[l].type])f=[yt(vt(f),n)];else{if(n=i.filter[e[l].type].apply(null,e[l].matches),n[v]){for(r=++l;o>r;r++)if(i.relative[e[r].type])break;return bt(l>1&&vt(f),l>1&&mt(e.slice(0,l-1).concat({value:" "===e[l-2].type?"*":""})).replace(z,"$1"),n,r>l&&wt(e.slice(l,r)),o>r&&wt(e=e.slice(r)),o>r&&mt(e))}f.push(n)}return vt(f)}function Tt(e,t){var n=0,o=t.length>0,s=e.length>0,a=function(a,l,c,f,h){var d,g,m,y=[],v=0,x="0",b=a&&[],T=null!=h,C=u,k=a||s&&i.find.TAG("*",h&&l.parentNode||l),N=w+=null==C?1:Math.random()||.1;for(T&&(u=l!==p&&l,r=n);null!=(d=k[x]);x++){if(s&&d){g=0;while(m=e[g++])if(m(d,l,c)){f.push(d);break}T&&(w=N,r=++n)}o&&((d=!m&&d)&&v--,a&&b.push(d))}if(v+=x,o&&x!==v){g=0;while(m=t[g++])m(b,y,l,c);if(a){if(v>0)while(x--)b[x]||y[x]||(y[x]=q.call(f));y=xt(y)}O.apply(f,y),T&&!a&&y.length>0&&v+t.length>1&&ot.uniqueSort(f)}return T&&(w=N,u=C),b};return o?at(a):a}a=ot.compile=function(e,t){var n,r=[],i=[],o=N[e+" "];if(!o){t||(t=gt(e)),n=t.length;while(n--)o=wt(t[n]),o[v]?r.push(o):i.push(o);o=N(e,Tt(i,r))}return o};function Ct(e,t,n){var r=0,i=t.length;for(;i>r;r++)ot(e,t[r],n);return n}function kt(e,t,r,o){var s,u,l,c,p,f=gt(e);if(!o&&1===f.length){if(u=f[0]=f[0].slice(0),u.length>2&&"ID"===(l=u[0]).type&&n.getById&&9===t.nodeType&&h&&i.relative[u[1].type]){if(t=(i.find.ID(l.matches[0].replace(nt,rt),t)||[])[0],!t)return r;e=e.slice(u.shift().value.length)}s=J.needsContext.test(e)?0:u.length;while(s--){if(l=u[s],i.relative[c=l.type])break;if((p=i.find[c])&&(o=p(l.matches[0].replace(nt,rt),U.test(u[0].type)&&t.parentNode||t))){if(u.splice(s,1),e=o.length&&mt(u),!e)return O.apply(r,o),r;break}}}return a(e,f)(o,t,!h,r,U.test(e)),r}n.sortStable=v.split("").sort(S).join("")===v,n.detectDuplicates=E,c(),n.sortDetached=ut(function(e){return 1&e.compareDocumentPosition(p.createElement("div"))}),ut(function(e){return e.innerHTML="","#"===e.firstChild.getAttribute("href")})||lt("type|href|height|width",function(e,t,n){return n?undefined:e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),n.attributes&&ut(function(e){return e.innerHTML="",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||lt("value",function(e,t,n){return n||"input"!==e.nodeName.toLowerCase()?undefined:e.defaultValue}),ut(function(e){return null==e.getAttribute("disabled")})||lt(R,function(e,t,n){var r;return n?undefined:(r=e.getAttributeNode(t))&&r.specified?r.value:e[t]===!0?t.toLowerCase():null}),x.find=ot,x.expr=ot.selectors,x.expr[":"]=x.expr.pseudos,x.unique=ot.uniqueSort,x.text=ot.getText,x.isXMLDoc=ot.isXML,x.contains=ot.contains}(e);var D={};function A(e){var t=D[e]={};return x.each(e.match(w)||[],function(e,n){t[n]=!0}),t}x.Callbacks=function(e){e="string"==typeof e?D[e]||A(e):x.extend({},e);var t,n,r,i,o,s,a=[],u=!e.once&&[],l=function(p){for(t=e.memory&&p,n=!0,s=i||0,i=0,o=a.length,r=!0;a&&o>s;s++)if(a[s].apply(p[0],p[1])===!1&&e.stopOnFalse){t=!1;break}r=!1,a&&(u?u.length&&l(u.shift()):t?a=[]:c.disable())},c={add:function(){if(a){var n=a.length;(function s(t){x.each(t,function(t,n){var r=x.type(n);"function"===r?e.unique&&c.has(n)||a.push(n):n&&n.length&&"string"!==r&&s(n)})})(arguments),r?o=a.length:t&&(i=n,l(t))}return this},remove:function(){return a&&x.each(arguments,function(e,t){var n;while((n=x.inArray(t,a,n))>-1)a.splice(n,1),r&&(o>=n&&o--,s>=n&&s--)}),this},has:function(e){return e?x.inArray(e,a)>-1:!(!a||!a.length)},empty:function(){return a=[],o=0,this},disable:function(){return a=u=t=undefined,this},disabled:function(){return!a},lock:function(){return u=undefined,t||c.disable(),this},locked:function(){return!u},fireWith:function(e,t){return!a||n&&!u||(t=t||[],t=[e,t.slice?t.slice():t],r?u.push(t):l(t)),this},fire:function(){return c.fireWith(this,arguments),this},fired:function(){return!!n}};return c},x.extend({Deferred:function(e){var t=[["resolve","done",x.Callbacks("once memory"),"resolved"],["reject","fail",x.Callbacks("once memory"),"rejected"],["notify","progress",x.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return x.Deferred(function(n){x.each(t,function(t,o){var s=o[0],a=x.isFunction(e[t])&&e[t];i[o[1]](function(){var e=a&&a.apply(this,arguments);e&&x.isFunction(e.promise)?e.promise().done(n.resolve).fail(n.reject).progress(n.notify):n[s+"With"](this===r?n.promise():this,a?[e]:arguments)})}),e=null}).promise()},promise:function(e){return null!=e?x.extend(e,r):r}},i={};return r.pipe=r.then,x.each(t,function(e,o){var s=o[2],a=o[3];r[o[1]]=s.add,a&&s.add(function(){n=a},t[1^e][2].disable,t[2][2].lock),i[o[0]]=function(){return i[o[0]+"With"](this===i?r:this,arguments),this},i[o[0]+"With"]=s.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t=0,n=d.call(arguments),r=n.length,i=1!==r||e&&x.isFunction(e.promise)?r:0,o=1===i?e:x.Deferred(),s=function(e,t,n){return function(r){t[e]=this,n[e]=arguments.length>1?d.call(arguments):r,n===a?o.notifyWith(t,n):--i||o.resolveWith(t,n)}},a,u,l;if(r>1)for(a=Array(r),u=Array(r),l=Array(r);r>t;t++)n[t]&&x.isFunction(n[t].promise)?n[t].promise().done(s(t,l,n)).fail(o.reject).progress(s(t,u,a)):--i;return i||o.resolveWith(l,n),o.promise()}}),x.support=function(t){var n=o.createElement("input"),r=o.createDocumentFragment(),i=o.createElement("div"),s=o.createElement("select"),a=s.appendChild(o.createElement("option"));return n.type?(n.type="checkbox",t.checkOn=""!==n.value,t.optSelected=a.selected,t.reliableMarginRight=!0,t.boxSizingReliable=!0,t.pixelPosition=!1,n.checked=!0,t.noCloneChecked=n.cloneNode(!0).checked,s.disabled=!0,t.optDisabled=!a.disabled,n=o.createElement("input"),n.value="t",n.type="radio",t.radioValue="t"===n.value,n.setAttribute("checked","t"),n.setAttribute("name","t"),r.appendChild(n),t.checkClone=r.cloneNode(!0).cloneNode(!0).lastChild.checked,t.focusinBubbles="onfocusin"in e,i.style.backgroundClip="content-box",i.cloneNode(!0).style.backgroundClip="",t.clearCloneStyle="content-box"===i.style.backgroundClip,x(function(){var n,r,s="padding:0;margin:0;border:0;display:block;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box",a=o.getElementsByTagName("body")[0];a&&(n=o.createElement("div"),n.style.cssText="border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px",a.appendChild(n).appendChild(i),i.innerHTML="",i.style.cssText="-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%",x.swap(a,null!=a.style.zoom?{zoom:1}:{},function(){t.boxSizing=4===i.offsetWidth}),e.getComputedStyle&&(t.pixelPosition="1%"!==(e.getComputedStyle(i,null)||{}).top,t.boxSizingReliable="4px"===(e.getComputedStyle(i,null)||{width:"4px"}).width,r=i.appendChild(o.createElement("div")),r.style.cssText=i.style.cssText=s,r.style.marginRight=r.style.width="0",i.style.width="1px",t.reliableMarginRight=!parseFloat((e.getComputedStyle(r,null)||{}).marginRight)),a.removeChild(n))}),t):t}({});var L,q,H=/(?:\{[\s\S]*\}|\[[\s\S]*\])$/,O=/([A-Z])/g;function F(){Object.defineProperty(this.cache={},0,{get:function(){return{}}}),this.expando=x.expando+Math.random()}F.uid=1,F.accepts=function(e){return e.nodeType?1===e.nodeType||9===e.nodeType:!0},F.prototype={key:function(e){if(!F.accepts(e))return 0;var t={},n=e[this.expando];if(!n){n=F.uid++;try{t[this.expando]={value:n},Object.defineProperties(e,t)}catch(r){t[this.expando]=n,x.extend(e,t)}}return this.cache[n]||(this.cache[n]={}),n},set:function(e,t,n){var r,i=this.key(e),o=this.cache[i];if("string"==typeof t)o[t]=n;else if(x.isEmptyObject(o))x.extend(this.cache[i],t);else for(r in t)o[r]=t[r];return o},get:function(e,t){var n=this.cache[this.key(e)];return t===undefined?n:n[t]},access:function(e,t,n){var r;return t===undefined||t&&"string"==typeof t&&n===undefined?(r=this.get(e,t),r!==undefined?r:this.get(e,x.camelCase(t))):(this.set(e,t,n),n!==undefined?n:t)},remove:function(e,t){var n,r,i,o=this.key(e),s=this.cache[o];if(t===undefined)this.cache[o]={};else{x.isArray(t)?r=t.concat(t.map(x.camelCase)):(i=x.camelCase(t),t in s?r=[t,i]:(r=i,r=r in s?[r]:r.match(w)||[])),n=r.length;while(n--)delete s[r[n]]}},hasData:function(e){return!x.isEmptyObject(this.cache[e[this.expando]]||{})},discard:function(e){e[this.expando]&&delete this.cache[e[this.expando]]}},L=new F,q=new F,x.extend({acceptData:F.accepts,hasData:function(e){return L.hasData(e)||q.hasData(e)},data:function(e,t,n){return L.access(e,t,n)},removeData:function(e,t){L.remove(e,t)},_data:function(e,t,n){return q.access(e,t,n)},_removeData:function(e,t){q.remove(e,t)}}),x.fn.extend({data:function(e,t){var n,r,i=this[0],o=0,s=null;if(e===undefined){if(this.length&&(s=L.get(i),1===i.nodeType&&!q.get(i,"hasDataAttrs"))){for(n=i.attributes;n.length>o;o++)r=n[o].name,0===r.indexOf("data-")&&(r=x.camelCase(r.slice(5)),P(i,r,s[r]));q.set(i,"hasDataAttrs",!0)}return s}return"object"==typeof e?this.each(function(){L.set(this,e)}):x.access(this,function(t){var n,r=x.camelCase(e);if(i&&t===undefined){if(n=L.get(i,e),n!==undefined)return n;if(n=L.get(i,r),n!==undefined)return n;if(n=P(i,r,undefined),n!==undefined)return n}else this.each(function(){var n=L.get(this,r);L.set(this,r,t),-1!==e.indexOf("-")&&n!==undefined&&L.set(this,e,t)})},null,t,arguments.length>1,null,!0)},removeData:function(e){return this.each(function(){L.remove(this,e)})}});function P(e,t,n){var r;if(n===undefined&&1===e.nodeType)if(r="data-"+t.replace(O,"-$1").toLowerCase(),n=e.getAttribute(r),"string"==typeof n){try{n="true"===n?!0:"false"===n?!1:"null"===n?null:+n+""===n?+n:H.test(n)?JSON.parse(n):n}catch(i){}L.set(e,t,n)}else n=undefined;return n}x.extend({queue:function(e,t,n){var r;return e?(t=(t||"fx")+"queue",r=q.get(e,t),n&&(!r||x.isArray(n)?r=q.access(e,t,x.makeArray(n)):r.push(n)),r||[]):undefined},dequeue:function(e,t){t=t||"fx";var n=x.queue(e,t),r=n.length,i=n.shift(),o=x._queueHooks(e,t),s=function(){x.dequeue(e,t) 5 | };"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,s,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return q.get(e,n)||q.access(e,n,{empty:x.Callbacks("once memory").add(function(){q.remove(e,[t+"queue",n])})})}}),x.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),n>arguments.length?x.queue(this[0],e):t===undefined?this:this.each(function(){var n=x.queue(this,e,t);x._queueHooks(this,e),"fx"===e&&"inprogress"!==n[0]&&x.dequeue(this,e)})},dequeue:function(e){return this.each(function(){x.dequeue(this,e)})},delay:function(e,t){return e=x.fx?x.fx.speeds[e]||e:e,t=t||"fx",this.queue(t,function(t,n){var r=setTimeout(t,e);n.stop=function(){clearTimeout(r)}})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,t){var n,r=1,i=x.Deferred(),o=this,s=this.length,a=function(){--r||i.resolveWith(o,[o])};"string"!=typeof e&&(t=e,e=undefined),e=e||"fx";while(s--)n=q.get(o[s],e+"queueHooks"),n&&n.empty&&(r++,n.empty.add(a));return a(),i.promise(t)}});var R,M,W=/[\t\r\n\f]/g,$=/\r/g,B=/^(?:input|select|textarea|button)$/i;x.fn.extend({attr:function(e,t){return x.access(this,x.attr,e,t,arguments.length>1)},removeAttr:function(e){return this.each(function(){x.removeAttr(this,e)})},prop:function(e,t){return x.access(this,x.prop,e,t,arguments.length>1)},removeProp:function(e){return this.each(function(){delete this[x.propFix[e]||e]})},addClass:function(e){var t,n,r,i,o,s=0,a=this.length,u="string"==typeof e&&e;if(x.isFunction(e))return this.each(function(t){x(this).addClass(e.call(this,t,this.className))});if(u)for(t=(e||"").match(w)||[];a>s;s++)if(n=this[s],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(W," "):" ")){o=0;while(i=t[o++])0>r.indexOf(" "+i+" ")&&(r+=i+" ");n.className=x.trim(r)}return this},removeClass:function(e){var t,n,r,i,o,s=0,a=this.length,u=0===arguments.length||"string"==typeof e&&e;if(x.isFunction(e))return this.each(function(t){x(this).removeClass(e.call(this,t,this.className))});if(u)for(t=(e||"").match(w)||[];a>s;s++)if(n=this[s],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(W," "):"")){o=0;while(i=t[o++])while(r.indexOf(" "+i+" ")>=0)r=r.replace(" "+i+" "," ");n.className=e?x.trim(r):""}return this},toggleClass:function(e,t){var n=typeof e;return"boolean"==typeof t&&"string"===n?t?this.addClass(e):this.removeClass(e):x.isFunction(e)?this.each(function(n){x(this).toggleClass(e.call(this,n,this.className,t),t)}):this.each(function(){if("string"===n){var t,i=0,o=x(this),s=e.match(w)||[];while(t=s[i++])o.hasClass(t)?o.removeClass(t):o.addClass(t)}else(n===r||"boolean"===n)&&(this.className&&q.set(this,"__className__",this.className),this.className=this.className||e===!1?"":q.get(this,"__className__")||"")})},hasClass:function(e){var t=" "+e+" ",n=0,r=this.length;for(;r>n;n++)if(1===this[n].nodeType&&(" "+this[n].className+" ").replace(W," ").indexOf(t)>=0)return!0;return!1},val:function(e){var t,n,r,i=this[0];{if(arguments.length)return r=x.isFunction(e),this.each(function(n){var i;1===this.nodeType&&(i=r?e.call(this,n,x(this).val()):e,null==i?i="":"number"==typeof i?i+="":x.isArray(i)&&(i=x.map(i,function(e){return null==e?"":e+""})),t=x.valHooks[this.type]||x.valHooks[this.nodeName.toLowerCase()],t&&"set"in t&&t.set(this,i,"value")!==undefined||(this.value=i))});if(i)return t=x.valHooks[i.type]||x.valHooks[i.nodeName.toLowerCase()],t&&"get"in t&&(n=t.get(i,"value"))!==undefined?n:(n=i.value,"string"==typeof n?n.replace($,""):null==n?"":n)}}}),x.extend({valHooks:{option:{get:function(e){var t=e.attributes.value;return!t||t.specified?e.value:e.text}},select:{get:function(e){var t,n,r=e.options,i=e.selectedIndex,o="select-one"===e.type||0>i,s=o?null:[],a=o?i+1:r.length,u=0>i?a:o?i:0;for(;a>u;u++)if(n=r[u],!(!n.selected&&u!==i||(x.support.optDisabled?n.disabled:null!==n.getAttribute("disabled"))||n.parentNode.disabled&&x.nodeName(n.parentNode,"optgroup"))){if(t=x(n).val(),o)return t;s.push(t)}return s},set:function(e,t){var n,r,i=e.options,o=x.makeArray(t),s=i.length;while(s--)r=i[s],(r.selected=x.inArray(x(r).val(),o)>=0)&&(n=!0);return n||(e.selectedIndex=-1),o}}},attr:function(e,t,n){var i,o,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return typeof e.getAttribute===r?x.prop(e,t,n):(1===s&&x.isXMLDoc(e)||(t=t.toLowerCase(),i=x.attrHooks[t]||(x.expr.match.bool.test(t)?M:R)),n===undefined?i&&"get"in i&&null!==(o=i.get(e,t))?o:(o=x.find.attr(e,t),null==o?undefined:o):null!==n?i&&"set"in i&&(o=i.set(e,n,t))!==undefined?o:(e.setAttribute(t,n+""),n):(x.removeAttr(e,t),undefined))},removeAttr:function(e,t){var n,r,i=0,o=t&&t.match(w);if(o&&1===e.nodeType)while(n=o[i++])r=x.propFix[n]||n,x.expr.match.bool.test(n)&&(e[r]=!1),e.removeAttribute(n)},attrHooks:{type:{set:function(e,t){if(!x.support.radioValue&&"radio"===t&&x.nodeName(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},propFix:{"for":"htmlFor","class":"className"},prop:function(e,t,n){var r,i,o,s=e.nodeType;if(e&&3!==s&&8!==s&&2!==s)return o=1!==s||!x.isXMLDoc(e),o&&(t=x.propFix[t]||t,i=x.propHooks[t]),n!==undefined?i&&"set"in i&&(r=i.set(e,n,t))!==undefined?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){return e.hasAttribute("tabindex")||B.test(e.nodeName)||e.href?e.tabIndex:-1}}}}),M={set:function(e,t,n){return t===!1?x.removeAttr(e,n):e.setAttribute(n,n),n}},x.each(x.expr.match.bool.source.match(/\w+/g),function(e,t){var n=x.expr.attrHandle[t]||x.find.attr;x.expr.attrHandle[t]=function(e,t,r){var i=x.expr.attrHandle[t],o=r?undefined:(x.expr.attrHandle[t]=undefined)!=n(e,t,r)?t.toLowerCase():null;return x.expr.attrHandle[t]=i,o}}),x.support.optSelected||(x.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null}}),x.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){x.propFix[this.toLowerCase()]=this}),x.each(["radio","checkbox"],function(){x.valHooks[this]={set:function(e,t){return x.isArray(t)?e.checked=x.inArray(x(e).val(),t)>=0:undefined}},x.support.checkOn||(x.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})});var I=/^key/,z=/^(?:mouse|contextmenu)|click/,_=/^(?:focusinfocus|focusoutblur)$/,X=/^([^.]*)(?:\.(.+)|)$/;function U(){return!0}function Y(){return!1}function V(){try{return o.activeElement}catch(e){}}x.event={global:{},add:function(e,t,n,i,o){var s,a,u,l,c,p,f,h,d,g,m,y=q.get(e);if(y){n.handler&&(s=n,n=s.handler,o=s.selector),n.guid||(n.guid=x.guid++),(l=y.events)||(l=y.events={}),(a=y.handle)||(a=y.handle=function(e){return typeof x===r||e&&x.event.triggered===e.type?undefined:x.event.dispatch.apply(a.elem,arguments)},a.elem=e),t=(t||"").match(w)||[""],c=t.length;while(c--)u=X.exec(t[c])||[],d=m=u[1],g=(u[2]||"").split(".").sort(),d&&(f=x.event.special[d]||{},d=(o?f.delegateType:f.bindType)||d,f=x.event.special[d]||{},p=x.extend({type:d,origType:m,data:i,handler:n,guid:n.guid,selector:o,needsContext:o&&x.expr.match.needsContext.test(o),namespace:g.join(".")},s),(h=l[d])||(h=l[d]=[],h.delegateCount=0,f.setup&&f.setup.call(e,i,g,a)!==!1||e.addEventListener&&e.addEventListener(d,a,!1)),f.add&&(f.add.call(e,p),p.handler.guid||(p.handler.guid=n.guid)),o?h.splice(h.delegateCount++,0,p):h.push(p),x.event.global[d]=!0);e=null}},remove:function(e,t,n,r,i){var o,s,a,u,l,c,p,f,h,d,g,m=q.hasData(e)&&q.get(e);if(m&&(u=m.events)){t=(t||"").match(w)||[""],l=t.length;while(l--)if(a=X.exec(t[l])||[],h=g=a[1],d=(a[2]||"").split(".").sort(),h){p=x.event.special[h]||{},h=(r?p.delegateType:p.bindType)||h,f=u[h]||[],a=a[2]&&RegExp("(^|\\.)"+d.join("\\.(?:.*\\.|)")+"(\\.|$)"),s=o=f.length;while(o--)c=f[o],!i&&g!==c.origType||n&&n.guid!==c.guid||a&&!a.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(f.splice(o,1),c.selector&&f.delegateCount--,p.remove&&p.remove.call(e,c));s&&!f.length&&(p.teardown&&p.teardown.call(e,d,m.handle)!==!1||x.removeEvent(e,h,m.handle),delete u[h])}else for(h in u)x.event.remove(e,h+t[l],n,r,!0);x.isEmptyObject(u)&&(delete m.handle,q.remove(e,"events"))}},trigger:function(t,n,r,i){var s,a,u,l,c,p,f,h=[r||o],d=y.call(t,"type")?t.type:t,g=y.call(t,"namespace")?t.namespace.split("."):[];if(a=u=r=r||o,3!==r.nodeType&&8!==r.nodeType&&!_.test(d+x.event.triggered)&&(d.indexOf(".")>=0&&(g=d.split("."),d=g.shift(),g.sort()),c=0>d.indexOf(":")&&"on"+d,t=t[x.expando]?t:new x.Event(d,"object"==typeof t&&t),t.isTrigger=i?2:3,t.namespace=g.join("."),t.namespace_re=t.namespace?RegExp("(^|\\.)"+g.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=undefined,t.target||(t.target=r),n=null==n?[t]:x.makeArray(n,[t]),f=x.event.special[d]||{},i||!f.trigger||f.trigger.apply(r,n)!==!1)){if(!i&&!f.noBubble&&!x.isWindow(r)){for(l=f.delegateType||d,_.test(l+d)||(a=a.parentNode);a;a=a.parentNode)h.push(a),u=a;u===(r.ownerDocument||o)&&h.push(u.defaultView||u.parentWindow||e)}s=0;while((a=h[s++])&&!t.isPropagationStopped())t.type=s>1?l:f.bindType||d,p=(q.get(a,"events")||{})[t.type]&&q.get(a,"handle"),p&&p.apply(a,n),p=c&&a[c],p&&x.acceptData(a)&&p.apply&&p.apply(a,n)===!1&&t.preventDefault();return t.type=d,i||t.isDefaultPrevented()||f._default&&f._default.apply(h.pop(),n)!==!1||!x.acceptData(r)||c&&x.isFunction(r[d])&&!x.isWindow(r)&&(u=r[c],u&&(r[c]=null),x.event.triggered=d,r[d](),x.event.triggered=undefined,u&&(r[c]=u)),t.result}},dispatch:function(e){e=x.event.fix(e);var t,n,r,i,o,s=[],a=d.call(arguments),u=(q.get(this,"events")||{})[e.type]||[],l=x.event.special[e.type]||{};if(a[0]=e,e.delegateTarget=this,!l.preDispatch||l.preDispatch.call(this,e)!==!1){s=x.event.handlers.call(this,e,u),t=0;while((i=s[t++])&&!e.isPropagationStopped()){e.currentTarget=i.elem,n=0;while((o=i.handlers[n++])&&!e.isImmediatePropagationStopped())(!e.namespace_re||e.namespace_re.test(o.namespace))&&(e.handleObj=o,e.data=o.data,r=((x.event.special[o.origType]||{}).handle||o.handler).apply(i.elem,a),r!==undefined&&(e.result=r)===!1&&(e.preventDefault(),e.stopPropagation()))}return l.postDispatch&&l.postDispatch.call(this,e),e.result}},handlers:function(e,t){var n,r,i,o,s=[],a=t.delegateCount,u=e.target;if(a&&u.nodeType&&(!e.button||"click"!==e.type))for(;u!==this;u=u.parentNode||this)if(u.disabled!==!0||"click"!==e.type){for(r=[],n=0;a>n;n++)o=t[n],i=o.selector+" ",r[i]===undefined&&(r[i]=o.needsContext?x(i,this).index(u)>=0:x.find(i,this,null,[u]).length),r[i]&&r.push(o);r.length&&s.push({elem:u,handlers:r})}return t.length>a&&s.push({elem:this,handlers:t.slice(a)}),s},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(e,t){return null==e.which&&(e.which=null!=t.charCode?t.charCode:t.keyCode),e}},mouseHooks:{props:"button buttons clientX clientY offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(e,t){var n,r,i,s=t.button;return null==e.pageX&&null!=t.clientX&&(n=e.target.ownerDocument||o,r=n.documentElement,i=n.body,e.pageX=t.clientX+(r&&r.scrollLeft||i&&i.scrollLeft||0)-(r&&r.clientLeft||i&&i.clientLeft||0),e.pageY=t.clientY+(r&&r.scrollTop||i&&i.scrollTop||0)-(r&&r.clientTop||i&&i.clientTop||0)),e.which||s===undefined||(e.which=1&s?1:2&s?3:4&s?2:0),e}},fix:function(e){if(e[x.expando])return e;var t,n,r,i=e.type,s=e,a=this.fixHooks[i];a||(this.fixHooks[i]=a=z.test(i)?this.mouseHooks:I.test(i)?this.keyHooks:{}),r=a.props?this.props.concat(a.props):this.props,e=new x.Event(s),t=r.length;while(t--)n=r[t],e[n]=s[n];return e.target||(e.target=o),3===e.target.nodeType&&(e.target=e.target.parentNode),a.filter?a.filter(e,s):e},special:{load:{noBubble:!0},focus:{trigger:function(){return this!==V()&&this.focus?(this.focus(),!1):undefined},delegateType:"focusin"},blur:{trigger:function(){return this===V()&&this.blur?(this.blur(),!1):undefined},delegateType:"focusout"},click:{trigger:function(){return"checkbox"===this.type&&this.click&&x.nodeName(this,"input")?(this.click(),!1):undefined},_default:function(e){return x.nodeName(e.target,"a")}},beforeunload:{postDispatch:function(e){e.result!==undefined&&(e.originalEvent.returnValue=e.result)}}},simulate:function(e,t,n,r){var i=x.extend(new x.Event,n,{type:e,isSimulated:!0,originalEvent:{}});r?x.event.trigger(i,null,t):x.event.dispatch.call(t,i),i.isDefaultPrevented()&&n.preventDefault()}},x.removeEvent=function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n,!1)},x.Event=function(e,t){return this instanceof x.Event?(e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||e.getPreventDefault&&e.getPreventDefault()?U:Y):this.type=e,t&&x.extend(this,t),this.timeStamp=e&&e.timeStamp||x.now(),this[x.expando]=!0,undefined):new x.Event(e,t)},x.Event.prototype={isDefaultPrevented:Y,isPropagationStopped:Y,isImmediatePropagationStopped:Y,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=U,e&&e.preventDefault&&e.preventDefault()},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=U,e&&e.stopPropagation&&e.stopPropagation()},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=U,this.stopPropagation()}},x.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(e,t){x.event.special[e]={delegateType:t,bindType:t,handle:function(e){var n,r=this,i=e.relatedTarget,o=e.handleObj;return(!i||i!==r&&!x.contains(r,i))&&(e.type=o.origType,n=o.handler.apply(this,arguments),e.type=t),n}}}),x.support.focusinBubbles||x.each({focus:"focusin",blur:"focusout"},function(e,t){var n=0,r=function(e){x.event.simulate(t,e.target,x.event.fix(e),!0)};x.event.special[t]={setup:function(){0===n++&&o.addEventListener(e,r,!0)},teardown:function(){0===--n&&o.removeEventListener(e,r,!0)}}}),x.fn.extend({on:function(e,t,n,r,i){var o,s;if("object"==typeof e){"string"!=typeof t&&(n=n||t,t=undefined);for(s in e)this.on(s,t,n,e[s],i);return this}if(null==n&&null==r?(r=t,n=t=undefined):null==r&&("string"==typeof t?(r=n,n=undefined):(r=n,n=t,t=undefined)),r===!1)r=Y;else if(!r)return this;return 1===i&&(o=r,r=function(e){return x().off(e),o.apply(this,arguments)},r.guid=o.guid||(o.guid=x.guid++)),this.each(function(){x.event.add(this,e,r,n,t)})},one:function(e,t,n,r){return this.on(e,t,n,r,1)},off:function(e,t,n){var r,i;if(e&&e.preventDefault&&e.handleObj)return r=e.handleObj,x(e.delegateTarget).off(r.namespace?r.origType+"."+r.namespace:r.origType,r.selector,r.handler),this;if("object"==typeof e){for(i in e)this.off(i,t,e[i]);return this}return(t===!1||"function"==typeof t)&&(n=t,t=undefined),n===!1&&(n=Y),this.each(function(){x.event.remove(this,e,n,t)})},trigger:function(e,t){return this.each(function(){x.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];return n?x.event.trigger(e,t,n,!0):undefined}});var G=/^.[^:#\[\.,]*$/,J=/^(?:parents|prev(?:Until|All))/,Q=x.expr.match.needsContext,K={children:!0,contents:!0,next:!0,prev:!0};x.fn.extend({find:function(e){var t,n=[],r=this,i=r.length;if("string"!=typeof e)return this.pushStack(x(e).filter(function(){for(t=0;i>t;t++)if(x.contains(r[t],this))return!0}));for(t=0;i>t;t++)x.find(e,r[t],n);return n=this.pushStack(i>1?x.unique(n):n),n.selector=this.selector?this.selector+" "+e:e,n},has:function(e){var t=x(e,this),n=t.length;return this.filter(function(){var e=0;for(;n>e;e++)if(x.contains(this,t[e]))return!0})},not:function(e){return this.pushStack(et(this,e||[],!0))},filter:function(e){return this.pushStack(et(this,e||[],!1))},is:function(e){return!!et(this,"string"==typeof e&&Q.test(e)?x(e):e||[],!1).length},closest:function(e,t){var n,r=0,i=this.length,o=[],s=Q.test(e)||"string"!=typeof e?x(e,t||this.context):0;for(;i>r;r++)for(n=this[r];n&&n!==t;n=n.parentNode)if(11>n.nodeType&&(s?s.index(n)>-1:1===n.nodeType&&x.find.matchesSelector(n,e))){n=o.push(n);break}return this.pushStack(o.length>1?x.unique(o):o)},index:function(e){return e?"string"==typeof e?g.call(x(e),this[0]):g.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){var n="string"==typeof e?x(e,t):x.makeArray(e&&e.nodeType?[e]:e),r=x.merge(this.get(),n);return this.pushStack(x.unique(r))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}});function Z(e,t){while((e=e[t])&&1!==e.nodeType);return e}x.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return x.dir(e,"parentNode")},parentsUntil:function(e,t,n){return x.dir(e,"parentNode",n)},next:function(e){return Z(e,"nextSibling")},prev:function(e){return Z(e,"previousSibling")},nextAll:function(e){return x.dir(e,"nextSibling")},prevAll:function(e){return x.dir(e,"previousSibling")},nextUntil:function(e,t,n){return x.dir(e,"nextSibling",n)},prevUntil:function(e,t,n){return x.dir(e,"previousSibling",n)},siblings:function(e){return x.sibling((e.parentNode||{}).firstChild,e)},children:function(e){return x.sibling(e.firstChild)},contents:function(e){return e.contentDocument||x.merge([],e.childNodes)}},function(e,t){x.fn[e]=function(n,r){var i=x.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=x.filter(r,i)),this.length>1&&(K[e]||x.unique(i),J.test(e)&&i.reverse()),this.pushStack(i)}}),x.extend({filter:function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?x.find.matchesSelector(r,e)?[r]:[]:x.find.matches(e,x.grep(t,function(e){return 1===e.nodeType}))},dir:function(e,t,n){var r=[],i=n!==undefined;while((e=e[t])&&9!==e.nodeType)if(1===e.nodeType){if(i&&x(e).is(n))break;r.push(e)}return r},sibling:function(e,t){var n=[];for(;e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n}});function et(e,t,n){if(x.isFunction(t))return x.grep(e,function(e,r){return!!t.call(e,r,e)!==n});if(t.nodeType)return x.grep(e,function(e){return e===t!==n});if("string"==typeof t){if(G.test(t))return x.filter(t,e,n);t=x.filter(t,e)}return x.grep(e,function(e){return g.call(t,e)>=0!==n})}var tt=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,nt=/<([\w:]+)/,rt=/<|&#?\w+;/,it=/<(?:script|style|link)/i,ot=/^(?:checkbox|radio)$/i,st=/checked\s*(?:[^=]|=\s*.checked.)/i,at=/^$|\/(?:java|ecma)script/i,ut=/^true\/(.*)/,lt=/^\s*\s*$/g,ct={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};ct.optgroup=ct.option,ct.tbody=ct.tfoot=ct.colgroup=ct.caption=ct.thead,ct.th=ct.td,x.fn.extend({text:function(e){return x.access(this,function(e){return e===undefined?x.text(this):this.empty().append((this[0]&&this[0].ownerDocument||o).createTextNode(e))},null,e,arguments.length)},append:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=pt(this,e);t.appendChild(e)}})},prepend:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=pt(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},remove:function(e,t){var n,r=e?x.filter(e,this):this,i=0;for(;null!=(n=r[i]);i++)t||1!==n.nodeType||x.cleanData(mt(n)),n.parentNode&&(t&&x.contains(n.ownerDocument,n)&&dt(mt(n,"script")),n.parentNode.removeChild(n));return this},empty:function(){var e,t=0;for(;null!=(e=this[t]);t++)1===e.nodeType&&(x.cleanData(mt(e,!1)),e.textContent="");return this},clone:function(e,t){return e=null==e?!1:e,t=null==t?e:t,this.map(function(){return x.clone(this,e,t)})},html:function(e){return x.access(this,function(e){var t=this[0]||{},n=0,r=this.length;if(e===undefined&&1===t.nodeType)return t.innerHTML;if("string"==typeof e&&!it.test(e)&&!ct[(nt.exec(e)||["",""])[1].toLowerCase()]){e=e.replace(tt,"<$1>");try{for(;r>n;n++)t=this[n]||{},1===t.nodeType&&(x.cleanData(mt(t,!1)),t.innerHTML=e);t=0}catch(i){}}t&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(){var e=x.map(this,function(e){return[e.nextSibling,e.parentNode]}),t=0;return this.domManip(arguments,function(n){var r=e[t++],i=e[t++];i&&(r&&r.parentNode!==i&&(r=this.nextSibling),x(this).remove(),i.insertBefore(n,r))},!0),t?this:this.remove()},detach:function(e){return this.remove(e,!0)},domManip:function(e,t,n){e=f.apply([],e);var r,i,o,s,a,u,l=0,c=this.length,p=this,h=c-1,d=e[0],g=x.isFunction(d);if(g||!(1>=c||"string"!=typeof d||x.support.checkClone)&&st.test(d))return this.each(function(r){var i=p.eq(r);g&&(e[0]=d.call(this,r,i.html())),i.domManip(e,t,n)});if(c&&(r=x.buildFragment(e,this[0].ownerDocument,!1,!n&&this),i=r.firstChild,1===r.childNodes.length&&(r=i),i)){for(o=x.map(mt(r,"script"),ft),s=o.length;c>l;l++)a=r,l!==h&&(a=x.clone(a,!0,!0),s&&x.merge(o,mt(a,"script"))),t.call(this[l],a,l);if(s)for(u=o[o.length-1].ownerDocument,x.map(o,ht),l=0;s>l;l++)a=o[l],at.test(a.type||"")&&!q.access(a,"globalEval")&&x.contains(u,a)&&(a.src?x._evalUrl(a.src):x.globalEval(a.textContent.replace(lt,"")))}return this}}),x.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,t){x.fn[e]=function(e){var n,r=[],i=x(e),o=i.length-1,s=0;for(;o>=s;s++)n=s===o?this:this.clone(!0),x(i[s])[t](n),h.apply(r,n.get());return this.pushStack(r)}}),x.extend({clone:function(e,t,n){var r,i,o,s,a=e.cloneNode(!0),u=x.contains(e.ownerDocument,e);if(!(x.support.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||x.isXMLDoc(e)))for(s=mt(a),o=mt(e),r=0,i=o.length;i>r;r++)yt(o[r],s[r]);if(t)if(n)for(o=o||mt(e),s=s||mt(a),r=0,i=o.length;i>r;r++)gt(o[r],s[r]);else gt(e,a);return s=mt(a,"script"),s.length>0&&dt(s,!u&&mt(e,"script")),a},buildFragment:function(e,t,n,r){var i,o,s,a,u,l,c=0,p=e.length,f=t.createDocumentFragment(),h=[];for(;p>c;c++)if(i=e[c],i||0===i)if("object"===x.type(i))x.merge(h,i.nodeType?[i]:i);else if(rt.test(i)){o=o||f.appendChild(t.createElement("div")),s=(nt.exec(i)||["",""])[1].toLowerCase(),a=ct[s]||ct._default,o.innerHTML=a[1]+i.replace(tt,"<$1>")+a[2],l=a[0];while(l--)o=o.lastChild;x.merge(h,o.childNodes),o=f.firstChild,o.textContent=""}else h.push(t.createTextNode(i));f.textContent="",c=0;while(i=h[c++])if((!r||-1===x.inArray(i,r))&&(u=x.contains(i.ownerDocument,i),o=mt(f.appendChild(i),"script"),u&&dt(o),n)){l=0;while(i=o[l++])at.test(i.type||"")&&n.push(i)}return f},cleanData:function(e){var t,n,r,i,o,s,a=x.event.special,u=0;for(;(n=e[u])!==undefined;u++){if(F.accepts(n)&&(o=n[q.expando],o&&(t=q.cache[o]))){if(r=Object.keys(t.events||{}),r.length)for(s=0;(i=r[s])!==undefined;s++)a[i]?x.event.remove(n,i):x.removeEvent(n,i,t.handle);q.cache[o]&&delete q.cache[o]}delete L.cache[n[L.expando]]}},_evalUrl:function(e){return x.ajax({url:e,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})}});function pt(e,t){return x.nodeName(e,"table")&&x.nodeName(1===t.nodeType?t:t.firstChild,"tr")?e.getElementsByTagName("tbody")[0]||e.appendChild(e.ownerDocument.createElement("tbody")):e}function ft(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function ht(e){var t=ut.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function dt(e,t){var n=e.length,r=0;for(;n>r;r++)q.set(e[r],"globalEval",!t||q.get(t[r],"globalEval"))}function gt(e,t){var n,r,i,o,s,a,u,l;if(1===t.nodeType){if(q.hasData(e)&&(o=q.access(e),s=q.set(t,o),l=o.events)){delete s.handle,s.events={};for(i in l)for(n=0,r=l[i].length;r>n;n++)x.event.add(t,i,l[i][n])}L.hasData(e)&&(a=L.access(e),u=x.extend({},a),L.set(t,u))}}function mt(e,t){var n=e.getElementsByTagName?e.getElementsByTagName(t||"*"):e.querySelectorAll?e.querySelectorAll(t||"*"):[];return t===undefined||t&&x.nodeName(e,t)?x.merge([e],n):n}function yt(e,t){var n=t.nodeName.toLowerCase();"input"===n&&ot.test(e.type)?t.checked=e.checked:("input"===n||"textarea"===n)&&(t.defaultValue=e.defaultValue)}x.fn.extend({wrapAll:function(e){var t;return x.isFunction(e)?this.each(function(t){x(this).wrapAll(e.call(this,t))}):(this[0]&&(t=x(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstElementChild)e=e.firstElementChild;return e}).append(this)),this)},wrapInner:function(e){return x.isFunction(e)?this.each(function(t){x(this).wrapInner(e.call(this,t))}):this.each(function(){var t=x(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=x.isFunction(e);return this.each(function(n){x(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){x.nodeName(this,"body")||x(this).replaceWith(this.childNodes)}).end()}});var vt,xt,bt=/^(none|table(?!-c[ea]).+)/,wt=/^margin/,Tt=RegExp("^("+b+")(.*)$","i"),Ct=RegExp("^("+b+")(?!px)[a-z%]+$","i"),kt=RegExp("^([+-])=("+b+")","i"),Nt={BODY:"block"},Et={position:"absolute",visibility:"hidden",display:"block"},St={letterSpacing:0,fontWeight:400},jt=["Top","Right","Bottom","Left"],Dt=["Webkit","O","Moz","ms"];function At(e,t){if(t in e)return t;var n=t.charAt(0).toUpperCase()+t.slice(1),r=t,i=Dt.length;while(i--)if(t=Dt[i]+n,t in e)return t;return r}function Lt(e,t){return e=t||e,"none"===x.css(e,"display")||!x.contains(e.ownerDocument,e)}function qt(t){return e.getComputedStyle(t,null)}function Ht(e,t){var n,r,i,o=[],s=0,a=e.length;for(;a>s;s++)r=e[s],r.style&&(o[s]=q.get(r,"olddisplay"),n=r.style.display,t?(o[s]||"none"!==n||(r.style.display=""),""===r.style.display&&Lt(r)&&(o[s]=q.access(r,"olddisplay",Rt(r.nodeName)))):o[s]||(i=Lt(r),(n&&"none"!==n||!i)&&q.set(r,"olddisplay",i?n:x.css(r,"display"))));for(s=0;a>s;s++)r=e[s],r.style&&(t&&"none"!==r.style.display&&""!==r.style.display||(r.style.display=t?o[s]||"":"none"));return e}x.fn.extend({css:function(e,t){return x.access(this,function(e,t,n){var r,i,o={},s=0;if(x.isArray(t)){for(r=qt(e),i=t.length;i>s;s++)o[t[s]]=x.css(e,t[s],!1,r);return o}return n!==undefined?x.style(e,t,n):x.css(e,t)},e,t,arguments.length>1)},show:function(){return Ht(this,!0)},hide:function(){return Ht(this)},toggle:function(e){return"boolean"==typeof e?e?this.show():this.hide():this.each(function(){Lt(this)?x(this).show():x(this).hide()})}}),x.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=vt(e,"opacity");return""===n?"1":n}}}},cssNumber:{columnCount:!0,fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":"cssFloat"},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,o,s,a=x.camelCase(t),u=e.style;return t=x.cssProps[a]||(x.cssProps[a]=At(u,a)),s=x.cssHooks[t]||x.cssHooks[a],n===undefined?s&&"get"in s&&(i=s.get(e,!1,r))!==undefined?i:u[t]:(o=typeof n,"string"===o&&(i=kt.exec(n))&&(n=(i[1]+1)*i[2]+parseFloat(x.css(e,t)),o="number"),null==n||"number"===o&&isNaN(n)||("number"!==o||x.cssNumber[a]||(n+="px"),x.support.clearCloneStyle||""!==n||0!==t.indexOf("background")||(u[t]="inherit"),s&&"set"in s&&(n=s.set(e,n,r))===undefined||(u[t]=n)),undefined)}},css:function(e,t,n,r){var i,o,s,a=x.camelCase(t);return t=x.cssProps[a]||(x.cssProps[a]=At(e.style,a)),s=x.cssHooks[t]||x.cssHooks[a],s&&"get"in s&&(i=s.get(e,!0,n)),i===undefined&&(i=vt(e,t,r)),"normal"===i&&t in St&&(i=St[t]),""===n||n?(o=parseFloat(i),n===!0||x.isNumeric(o)?o||0:i):i}}),vt=function(e,t,n){var r,i,o,s=n||qt(e),a=s?s.getPropertyValue(t)||s[t]:undefined,u=e.style;return s&&(""!==a||x.contains(e.ownerDocument,e)||(a=x.style(e,t)),Ct.test(a)&&wt.test(t)&&(r=u.width,i=u.minWidth,o=u.maxWidth,u.minWidth=u.maxWidth=u.width=a,a=s.width,u.width=r,u.minWidth=i,u.maxWidth=o)),a};function Ot(e,t,n){var r=Tt.exec(t);return r?Math.max(0,r[1]-(n||0))+(r[2]||"px"):t}function Ft(e,t,n,r,i){var o=n===(r?"border":"content")?4:"width"===t?1:0,s=0;for(;4>o;o+=2)"margin"===n&&(s+=x.css(e,n+jt[o],!0,i)),r?("content"===n&&(s-=x.css(e,"padding"+jt[o],!0,i)),"margin"!==n&&(s-=x.css(e,"border"+jt[o]+"Width",!0,i))):(s+=x.css(e,"padding"+jt[o],!0,i),"padding"!==n&&(s+=x.css(e,"border"+jt[o]+"Width",!0,i)));return s}function Pt(e,t,n){var r=!0,i="width"===t?e.offsetWidth:e.offsetHeight,o=qt(e),s=x.support.boxSizing&&"border-box"===x.css(e,"boxSizing",!1,o);if(0>=i||null==i){if(i=vt(e,t,o),(0>i||null==i)&&(i=e.style[t]),Ct.test(i))return i;r=s&&(x.support.boxSizingReliable||i===e.style[t]),i=parseFloat(i)||0}return i+Ft(e,t,n||(s?"border":"content"),r,o)+"px"}function Rt(e){var t=o,n=Nt[e];return n||(n=Mt(e,t),"none"!==n&&n||(xt=(xt||x("