Repository: muaz-khan/WebRTC-Experiment Branch: master Commit: bbcdbc13b3d7 Files: 981 Total size: 15.3 MB Directory structure: gitextract_ce2vii70/ ├── .gitignore ├── Canvas-Designer/ │ ├── .gitignore │ ├── .npmignore │ ├── .travis.yml │ ├── Gruntfile.js │ ├── Help/ │ │ └── index.html │ ├── LICENSE │ ├── README.md │ ├── bower.json │ ├── canvas-designer-widget.js │ ├── dev/ │ │ ├── amd.js │ │ ├── arc-handler.js │ │ ├── arrow-handler.js │ │ ├── bezier-handler.js │ │ ├── common.js │ │ ├── data-uris.js │ │ ├── decorator.js │ │ ├── drag-helper.js │ │ ├── draw-helper.js │ │ ├── eraser-handler.js │ │ ├── events-handler.js │ │ ├── file-selector.js │ │ ├── head.js │ │ ├── image-handler.js │ │ ├── line-handler.js │ │ ├── marker-handler.js │ │ ├── pdf-handler.js │ │ ├── pencil-handler.js │ │ ├── quadratic-handler.js │ │ ├── rect-handler.js │ │ ├── share-drawings.js │ │ ├── tail.js │ │ ├── text-handler.js │ │ ├── webrtc-handler.js │ │ └── zoom-handler.js │ ├── index.html │ ├── multiple.html │ ├── package.json │ ├── server.js │ ├── simple.html │ ├── widget.html │ └── widget.js ├── Chrome-Extensions/ │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── Screen-Capturing.js/ │ │ ├── README.md │ │ ├── Screen-Capturing.js │ │ ├── index.html │ │ └── server.js │ ├── desktopCapture/ │ │ ├── README.md │ │ ├── background-script.js │ │ ├── content-script.js │ │ ├── different-api/ │ │ │ ├── background-script.js │ │ │ └── manifest.json │ │ └── manifest.json │ ├── desktopCapture-p2p/ │ │ ├── README.md │ │ ├── background/ │ │ │ ├── captureCamera.js │ │ │ ├── captureDesktop.js │ │ │ ├── captureTabUsingTabCapture.js │ │ │ ├── common.js │ │ │ ├── globals.js │ │ │ ├── gotStream.js │ │ │ ├── gotTabCaptureStream.js │ │ │ ├── helpers/ │ │ │ │ ├── AntMediaWrapper.js │ │ │ │ ├── CodecsHandler.js │ │ │ │ ├── IceServersHandler.js │ │ │ │ ├── MultiStreamsMixer.js │ │ │ │ ├── adapter.js │ │ │ │ ├── getStats.js │ │ │ │ └── socket.io.js │ │ │ ├── onAccessApproved.js │ │ │ ├── online-offline.js │ │ │ ├── runtimePort.js │ │ │ ├── setDefaults.js │ │ │ ├── setupWebRTCConnection.js │ │ │ ├── shareStreamUsingAntMediaServer.js │ │ │ └── shareStreamUsingRTCMultiConnection.js │ │ ├── extension-pages/ │ │ │ ├── camera-mic.html │ │ │ ├── camera-mic.js │ │ │ ├── chat.html │ │ │ ├── chat.js │ │ │ ├── dropdown.html │ │ │ ├── dropdown.js │ │ │ ├── options.html │ │ │ ├── options.js │ │ │ ├── video.html │ │ │ └── video.js │ │ ├── index.html │ │ ├── manifest.json │ │ ├── screen-receivers/ │ │ │ └── ant/ │ │ │ └── index.html │ │ └── server.js │ ├── file-sharing/ │ │ ├── README.md │ │ ├── background.js │ │ ├── fonts/ │ │ │ └── MyriadPro-Light.otf │ │ ├── info.html │ │ ├── manifest.json │ │ ├── options.html │ │ ├── options.js │ │ ├── popup.html │ │ ├── popup.js │ │ └── rmc-files-handler.js │ ├── get-any-webrtc-peer-stream/ │ │ ├── RTCPeerConnection-override.js │ │ ├── content-script.js │ │ └── manifest.json │ ├── getUserMedia-on-http/ │ │ ├── README.md │ │ ├── background-script.js │ │ ├── camera-mic.html │ │ ├── camera-mic.js │ │ ├── content-script.js │ │ ├── example/ │ │ │ ├── index.html │ │ │ └── index.js │ │ ├── manifest.json │ │ └── webrtc-handler.js │ ├── screen-recording/ │ │ ├── README.md │ │ ├── RecordRTC/ │ │ │ ├── DiskStorage.js │ │ │ ├── EBML.js │ │ │ ├── MediaStreamRecorder.js │ │ │ ├── MultiStreamRecorder.js │ │ │ ├── MultiStreamsMixer.js │ │ │ ├── StereoAudioRecorder.js │ │ │ └── getAllAudioVideoDevices.js │ │ ├── background/ │ │ │ ├── background.badgeText.js │ │ │ ├── background.common.js │ │ │ ├── background.contentScript.js │ │ │ ├── background.desktopCapture.js │ │ │ ├── background.getUserMedia.js │ │ │ ├── background.js │ │ │ ├── background.messaging.js │ │ │ ├── background.players.js │ │ │ └── background.tabCapture.js │ │ ├── camera-mic.html │ │ ├── camera-mic.js │ │ ├── dropdown.html │ │ ├── dropdown.js │ │ ├── manifest.json │ │ ├── options.html │ │ ├── options.js │ │ ├── preview/ │ │ │ ├── preview.js │ │ │ ├── preview.php.upload.js │ │ │ └── preview.youtube.upload.js │ │ ├── preview.html │ │ ├── video.html │ │ └── video.js │ └── tabCapture/ │ ├── CodecsHandler.js │ ├── IceServersHandler.js │ ├── README.md │ ├── index.html │ ├── manifest.json │ ├── options.html │ ├── options.js │ ├── shareStreamUsingRTCMultiConnection.js │ ├── socket.io.js │ └── tab-capturing.js ├── ConcatenateBlobs/ │ ├── ConcatenateBlobs.js │ ├── README.md │ ├── index.html │ └── package.json ├── Conversation.js/ │ ├── AndroidRTC/ │ │ ├── fonts/ │ │ │ ├── MyriadPro-Bold.otf │ │ │ ├── MyriadPro-Light.otf │ │ │ └── MyriadPro-Regular.otf │ │ ├── index.html │ │ ├── manifest.json │ │ ├── scripts/ │ │ │ ├── FileBufferReader.js │ │ │ ├── RTCMultiConnection.js │ │ │ ├── common-signaling.js │ │ │ ├── conversation.js │ │ │ └── ui-handler.js │ │ └── styles/ │ │ └── ui-styles.css │ ├── README.md │ ├── conversation.js │ ├── demos/ │ │ ├── common-signaling.js │ │ ├── common-styles.css │ │ ├── cross-language-chat.html │ │ └── search-user.html │ ├── package.json │ └── server.js ├── DataChannel/ │ ├── .gitignore │ ├── .jshintrc │ ├── .npmignore │ ├── .travis.yml │ ├── DataChannel.js │ ├── Gruntfile.js │ ├── README.md │ ├── auto-session-establishment.html │ ├── bower.json │ ├── dev/ │ │ ├── DataChannel.js │ │ ├── DataConnector.js │ │ ├── FileConverter.js │ │ ├── FileReceiver.js │ │ ├── FileSaver.js │ │ ├── FileSender.js │ │ ├── IceServersHandler.js │ │ ├── RTCPeerConnection.js │ │ ├── SocketConnector.js │ │ ├── TextReceiver.js │ │ ├── TextSender.js │ │ ├── externalIceServers.js │ │ ├── globals.js │ │ ├── head.js │ │ └── tail.js │ ├── index.html │ ├── package.json │ ├── server.js │ └── simple.html ├── DetectRTC/ │ ├── .gitignore │ ├── .jshintrc │ ├── .npmignore │ ├── .travis.yml │ ├── DetectRTC.js │ ├── Gruntfile.js │ ├── LICENSE │ ├── README.md │ ├── bower.json │ ├── dev/ │ │ ├── CheckDeviceSupport.js │ │ ├── DetectLocalIPAddress.js │ │ ├── DetectRTC.js │ │ ├── Objects.js │ │ ├── common.js │ │ ├── detectCaptureStream.js │ │ ├── detectDesktopOS.js │ │ ├── detectOSName.js │ │ ├── detectPrivateBrowsing.js │ │ ├── getBrowserInfo.js │ │ ├── head.js │ │ ├── isMobile.js │ │ └── tail.js │ ├── index.html │ ├── npm-test.js │ ├── package.json │ ├── server.js │ ├── simple-demos/ │ │ ├── select-cameras.html │ │ └── simple-demo.html │ └── test/ │ ├── CheckDeviceSupport.js │ ├── DetectRTC.js │ ├── browserstack.config.js │ ├── detectOSName.js │ ├── getBrowserInfo.js │ └── html-test-files/ │ ├── CheckDeviceSupport.html │ ├── DetectRTC.html │ ├── detectOSName.html │ └── getBrowserInfo.html ├── FileBufferReader/ │ ├── .gitignore │ ├── .npmignore │ ├── .travis.yml │ ├── FileBufferReader.js │ ├── Gruntfile.js │ ├── LICENSE │ ├── README.md │ ├── bower.json │ ├── demo/ │ │ ├── IceServersHandler.js │ │ ├── PeerConnection.js │ │ ├── PeerUI.js │ │ ├── adapter-latest.js │ │ └── index.html │ ├── dev/ │ │ ├── FileBufferReader.js │ │ ├── FileBufferReaderHelper.js │ │ ├── FileBufferReceiver.js │ │ ├── FileConverter.js │ │ ├── FileSelector.js │ │ ├── binarize.js │ │ ├── common.js │ │ ├── head.js │ │ └── tail.js │ ├── fbr-client/ │ │ ├── .gitignore │ │ ├── .npmignore │ │ ├── README.md │ │ ├── demo/ │ │ │ ├── circular-progress-bar.css │ │ │ ├── style.css │ │ │ └── ui.js │ │ ├── index.html │ │ ├── package.json │ │ └── server.js │ ├── index.html │ ├── package.json │ └── server.js ├── Firefox-Extensions/ │ ├── README.md │ ├── enable-screen-capturing/ │ │ ├── README.md │ │ ├── content-script.js │ │ ├── index.js │ │ ├── package.json │ │ └── test/ │ │ └── test-index.js │ ├── enable-screen-capturing-old/ │ │ ├── README.md │ │ ├── bootstrap.js │ │ ├── enable-screen-capturing.xpi │ │ └── install.rdf │ └── enable-screen-capturing-old2/ │ ├── FirefoxScreenAddon.js │ ├── README.md │ ├── content-script.js │ ├── index.js │ ├── package.json │ └── test/ │ └── test-index.js ├── LICENSE ├── MediaStreamRecorder/ │ ├── .gitignore │ ├── .npmignore │ ├── .travis.yml │ ├── AudioStreamRecorder/ │ │ ├── FlashAudioRecorder.js │ │ ├── FlashAudioRecorder.md │ │ ├── MediaRecorderWrapper.js │ │ ├── README.md │ │ ├── StereoAudioRecorder.js │ │ ├── StereoAudioRecorderHelper.js │ │ └── lib/ │ │ ├── recorder.js/ │ │ │ ├── recorder.js │ │ │ └── recorder.swf │ │ └── wavencoder/ │ │ └── wavencoder.js │ ├── FileSystem/ │ │ ├── FileReader.js │ │ ├── FileWriter.js │ │ └── README.md │ ├── Gruntfile.js │ ├── LICENSE │ ├── MediaStreamRecorder.js │ ├── README.md │ ├── VideoStreamRecorder/ │ │ ├── GifRecorder.js │ │ ├── README.md │ │ ├── WhammyRecorder.js │ │ ├── WhammyRecorderHelper.js │ │ └── lib/ │ │ ├── gif-encoder.js │ │ └── whammy.js │ ├── bower.json │ ├── common/ │ │ ├── ConcatenateBlobs.js │ │ ├── Cross-Browser-Declarations.js │ │ ├── MediaStreamRecorder.js │ │ ├── MultiStreamRecorder.js │ │ ├── MultiStreamsMixer.js │ │ ├── OpentTokStreamRecorder.js │ │ └── amd.js │ ├── demos/ │ │ ├── MultiStreamRecorder.html │ │ ├── README.md │ │ ├── audio-recorder.html │ │ ├── flash-audio-recorder-upload.html │ │ ├── flash-audio-recorder.html │ │ ├── gif-recorder.html │ │ ├── index.html │ │ ├── opentok-stream-recorder.html │ │ └── video-recorder.html │ ├── fake-keys/ │ │ ├── certificate.pem │ │ └── privatekey.pem │ ├── lib/ │ │ └── AjaxRequest/ │ │ └── AjaxRequest.js │ ├── npm-test.js │ ├── package.json │ ├── server.js │ └── tests/ │ └── flash-audio-recorder-memory-usage.html ├── MultiRTC/ │ ├── .gitignore │ ├── MultiRTC-firebase/ │ │ ├── FileBufferReader.js │ │ ├── README.md │ │ ├── RTCMultiConnection.js │ │ ├── firebase.js │ │ ├── index.html │ │ ├── linkify.js │ │ ├── package.json │ │ ├── scrol-bars.css │ │ ├── style.css │ │ ├── ui.main.js │ │ ├── ui.peer-connection.js │ │ ├── ui.settings.js │ │ ├── ui.share-files.js │ │ └── ui.users-list.js │ ├── MultiRTC-socketio/ │ │ ├── README.md │ │ ├── certificate.pem │ │ ├── package.json │ │ ├── privatekey.pem │ │ ├── public/ │ │ │ ├── FileBufferReader.js │ │ │ ├── RTCMultiConnection.js │ │ │ ├── index.html │ │ │ ├── linkify.js │ │ │ ├── scrol-bars.css │ │ │ ├── style.css │ │ │ ├── ui.main.js │ │ │ ├── ui.peer-connection.js │ │ │ ├── ui.settings.js │ │ │ ├── ui.share-files.js │ │ │ └── ui.users-list.js │ │ └── signaler.js │ ├── MultiRTC-websocket/ │ │ ├── FileBufferReader.js │ │ ├── README.md │ │ ├── RTCMultiConnection.js │ │ ├── index.html │ │ ├── linkify.js │ │ ├── package.json │ │ ├── scrol-bars.css │ │ ├── style.css │ │ ├── ui.main.js │ │ ├── ui.peer-connection.js │ │ ├── ui.settings.js │ │ ├── ui.share-files.js │ │ └── ui.users-list.js │ └── README.md ├── MultiStreamsMixer/ │ ├── .gitignore │ ├── .npmignore │ ├── .travis.yml │ ├── Gruntfile.js │ ├── LICENSE │ ├── MultiStreamsMixer.js │ ├── MultiStreamsMixer.ts │ ├── README.md │ ├── bower.json │ ├── dev/ │ │ ├── README.md │ │ ├── amd.js │ │ ├── append-streams.js │ │ ├── cross-browser-declarations.js │ │ ├── draw-videos-on-canvas.js │ │ ├── get-mixed-audio-stream.js │ │ ├── get-mixed-stream.js │ │ ├── get-mixed-video-stream.js │ │ ├── get-video-element.js │ │ ├── head.js │ │ ├── init.js │ │ ├── module.exports.js │ │ ├── release-streams.js │ │ ├── replace-streams.js │ │ ├── start-drawing-frames.js │ │ └── tail.js │ ├── index.html │ ├── npm-test.js │ ├── package.json │ └── server.js ├── PluginRTC/ │ ├── Plugin.EveryWhere.js │ ├── Plugin.Temasys.js │ └── README.md ├── Pluginfree-Screen-Sharing/ │ ├── README.md │ ├── conference.js │ └── index.html ├── Pre-recorded-Media-Streaming/ │ ├── MediaStreamer.js │ ├── README.md │ ├── index-2.html │ ├── index-old.html │ ├── index.html │ └── streamer.js ├── README.md ├── RTCMultiConnection/ │ ├── .gitignore │ ├── .jshintrc │ ├── .npmignore │ ├── .travis.yml │ ├── CONTRIBUTING.md │ ├── Gruntfile.js │ ├── LICENSE.md │ ├── README.md │ ├── admin/ │ │ ├── admin-ui.js │ │ └── index.html │ ├── bower.json │ ├── config.json │ ├── demos/ │ │ ├── Audio-Conferencing.html │ │ ├── Call-By-UserName.html │ │ ├── One-to-One.html │ │ ├── README.md │ │ ├── SSEConnection/ │ │ │ ├── README.md │ │ │ ├── SSE.php │ │ │ ├── checkPresence.php │ │ │ ├── enableCORS.php │ │ │ ├── get-param.php │ │ │ ├── publish.php │ │ │ ├── rooms/ │ │ │ │ └── README.md │ │ │ └── write-json.php │ │ ├── SSEConnection.html │ │ ├── Scalable-Broadcast.html │ │ ├── Video-Conferencing.html │ │ ├── camera-zoom.html │ │ ├── dashboard/ │ │ │ ├── canvas-designer-old.html │ │ │ ├── canvas-designer.html │ │ │ ├── index-old.html │ │ │ └── index.html │ │ ├── file-sharing.html │ │ ├── getStats.html │ │ ├── index.html │ │ ├── menu.js │ │ ├── screen-sharing.html │ │ ├── stylesheet.css │ │ ├── text-chat-file-sharing.html │ │ ├── translate-text-chat.html │ │ ├── video-and-screen-sharing.html │ │ ├── video-broadcasting.html │ │ ├── video-conference/ │ │ │ ├── index.html │ │ │ └── video-conference.html │ │ ├── video-conferencing-chat-filesharing.html │ │ └── vuejs-video-conferencing.html │ ├── dev/ │ │ ├── BandwidthHandler.js │ │ ├── BluetoothConnection.js │ │ ├── CodecsHandler.js │ │ ├── FileProgressBarHandler.js │ │ ├── FileSelector.js │ │ ├── FirebaseConnection.js │ │ ├── IceServersHandler.js │ │ ├── MediaStreamRecorder.js │ │ ├── MultiPeersHandler.js │ │ ├── MultiStreamsMixer.js │ │ ├── OnIceCandidateHandler.js │ │ ├── Plugin.EveryWhere.js │ │ ├── PubNubConnection.js │ │ ├── README.md │ │ ├── RTCMultiConnection.js │ │ ├── RTCPeerConnection.js │ │ ├── RecordingHandler.js │ │ ├── SSEConnection.js │ │ ├── SignalRConnection.js │ │ ├── SipConnection.js │ │ ├── SocketConnection.js │ │ ├── StreamHasData.js │ │ ├── StreamsHandler.js │ │ ├── TextSenderReceiver.js │ │ ├── TranslationHandler.js │ │ ├── WebSocketConnection.js │ │ ├── WebSyncConnection.js │ │ ├── XHRConnection.js │ │ ├── amd.js │ │ ├── enableV2Api.js │ │ ├── getHTMLMediaElement.css │ │ ├── getHTMLMediaElement.js │ │ ├── getUserMedia.js │ │ ├── globals.js │ │ ├── gumadapter.js │ │ ├── head.js │ │ ├── ios-hacks.js │ │ └── tail.js │ ├── dist/ │ │ ├── README.md │ │ └── RTCMultiConnection.js │ ├── docs/ │ │ ├── README.md │ │ ├── api.md │ │ ├── getting-started.md │ │ ├── how-to-use.md │ │ ├── installation-guide.md │ │ ├── ios-android.md │ │ ├── tips-tricks.md │ │ └── upgrade.md │ ├── fake-keys/ │ │ ├── certificate.pem │ │ └── privatekey.pem │ ├── logs.json │ ├── npm-test.js │ ├── package.json │ └── server.js ├── RTCMultiConnection-Server/ │ ├── .gitignore │ ├── .npmignore │ ├── LICENSE │ ├── README.md │ ├── bower.json │ ├── config.json │ ├── fake-keys/ │ │ ├── certificate.pem │ │ └── privatekey.pem │ ├── logs.json │ ├── package.json │ └── server.js ├── RTCMultiConnection-SignalR/ │ ├── LICENSE │ ├── README.md │ ├── RTCMultiConnection/ │ │ ├── Properties/ │ │ │ └── AssemblyInfo.cs │ │ ├── RTCMultiConnection.csproj │ │ ├── RTCMultiConnectionHub.cs │ │ ├── RTCMultiConnectionSignaling.cs │ │ ├── Scripts/ │ │ │ ├── RTCMultiConnection.js │ │ │ ├── jquery-1.10.2.intellisense.js │ │ │ ├── jquery-1.10.2.js │ │ │ └── jquery.signalR-2.1.2.js │ │ ├── SignalRConnection.js │ │ ├── Web.Debug.config │ │ ├── Web.Release.config │ │ ├── Web.config │ │ ├── index.html │ │ └── packages.config │ ├── RTCMultiConnection.sln │ └── packages/ │ └── README.md ├── RTCPeerConnection/ │ ├── README.md │ ├── RTCPeerConnection-Helpers.js │ ├── RTCPeerConnection-v1.1.js │ ├── RTCPeerConnection-v1.2.js │ ├── RTCPeerConnection-v1.3.js │ ├── RTCPeerConnection-v1.4.js │ ├── RTCPeerConnection-v1.5.js │ ├── RTCPeerConnection-v1.6.js │ └── RTCPeerConnection.js ├── RTCall/ │ ├── README.md │ ├── RTCall.js │ └── index.html ├── Record-Entire-Meeting/ │ ├── .gitignore │ ├── .npmignore │ ├── Browser-Recording-Helper.js │ ├── Concatenate-Recordings.js │ ├── MediaStreamRecorder.js │ ├── Nodejs-Recording-Handler.js │ ├── README.md │ ├── Write-Recordings-To-Disk.js │ ├── fake-keys/ │ │ ├── certificate.pem │ │ └── privatekey.pem │ ├── index.html │ ├── package.json │ └── server.js ├── RecordRTC/ │ ├── .gitignore │ ├── .jshintrc │ ├── .npmignore │ ├── .travis.yml │ ├── CONTRIBUTING.md │ ├── Canvas-Recording/ │ │ ├── Canvas-Animation-Recording-Plus-Microphone-Plus-Mp3.html │ │ ├── Canvas-Animation-Recording-Plus-Microphone.html │ │ ├── Canvas-Animation-Recording-Plus-Mp3.html │ │ ├── Canvas-Animation-Recording.html │ │ ├── README.md │ │ ├── canvas-designer.js │ │ ├── index.html │ │ ├── record-canvas-drawings.html │ │ ├── video.webm │ │ └── webpage-recording.html │ ├── Gruntfile.js │ ├── LICENSE │ ├── MRecordRTC/ │ │ ├── README.md │ │ └── index.html │ ├── PHP-and-FFmpeg/ │ │ ├── .htaccess │ │ ├── README.md │ │ ├── index.html │ │ ├── save.php │ │ └── uploads/ │ │ └── README.md │ ├── README.md │ ├── RecordRTC-over-Socketio/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── ffmpeg-output/ │ │ │ └── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── server.js │ │ └── uploads/ │ │ └── README.md │ ├── RecordRTC-to-ASPNETMVC/ │ │ ├── README.md │ │ ├── RecordRTC_to_ASPNETMVC/ │ │ │ ├── Controllers/ │ │ │ │ ├── README.md │ │ │ │ └── RecordRTCController.cs │ │ │ ├── Global.asax │ │ │ ├── Global.asax.cs │ │ │ ├── Properties/ │ │ │ │ └── AssemblyInfo.cs │ │ │ ├── RecordRTC_to_ASPNETMVC.csproj │ │ │ ├── RecordRTC_to_ASPNETMVC.csproj.user │ │ │ ├── Views/ │ │ │ │ ├── RecordRTC/ │ │ │ │ │ └── Index.cshtml │ │ │ │ ├── Web.config │ │ │ │ └── _ViewStart.cshtml │ │ │ ├── Web.Debug.config │ │ │ ├── Web.Release.config │ │ │ ├── Web.config │ │ │ ├── bin/ │ │ │ │ └── RecordRTC_to_ASPNETMVC.pdb │ │ │ └── uploads/ │ │ │ └── README.md │ │ └── RecordRTC_to_ASPNETMVC.sln │ ├── RecordRTC-to-Nodejs/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ └── server.js │ ├── RecordRTC-to-PHP/ │ │ ├── .htaccess │ │ ├── README.md │ │ ├── delete.php │ │ ├── index.html │ │ ├── save.php │ │ └── uploads/ │ │ └── README.md │ ├── RecordRTC.js │ ├── WebGL-Recording/ │ │ ├── README.md │ │ ├── assets/ │ │ │ ├── DST-Canopy.ogg │ │ │ ├── blank.cur │ │ │ ├── duck.dae │ │ │ ├── pickup.ogg │ │ │ ├── scene.xml │ │ │ ├── seymourplane_triangulate.dae │ │ │ └── target.dae │ │ ├── index.html │ │ ├── logic.js │ │ ├── plotly.html │ │ ├── style.css │ │ ├── vendor/ │ │ │ ├── README.md │ │ │ ├── backbone-min.js │ │ │ ├── buzz.js │ │ │ ├── glge-compiled-min.js │ │ │ ├── glge-compiled.js │ │ │ └── underscore-min.js │ │ └── view.js │ ├── bower.json │ ├── dev/ │ │ ├── CanvasRecorder.js │ │ ├── Cross-Browser-Declarations.js │ │ ├── DiskStorage.js │ │ ├── GetRecorderType.js │ │ ├── GifRecorder.js │ │ ├── MRecordRTC.js │ │ ├── MediaStreamRecorder.js │ │ ├── MultiStreamRecorder.js │ │ ├── MultiStreamsMixer.js │ │ ├── README.md │ │ ├── RecordRTC-Configuration.js │ │ ├── RecordRTC.IndexedDB.js │ │ ├── RecordRTC.js │ │ ├── RecordRTC.promises.js │ │ ├── StereoAudioRecorder.js │ │ ├── Storage.js │ │ ├── WebAssemblyRecorder.js │ │ ├── Whammy.js │ │ ├── WhammyRecorder.js │ │ ├── amd.js │ │ └── isMediaRecorderCompatible.js │ ├── index.html │ ├── libs/ │ │ ├── EBML.js │ │ ├── gif-recorder.js │ │ ├── webm-wasm.js │ │ ├── webm-wasm.wasm │ │ └── webm-worker.js │ ├── npm-test.js │ ├── package.json │ ├── server.js │ ├── simple-demos/ │ │ ├── 16khz-audio-recording.html │ │ ├── README.md │ │ ├── Record-Mp3-or-Wav.html │ │ ├── RecordRTCPromisesHandler.html │ │ ├── RecordRTC_Extension.html │ │ ├── WebAssemblyRecorder.html │ │ ├── audio-recording.html │ │ ├── auto-stop-on-silence.html │ │ ├── bitsPerSecond.html │ │ ├── calculate-recording-duration.html │ │ ├── destroy.html │ │ ├── edge-audio-recording.html │ │ ├── embedded-iframes.html │ │ ├── gif-recording.html │ │ ├── index.html │ │ ├── isTypeSupported.html │ │ ├── multi-audios-recording.html │ │ ├── multi-cameras-recording.html │ │ ├── onStateChanged.html │ │ ├── onTimeStamp.html │ │ ├── ondataavailable-StereoAudioRecorder.html │ │ ├── ondataavailable.html │ │ ├── pass-getUserMedia-constraints.html │ │ ├── php-upload-jquery.html │ │ ├── php-upload-simple-javascript.html │ │ ├── preview-blob-size-during-recording.html │ │ ├── raw-pcm.html │ │ ├── record-cropped-screen.html │ │ ├── recording-html-element.html │ │ ├── reuse-same-instance.html │ │ ├── screen-recording.html │ │ ├── seeking-workaround.html │ │ ├── setRecordingDuration.html │ │ ├── show-animated-bar-on-video.html │ │ ├── show-logo-on-recorded-video.html │ │ ├── video-mirror-recording.html │ │ ├── video-plus-screen-recording.html │ │ └── video-recording.html │ └── test/ │ ├── README.md │ ├── audio-recording-using-StereoAudioRecorder.js │ ├── audio-recording.js │ ├── browserstack.config.js │ ├── canvas-recording.js │ ├── html-test-files/ │ │ ├── README.md │ │ ├── audio-recording-using-StereoAudioRecorder.html │ │ ├── audio-recording.html │ │ ├── canvas-recording.html │ │ ├── video-recording-using-WhammyRecorder.html │ │ └── video-recording.html │ ├── video-recording-using-WhammyRecorder.js │ └── video-recording.js ├── Reliable-Signaler/ │ ├── README.md │ ├── datachannel-client/ │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── server.js │ │ └── style.css │ ├── index.js │ ├── package.json │ ├── reliable-signaler.js │ ├── rtcmulticonnection-client/ │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── server.js │ │ └── style.css │ ├── signaler.js │ └── videoconferencing-client/ │ ├── README.md │ ├── RTCPeerConnection-v1.5.js │ ├── conference.js │ ├── index.html │ ├── package.json │ ├── server.js │ └── style.css ├── Signaling.md ├── Translator.js/ │ ├── README.md │ ├── Robot-Speaker.js │ ├── Translator.js │ └── index.html ├── WebRTC-File-Sharing/ │ ├── File.js │ ├── PeerConnection.js │ ├── README.md │ └── index.html ├── WebRTC-Scalable-Broadcast/ │ ├── .gitignore │ ├── .npmignore │ ├── README.md │ ├── RTCMultiConnection.js │ ├── WebRTC-Scalable-Broadcast.js │ ├── index.html │ ├── package.json │ ├── server.js │ └── share-files.html ├── audio-broadcast/ │ ├── README.md │ ├── broadcast-ui.js │ ├── broadcast.js │ └── index.html ├── broadcast/ │ ├── README.md │ ├── broadcast-ui.js │ ├── broadcast.js │ └── index.html ├── chat-hangout/ │ ├── README.md │ ├── hangout-ui.js │ ├── hangout.js │ └── index.html ├── demos/ │ ├── MediaStreamTrack.getSources.html │ ├── README.md │ ├── audio-only-streaming.html │ ├── client-side-datachannel.html │ ├── client-side-socket-io.html │ ├── client-side-websocket.html │ ├── client-side.html │ ├── remote-stream-recording.html │ ├── screen-and-video-from-single-peer.html │ └── switch-streams.html ├── desktop-sharing/ │ ├── README.md │ ├── index.html │ └── old.html ├── docs/ │ ├── How-to-Broadcast-Screen-using-WebRTC.html │ ├── README.md │ ├── RTP-usage.html │ ├── STUN-or-TURN.html │ ├── Share-Files-using-Filejs.html │ ├── TURN-server-installation-guide.html │ ├── WebRTC-PeerConnection.html │ ├── WebRTC-Signaling-Concepts.html │ ├── echo-cancellation.html │ ├── how-file-broadcast-works.html │ ├── how-to-WebRTC-video-conferencing.html │ ├── how-to-broadcast-video-using-RTCWeb-APIs.html │ ├── how-to-install-tabCapture-extension.html │ ├── how-to-share-audio-only-streams.html │ ├── how-to-switch-streams.html │ ├── how-to-use-plugin-free-calls.html │ ├── how-to-use-rtcdatachannel-and-rtcpeerconnectionjs.html │ ├── how-to-use-rtcdatachannel.html │ ├── how-to-use-rtcpeerconnection-js-v1.1.html │ ├── rtc-datachannel-for-beginners.html │ ├── webrtc-for-beginners.html │ ├── webrtc-for-newbies.html │ └── webrtcpedia/ │ └── index.html ├── experimental/ │ ├── README.md │ ├── mozCaptureStreamUntilEnded/ │ │ ├── README.md │ │ └── index.html │ ├── remote-media-stream-attachment/ │ │ ├── README.md │ │ └── index.html │ └── remote-stream-recording.html ├── ffmpeg/ │ ├── LICENSE │ ├── README.md │ ├── audio-plus-canvas-recording.html │ ├── audio-plus-screen-recording.html │ ├── index.html │ ├── merging-wav-and-webm-into-mp4.html │ ├── server.js │ ├── video-cropping.html │ ├── wav-to-aac.html │ ├── wav-to-ogg.html │ ├── webm-to-mp4.html │ └── worker-asm.js ├── file-hangout/ │ ├── README.md │ ├── file-hangout.js │ └── index.html ├── file-sharing/ │ ├── README.md │ ├── data-connection.js │ └── index.html ├── getDisplayMedia/ │ ├── README.md │ └── index.html ├── getMediaElement/ │ ├── .npmignore │ ├── README.md │ ├── getAudioElement.html │ ├── getMediaElement-v1.2/ │ │ ├── getMediaElement-v1.2.css │ │ └── getMediaElement-v1.2.js │ ├── getMediaElement.css │ ├── getMediaElement.js │ ├── getVideoElement.html │ ├── index.html │ └── package.json ├── getScreenId.js/ │ ├── .npmignore │ ├── LICENSE │ ├── README.md │ ├── Screen-Capturing.js │ ├── getScreenId.html │ ├── getScreenId.js │ ├── index.html │ ├── package.json │ └── server.js ├── getStats/ │ ├── .gitignore │ ├── .npmignore │ ├── .travis.yml │ ├── Gruntfile.js │ ├── LICENSE.md │ ├── README.md │ ├── bower.json │ ├── dev/ │ │ ├── README.md │ │ ├── bweforvideo.js │ │ ├── candidate-pair.js │ │ ├── dataSentReceived.js │ │ ├── datachannel.js │ │ ├── getStats.js │ │ ├── globals.js │ │ ├── googCertificate.js │ │ ├── googCodecName.audio.js │ │ ├── googCodecName.video.js │ │ ├── head.js │ │ ├── inbound-rtp.js │ │ ├── local-candidate.js │ │ ├── module.exports.js │ │ ├── outbound-rtp.js │ │ ├── parameters.js │ │ ├── remote-candidate.js │ │ ├── ssrc.js │ │ ├── tail.js │ │ ├── track.js │ │ └── wrapper.js │ ├── getStats.js │ ├── index.html │ ├── package.json │ └── server.js ├── gumadapter/ │ ├── README.md │ ├── gumadapter.js │ └── package.json ├── hark/ │ ├── README.md │ └── hark.js ├── meeting/ │ ├── README.md │ ├── index.html │ ├── meeting.js │ └── simple.html ├── navigator.customGetUserMediaBar/ │ ├── README.md │ ├── index.html │ └── navigator.customGetUserMediaBar.js ├── one-to-many-audio-broadcasting/ │ ├── README.md │ ├── index.html │ └── meeting.js ├── one-to-many-video-broadcasting/ │ ├── README.md │ ├── index.html │ └── meeting.js ├── part-of-screen-sharing/ │ ├── README.md │ ├── firebase/ │ │ └── index.html │ ├── iframe/ │ │ ├── index.html │ │ └── otherpage.html │ ├── realtime-chat/ │ │ ├── No-WebRTC-Chat.html │ │ ├── README.md │ │ └── how-this-work.html │ ├── screenshot-dev.js │ ├── screenshot.js │ └── webrtc-data-channel/ │ └── index.html ├── realtime-pluginfree-calls/ │ ├── README.md │ ├── index.html │ ├── js/ │ │ ├── PeerConnection.js │ │ ├── linkify.js │ │ └── script.js │ ├── old/ │ │ ├── README.md │ │ ├── call-initiator.js │ │ ├── index.html │ │ └── ui.js │ ├── old2/ │ │ └── index.html │ └── style.css ├── screen-broadcast/ │ ├── README.md │ └── index.html ├── screen-sharing/ │ ├── README.md │ ├── index.html │ └── screen.js ├── server.js ├── socket.io/ │ ├── PeerConnection.js │ ├── README.md │ └── index.html ├── socketio-over-nodejs/ │ ├── README.md │ ├── fake-keys/ │ │ ├── certificate.pem │ │ └── privatekey.pem │ ├── package.json │ └── server.js ├── text-chat/ │ ├── README.md │ ├── data-connection.js │ └── index.html ├── video-conferencing/ │ ├── README.md │ ├── conference.js │ ├── index.html │ └── server.js ├── webrtc-broadcasting/ │ ├── README.md │ ├── broadcast.js │ ├── index.html │ └── server.js ├── websocket/ │ ├── PeerConnection.js │ ├── README.md │ └── index.html └── websocket-over-nodejs/ ├── README.md ├── fake-keys/ │ ├── certificate.pem │ └── privatekey.pem ├── package.json └── server.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ RecordRTC/RecordRTC-to-Nodejs/uploads/ *.DS_Store ffmpeg/ffmpeg_asm.js node_modules RecordRTC/node_modules DetectRTC/node_modules RTCMultiConnection/node_modules ================================================ FILE: Canvas-Designer/.gitignore ================================================ node_modules bower_components *.tar.gz lib-cov .*.swp ._* .DS_Store .git .hg .npmrc .lock-wscript .svn .wafpickle-* config.gypi CVS npm-debug.log ================================================ FILE: Canvas-Designer/.npmignore ================================================ # ignore everything * # but not these files... !canvas-designer-widget.js !widget.html !widget.js !widget.min.js !dev/webrtc-handler.js !index.html !package.json !bower.json !server.js !README.md ================================================ FILE: Canvas-Designer/.travis.yml ================================================ language: node_js node_js: - "0.11" install: npm install before_script: - npm install grunt-cli@0.1.13 -g - npm install grunt@0.4.5 - grunt --verbose after_failure: npm install && grunt matrix: fast_finish: true ================================================ FILE: Canvas-Designer/Gruntfile.js ================================================ 'use strict'; module.exports = function(grunt) { require('load-grunt-tasks')(grunt, { pattern: 'grunt-*', config: 'package.json', scope: 'devDependencies' }); var banner = '// Last time updated: <%= grunt.template.today("UTC:yyyy-mm-dd h:MM:ss TT Z") %>\n\n'; // configure project grunt.initConfig({ // make node configurations available pkg: grunt.file.readJSON('package.json'), concat: { options: { stripBanners: true, separator: '\n', banner: banner }, dist: { src: [ 'dev/head.js', 'dev/common.js', 'dev/draw-helper.js', 'dev/drag-helper.js', 'dev/pencil-handler.js', 'dev/marker-handler.js', 'dev/eraser-handler.js', 'dev/text-handler.js', 'dev/arc-handler.js', 'dev/line-handler.js', 'dev/arrow-handler.js', 'dev/rect-handler.js', 'dev/quadratic-handler.js', 'dev/bezier-handler.js', 'dev/zoom-handler.js', 'dev/file-selector.js', 'dev/image-handler.js', 'dev/pdf-handler.js', 'dev/data-uris.js', 'dev/decorator.js', 'dev/events-handler.js', 'dev/share-drawings.js', 'dev/webrtc-handler.js', 'dev/canvas-designer-widget.js', 'dev/tail.js' ], dest: 'widget.js', }, }, uglify: { options: { mangle: false, banner: banner }, my_target: { files: { 'widget.min.js': ['widget.js'] } } }, jsbeautifier: { files: ['widget.js', 'dev/*.js'], options: { js: { braceStyle: "collapse", breakChainedMethods: false, e4x: false, evalCode: false, indentChar: " ", indentLevel: 0, indentSize: 4, indentWithTabs: false, jslintHappy: false, keepArrayIndentation: false, keepFunctionIndentation: false, maxPreserveNewlines: 10, preserveNewlines: true, spaceBeforeConditional: true, spaceInParen: false, unescapeStrings: false, wrapLineLength: 0 }, html: { braceStyle: "collapse", indentChar: " ", indentScripts: "keep", indentSize: 4, maxPreserveNewlines: 10, preserveNewlines: true, unformatted: ["a", "sub", "sup", "b", "i", "u"], wrapLineLength: 0 }, css: { indentChar: " ", indentSize: 4 } } }, bump: { options: { files: ['package.json', 'bower.json'], updateConfigs: [], commit: true, commitMessage: 'v%VERSION%', commitFiles: ['package.json', 'bower.json'], createTag: true, tagName: '%VERSION%', tagMessage: '%VERSION%', push: false, pushTo: 'upstream', gitDescribeOptions: '--tags --always --abbrev=1 --dirty=-d' } }, watch: { scripts: { files: ['dev/*.js'], tasks: ['concat', 'jsbeautifier', 'uglify'], options: { spawn: false, }, }, } }); // enable plugins // set default tasks to run when grunt is called without parameters // http://gruntjs.com/api/grunt.task grunt.registerTask('default', ['concat', 'jsbeautifier', 'uglify']); grunt.loadNpmTasks('grunt-contrib-watch'); }; ================================================ FILE: Canvas-Designer/Help/index.html ================================================  Canvas Designer Help ® Muaz Khan

Canvas Designer Help

Introducing the tool!

Canvas Designer is a tool aimed to give you a full-fledged drawing surface and also auto generate appropriate code for you in different formats!

It targets Canvas 2D Context – i.e. it gives you a built-in IDE for Canvas 2D APIs!

Obviously, it is developed & designed solely by !

How files are arranged?

Each task or feature has a unique JavaScript file. It simplifies not only coding but also later edits and additions!

Common task like output in textarea, global variables/objects etc. have been placed in common.js file.

Dragging relevant code has been placed in drag-helper.js and drawing relevant code has been placed in draw-helper.js file. (Note: draw-helper.js is most important file in this project because functions it contains are repeatedly called from all portions of the project!)

events-handler.js is used to handle global events like mousemove, mouseup, mousedown and their touch relevant!

There is a file decorator.js which is aimed to decorate the designer; i.e. to decorate the design surface, toolbox, etc.

How I store coordinates?

There is a global variable named points of type array which stores all x/y coordinates plus extra values in the following style:

var points = [['shape', [x, y], [lineWidth, strokeStyle, fillStyle, globalAlpha, globalCompositeOperation, lineCap, lineJoin]]];

i.e.

var points = [path-name, path-coordinates, path-options];

Where:

  1. path-name can be line/arc/rect/quadratic/bezier etc.
  2. path-coordinates are the shape's x/y coordinates + width, height, radius etc.
  3. path-options are line-width, stroke/fill styles, global alpha/composite-operation, line cap/join etc.

Digging into the code!

draw-helper.js defines a method named "redraw" that is one of the most called functions in this project. redraw aimed to refresh all points on the drawing surface! By default, this function is called on each mousedown/mouseup event; however, in some cases, it is also called in mousemove event. (See drag-helper.js file)

common.js defines second most called function "updateTextArea" which is aimed to update the output accordingly.

In the top of common.js file; there is an object "is" which contains Boolean properties for selected toolbox shape(s). Mouse/Touch events in the events-handler.js are using these properties to call appropriate objects!

How to copy and paste?

You can use Ctrl + C for copy and Ctrl + V for paste. You can also use Ctrl + MouseDown for directly copy and paste!

You can copy in two formats:

  1. Copy last path
  2. Copy all paths (at a time!)

Remember: Ctrl + MouseDown will only work if one option is selected: DragAllPaths or DragLastPath (from tool-box!)

How to Undo?

You can undo using Ctrl + Z.

You can also use Ctrl + A to select all drawing for dragging purpose!

How to Contribute?

If you want to add new features in the Canvas Designer; follow these steps:

1st Step: Add new toolbox icon!

You've to add something through which end-users can use/access your feature – you can add any HTML element for this purpose (e.g. checkbox, radio-button, button, etc.) – however I recommend you add new toolbox icon instead! (which is a <canvas> element!)

<div id="tool-box" class="tool-box">
	........................................
	<canvas id="icon-unique-id" width="40" height="40"></canvas>
</div>

2nd Step: Decorate the icon!

After adding above HTML code, if you refresh the page; you'll see an empty icon appended in the bottom of the toolbox. Now you've to decorate this icon so end users can understand it!

Open decorator.js file and append following code anywhere in the file.

function givenName() {
	var context = getContext('icon-unique-id');

	// 2d context relevant code here! (decoration code!!!)

	bindEvent(context, 'FeatureSelected');
}
givenName();

FeatureSelected is a global property used to understand the current state of your new toolbox icon (whether icon is selected or not). (Open common.js file)

var is = {
	......................
	isFeatureSelected: false,

	set: function (shape) {
		...................... = is.isFeatureSelected = false;
		.............................................
	}
};

3rd Step: Create new JavaScript file

Now you've to create a new javascript file where you'll put all your "new feature" relevant code! Name the file like this: feature-handler.js

var featureHandler = {
	ismousedown: false,

	mousedown: function(e) {
		this.ismousedown = true;
	},
	
	mouseup: function(e) { 
		this.ismousedown = false;
	},
	
	mousemove: function(e) {
		if(this.ismousedown) { ... }
	}
};

4th Step: Bind mouse events

Now you've to bind mouse events to your newly created object. Open events-handler.js and append following code:

addEvent(canvas, isTouch ? 'touchstart' : 'mousedown', function (e) {
	............................................................
	else if (is.isFeatureSelected) featureHandler.mousedown(e);
	............................................................
});

addEvent(document, isTouch ? 'touchend' : 'mouseup', function (e) {
	............................................................
	else if (is.isFeatureSelected) featureHandler.mouseup(e);
	............................................................
});

addEvent(canvas, isTouch ? 'touchmove' : 'mousemove', function (e) {
	............................................................
	else if (is.isFeatureSelected) featureHandler.mousemove(e);
	............................................................
});

Now you are 80% done! I strongly recommend you place your feature's drawing relevant code in the draw-helper.js file as I did for other features! - Reusability!!

5th Step: Handle the output (textarea!)

This is the last step. You've to handle the output for textarea.

Open common.js file; there is a function "updateTextArea" in the "common" object – which is aimed to output into textarea element.

You don't have to change "updateTextArea". For simplicity purpose, code is separated in different functions/properties that you've to edit:

  1. common.forLoop
  2. common.absoluteNOTShortened
  3. common.relativeShortened
  4. common.relativeNOTShortened

Wait! – You've to edit "init" function and drag-helper.js file too! – See below div.

What "init" function in drag-helper.js do?

This function is aimed to draw little transparent-red circles around the stretchable points of the shape so end-user can stretch the shape using those points! (To test it; draw a line, then click arrow [first-icon] on toolbox!) – This function is called on mousemove event.

if (p[0] === 'your-shape') {

	tempContext.beginPath();

	tempContext.arc(point[0], point[1], 10, Math.PI * 2, 0, !1);
	tempContext.arc(point[2], point[3], 10, Math.PI * 2, 0, !1);
	
	// For additional circles, copy above line and only change point's index!

	tempContext.fill();
}

You've to edit other two functions in the drag-helper.js file (It is absolutely necessary because it helps change points accordingly while dragging the shape!):

  1. dragHelper.dragLastPath
  2. dragHelper.dragAllPaths
/*≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡*/
// in "dragAllPaths":
if (p[0] === 'your-shape-name') {
	
	points[i] = [p[0], [
		getPoint(x, prevX, point[0]),
		getPoint(y, prevY, point[1]),
	], p[2]];
}

/*≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡*/
// in "dragLastPath":
if (p[0] === 'your-shape-name') {

	if (g.pointsToMove === 'moveTo-points' || isMoveAllPoints) {
		point[0] = getPoint(x, prevX, point[0]);
		point[1] = getPoint(y, prevY, point[1]);
	}

	points[points.length - 1] = [p[0], point, p[2]];
}

/*≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡*/
// in "mousedown" event (in the same file!)
if (p[0] === 'your-shape-name') {

	if (dHelper.isPointInPath(x, y, point[0], point[1])) {
		g.pointsToMove = 'moveTo-points';
	}
}
/*≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡*/

Now you're 100% done!

How I drag shapes?

I did a detailed post about this: Dragging/Moving shapes smoothly using Canvas 2d APIs

In the drag-helper.js file – "global" object contains following three properties:

  1. prevX
  2. prevY
  3. pointsToMove

prevX/prevY aimed to save previous mousedown points so we can extract the dragged distance accordingly!

pointsToMove aimed to understand which point of the shape is to move – e.g. first control points of the last Bezier curve or second control points (or moving/ending points!)

dragHelper.getPoint: function (point, prev, otherPoint) {
	if (point > prev) point = otherPoint + (point - prev);
	else point = otherPoint - (prev - point);

	return point;
}

dragHelper.getPoint returns the dragged distance!

Explaining relative and absolute coordinates

By default, output in the textarea is provided in absolute coordinates. However, you can choose relative option too. Relative means points are relative to each other. If one or two points changes; all subsequent also changes, accordingly. Relative coordinates help you drag/move shape(s) at any portion of the screen without any bit of disturbance in the original shape!

You can set pageX/pageY (or offset top/left) values to x-y objects and move whole shape on mousemove/mouseup/mousedown or at any other event!

You can get shortened code for both formats: relative and absolute! Normal code is also provided there!

================================================ FILE: Canvas-Designer/LICENSE ================================================ The MIT License (MIT) Copyright (c) 2017 [Muaz Khan](https://github.com/muaz-khan) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Canvas-Designer/README.md ================================================ # [Canvas Designer](https://github.com/muaz-khan/Canvas-Designer) / [API Referencee](https://github.com/muaz-khan/Canvas-Designer#api-reference) ## Demo: https://www.webrtc-experiment.com/Canvas-Designer/ ## Advance Demo: [demos/dashboard/](https://rtcmulticonnection.herokuapp.com/demos/dashboard/) Multiple designers demo: https://www.webrtc-experiment.com/Canvas-Designer/multiple.html ### YouTube video: * https://www.youtube.com/watch?v=pvAj5l_v3cM [![npm](https://img.shields.io/npm/v/canvas-designer.svg)](https://npmjs.org/package/canvas-designer) [![downloads](https://img.shields.io/npm/dm/canvas-designer.svg)](https://npmjs.org/package/canvas-designer) [![Build Status: Linux](https://travis-ci.org/muaz-khan/Canvas-Designer.png?branch=master)](https://travis-ci.org/muaz-khan/Canvas-Designer) > "Collaborative" [Canvas Designer](https://github.com/muaz-khan/Canvas-Designer) i.e. Canvas-Drawing tool allows you draw bezier/quadratic curves, rectangles, circles and lines. You can also set strokes, back/forth colors and much more. You can draw using pencils, erase drawing, type texts etc. You can [easily add your own tools](https://www.webrtc-experiment.com/Canvas-Designer/Help/#contribute). **You can check all releases here:** * https://github.com/muaz-khan/Canvas-Designer/releases The specialty of this drawing-tool is that, it generates Canvas2D code for you; so simply draw and get the code! That code can be used in any javascript Canvas2D application. **You can submit issues here:** * https://github.com/muaz-khan/Canvas-Designer/issues Also, you can collaborate your drawing with up to 15 users; and everything is synced from all users. So, if you draw a line and your friend-A draws quadratic curve and friend-B draws rectangle then everything will be synced among all users! ### Youtube Videos * https://www.youtube.com/watch?v=oSSwMlBu8SY Gif images: * https://www.webrtc-experiment.com/images/Canvas-Designer.gif # Built-in tools You can use [`designer.setSelected`](https://github.com/muaz-khan/Canvas-Designer#setselected) or [`designer.setTools`](https://github.com/muaz-khan/Canvas-Designer#settools) for below tools. 1. `line` --- to draw straight lines 2. `pencil` --- to write/draw shapes 3. `dragSingle` --- to drag/ove and especially **resize** last selected shape 4. `dragMultiple` --- to drag/move all shapes 5. `eraser` --- to erase/clear specific portion of shapes 6. `rectangle` --- to draw rectangles 7. `arc` --- to draw circles 8. `bezier` --- to draw bezier curves 9. `quadratic` --- to draw quadratic curves 10. `text` --- to write texts on single or multiple lines, select font families/sizes and more 11. `image` --- add external images 12. `arrow` --- draw arrow lines 13. `marker` --- draw markers 14. `lineWidth` --- set line width 15. `colorsPicker` --- background and foreground colors picker 16. `extraOptions` --- extra options eg. lineCap, lineJoin, globalAlpha, globalCompositeOperation etc. 17. `pdf` --- to import PDF 18. `code` --- to enable/disable code view 19. `undo` --- undo recent shapes The correct name for `dragSingle` should be: `drag-move-resize last-selected-shape`. The correct name for `dragMultiple` should be: `drag-move all-shapes`. ### Upcoming tools 1. Allow users to add video-streams or screen-streams or existing-webm-mp4-videos 2. Resize all shapes at once (currently you can resize last selected shape only) # Features 1. Draw single or multiple shapes of any kind (according to toolbox) 2. Drag/resize/adjust all the shapes in any possible direction 3. Rectangles and images can be resized in 4-directions Red transparent small circles helps you understand how to resize. 4. Undo drawings using `ctrl+z` keys (undo all shapes, undo last 10 or specific shapes, undo range of shapes or undo last shape) 5. Drag/move single or all the shapes without affecting any single coordinate More importantly, you can use unlimited designers on a single page. Each will have its own surface and its own tools. # Chinese, Arabic, other languages You can install following chrome extension for multi-language input tools: * https://chrome.google.com/webstore/detail/google-input-tools/mclkkofklkfljcocdinagocijmpgbhab?hl=en Now type your own language text in any `` box or anywhere, and simply copy that text. Now click `T` tool icon from the tool-box and press `ctrl+v` to paste your own language's text. **To repeat it:** 1. Type your own language texts anywhere and make sure to copy to clipboard using `ctrl+v` 2. Then click `T` icon, and then press `ctrl+v` to paste your copied text You can paste any text: English, Arabic, Chinese etc. # How to Use 1. Download/link `canvas-designer-widget.js` from [this github repository](https://github.com/muaz-khan/Canvas-Designer). 2. Set `designer.widgetHtmlURL` and `designer.widgetJsURL` in your HTML file. 3. Use this command to append widget in your HTML page: `var designer = new CanvasDesigner();` `designer.appendTo(document.body);` ```html ``` # Complete Usage ```javascript var designer = new CanvasDesigner(); websocket.onmessage = function(event) { designer.syncData( JSON.parse(event.data) ); }; designer.addSyncListener(function(data) { websocket.send(JSON.stringify(data)); }); designer.setSelected('pencil'); designer.setTools({ pencil: true, text: true }); designer.appendTo(document.documentElement); ``` It is having `designer.destroy()` method as well. # Use [WebRTC](http://www.rtcmulticonnection.org/docs/)! ```javascript webrtc.onmessage = function(event) { designer.syncData( event.data ); }; designer.addSyncListener(function(data) { webrtc.send(data); }); ``` # Use Socket.io ```javascript socket.on('message', function(data) { designer.syncData( data ); }); designer.addSyncListener(function(data) { socket.emit('message', data); }); ``` # API Reference ## `widgetHtmlURL` You can place `widget.html` file anywhere on your site. ```javascript designer.widgetHtmlURL = '/html-files/widget.html'; ``` By default `widget.html` is placed in the same directory of `index.html`. ```javascript // here is default value designer.widgetHtmlURL = 'widget.html'; ``` Remember, `widget.html` is loaded using ` --> WebRTC AntMedia Broadcast Viewer
================================================ FILE: Chrome-Extensions/desktopCapture-p2p/server.js ================================================ // http://127.0.0.1:9001 // http://localhost:9001 var server = require('http'), url = require('url'), path = require('path'), fs = require('fs'); function serverHandler(request, response) { var uri = url.parse(request.url).pathname, filename = path.join(process.cwd(), uri); fs.exists(filename, function(exists) { if (!exists) { response.writeHead(404, { 'Content-Type': 'text/plain' }); response.write('404 Not Found: ' + filename + '\n'); response.end(); return; } if (filename.indexOf('favicon.ico') !== -1) { return; } var isWin = !!process.platform.match(/^win/); if (fs.statSync(filename).isDirectory() && !isWin) { filename += '/index.html'; } else if (fs.statSync(filename).isDirectory() && !!isWin) { filename += '\\index.html'; } fs.readFile(filename, 'binary', function(err, file) { if (err) { response.writeHead(500, { 'Content-Type': 'text/plain' }); response.write(err + '\n'); response.end(); return; } var contentType; if (filename.indexOf('.html') !== -1) { contentType = 'text/html'; } if (filename.indexOf('.js') !== -1) { contentType = 'application/javascript'; } if (contentType) { response.writeHead(200, { 'Content-Type': contentType }); } else response.writeHead(200); response.write(file, 'binary'); response.end(); }); }); } var app; app = server.createServer(serverHandler); app = app.listen(process.env.PORT || 9001, process.env.IP || "0.0.0.0", function() { var addr = app.address(); console.log("Server listening at", addr.address + ":" + addr.port); }); ================================================ FILE: Chrome-Extensions/file-sharing/README.md ================================================ # Chrome Extension to share files ## Disclaimer No more maintaining this extension; as of 2019. So please use at your own risk. * https://www.webrtc-experiment.com/disclaimer/ ## License [Chrome-Extensions](https://github.com/muaz-khan/Chrome-Extensions) are released under [MIT license](https://github.com/muaz-khan/Chrome-Extensions/blob/master/LICENSE) . Copyright (c) [Muaz Khan](https://MuazKhan.com). ================================================ FILE: Chrome-Extensions/file-sharing/background.js ================================================ // background.js chrome.runtime.onConnect.addListener(function(port) { port.onMessage.addListener(portOnMessageHanlder); var progressStarted = false; function portOnMessageHanlder(message) { if (!message || !message.changeIcon) { return; } if (message.defaultIcon) { chrome.browserAction.setIcon({ path: 'images/fileCapture128.png' }); progressStarted = false; } if (message.path && !progressStarted) { progressStarted = true; chrome.browserAction.setIcon({ path: 'images/progress.gif' }); } if(message.path){ setBadgeText(message.percentage); } if(message.connected) { // setBadgeText('conn', message.color); } } }); function setBadgeText(percentage, color) { chrome.browserAction.setBadgeBackgroundColor({ color: color || [0, 0, 0, 255] }); chrome.browserAction.setBadgeText({ text: percentage }); chrome.browserAction.setTitle({ title: percentage + ' file progress' }); } ================================================ FILE: Chrome-Extensions/file-sharing/info.html ================================================  About | Help | How to use?

How to use?

You may wanna check options page.

This chrome extension is open-sourced here:
https://github.com/muaz-khan/Chrome-Extensions/tree/master/file-sharing

Here is Live URL: webrtcweb.com/fs

You can share files with android/ios users as well:
https://play.google.com/store/apps/details?id=com.webrtc.experiment
iOS link will be added soon.

Hints:



How it use?



How it works?



Please read more on wikipedia: https://en.wikipedia.org/wiki/Stream_Control_Transmission_Protocol

This extension is deployed here: https://chrome.google.com/webstore/detail/webrtc-file-sharing/nbnncbdkhpmbnkfngmkdbepoemljbnfo ================================================ FILE: Chrome-Extensions/file-sharing/manifest.json ================================================ { "name" : "WebRTC File Sharing", "short_name" : "FileSharing", "author": "Muaz Khan", "version" : "2.6", "manifest_version" : 2, "minimum_chrome_version": "34", "description" : "Instant/Private/Reliable file sharing across all devices(mobile/desktop), among single or multiple users.", "homepage_url": "https://webrtcweb.com/fs", "background": { "scripts": ["background.js"], "persistent": false }, "browser_action" : { "default_icon" : "images/fileCapture22.png", "default_title" : "File Sharing", "default_popup": "popup.html" }, "icons" : { "16" : "images/fileCapture16.png", "22" : "images/fileCapture22.png", "32" : "images/fileCapture32.png", "48" : "images/fileCapture48.png", "128": "images/fileCapture128.png" }, "permissions": ["tabs", "storage", ""], "web_accessible_resources": [ "images/fileCapture48.png" ], "options_ui": { "page": "options.html", "chrome_style": true } } ================================================ FILE: Chrome-Extensions/file-sharing/options.html ================================================ 

Set Your Own Room ID:


It will use your room-id instead of generating random string.
E.g. You can always share this with screen viewers:
https://webrtcweb.com/fs#your_room_id

Set File Chunk Size:


If your file receivers are using Firefox, then set "15000" which is maximum reciving limit in Firefox.
Wanna learn "how to use"? ================================================ FILE: Chrome-Extensions/file-sharing/options.js ================================================ chrome.storage.sync.get(null, function(items) { if (items['room_id']) { document.getElementById('room_id').value = items['room_id']; } if (items['chunk_size']) { document.getElementById('chunk_size').value = items['chunk_size']; } }); document.getElementById('room_id').onblur = function() { this.disabled = true; chrome.storage.sync.set({ room_id: this.value }, function() { document.getElementById('room_id').disabled = false; }); }; document.getElementById('chunk_size').onblur = function() { this.disabled = true; chrome.storage.sync.set({ chunk_size: this.value }, function() { document.getElementById('chunk_size').disabled = false; }); }; ================================================ FILE: Chrome-Extensions/file-sharing/popup.html ================================================ 
You are NOT connected to any room.
Click to connect.

You didn't send/receve any file yet.

Select & share your own files.

Change RoomID or learn how to use?
================================================ FILE: Chrome-Extensions/file-sharing/popup.js ================================================ // popup.js /*! jQuery v2.2.2 | (c) jQuery Foundation | jquery.org/license */ !function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=a.document,e=c.slice,f=c.concat,g=c.push,h=c.indexOf,i={},j=i.toString,k=i.hasOwnProperty,l={},m="2.2.2",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return e.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:e.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a){return n.each(this,a)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(e.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor()},push:g,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(n.isPlainObject(d)||(e=n.isArray(d)))?(e?(e=!1,f=c&&n.isArray(c)?c:[]):f=c&&n.isPlainObject(c)?c:{},g[b]=n.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray,isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){var b=a&&a.toString();return!n.isArray(a)&&b-parseFloat(b)+1>=0},isPlainObject:function(a){var b;if("object"!==n.type(a)||a.nodeType||n.isWindow(a))return!1;if(a.constructor&&!k.call(a,"constructor")&&!k.call(a.constructor.prototype||{},"isPrototypeOf"))return!1;for(b in a);return void 0===b||k.call(a,b)},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?i[j.call(a)]||"object":typeof a},globalEval:function(a){var b,c=eval;a=n.trim(a),a&&(1===a.indexOf("use strict")?(b=d.createElement("script"),b.text=a,d.head.appendChild(b).parentNode.removeChild(b)):c(a))},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b){var c,d=0;if(s(a)){for(c=a.length;c>d;d++)if(b.call(a[d],d,a[d])===!1)break}else for(d in a)if(b.call(a[d],d,a[d])===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):g.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:h.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;c>d;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,e,g=0,h=[];if(s(a))for(d=a.length;d>g;g++)e=b(a[g],g,c),null!=e&&h.push(e);else for(g in a)e=b(a[g],g,c),null!=e&&h.push(e);return f.apply([],h)},guid:1,proxy:function(a,b){var c,d,f;return"string"==typeof b&&(c=a[b],b=a,a=c),n.isFunction(a)?(d=e.call(arguments,2),f=function(){return a.apply(b||this,d.concat(e.call(arguments)))},f.guid=a.guid=a.guid||n.guid++,f):void 0},now:Date.now,support:l}),"function"==typeof Symbol&&(n.fn[Symbol.iterator]=c[Symbol.iterator]),n.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(a,b){i["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=!!a&&"length"in a&&a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ga(),z=ga(),A=ga(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+M+"))|)"+L+"*\\]",O=":("+M+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+N+")*)|.*)\\)|)",P=new RegExp(L+"+","g"),Q=new RegExp("^"+L+"+|((?:^|[^\\\\])(?:\\\\.)*)"+L+"+$","g"),R=new RegExp("^"+L+"*,"+L+"*"),S=new RegExp("^"+L+"*([>+~]|"+L+")"+L+"*"),T=new RegExp("="+L+"*([^\\]'\"]*?)"+L+"*\\]","g"),U=new RegExp(O),V=new RegExp("^"+M+"$"),W={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),TAG:new RegExp("^("+M+"|[*])"),ATTR:new RegExp("^"+N),PSEUDO:new RegExp("^"+O),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+L+"*(even|odd|(([+-]|)(\\d*)n|)"+L+"*(?:([+-]|)"+L+"*(\\d+)|))"+L+"*\\)|)","i"),bool:new RegExp("^(?:"+K+")$","i"),needsContext:new RegExp("^"+L+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+L+"*((?:-\\d)?\\d*)"+L+"*\\)|)(?=[^-]|$)","i")},X=/^(?:input|select|textarea|button)$/i,Y=/^h\d$/i,Z=/^[^{]+\{\s*\[native \w/,$=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,_=/[+~]/,aa=/'|\\/g,ba=new RegExp("\\\\([\\da-f]{1,6}"+L+"?|("+L+")|.)","ig"),ca=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},da=function(){m()};try{H.apply(E=I.call(v.childNodes),v.childNodes),E[v.childNodes.length].nodeType}catch(ea){H={apply:E.length?function(a,b){G.apply(a,I.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function fa(a,b,d,e){var f,h,j,k,l,o,r,s,w=b&&b.ownerDocument,x=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==x&&9!==x&&11!==x)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==x&&(o=$.exec(a)))if(f=o[1]){if(9===x){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(w&&(j=w.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(o[2])return H.apply(d,b.getElementsByTagName(a)),d;if((f=o[3])&&c.getElementsByClassName&&b.getElementsByClassName)return H.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==x)w=b,s=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(aa,"\\$&"):b.setAttribute("id",k=u),r=g(a),h=r.length,l=V.test(k)?"#"+k:"[id='"+k+"']";while(h--)r[h]=l+" "+qa(r[h]);s=r.join(","),w=_.test(a)&&oa(b.parentNode)||b}if(s)try{return H.apply(d,w.querySelectorAll(s)),d}catch(y){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(Q,"$1"),b,d,e)}function ga(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ha(a){return a[u]=!0,a}function ia(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ja(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function ka(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||C)-(~a.sourceIndex||C);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function la(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function na(a){return ha(function(b){return b=+b,ha(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function oa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=fa.support={},f=fa.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=fa.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ia(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ia(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Z.test(n.getElementsByClassName),c.getById=ia(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}},d.filter.ID=function(a){var b=a.replace(ba,ca);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(ba,ca);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return"undefined"!=typeof b.getElementsByClassName&&p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=Z.test(n.querySelectorAll))&&(ia(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+L+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+L+"*(?:value|"+K+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ia(function(a){var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+L+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Z.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ia(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",O)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Z.test(o.compareDocumentPosition),t=b||Z.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return ka(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?ka(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},fa.matches=function(a,b){return fa(a,null,null,b)},fa.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(T,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return fa(b,n,null,[a]).length>0},fa.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},fa.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},fa.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},fa.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=fa.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=fa.selectors={cacheLength:50,createPseudo:ha,match:W,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(ba,ca),a[3]=(a[3]||a[4]||a[5]||"").replace(ba,ca),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||fa.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&fa.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return W.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&U.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(ba,ca).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+L+")"+a+"("+L+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=fa.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(P," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||fa.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ha(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ha(function(a){var b=[],c=[],d=h(a.replace(Q,"$1"));return d[u]?ha(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ha(function(a){return function(b){return fa(a,b).length>0}}),contains:ha(function(a){return a=a.replace(ba,ca),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ha(function(a){return V.test(a||"")||fa.error("unsupported lang: "+a),a=a.replace(ba,ca).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Y.test(a.nodeName)},input:function(a){return X.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:na(function(){return[0]}),last:na(function(a,b){return[b-1]}),eq:na(function(a,b,c){return[0>c?c+b:c]}),even:na(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:na(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:na(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:na(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function ra(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j,k=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(j=b[u]||(b[u]={}),i=j[b.uniqueID]||(j[b.uniqueID]={}),(h=i[d])&&h[0]===w&&h[1]===f)return k[2]=h[2];if(i[d]=k,k[2]=a(b,c,g))return!0}}}function sa(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ta(a,b,c){for(var d=0,e=b.length;e>d;d++)fa(a,b[d],c);return c}function ua(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(c&&!c(f,d,e)||(g.push(f),j&&b.push(h)));return g}function va(a,b,c,d,e,f){return d&&!d[u]&&(d=va(d)),e&&!e[u]&&(e=va(e,f)),ha(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ta(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:ua(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=ua(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=ua(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function wa(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ra(function(a){return a===b},h,!0),l=ra(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[ra(sa(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return va(i>1&&sa(m),i>1&&qa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(Q,"$1"),c,e>i&&wa(a.slice(i,e)),f>e&&wa(a=a.slice(e)),f>e&&qa(a))}m.push(c)}return sa(m)}function xa(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=F.call(i));u=ua(u)}H.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&fa.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ha(f):f}return h=fa.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=wa(b[c]),f[u]?d.push(f):e.push(f);f=A(a,xa(e,d)),f.selector=a}return f},i=fa.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(ba,ca),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=W.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(ba,ca),_.test(j[0].type)&&oa(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&qa(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,!b||_.test(a)&&oa(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ia(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ia(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ja("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ia(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ja("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ia(function(a){return null==a.getAttribute("disabled")})||ja(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),fa}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.uniqueSort=n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},v=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},w=n.expr.match.needsContext,x=/^<([\w-]+)\s*\/?>(?:<\/\1>|)$/,y=/^.[^:#\[\.,]*$/;function z(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(y.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return h.call(b,a)>-1!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=this.length,d=[],e=this;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;c>b;b++)if(n.contains(e[b],this))return!0}));for(b=0;c>b;b++)n.find(a,e[b],d);return d=this.pushStack(c>1?n.unique(d):d),d.selector=this.selector?this.selector+" "+a:a,d},filter:function(a){return this.pushStack(z(this,a||[],!1))},not:function(a){return this.pushStack(z(this,a||[],!0))},is:function(a){return!!z(this,"string"==typeof a&&w.test(a)?n(a):a||[],!1).length}});var A,B=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,C=n.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||A,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:B.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),x.test(e[1])&&n.isPlainObject(b))for(e in b)n.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&f.parentNode&&(this.length=1,this[0]=f),this.context=d,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?void 0!==c.ready?c.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};C.prototype=n.fn,A=n(d);var D=/^(?:parents|prev(?:Until|All))/,E={children:!0,contents:!0,next:!0,prev:!0};n.fn.extend({has:function(a){var b=n(a,this),c=b.length;return this.filter(function(){for(var a=0;c>a;a++)if(n.contains(this,b[a]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=w.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?h.call(n(a),this[0]):h.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.uniqueSort(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function F(a,b){while((a=a[b])&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return u(a,"parentNode")},parentsUntil:function(a,b,c){return u(a,"parentNode",c)},next:function(a){return F(a,"nextSibling")},prev:function(a){return F(a,"previousSibling")},nextAll:function(a){return u(a,"nextSibling")},prevAll:function(a){return u(a,"previousSibling")},nextUntil:function(a,b,c){return u(a,"nextSibling",c)},prevUntil:function(a,b,c){return u(a,"previousSibling",c)},siblings:function(a){return v((a.parentNode||{}).firstChild,a)},children:function(a){return v(a.firstChild)},contents:function(a){return a.contentDocument||n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(E[a]||n.uniqueSort(e),D.test(a)&&e.reverse()),this.pushStack(e)}});var G=/\S+/g;function H(a){var b={};return n.each(a.match(G)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?H(a):n.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),h>=c&&h--}),this},has:function(a){return a?n.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().progress(c.notify).done(c.resolve).fail(c.reject):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=e.call(arguments),d=c.length,f=1!==d||a&&n.isFunction(a.promise)?d:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(d){b[a]=this,c[a]=arguments.length>1?e.call(arguments):d,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(d>1)for(i=new Array(d),j=new Array(d),k=new Array(d);d>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().progress(h(b,j,i)).done(h(b,k,c)).fail(g.reject):--f;return f||g.resolveWith(k,c),g.promise()}});var I;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(I.resolveWith(d,[n]),n.fn.triggerHandler&&(n(d).triggerHandler("ready"),n(d).off("ready"))))}});function J(){d.removeEventListener("DOMContentLoaded",J),a.removeEventListener("load",J),n.ready()}n.ready.promise=function(b){return I||(I=n.Deferred(),"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(n.ready):(d.addEventListener("DOMContentLoaded",J),a.addEventListener("load",J))),I.promise(b)},n.ready.promise();var K=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===n.type(c)){e=!0;for(h in c)K(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,n.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(n(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},L=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function M(){this.expando=n.expando+M.uid++}M.uid=1,M.prototype={register:function(a,b){var c=b||{};return a.nodeType?a[this.expando]=c:Object.defineProperty(a,this.expando,{value:c,writable:!0,configurable:!0}),a[this.expando]},cache:function(a){if(!L(a))return{};var b=a[this.expando];return b||(b={},L(a)&&(a.nodeType?a[this.expando]=b:Object.defineProperty(a,this.expando,{value:b,configurable:!0}))),b},set:function(a,b,c){var d,e=this.cache(a);if("string"==typeof b)e[b]=c;else for(d in b)e[d]=b[d];return e},get:function(a,b){return void 0===b?this.cache(a):a[this.expando]&&a[this.expando][b]},access:function(a,b,c){var d;return void 0===b||b&&"string"==typeof b&&void 0===c?(d=this.get(a,b),void 0!==d?d:this.get(a,n.camelCase(b))):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d,e,f=a[this.expando];if(void 0!==f){if(void 0===b)this.register(a);else{n.isArray(b)?d=b.concat(b.map(n.camelCase)):(e=n.camelCase(b),b in f?d=[b,e]:(d=e,d=d in f?[d]:d.match(G)||[])),c=d.length;while(c--)delete f[d[c]]}(void 0===b||n.isEmptyObject(f))&&(a.nodeType?a[this.expando]=void 0:delete a[this.expando])}},hasData:function(a){var b=a[this.expando];return void 0!==b&&!n.isEmptyObject(b)}};var N=new M,O=new M,P=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,Q=/[A-Z]/g;function R(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(Q,"-$&").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:P.test(c)?n.parseJSON(c):c; }catch(e){}O.set(a,b,c)}else c=void 0;return c}n.extend({hasData:function(a){return O.hasData(a)||N.hasData(a)},data:function(a,b,c){return O.access(a,b,c)},removeData:function(a,b){O.remove(a,b)},_data:function(a,b,c){return N.access(a,b,c)},_removeData:function(a,b){N.remove(a,b)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=O.get(f),1===f.nodeType&&!N.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),R(f,d,e[d])));N.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){O.set(this,a)}):K(this,function(b){var c,d;if(f&&void 0===b){if(c=O.get(f,a)||O.get(f,a.replace(Q,"-$&").toLowerCase()),void 0!==c)return c;if(d=n.camelCase(a),c=O.get(f,d),void 0!==c)return c;if(c=R(f,d,void 0),void 0!==c)return c}else d=n.camelCase(a),this.each(function(){var c=O.get(this,d);O.set(this,d,b),a.indexOf("-")>-1&&void 0!==c&&O.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){O.remove(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=N.get(a,b),c&&(!d||n.isArray(c)?d=N.access(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return N.get(a,c)||N.access(a,c,{empty:n.Callbacks("once memory").add(function(){N.remove(a,[b+"queue",c])})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length",""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};$.optgroup=$.option,$.tbody=$.tfoot=$.colgroup=$.caption=$.thead,$.th=$.td;function _(a,b){var c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[];return void 0===b||b&&n.nodeName(a,b)?n.merge([a],c):c}function aa(a,b){for(var c=0,d=a.length;d>c;c++)N.set(a[c],"globalEval",!b||N.get(b[c],"globalEval"))}var ba=/<|&#?\w+;/;function ca(a,b,c,d,e){for(var f,g,h,i,j,k,l=b.createDocumentFragment(),m=[],o=0,p=a.length;p>o;o++)if(f=a[o],f||0===f)if("object"===n.type(f))n.merge(m,f.nodeType?[f]:f);else if(ba.test(f)){g=g||l.appendChild(b.createElement("div")),h=(Y.exec(f)||["",""])[1].toLowerCase(),i=$[h]||$._default,g.innerHTML=i[1]+n.htmlPrefilter(f)+i[2],k=i[0];while(k--)g=g.lastChild;n.merge(m,g.childNodes),g=l.firstChild,g.textContent=""}else m.push(b.createTextNode(f));l.textContent="",o=0;while(f=m[o++])if(d&&n.inArray(f,d)>-1)e&&e.push(f);else if(j=n.contains(f.ownerDocument,f),g=_(l.appendChild(f),"script"),j&&aa(g),c){k=0;while(f=g[k++])Z.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),l.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="",l.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var da=/^key/,ea=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,fa=/^([^.]*)(?:\.(.+)|)/;function ga(){return!0}function ha(){return!1}function ia(){try{return d.activeElement}catch(a){}}function ja(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)ja(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=ha;else if(!e)return a;return 1===f&&(g=e,e=function(a){return n().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=n.guid++)),a.each(function(){n.event.add(this,b,e,d,c)})}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=N.get(a);if(r){c.handler&&(f=c,c=f.handler,e=f.selector),c.guid||(c.guid=n.guid++),(i=r.events)||(i=r.events={}),(g=r.handle)||(g=r.handle=function(b){return"undefined"!=typeof n&&n.event.triggered!==b.type?n.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(G)||[""],j=b.length;while(j--)h=fa.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o&&(l=n.event.special[o]||{},o=(e?l.delegateType:l.bindType)||o,l=n.event.special[o]||{},k=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},f),(m=i[o])||(m=i[o]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,p,g)!==!1||a.addEventListener&&a.addEventListener(o,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),n.event.global[o]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=N.hasData(a)&&N.get(a);if(r&&(i=r.events)){b=(b||"").match(G)||[""],j=b.length;while(j--)if(h=fa.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=i[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&q!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete i[o])}else for(o in i)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(i)&&N.remove(a,"handle events")}},dispatch:function(a){a=n.event.fix(a);var b,c,d,f,g,h=[],i=e.call(arguments),j=(N.get(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())a.rnamespace&&!a.rnamespace.test(g.namespace)||(a.handleObj=g,a.data=g.data,d=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==d&&(a.result=d)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&("click"!==a.type||isNaN(a.button)||a.button<1))for(;i!==this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>-1:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h]*)\/>/gi,la=/\s*$/g;function pa(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function qa(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function ra(a){var b=na.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function sa(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(N.hasData(a)&&(f=N.access(a),g=N.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;d>c;c++)n.event.add(b,e,j[e][c])}O.hasData(a)&&(h=O.access(a),i=n.extend({},h),O.set(b,i))}}function ta(a,b){var c=b.nodeName.toLowerCase();"input"===c&&X.test(a.type)?b.checked=a.checked:"input"!==c&&"textarea"!==c||(b.defaultValue=a.defaultValue)}function ua(a,b,c,d){b=f.apply([],b);var e,g,h,i,j,k,m=0,o=a.length,p=o-1,q=b[0],r=n.isFunction(q);if(r||o>1&&"string"==typeof q&&!l.checkClone&&ma.test(q))return a.each(function(e){var f=a.eq(e);r&&(b[0]=q.call(this,e,f.html())),ua(f,b,c,d)});if(o&&(e=ca(b,a[0].ownerDocument,!1,a,d),g=e.firstChild,1===e.childNodes.length&&(e=g),g||d)){for(h=n.map(_(e,"script"),qa),i=h.length;o>m;m++)j=e,m!==p&&(j=n.clone(j,!0,!0),i&&n.merge(h,_(j,"script"))),c.call(a[m],j,m);if(i)for(k=h[h.length-1].ownerDocument,n.map(h,ra),m=0;i>m;m++)j=h[m],Z.test(j.type||"")&&!N.access(j,"globalEval")&&n.contains(k,j)&&(j.src?n._evalUrl&&n._evalUrl(j.src):n.globalEval(j.textContent.replace(oa,"")))}return a}function va(a,b,c){for(var d,e=b?n.filter(b,a):a,f=0;null!=(d=e[f]);f++)c||1!==d.nodeType||n.cleanData(_(d)),d.parentNode&&(c&&n.contains(d.ownerDocument,d)&&aa(_(d,"script")),d.parentNode.removeChild(d));return a}n.extend({htmlPrefilter:function(a){return a.replace(ka,"<$1>")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=n.contains(a.ownerDocument,a);if(!(l.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(g=_(h),f=_(a),d=0,e=f.length;e>d;d++)ta(f[d],g[d]);if(b)if(c)for(f=f||_(a),g=g||_(h),d=0,e=f.length;e>d;d++)sa(f[d],g[d]);else sa(a,h);return g=_(h,"script"),g.length>0&&aa(g,!i&&_(a,"script")),h},cleanData:function(a){for(var b,c,d,e=n.event.special,f=0;void 0!==(c=a[f]);f++)if(L(c)){if(b=c[N.expando]){if(b.events)for(d in b.events)e[d]?n.event.remove(c,d):n.removeEvent(c,d,b.handle);c[N.expando]=void 0}c[O.expando]&&(c[O.expando]=void 0)}}}),n.fn.extend({domManip:ua,detach:function(a){return va(this,a,!0)},remove:function(a){return va(this,a)},text:function(a){return K(this,function(a){return void 0===a?n.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return ua(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=pa(this,a);b.appendChild(a)}})},prepend:function(){return ua(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=pa(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return ua(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return ua(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(n.cleanData(_(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return K(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!la.test(a)&&!$[(Y.exec(a)||["",""])[1].toLowerCase()]){a=n.htmlPrefilter(a);try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(_(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=[];return ua(this,arguments,function(b){var c=this.parentNode;n.inArray(this,a)<0&&(n.cleanData(_(this)),c&&c.replaceChild(b,this))},a)}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=[],e=n(a),f=e.length-1,h=0;f>=h;h++)c=h===f?this:this.clone(!0),n(e[h])[b](c),g.apply(d,c.get());return this.pushStack(d)}});var wa,xa={HTML:"block",BODY:"block"};function ya(a,b){var c=n(b.createElement(a)).appendTo(b.body),d=n.css(c[0],"display");return c.detach(),d}function za(a){var b=d,c=xa[a];return c||(c=ya(a,b),"none"!==c&&c||(wa=(wa||n("'}}}return{handle:handle}}(),TranslationHandler=function(){function handle(connection){connection.autoTranslateText=!1,connection.language="en",connection.googKey="AIzaSyCgB5hmFY74WYB-EoWkhr9cAGr6TiTHrEE",connection.Translator={TranslateText:function(text,callback){var newScript=document.createElement("script");newScript.type="text/javascript";var sourceText=encodeURIComponent(text),randomNumber="method"+connection.token();window[randomNumber]=function(response){return response.data&&response.data.translations[0]&&callback?void callback(response.data.translations[0].translatedText):response.error&&"Daily Limit Exceeded"===response.error.message?void console.error('Text translation failed. Error message: "Daily Limit Exceeded."'):response.error?void console.error(response.error.message):void console.error(response)};var source="https://www.googleapis.com/language/translate/v2?key="+connection.googKey+"&target="+(connection.language||"en-US")+"&callback=window."+randomNumber+"&q="+sourceText;newScript.src=source,document.getElementsByTagName("head")[0].appendChild(newScript)},getListOfLanguages:function(callback){var xhr=new XMLHttpRequest;xhr.onreadystatechange=function(){if(xhr.readyState==XMLHttpRequest.DONE){var response=JSON.parse(xhr.responseText);if(response&&response.data&&response.data.languages)return void callback(response.data.languages);if(response.error&&"Daily Limit Exceeded"===response.error.message)return void console.error('Text translation failed. Error message: "Daily Limit Exceeded."');if(response.error)return void console.error(response.error.message);console.error(response)}};var url="https://www.googleapis.com/language/translate/v2/languages?key="+connection.googKey+"&target=en";xhr.open("GET",url,!0),xhr.send(null)}}}return{handle:handle}}();!function(connection){function onUserLeft(remoteUserId){connection.deletePeer(remoteUserId)}function connectSocket(connectCallback){if(connection.socketAutoReConnect=!0,connection.socket)return void(connectCallback&&connectCallback(connection.socket));if("undefined"==typeof SocketConnection)if("undefined"!=typeof FirebaseConnection)window.SocketConnection=FirebaseConnection;else{if("undefined"==typeof PubNubConnection)throw"SocketConnection.js seems missed.";window.SocketConnection=PubNubConnection}new SocketConnection(connection,function(s){connectCallback&&connectCallback(connection.socket)})}function joinRoom(connectionDescription,cb){connection.socket.emit("join-room",{sessionid:connection.sessionid,session:connection.session,mediaConstraints:connection.mediaConstraints,sdpConstraints:connection.sdpConstraints,streams:getStreamInfoForAdmin(),extra:connection.extra,password:"undefined"!=typeof connection.password&&"object"!=typeof connection.password?connection.password:""},function(isRoomJoined,error){if(isRoomJoined===!0){if(connection.enableLogs&&console.log("isRoomJoined: ",isRoomJoined," roomid: ",connection.sessionid),connection.peers[connection.sessionid])return;mPeer.onNegotiationNeeded(connectionDescription)}isRoomJoined===!1&&connection.enableLogs&&console.warn("isRoomJoined: ",error," roomid: ",connection.sessionid),cb(isRoomJoined,connection.sessionid,error)})}function openRoom(callback){connection.enableLogs&&console.log("Sending open-room signal to socket.io"),connection.waitingForLocalMedia=!1,connection.socket.emit("open-room",{sessionid:connection.sessionid,session:connection.session,mediaConstraints:connection.mediaConstraints,sdpConstraints:connection.sdpConstraints,streams:getStreamInfoForAdmin(),extra:connection.extra,identifier:connection.publicRoomIdentifier,password:"undefined"!=typeof connection.password&&"object"!=typeof connection.password?connection.password:""},function(isRoomOpened,error){isRoomOpened===!0&&(connection.enableLogs&&console.log("isRoomOpened: ",isRoomOpened," roomid: ",connection.sessionid),callback(isRoomOpened,connection.sessionid)),isRoomOpened===!1&&(connection.enableLogs&&console.warn("isRoomOpened: ",error," roomid: ",connection.sessionid),callback(isRoomOpened,connection.sessionid,error))})}function getStreamInfoForAdmin(){try{return connection.streamEvents.selectAll("local").map(function(event){return{streamid:event.streamid,tracks:event.stream.getTracks().length}})}catch(e){return[]}}function beforeJoin(userPreferences,callback){if(connection.dontCaptureUserMedia||userPreferences.isDataOnly)return void callback();var localMediaConstraints={};userPreferences.localPeerSdpConstraints.OfferToReceiveAudio&&(localMediaConstraints.audio=connection.mediaConstraints.audio),userPreferences.localPeerSdpConstraints.OfferToReceiveVideo&&(localMediaConstraints.video=connection.mediaConstraints.video);var session=userPreferences.session||connection.session;return session.oneway&&"two-way"!==session.audio&&"two-way"!==session.video&&"two-way"!==session.screen?void callback():(session.oneway&&session.audio&&"two-way"===session.audio&&(session={audio:!0}),void((session.audio||session.video||session.screen)&&(session.screen?"Edge"===DetectRTC.browser.name?navigator.getDisplayMedia({video:!0,audio:isAudioPlusTab(connection)}).then(function(screen){screen.isScreen=!0,mPeer.onGettingLocalMedia(screen),!session.audio&&!session.video||isAudioPlusTab(connection)?callback(screen):connection.invokeGetUserMedia(null,callback)},function(error){console.error("Unable to capture screen on Edge. HTTPs and version 17+ is required.")}):connection.getScreenConstraints(function(error,screen_constraints){connection.invokeGetUserMedia({audio:!!isAudioPlusTab(connection)&&getAudioScreenConstraints(screen_constraints),video:screen_constraints,isScreen:!0},!session.audio&&!session.video||isAudioPlusTab(connection)?callback:connection.invokeGetUserMedia(null,callback))}):(session.audio||session.video)&&connection.invokeGetUserMedia(null,callback,session))))}function applyConstraints(stream,mediaConstraints){return stream?(mediaConstraints.audio&&stream.getAudioTracks().forEach(function(track){track.applyConstraints(mediaConstraints.audio)}),void(mediaConstraints.video&&stream.getVideoTracks().forEach(function(track){track.applyConstraints(mediaConstraints.video)}))):void(connection.enableLogs&&console.error("No stream to applyConstraints."))}function replaceTrack(track,remoteUserId,isVideoTrack){return remoteUserId?void mPeer.replaceTrack(track,remoteUserId,isVideoTrack):void connection.peers.getAllParticipants().forEach(function(participant){mPeer.replaceTrack(track,participant,isVideoTrack)})}forceOptions=forceOptions||{useDefaultDevices:!0},connection.channel=connection.sessionid=(roomid||location.href.replace(/\/|:|#|\?|\$|\^|%|\.|`|~|!|\+|@|\[|\||]|\|*. /g,"").split("\n").join("").split("\r").join(""))+"";var mPeer=new MultiPeers(connection),preventDuplicateOnStreamEvents={};mPeer.onGettingLocalMedia=function(stream,callback){if(callback=callback||function(){},preventDuplicateOnStreamEvents[stream.streamid])return void callback();preventDuplicateOnStreamEvents[stream.streamid]=!0;try{stream.type="local"}catch(e){}connection.setStreamEndHandler(stream),getRMCMediaElement(stream,function(mediaElement){mediaElement.id=stream.streamid,mediaElement.muted=!0,mediaElement.volume=0,connection.attachStreams.indexOf(stream)===-1&&connection.attachStreams.push(stream),"undefined"!=typeof StreamsHandler&&StreamsHandler.setHandlers(stream,!0,connection),connection.streamEvents[stream.streamid]={stream:stream,type:"local",mediaElement:mediaElement,userid:connection.userid,extra:connection.extra,streamid:stream.streamid,isAudioMuted:!0};try{setHarkEvents(connection,connection.streamEvents[stream.streamid]),setMuteHandlers(connection,connection.streamEvents[stream.streamid]),connection.onstream(connection.streamEvents[stream.streamid])}catch(e){}callback()},connection)},mPeer.onGettingRemoteMedia=function(stream,remoteUserId){try{stream.type="remote"}catch(e){}connection.setStreamEndHandler(stream,"remote-stream"),getRMCMediaElement(stream,function(mediaElement){mediaElement.id=stream.streamid,"undefined"!=typeof StreamsHandler&&StreamsHandler.setHandlers(stream,!1,connection),connection.streamEvents[stream.streamid]={stream:stream,type:"remote",userid:remoteUserId,extra:connection.peers[remoteUserId]?connection.peers[remoteUserId].extra:{},mediaElement:mediaElement,streamid:stream.streamid},setMuteHandlers(connection,connection.streamEvents[stream.streamid]),connection.onstream(connection.streamEvents[stream.streamid])},connection)},mPeer.onRemovingRemoteMedia=function(stream,remoteUserId){var streamEvent=connection.streamEvents[stream.streamid];streamEvent||(streamEvent={stream:stream,type:"remote",userid:remoteUserId,extra:connection.peers[remoteUserId]?connection.peers[remoteUserId].extra:{},streamid:stream.streamid,mediaElement:connection.streamEvents[stream.streamid]?connection.streamEvents[stream.streamid].mediaElement:null}),connection.peersBackup[streamEvent.userid]&&(streamEvent.extra=connection.peersBackup[streamEvent.userid].extra),connection.onstreamended(streamEvent),delete connection.streamEvents[stream.streamid]},mPeer.onNegotiationNeeded=function(message,remoteUserId,callback){callback=callback||function(){},remoteUserId=remoteUserId||message.remoteUserId,message=message||"";var messageToDeliver={remoteUserId:remoteUserId,message:message,sender:connection.userid};message.remoteUserId&&message.message&&message.sender&&(messageToDeliver=message),connectSocket(function(){connection.socket.emit(connection.socketMessageEvent,messageToDeliver,callback)})},mPeer.onUserLeft=onUserLeft,mPeer.disconnectWith=function(remoteUserId,callback){connection.socket&&connection.socket.emit("disconnect-with",remoteUserId,callback||function(){}),connection.deletePeer(remoteUserId)},connection.socketOptions={transport:"polling"},connection.openOrJoin=function(roomid,callback){callback=callback||function(){},connection.checkPresence(roomid,function(isRoomExist,roomid){if(isRoomExist){connection.sessionid=roomid;var localPeerSdpConstraints=!1,remotePeerSdpConstraints=!1,isOneWay=!!connection.session.oneway,isDataOnly=isData(connection.session);remotePeerSdpConstraints={OfferToReceiveAudio:connection.sdpConstraints.mandatory.OfferToReceiveAudio,OfferToReceiveVideo:connection.sdpConstraints.mandatory.OfferToReceiveVideo},localPeerSdpConstraints={OfferToReceiveAudio:isOneWay?!!connection.session.audio:connection.sdpConstraints.mandatory.OfferToReceiveAudio,OfferToReceiveVideo:isOneWay?!!connection.session.video||!!connection.session.screen:connection.sdpConstraints.mandatory.OfferToReceiveVideo};var connectionDescription={remoteUserId:connection.sessionid,message:{newParticipationRequest:!0,isOneWay:isOneWay,isDataOnly:isDataOnly,localPeerSdpConstraints:localPeerSdpConstraints,remotePeerSdpConstraints:remotePeerSdpConstraints},sender:connection.userid};return void beforeJoin(connectionDescription.message,function(){joinRoom(connectionDescription,callback)})}return connection.waitingForLocalMedia=!0,connection.isInitiator=!0,connection.sessionid=roomid||connection.sessionid,isData(connection.session)?void openRoom(callback):void connection.captureUserMedia(function(){openRoom(callback)})})},connection.waitingForLocalMedia=!1,connection.open=function(roomid,callback){callback=callback||function(){},connection.waitingForLocalMedia=!0,connection.isInitiator=!0,connection.sessionid=roomid||connection.sessionid,connectSocket(function(){return isData(connection.session)?void openRoom(callback):void connection.captureUserMedia(function(){openRoom(callback)})})},connection.peersBackup={},connection.deletePeer=function(remoteUserId){if(remoteUserId&&connection.peers[remoteUserId]){var eventObject={userid:remoteUserId,extra:connection.peers[remoteUserId]?connection.peers[remoteUserId].extra:{}};if(connection.peersBackup[eventObject.userid]&&(eventObject.extra=connection.peersBackup[eventObject.userid].extra),connection.onleave(eventObject),connection.peers[remoteUserId]){connection.peers[remoteUserId].streams.forEach(function(stream){stream.stop()});var peer=connection.peers[remoteUserId].peer;if(peer&&"closed"!==peer.iceConnectionState)try{peer.close()}catch(e){}connection.peers[remoteUserId]&&(connection.peers[remoteUserId].peer=null,delete connection.peers[remoteUserId])}}},connection.rejoin=function(connectionDescription){if(!connection.isInitiator&&connectionDescription&&Object.keys(connectionDescription).length){var extra={}; connection.peers[connectionDescription.remoteUserId]&&(extra=connection.peers[connectionDescription.remoteUserId].extra,connection.deletePeer(connectionDescription.remoteUserId)),connectionDescription&&connectionDescription.remoteUserId&&(connection.join(connectionDescription.remoteUserId),connection.onReConnecting({userid:connectionDescription.remoteUserId,extra:extra}))}},connection.join=function(remoteUserId,options){connection.sessionid=!!remoteUserId&&(remoteUserId.sessionid||remoteUserId.remoteUserId||remoteUserId)||connection.sessionid,connection.sessionid+="";var localPeerSdpConstraints=!1,remotePeerSdpConstraints=!1,isOneWay=!1,isDataOnly=!1;if(remoteUserId&&remoteUserId.session||!remoteUserId||"string"==typeof remoteUserId){var session=remoteUserId?remoteUserId.session||connection.session:connection.session;isOneWay=!!session.oneway,isDataOnly=isData(session),remotePeerSdpConstraints={OfferToReceiveAudio:connection.sdpConstraints.mandatory.OfferToReceiveAudio,OfferToReceiveVideo:connection.sdpConstraints.mandatory.OfferToReceiveVideo},localPeerSdpConstraints={OfferToReceiveAudio:isOneWay?!!connection.session.audio:connection.sdpConstraints.mandatory.OfferToReceiveAudio,OfferToReceiveVideo:isOneWay?!!connection.session.video||!!connection.session.screen:connection.sdpConstraints.mandatory.OfferToReceiveVideo}}options=options||{};var cb=function(){};"function"==typeof options&&(cb=options,options={}),"undefined"!=typeof options.localPeerSdpConstraints&&(localPeerSdpConstraints=options.localPeerSdpConstraints),"undefined"!=typeof options.remotePeerSdpConstraints&&(remotePeerSdpConstraints=options.remotePeerSdpConstraints),"undefined"!=typeof options.isOneWay&&(isOneWay=options.isOneWay),"undefined"!=typeof options.isDataOnly&&(isDataOnly=options.isDataOnly);var connectionDescription={remoteUserId:connection.sessionid,message:{newParticipationRequest:!0,isOneWay:isOneWay,isDataOnly:isDataOnly,localPeerSdpConstraints:localPeerSdpConstraints,remotePeerSdpConstraints:remotePeerSdpConstraints},sender:connection.userid};return beforeJoin(connectionDescription.message,function(){connectSocket(function(){joinRoom(connectionDescription,cb)})}),connectionDescription},connection.publicRoomIdentifier="",connection.getUserMedia=connection.captureUserMedia=function(callback,sessionForced){callback=callback||function(){};var session=sessionForced||connection.session;return connection.dontCaptureUserMedia||isData(session)?void callback():void((session.audio||session.video||session.screen)&&(session.screen?"Edge"===DetectRTC.browser.name?navigator.getDisplayMedia({video:!0,audio:isAudioPlusTab(connection)}).then(function(screen){if(screen.isScreen=!0,mPeer.onGettingLocalMedia(screen),(session.audio||session.video)&&!isAudioPlusTab(connection)){var nonScreenSession={};for(var s in session)"screen"!==s&&(nonScreenSession[s]=session[s]);return void connection.invokeGetUserMedia(sessionForced,callback,nonScreenSession)}callback(screen)},function(error){console.error("Unable to capture screen on Edge. HTTPs and version 17+ is required.")}):connection.getScreenConstraints(function(error,screen_constraints){if(error)throw error;connection.invokeGetUserMedia({audio:!!isAudioPlusTab(connection)&&getAudioScreenConstraints(screen_constraints),video:screen_constraints,isScreen:!0},function(stream){if((session.audio||session.video)&&!isAudioPlusTab(connection)){var nonScreenSession={};for(var s in session)"screen"!==s&&(nonScreenSession[s]=session[s]);return void connection.invokeGetUserMedia(sessionForced,callback,nonScreenSession)}callback(stream)})}):(session.audio||session.video)&&connection.invokeGetUserMedia(sessionForced,callback,session)))},connection.onbeforeunload=function(arg1,dontCloseSocket){connection.closeBeforeUnload&&(connection.peers.getAllParticipants().forEach(function(participant){mPeer.onNegotiationNeeded({userLeft:!0},participant),connection.peers[participant]&&connection.peers[participant].peer&&connection.peers[participant].peer.close(),delete connection.peers[participant]}),dontCloseSocket||connection.closeSocket(),connection.isInitiator=!1)},window.ignoreBeforeUnload?connection.closeBeforeUnload=!1:(connection.closeBeforeUnload=!0,window.addEventListener("beforeunload",connection.onbeforeunload,!1)),connection.userid=getRandomString(),connection.changeUserId=function(newUserId,callback){callback=callback||function(){},connection.userid=newUserId||getRandomString(),connection.socket.emit("changed-uuid",connection.userid,callback)},connection.extra={},connection.attachStreams=[],connection.session={audio:!0,video:!0},connection.enableFileSharing=!1,connection.bandwidth={screen:!1,audio:!1,video:!1},connection.codecs={audio:"opus",video:"VP9"},connection.processSdp=function(sdp){return"Safari"===DetectRTC.browser.name?sdp:("VP8"===connection.codecs.video.toUpperCase()&&(sdp=CodecsHandler.preferCodec(sdp,"vp8")),"VP9"===connection.codecs.video.toUpperCase()&&(sdp=CodecsHandler.preferCodec(sdp,"vp9")),"H264"===connection.codecs.video.toUpperCase()&&(sdp=CodecsHandler.preferCodec(sdp,"h264")),"G722"===connection.codecs.audio&&(sdp=CodecsHandler.removeNonG722(sdp)),"Firefox"===DetectRTC.browser.name?sdp:((connection.bandwidth.video||connection.bandwidth.screen)&&(sdp=CodecsHandler.setApplicationSpecificBandwidth(sdp,connection.bandwidth,!!connection.session.screen)),connection.bandwidth.video&&(sdp=CodecsHandler.setVideoBitrates(sdp,{min:8*connection.bandwidth.video*1024,max:8*connection.bandwidth.video*1024})),connection.bandwidth.audio&&(sdp=CodecsHandler.setOpusAttributes(sdp,{maxaveragebitrate:8*connection.bandwidth.audio*1024,maxplaybackrate:8*connection.bandwidth.audio*1024,stereo:1,maxptime:3})),sdp))},"undefined"!=typeof CodecsHandler&&(connection.BandwidthHandler=connection.CodecsHandler=CodecsHandler),connection.mediaConstraints={audio:{mandatory:{},optional:connection.bandwidth.audio?[{bandwidth:8*connection.bandwidth.audio*1024||1048576}]:[]},video:{mandatory:{},optional:connection.bandwidth.video?[{bandwidth:8*connection.bandwidth.video*1024||1048576},{facingMode:"user"}]:[{facingMode:"user"}]}},"Firefox"===DetectRTC.browser.name&&(connection.mediaConstraints={audio:!0,video:!0}),forceOptions.useDefaultDevices||DetectRTC.isMobileDevice||DetectRTC.load(function(){var lastAudioDevice,lastVideoDevice;if(DetectRTC.MediaDevices.forEach(function(device){"audioinput"===device.kind&&connection.mediaConstraints.audio!==!1&&(lastAudioDevice=device),"videoinput"===device.kind&&connection.mediaConstraints.video!==!1&&(lastVideoDevice=device)}),lastAudioDevice){if("Firefox"===DetectRTC.browser.name)return void(connection.mediaConstraints.audio!==!0?connection.mediaConstraints.audio.deviceId=lastAudioDevice.id:connection.mediaConstraints.audio={deviceId:lastAudioDevice.id});1==connection.mediaConstraints.audio&&(connection.mediaConstraints.audio={mandatory:{},optional:[]}),connection.mediaConstraints.audio.optional||(connection.mediaConstraints.audio.optional=[]);var optional=[{sourceId:lastAudioDevice.id}];connection.mediaConstraints.audio.optional=optional.concat(connection.mediaConstraints.audio.optional)}if(lastVideoDevice){if("Firefox"===DetectRTC.browser.name)return void(connection.mediaConstraints.video!==!0?connection.mediaConstraints.video.deviceId=lastVideoDevice.id:connection.mediaConstraints.video={deviceId:lastVideoDevice.id});1==connection.mediaConstraints.video&&(connection.mediaConstraints.video={mandatory:{},optional:[]}),connection.mediaConstraints.video.optional||(connection.mediaConstraints.video.optional=[]);var optional=[{sourceId:lastVideoDevice.id}];connection.mediaConstraints.video.optional=optional.concat(connection.mediaConstraints.video.optional)}}),connection.sdpConstraints={mandatory:{OfferToReceiveAudio:!0,OfferToReceiveVideo:!0},optional:[{VoiceActivityDetection:!1}]},connection.rtcpMuxPolicy="require",connection.iceTransportPolicy=null,connection.optionalArgument={optional:[{DtlsSrtpKeyAgreement:!0},{googImprovedWifiBwe:!0},{googScreencastMinBitrate:300},{googIPv6:!0},{googDscp:!0},{googCpuUnderuseThreshold:55},{googCpuOveruseThreshold:85},{googSuspendBelowMinBitrate:!0},{googCpuOveruseDetection:!0}],mandatory:{}},connection.iceServers=IceServersHandler.getIceServers(connection),connection.candidates={host:!0,stun:!0,turn:!0},connection.iceProtocols={tcp:!0,udp:!0},connection.onopen=function(event){connection.enableLogs&&console.info("Data connection has been opened between you & ",event.userid)},connection.onclose=function(event){connection.enableLogs&&console.warn("Data connection has been closed between you & ",event.userid)},connection.onerror=function(error){connection.enableLogs&&console.error(error.userid,"data-error",error)},connection.onmessage=function(event){connection.enableLogs&&console.debug("data-message",event.userid,event.data)},connection.send=function(data,remoteUserId){connection.peers.send(data,remoteUserId)},connection.close=connection.disconnect=connection.leave=function(){connection.onbeforeunload(!1,!0)},connection.closeEntireSession=function(callback){callback=callback||function(){},connection.socket.emit("close-entire-session",function looper(){return connection.getAllParticipants().length?void setTimeout(looper,100):(connection.onEntireSessionClosed({sessionid:connection.sessionid,userid:connection.userid,extra:connection.extra}),void connection.changeUserId(null,function(){connection.close(),callback()}))})},connection.onEntireSessionClosed=function(event){connection.enableLogs&&console.info("Entire session is closed: ",event.sessionid,event.extra)},connection.onstream=function(e){var parentNode=connection.videosContainer;parentNode.insertBefore(e.mediaElement,parentNode.firstChild);var played=e.mediaElement.play();return"undefined"!=typeof played?void played["catch"](function(){}).then(function(){setTimeout(function(){e.mediaElement.play()},2e3)}):void setTimeout(function(){e.mediaElement.play()},2e3)},connection.onstreamended=function(e){e.mediaElement||(e.mediaElement=document.getElementById(e.streamid)),e.mediaElement&&e.mediaElement.parentNode&&e.mediaElement.parentNode.removeChild(e.mediaElement)},connection.direction="many-to-many",connection.removeStream=function(streamid,remoteUserId){var stream;return connection.attachStreams.forEach(function(localStream){localStream.id===streamid&&(stream=localStream)}),stream?(connection.peers.getAllParticipants().forEach(function(participant){if(!remoteUserId||participant===remoteUserId){var user=connection.peers[participant];try{user.peer.removeStream(stream)}catch(e){}}}),void connection.renegotiate()):void console.warn("No such stream exist.",streamid)},connection.addStream=function(session,remoteUserId){function gumCallback(stream){session.streamCallback&&session.streamCallback(stream),connection.renegotiate(remoteUserId)}return session.getAudioTracks?(connection.attachStreams.indexOf(session)===-1&&(session.streamid||(session.streamid=session.id),connection.attachStreams.push(session)),void connection.renegotiate(remoteUserId)):isData(session)?void connection.renegotiate(remoteUserId):void((session.audio||session.video||session.screen)&&(session.screen?"Edge"===DetectRTC.browser.name?navigator.getDisplayMedia({video:!0,audio:isAudioPlusTab(connection)}).then(function(screen){screen.isScreen=!0,mPeer.onGettingLocalMedia(screen),!session.audio&&!session.video||isAudioPlusTab(connection)?gumCallback(screen):connection.invokeGetUserMedia(null,function(stream){gumCallback(stream)})},function(error){console.error("Unable to capture screen on Edge. HTTPs and version 17+ is required.")}):connection.getScreenConstraints(function(error,screen_constraints){return error?"PermissionDeniedError"===error?(session.streamCallback&&session.streamCallback(null),void(connection.enableLogs&&console.error("User rejected to share his screen."))):alert(error):void connection.invokeGetUserMedia({audio:!!isAudioPlusTab(connection)&&getAudioScreenConstraints(screen_constraints),video:screen_constraints,isScreen:!0},function(stream){!session.audio&&!session.video||isAudioPlusTab(connection)?gumCallback(stream):connection.invokeGetUserMedia(null,function(stream){gumCallback(stream)})})}):(session.audio||session.video)&&connection.invokeGetUserMedia(null,gumCallback)))},connection.invokeGetUserMedia=function(localMediaConstraints,callback,session){session||(session=connection.session),localMediaConstraints||(localMediaConstraints=connection.mediaConstraints),getUserMediaHandler({onGettingLocalMedia:function(stream){var videoConstraints=localMediaConstraints.video;videoConstraints&&(videoConstraints.mediaSource||videoConstraints.mozMediaSource?stream.isScreen=!0:videoConstraints.mandatory&&videoConstraints.mandatory.chromeMediaSource&&(stream.isScreen=!0)),stream.isScreen||(stream.isVideo=stream.getVideoTracks().length,stream.isAudio=!stream.isVideo&&stream.getAudioTracks().length),mPeer.onGettingLocalMedia(stream,function(){"function"==typeof callback&&callback(stream)})},onLocalMediaError:function(error,constraints){mPeer.onLocalMediaError(error,constraints)},localMediaConstraints:localMediaConstraints||{audio:!!session.audio&&localMediaConstraints.audio,video:!!session.video&&localMediaConstraints.video}})},connection.applyConstraints=function(mediaConstraints,streamid){if(!MediaStreamTrack||!MediaStreamTrack.prototype.applyConstraints)return void alert("track.applyConstraints is NOT supported in your browser.");if(streamid){var stream;return connection.streamEvents[streamid]&&(stream=connection.streamEvents[streamid].stream),void applyConstraints(stream,mediaConstraints)}connection.attachStreams.forEach(function(stream){applyConstraints(stream,mediaConstraints)})},connection.replaceTrack=function(session,remoteUserId,isVideoTrack){function gumCallback(stream){connection.replaceTrack(stream,remoteUserId,isVideoTrack||session.video||session.screen)}if(session=session||{},!RTCPeerConnection.prototype.getSenders)return void connection.addStream(session);if(session instanceof MediaStreamTrack)return void replaceTrack(session,remoteUserId,isVideoTrack);if(session instanceof MediaStream)return session.getVideoTracks().length&&replaceTrack(session.getVideoTracks()[0],remoteUserId,!0),void(session.getAudioTracks().length&&replaceTrack(session.getAudioTracks()[0],remoteUserId,!1));if(isData(session))throw"connection.replaceTrack requires audio and/or video and/or screen.";(session.audio||session.video||session.screen)&&(session.screen?"Edge"===DetectRTC.browser.name?navigator.getDisplayMedia({video:!0,audio:isAudioPlusTab(connection)}).then(function(screen){screen.isScreen=!0,mPeer.onGettingLocalMedia(screen),!session.audio&&!session.video||isAudioPlusTab(connection)?gumCallback(screen):connection.invokeGetUserMedia(null,gumCallback)},function(error){console.error("Unable to capture screen on Edge. HTTPs and version 17+ is required.")}):connection.getScreenConstraints(function(error,screen_constraints){return error?alert(error):void connection.invokeGetUserMedia({audio:!!isAudioPlusTab(connection)&&getAudioScreenConstraints(screen_constraints),video:screen_constraints,isScreen:!0},!session.audio&&!session.video||isAudioPlusTab(connection)?gumCallback:connection.invokeGetUserMedia(null,gumCallback))}):(session.audio||session.video)&&connection.invokeGetUserMedia(null,gumCallback))},connection.resetTrack=function(remoteUsersIds,isVideoTrack){remoteUsersIds||(remoteUsersIds=connection.getAllParticipants()),"string"==typeof remoteUsersIds&&(remoteUsersIds=[remoteUsersIds]),remoteUsersIds.forEach(function(participant){var peer=connection.peers[participant].peer;"undefined"!=typeof isVideoTrack&&isVideoTrack!==!0||!peer.lastVideoTrack||connection.replaceTrack(peer.lastVideoTrack,participant,!0),"undefined"!=typeof isVideoTrack&&isVideoTrack!==!1||!peer.lastAudioTrack||connection.replaceTrack(peer.lastAudioTrack,participant,!1)})},connection.renegotiate=function(remoteUserId){return remoteUserId?void mPeer.renegotiatePeer(remoteUserId):void connection.peers.getAllParticipants().forEach(function(participant){mPeer.renegotiatePeer(participant)})},connection.setStreamEndHandler=function(stream,isRemote){if(stream&&stream.addEventListener&&(isRemote=!!isRemote,!stream.alreadySetEndHandler)){stream.alreadySetEndHandler=!0;var streamEndedEvent="ended";"oninactive"in stream&&(streamEndedEvent="inactive"),stream.addEventListener(streamEndedEvent,function(){if(stream.idInstance&¤tUserMediaRequest.remove(stream.idInstance),!isRemote){var streams=[];connection.attachStreams.forEach(function(s){s.id!=stream.id&&streams.push(s)}),connection.attachStreams=streams}var streamEvent=connection.streamEvents[stream.streamid];if(streamEvent||(streamEvent={stream:stream,streamid:stream.streamid,type:isRemote?"remote":"local",userid:connection.userid,extra:connection.extra,mediaElement:connection.streamEvents[stream.streamid]?connection.streamEvents[stream.streamid].mediaElement:null}),isRemote&&connection.peers[streamEvent.userid]){var peer=connection.peers[streamEvent.userid].peer,streams=[];peer.getRemoteStreams().forEach(function(s){s.id!=stream.id&&streams.push(s)}),connection.peers[streamEvent.userid].streams=streams}streamEvent.userid===connection.userid&&"remote"===streamEvent.type||(connection.peersBackup[streamEvent.userid]&&(streamEvent.extra=connection.peersBackup[streamEvent.userid].extra),connection.onstreamended(streamEvent),delete connection.streamEvents[stream.streamid])},!1)}},connection.onMediaError=function(error,constraints){connection.enableLogs&&console.error(error,constraints)},connection.autoCloseEntireSession=!1,connection.filesContainer=connection.videosContainer=document.body||document.documentElement,connection.isInitiator=!1,connection.shareFile=mPeer.shareFile,"undefined"!=typeof FileProgressBarHandler&&FileProgressBarHandler.handle(connection),"undefined"!=typeof TranslationHandler&&TranslationHandler.handle(connection),connection.token=getRandomString,connection.onNewParticipant=function(participantId,userPreferences){connection.acceptParticipationRequest(participantId,userPreferences)},connection.acceptParticipationRequest=function(participantId,userPreferences){userPreferences.successCallback&&(userPreferences.successCallback(),delete userPreferences.successCallback),mPeer.createNewPeer(participantId,userPreferences)},"undefined"!=typeof StreamsHandler&&(connection.StreamsHandler=StreamsHandler),connection.onleave=function(userid){},connection.invokeSelectFileDialog=function(callback){var selector=new FileSelector;selector.accept="*.*",selector.selectSingleFile(callback)},connection.onmute=function(e){if(e&&e.mediaElement)if("both"===e.muteType||"video"===e.muteType){e.mediaElement.src=null;var paused=e.mediaElement.pause();"undefined"!=typeof paused?paused.then(function(){e.mediaElement.poster=e.snapshot||"https://cdn.webrtc-experiment.com/images/muted.png"}):e.mediaElement.poster=e.snapshot||"https://cdn.webrtc-experiment.com/images/muted.png"}else"audio"===e.muteType&&(e.mediaElement.muted=!0)},connection.onunmute=function(e){e&&e.mediaElement&&e.stream&&("both"===e.unmuteType||"video"===e.unmuteType?(e.mediaElement.poster=null,e.mediaElement.srcObject=e.stream,e.mediaElement.play()):"audio"===e.unmuteType&&(e.mediaElement.muted=!1))},connection.onExtraDataUpdated=function(event){event.status="online",connection.onUserStatusChanged(event,!0)},connection.getAllParticipants=function(sender){return connection.peers.getAllParticipants(sender)},"undefined"!=typeof StreamsHandler&&(StreamsHandler.onSyncNeeded=function(streamid,action,type){connection.peers.getAllParticipants().forEach(function(participant){mPeer.onNegotiationNeeded({streamid:streamid,action:action,streamSyncNeeded:!0,type:type||"both"},participant)})}),connection.connectSocket=function(callback){connectSocket(callback)},connection.closeSocket=function(){try{io.sockets={}}catch(e){}connection.socket&&("function"==typeof connection.socket.disconnect&&connection.socket.disconnect(),"function"==typeof connection.socket.resetProps&&connection.socket.resetProps(),connection.socket=null)},connection.getSocket=function(callback){return!callback&&connection.enableLogs&&console.warn("getSocket.callback paramter is required."),callback=callback||function(){},connection.socket?callback(connection.socket):connectSocket(function(){callback(connection.socket)}),connection.socket},connection.getRemoteStreams=mPeer.getRemoteStreams;var skipStreams=["selectFirst","selectAll","forEach"];if(connection.streamEvents={selectFirst:function(options){return connection.streamEvents.selectAll(options)[0]},selectAll:function(options){options||(options={local:!0,remote:!0,isScreen:!0,isAudio:!0,isVideo:!0}),"local"==options&&(options={local:!0}),"remote"==options&&(options={remote:!0}),"screen"==options&&(options={isScreen:!0}),"audio"==options&&(options={isAudio:!0}),"video"==options&&(options={isVideo:!0});var streams=[];return Object.keys(connection.streamEvents).forEach(function(key){var event=connection.streamEvents[key];if(skipStreams.indexOf(key)===-1){var ignore=!0;options.local&&"local"===event.type&&(ignore=!1),options.remote&&"remote"===event.type&&(ignore=!1),options.isScreen&&event.stream.isScreen&&(ignore=!1),options.isVideo&&event.stream.isVideo&&(ignore=!1),options.isAudio&&event.stream.isAudio&&(ignore=!1),options.userid&&event.userid===options.userid&&(ignore=!1),ignore===!1&&streams.push(event)}}),streams}},connection.socketURL="/",connection.socketMessageEvent="RTCMultiConnection-Message",connection.socketCustomEvent="RTCMultiConnection-Custom-Message",connection.DetectRTC=DetectRTC,connection.setCustomSocketEvent=function(customEvent){customEvent&&(connection.socketCustomEvent=customEvent),connection.socket&&connection.socket.emit("set-custom-socket-event-listener",connection.socketCustomEvent)},connection.getNumberOfBroadcastViewers=function(broadcastId,callback){connection.socket&&broadcastId&&callback&&connection.socket.emit("get-number-of-users-in-specific-broadcast",broadcastId,callback)},connection.onNumberOfBroadcastViewersUpdated=function(event){connection.enableLogs&&connection.isInitiator&&console.info("Number of broadcast (",event.broadcastId,") viewers",event.numberOfBroadcastViewers)},connection.onUserStatusChanged=function(event,dontWriteLogs){connection.enableLogs&&!dontWriteLogs&&console.info(event.userid,event.status)},connection.getUserMediaHandler=getUserMediaHandler,connection.multiPeersHandler=mPeer,connection.enableLogs=!0,connection.setCustomSocketHandler=function(customSocketHandler){"undefined"!=typeof SocketConnection&&(SocketConnection=customSocketHandler)},connection.chunkSize=65e3,connection.maxParticipantsAllowed=1e3,connection.disconnectWith=mPeer.disconnectWith,connection.checkPresence=function(roomid,callback){return roomid=roomid||connection.sessionid,"SSEConnection"===SocketConnection.name?void SSEConnection.checkPresence(roomid,function(isRoomExist,_roomid,extra){return connection.socket?void callback(isRoomExist,_roomid):(isRoomExist||(connection.userid=_roomid),void connection.connectSocket(function(){callback(isRoomExist,_roomid,extra)}))}):connection.socket?void connection.socket.emit("check-presence",roomid+"",function(isRoomExist,_roomid,extra){connection.enableLogs&&console.log("checkPresence.isRoomExist: ",isRoomExist," roomid: ",_roomid),callback(isRoomExist,_roomid,extra)}):void connection.connectSocket(function(){connection.checkPresence(roomid,callback)})},connection.onReadyForOffer=function(remoteUserId,userPreferences){connection.multiPeersHandler.createNewPeer(remoteUserId,userPreferences)},connection.setUserPreferences=function(userPreferences){return connection.dontAttachStream&&(userPreferences.dontAttachLocalStream=!0),connection.dontGetRemoteStream&&(userPreferences.dontGetRemoteStream=!0),userPreferences},connection.updateExtraData=function(){connection.socket.emit("extra-data-updated",connection.extra)},connection.enableScalableBroadcast=!1,connection.maxRelayLimitPerUser=3,connection.dontCaptureUserMedia=!1,connection.dontAttachStream=!1,connection.dontGetRemoteStream=!1,connection.onReConnecting=function(event){connection.enableLogs&&console.info("ReConnecting with",event.userid,"...")},connection.beforeAddingStream=function(stream){return stream},connection.beforeRemovingStream=function(stream){return stream},"undefined"!=typeof isChromeExtensionAvailable&&(connection.checkIfChromeExtensionAvailable=isChromeExtensionAvailable),"undefined"!=typeof isFirefoxExtensionAvailable&&(connection.checkIfChromeExtensionAvailable=isFirefoxExtensionAvailable),"undefined"!=typeof getChromeExtensionStatus&&(connection.getChromeExtensionStatus=getChromeExtensionStatus),connection.getScreenConstraints=function(callback,audioPlusTab){isAudioPlusTab(connection,audioPlusTab)&&(audioPlusTab=!0),getScreenConstraints(function(error,screen_constraints){error||(screen_constraints=connection.modifyScreenConstraints(screen_constraints),callback(error,screen_constraints))},audioPlusTab)},connection.modifyScreenConstraints=function(screen_constraints){return screen_constraints},connection.onPeerStateChanged=function(state){connection.enableLogs&&state.iceConnectionState.search(/closed|failed/gi)!==-1&&console.error("Peer connection is closed between you & ",state.userid,state.extra,"state:",state.iceConnectionState)},connection.isOnline=!0,listenEventHandler("online",function(){connection.isOnline=!0}),listenEventHandler("offline",function(){connection.isOnline=!1}),connection.isLowBandwidth=!1,navigator&&navigator.connection&&navigator.connection.type&&(connection.isLowBandwidth=navigator.connection.type.toString().toLowerCase().search(/wifi|cell/g)!==-1,connection.isLowBandwidth)){if(connection.bandwidth={audio:!1,video:!1,screen:!1},connection.mediaConstraints.audio&&connection.mediaConstraints.audio.optional&&connection.mediaConstraints.audio.optional.length){var newArray=[];connection.mediaConstraints.audio.optional.forEach(function(opt){"undefined"==typeof opt.bandwidth&&newArray.push(opt)}),connection.mediaConstraints.audio.optional=newArray}if(connection.mediaConstraints.video&&connection.mediaConstraints.video.optional&&connection.mediaConstraints.video.optional.length){var newArray=[];connection.mediaConstraints.video.optional.forEach(function(opt){"undefined"==typeof opt.bandwidth&&newArray.push(opt)}),connection.mediaConstraints.video.optional=newArray}}connection.getExtraData=function(remoteUserId){if(!remoteUserId)throw"remoteUserId is required.";return connection.peers[remoteUserId]?connection.peers[remoteUserId].extra:{}},forceOptions.autoOpenOrJoin&&connection.openOrJoin(connection.sessionid),connection.onUserIdAlreadyTaken=function(useridAlreadyTaken,yourNewUserId){connection.enableLogs&&console.warn("Userid already taken.",useridAlreadyTaken,"Your new userid:",yourNewUserId),connection.userid=connection.token(),connection.join(connection.sessionid)},connection.trickleIce=!0,connection.version="3.5.3",connection.onSettingLocalDescription=function(event){connection.enableLogs&&console.info("Set local description for remote user",event.userid)},connection.resetScreen=function(){sourceId=null,DetectRTC&&DetectRTC.screen&&delete DetectRTC.screen.sourceId,currentUserMediaRequest={streams:[],mutex:!1,queueRequests:[]}},connection.autoCreateMediaElement=!0,connection.password=null,connection.setPassword=function(password,callback){callback=callback||function(){},connection.socket?connection.socket.emit("set-password",password,callback):(connection.password=password,callback(!0,connection.sessionid,null))},connection.errors={ROOM_NOT_AVAILABLE:"Room not available",INVALID_PASSWORD:"Invalid password",USERID_NOT_AVAILABLE:"User ID does not exist",ROOM_PERMISSION_DENIED:"Room permission denied",ROOM_FULL:"Room full",DID_NOT_JOIN_ANY_ROOM:"Did not join any room yet",INVALID_SOCKET:"Invalid socket",PUBLIC_IDENTIFIER_MISSING:"publicRoomIdentifier is required",INVALID_ADMIN_CREDENTIAL:"Invalid username or password attempted"}}(this)};"undefined"!=typeof module&&(module.exports=exports=RTCMultiConnection),"function"==typeof define&&define.amd&&define("RTCMultiConnection",[],function(){return RTCMultiConnection}); /*! jQuery v2.2.2 | (c) jQuery Foundation | jquery.org/license */ !function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=a.document,e=c.slice,f=c.concat,g=c.push,h=c.indexOf,i={},j=i.toString,k=i.hasOwnProperty,l={},m="2.2.2",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return e.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:e.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a){return n.each(this,a)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(e.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor()},push:g,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(n.isPlainObject(d)||(e=n.isArray(d)))?(e?(e=!1,f=c&&n.isArray(c)?c:[]):f=c&&n.isPlainObject(c)?c:{},g[b]=n.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray,isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){var b=a&&a.toString();return!n.isArray(a)&&b-parseFloat(b)+1>=0},isPlainObject:function(a){var b;if("object"!==n.type(a)||a.nodeType||n.isWindow(a))return!1;if(a.constructor&&!k.call(a,"constructor")&&!k.call(a.constructor.prototype||{},"isPrototypeOf"))return!1;for(b in a);return void 0===b||k.call(a,b)},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?i[j.call(a)]||"object":typeof a},globalEval:function(a){var b,c=eval;a=n.trim(a),a&&(1===a.indexOf("use strict")?(b=d.createElement("script"),b.text=a,d.head.appendChild(b).parentNode.removeChild(b)):c(a))},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b){var c,d=0;if(s(a)){for(c=a.length;c>d;d++)if(b.call(a[d],d,a[d])===!1)break}else for(d in a)if(b.call(a[d],d,a[d])===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):g.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:h.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;c>d;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,e,g=0,h=[];if(s(a))for(d=a.length;d>g;g++)e=b(a[g],g,c),null!=e&&h.push(e);else for(g in a)e=b(a[g],g,c),null!=e&&h.push(e);return f.apply([],h)},guid:1,proxy:function(a,b){var c,d,f;return"string"==typeof b&&(c=a[b],b=a,a=c),n.isFunction(a)?(d=e.call(arguments,2),f=function(){return a.apply(b||this,d.concat(e.call(arguments)))},f.guid=a.guid=a.guid||n.guid++,f):void 0},now:Date.now,support:l}),"function"==typeof Symbol&&(n.fn[Symbol.iterator]=c[Symbol.iterator]),n.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(a,b){i["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=!!a&&"length"in a&&a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ga(),z=ga(),A=ga(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+M+"))|)"+L+"*\\]",O=":("+M+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+N+")*)|.*)\\)|)",P=new RegExp(L+"+","g"),Q=new RegExp("^"+L+"+|((?:^|[^\\\\])(?:\\\\.)*)"+L+"+$","g"),R=new RegExp("^"+L+"*,"+L+"*"),S=new RegExp("^"+L+"*([>+~]|"+L+")"+L+"*"),T=new RegExp("="+L+"*([^\\]'\"]*?)"+L+"*\\]","g"),U=new RegExp(O),V=new RegExp("^"+M+"$"),W={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),TAG:new RegExp("^("+M+"|[*])"),ATTR:new RegExp("^"+N),PSEUDO:new RegExp("^"+O),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+L+"*(even|odd|(([+-]|)(\\d*)n|)"+L+"*(?:([+-]|)"+L+"*(\\d+)|))"+L+"*\\)|)","i"),bool:new RegExp("^(?:"+K+")$","i"),needsContext:new RegExp("^"+L+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+L+"*((?:-\\d)?\\d*)"+L+"*\\)|)(?=[^-]|$)","i")},X=/^(?:input|select|textarea|button)$/i,Y=/^h\d$/i,Z=/^[^{]+\{\s*\[native \w/,$=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,_=/[+~]/,aa=/'|\\/g,ba=new RegExp("\\\\([\\da-f]{1,6}"+L+"?|("+L+")|.)","ig"),ca=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},da=function(){m()};try{H.apply(E=I.call(v.childNodes),v.childNodes),E[v.childNodes.length].nodeType}catch(ea){H={apply:E.length?function(a,b){G.apply(a,I.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function fa(a,b,d,e){var f,h,j,k,l,o,r,s,w=b&&b.ownerDocument,x=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==x&&9!==x&&11!==x)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==x&&(o=$.exec(a)))if(f=o[1]){if(9===x){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(w&&(j=w.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(o[2])return H.apply(d,b.getElementsByTagName(a)),d;if((f=o[3])&&c.getElementsByClassName&&b.getElementsByClassName)return H.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==x)w=b,s=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(aa,"\\$&"):b.setAttribute("id",k=u),r=g(a),h=r.length,l=V.test(k)?"#"+k:"[id='"+k+"']";while(h--)r[h]=l+" "+qa(r[h]);s=r.join(","),w=_.test(a)&&oa(b.parentNode)||b}if(s)try{return H.apply(d,w.querySelectorAll(s)),d}catch(y){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(Q,"$1"),b,d,e)}function ga(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ha(a){return a[u]=!0,a}function ia(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ja(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function ka(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||C)-(~a.sourceIndex||C);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function la(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function na(a){return ha(function(b){return b=+b,ha(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function oa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=fa.support={},f=fa.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=fa.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ia(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ia(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Z.test(n.getElementsByClassName),c.getById=ia(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}},d.filter.ID=function(a){var b=a.replace(ba,ca);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(ba,ca);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return"undefined"!=typeof b.getElementsByClassName&&p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=Z.test(n.querySelectorAll))&&(ia(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+L+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+L+"*(?:value|"+K+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ia(function(a){var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+L+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Z.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ia(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",O)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Z.test(o.compareDocumentPosition),t=b||Z.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return ka(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?ka(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},fa.matches=function(a,b){return fa(a,null,null,b)},fa.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(T,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return fa(b,n,null,[a]).length>0},fa.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},fa.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},fa.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},fa.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=fa.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=fa.selectors={cacheLength:50,createPseudo:ha,match:W,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(ba,ca),a[3]=(a[3]||a[4]||a[5]||"").replace(ba,ca),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||fa.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&fa.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return W.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&U.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(ba,ca).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+L+")"+a+"("+L+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=fa.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(P," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||fa.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ha(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ha(function(a){var b=[],c=[],d=h(a.replace(Q,"$1"));return d[u]?ha(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ha(function(a){return function(b){return fa(a,b).length>0}}),contains:ha(function(a){return a=a.replace(ba,ca),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ha(function(a){return V.test(a||"")||fa.error("unsupported lang: "+a),a=a.replace(ba,ca).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Y.test(a.nodeName)},input:function(a){return X.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:na(function(){return[0]}),last:na(function(a,b){return[b-1]}),eq:na(function(a,b,c){return[0>c?c+b:c]}),even:na(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:na(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:na(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:na(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function ra(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j,k=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(j=b[u]||(b[u]={}),i=j[b.uniqueID]||(j[b.uniqueID]={}),(h=i[d])&&h[0]===w&&h[1]===f)return k[2]=h[2];if(i[d]=k,k[2]=a(b,c,g))return!0}}}function sa(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ta(a,b,c){for(var d=0,e=b.length;e>d;d++)fa(a,b[d],c);return c}function ua(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(c&&!c(f,d,e)||(g.push(f),j&&b.push(h)));return g}function va(a,b,c,d,e,f){return d&&!d[u]&&(d=va(d)),e&&!e[u]&&(e=va(e,f)),ha(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ta(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:ua(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=ua(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=ua(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function wa(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ra(function(a){return a===b},h,!0),l=ra(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[ra(sa(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return va(i>1&&sa(m),i>1&&qa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(Q,"$1"),c,e>i&&wa(a.slice(i,e)),f>e&&wa(a=a.slice(e)),f>e&&qa(a))}m.push(c)}return sa(m)}function xa(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=F.call(i));u=ua(u)}H.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&fa.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ha(f):f}return h=fa.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=wa(b[c]),f[u]?d.push(f):e.push(f);f=A(a,xa(e,d)),f.selector=a}return f},i=fa.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(ba,ca),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=W.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(ba,ca),_.test(j[0].type)&&oa(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&qa(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,!b||_.test(a)&&oa(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ia(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ia(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ja("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ia(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ja("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ia(function(a){return null==a.getAttribute("disabled")})||ja(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),fa}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.uniqueSort=n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},v=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},w=n.expr.match.needsContext,x=/^<([\w-]+)\s*\/?>(?:<\/\1>|)$/,y=/^.[^:#\[\.,]*$/;function z(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(y.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return h.call(b,a)>-1!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=this.length,d=[],e=this;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;c>b;b++)if(n.contains(e[b],this))return!0}));for(b=0;c>b;b++)n.find(a,e[b],d);return d=this.pushStack(c>1?n.unique(d):d),d.selector=this.selector?this.selector+" "+a:a,d},filter:function(a){return this.pushStack(z(this,a||[],!1))},not:function(a){return this.pushStack(z(this,a||[],!0))},is:function(a){return!!z(this,"string"==typeof a&&w.test(a)?n(a):a||[],!1).length}});var A,B=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,C=n.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||A,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:B.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),x.test(e[1])&&n.isPlainObject(b))for(e in b)n.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&f.parentNode&&(this.length=1,this[0]=f),this.context=d,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?void 0!==c.ready?c.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};C.prototype=n.fn,A=n(d);var D=/^(?:parents|prev(?:Until|All))/,E={children:!0,contents:!0,next:!0,prev:!0};n.fn.extend({has:function(a){var b=n(a,this),c=b.length;return this.filter(function(){for(var a=0;c>a;a++)if(n.contains(this,b[a]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=w.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?h.call(n(a),this[0]):h.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.uniqueSort(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function F(a,b){while((a=a[b])&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return u(a,"parentNode")},parentsUntil:function(a,b,c){return u(a,"parentNode",c)},next:function(a){return F(a,"nextSibling")},prev:function(a){return F(a,"previousSibling")},nextAll:function(a){return u(a,"nextSibling")},prevAll:function(a){return u(a,"previousSibling")},nextUntil:function(a,b,c){return u(a,"nextSibling",c)},prevUntil:function(a,b,c){return u(a,"previousSibling",c)},siblings:function(a){return v((a.parentNode||{}).firstChild,a)},children:function(a){return v(a.firstChild)},contents:function(a){return a.contentDocument||n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(E[a]||n.uniqueSort(e),D.test(a)&&e.reverse()),this.pushStack(e)}});var G=/\S+/g;function H(a){var b={};return n.each(a.match(G)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?H(a):n.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),h>=c&&h--}),this},has:function(a){return a?n.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().progress(c.notify).done(c.resolve).fail(c.reject):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=e.call(arguments),d=c.length,f=1!==d||a&&n.isFunction(a.promise)?d:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(d){b[a]=this,c[a]=arguments.length>1?e.call(arguments):d,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(d>1)for(i=new Array(d),j=new Array(d),k=new Array(d);d>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().progress(h(b,j,i)).done(h(b,k,c)).fail(g.reject):--f;return f||g.resolveWith(k,c),g.promise()}});var I;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(I.resolveWith(d,[n]),n.fn.triggerHandler&&(n(d).triggerHandler("ready"),n(d).off("ready"))))}});function J(){d.removeEventListener("DOMContentLoaded",J),a.removeEventListener("load",J),n.ready()}n.ready.promise=function(b){return I||(I=n.Deferred(),"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(n.ready):(d.addEventListener("DOMContentLoaded",J),a.addEventListener("load",J))),I.promise(b)},n.ready.promise();var K=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===n.type(c)){e=!0;for(h in c)K(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,n.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(n(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},L=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function M(){this.expando=n.expando+M.uid++}M.uid=1,M.prototype={register:function(a,b){var c=b||{};return a.nodeType?a[this.expando]=c:Object.defineProperty(a,this.expando,{value:c,writable:!0,configurable:!0}),a[this.expando]},cache:function(a){if(!L(a))return{};var b=a[this.expando];return b||(b={},L(a)&&(a.nodeType?a[this.expando]=b:Object.defineProperty(a,this.expando,{value:b,configurable:!0}))),b},set:function(a,b,c){var d,e=this.cache(a);if("string"==typeof b)e[b]=c;else for(d in b)e[d]=b[d];return e},get:function(a,b){return void 0===b?this.cache(a):a[this.expando]&&a[this.expando][b]},access:function(a,b,c){var d;return void 0===b||b&&"string"==typeof b&&void 0===c?(d=this.get(a,b),void 0!==d?d:this.get(a,n.camelCase(b))):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d,e,f=a[this.expando];if(void 0!==f){if(void 0===b)this.register(a);else{n.isArray(b)?d=b.concat(b.map(n.camelCase)):(e=n.camelCase(b),b in f?d=[b,e]:(d=e,d=d in f?[d]:d.match(G)||[])),c=d.length;while(c--)delete f[d[c]]}(void 0===b||n.isEmptyObject(f))&&(a.nodeType?a[this.expando]=void 0:delete a[this.expando])}},hasData:function(a){var b=a[this.expando];return void 0!==b&&!n.isEmptyObject(b)}};var N=new M,O=new M,P=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,Q=/[A-Z]/g;function R(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(Q,"-$&").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:P.test(c)?n.parseJSON(c):c; }catch(e){}O.set(a,b,c)}else c=void 0;return c}n.extend({hasData:function(a){return O.hasData(a)||N.hasData(a)},data:function(a,b,c){return O.access(a,b,c)},removeData:function(a,b){O.remove(a,b)},_data:function(a,b,c){return N.access(a,b,c)},_removeData:function(a,b){N.remove(a,b)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=O.get(f),1===f.nodeType&&!N.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),R(f,d,e[d])));N.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){O.set(this,a)}):K(this,function(b){var c,d;if(f&&void 0===b){if(c=O.get(f,a)||O.get(f,a.replace(Q,"-$&").toLowerCase()),void 0!==c)return c;if(d=n.camelCase(a),c=O.get(f,d),void 0!==c)return c;if(c=R(f,d,void 0),void 0!==c)return c}else d=n.camelCase(a),this.each(function(){var c=O.get(this,d);O.set(this,d,b),a.indexOf("-")>-1&&void 0!==c&&O.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){O.remove(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=N.get(a,b),c&&(!d||n.isArray(c)?d=N.access(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return N.get(a,c)||N.access(a,c,{empty:n.Callbacks("once memory").add(function(){N.remove(a,[b+"queue",c])})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length",""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};$.optgroup=$.option,$.tbody=$.tfoot=$.colgroup=$.caption=$.thead,$.th=$.td;function _(a,b){var c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[];return void 0===b||b&&n.nodeName(a,b)?n.merge([a],c):c}function aa(a,b){for(var c=0,d=a.length;d>c;c++)N.set(a[c],"globalEval",!b||N.get(b[c],"globalEval"))}var ba=/<|&#?\w+;/;function ca(a,b,c,d,e){for(var f,g,h,i,j,k,l=b.createDocumentFragment(),m=[],o=0,p=a.length;p>o;o++)if(f=a[o],f||0===f)if("object"===n.type(f))n.merge(m,f.nodeType?[f]:f);else if(ba.test(f)){g=g||l.appendChild(b.createElement("div")),h=(Y.exec(f)||["",""])[1].toLowerCase(),i=$[h]||$._default,g.innerHTML=i[1]+n.htmlPrefilter(f)+i[2],k=i[0];while(k--)g=g.lastChild;n.merge(m,g.childNodes),g=l.firstChild,g.textContent=""}else m.push(b.createTextNode(f));l.textContent="",o=0;while(f=m[o++])if(d&&n.inArray(f,d)>-1)e&&e.push(f);else if(j=n.contains(f.ownerDocument,f),g=_(l.appendChild(f),"script"),j&&aa(g),c){k=0;while(f=g[k++])Z.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),l.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="",l.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var da=/^key/,ea=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,fa=/^([^.]*)(?:\.(.+)|)/;function ga(){return!0}function ha(){return!1}function ia(){try{return d.activeElement}catch(a){}}function ja(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)ja(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=ha;else if(!e)return a;return 1===f&&(g=e,e=function(a){return n().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=n.guid++)),a.each(function(){n.event.add(this,b,e,d,c)})}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=N.get(a);if(r){c.handler&&(f=c,c=f.handler,e=f.selector),c.guid||(c.guid=n.guid++),(i=r.events)||(i=r.events={}),(g=r.handle)||(g=r.handle=function(b){return"undefined"!=typeof n&&n.event.triggered!==b.type?n.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(G)||[""],j=b.length;while(j--)h=fa.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o&&(l=n.event.special[o]||{},o=(e?l.delegateType:l.bindType)||o,l=n.event.special[o]||{},k=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},f),(m=i[o])||(m=i[o]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,p,g)!==!1||a.addEventListener&&a.addEventListener(o,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),n.event.global[o]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=N.hasData(a)&&N.get(a);if(r&&(i=r.events)){b=(b||"").match(G)||[""],j=b.length;while(j--)if(h=fa.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=i[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&q!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete i[o])}else for(o in i)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(i)&&N.remove(a,"handle events")}},dispatch:function(a){a=n.event.fix(a);var b,c,d,f,g,h=[],i=e.call(arguments),j=(N.get(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())a.rnamespace&&!a.rnamespace.test(g.namespace)||(a.handleObj=g,a.data=g.data,d=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==d&&(a.result=d)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&("click"!==a.type||isNaN(a.button)||a.button<1))for(;i!==this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>-1:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h]*)\/>/gi,la=/\s*$/g;function pa(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function qa(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function ra(a){var b=na.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function sa(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(N.hasData(a)&&(f=N.access(a),g=N.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;d>c;c++)n.event.add(b,e,j[e][c])}O.hasData(a)&&(h=O.access(a),i=n.extend({},h),O.set(b,i))}}function ta(a,b){var c=b.nodeName.toLowerCase();"input"===c&&X.test(a.type)?b.checked=a.checked:"input"!==c&&"textarea"!==c||(b.defaultValue=a.defaultValue)}function ua(a,b,c,d){b=f.apply([],b);var e,g,h,i,j,k,m=0,o=a.length,p=o-1,q=b[0],r=n.isFunction(q);if(r||o>1&&"string"==typeof q&&!l.checkClone&&ma.test(q))return a.each(function(e){var f=a.eq(e);r&&(b[0]=q.call(this,e,f.html())),ua(f,b,c,d)});if(o&&(e=ca(b,a[0].ownerDocument,!1,a,d),g=e.firstChild,1===e.childNodes.length&&(e=g),g||d)){for(h=n.map(_(e,"script"),qa),i=h.length;o>m;m++)j=e,m!==p&&(j=n.clone(j,!0,!0),i&&n.merge(h,_(j,"script"))),c.call(a[m],j,m);if(i)for(k=h[h.length-1].ownerDocument,n.map(h,ra),m=0;i>m;m++)j=h[m],Z.test(j.type||"")&&!N.access(j,"globalEval")&&n.contains(k,j)&&(j.src?n._evalUrl&&n._evalUrl(j.src):n.globalEval(j.textContent.replace(oa,"")))}return a}function va(a,b,c){for(var d,e=b?n.filter(b,a):a,f=0;null!=(d=e[f]);f++)c||1!==d.nodeType||n.cleanData(_(d)),d.parentNode&&(c&&n.contains(d.ownerDocument,d)&&aa(_(d,"script")),d.parentNode.removeChild(d));return a}n.extend({htmlPrefilter:function(a){return a.replace(ka,"<$1>")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=n.contains(a.ownerDocument,a);if(!(l.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(g=_(h),f=_(a),d=0,e=f.length;e>d;d++)ta(f[d],g[d]);if(b)if(c)for(f=f||_(a),g=g||_(h),d=0,e=f.length;e>d;d++)sa(f[d],g[d]);else sa(a,h);return g=_(h,"script"),g.length>0&&aa(g,!i&&_(a,"script")),h},cleanData:function(a){for(var b,c,d,e=n.event.special,f=0;void 0!==(c=a[f]);f++)if(L(c)){if(b=c[N.expando]){if(b.events)for(d in b.events)e[d]?n.event.remove(c,d):n.removeEvent(c,d,b.handle);c[N.expando]=void 0}c[O.expando]&&(c[O.expando]=void 0)}}}),n.fn.extend({domManip:ua,detach:function(a){return va(this,a,!0)},remove:function(a){return va(this,a)},text:function(a){return K(this,function(a){return void 0===a?n.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return ua(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=pa(this,a);b.appendChild(a)}})},prepend:function(){return ua(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=pa(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return ua(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return ua(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(n.cleanData(_(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return K(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!la.test(a)&&!$[(Y.exec(a)||["",""])[1].toLowerCase()]){a=n.htmlPrefilter(a);try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(_(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=[];return ua(this,arguments,function(b){var c=this.parentNode;n.inArray(this,a)<0&&(n.cleanData(_(this)),c&&c.replaceChild(b,this))},a)}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=[],e=n(a),f=e.length-1,h=0;f>=h;h++)c=h===f?this:this.clone(!0),n(e[h])[b](c),g.apply(d,c.get());return this.pushStack(d)}});var wa,xa={HTML:"block",BODY:"block"};function ya(a,b){var c=n(b.createElement(a)).appendTo(b.body),d=n.css(c[0],"display");return c.detach(),d}function za(a){var b=d,c=xa[a];return c||(c=ya(a,b),"none"!==c&&c||(wa=(wa||n("

DetectRTC Issues

Latest Updates

How to use DetectRTC?

<script src="https://www.webrtc-experiment.com/DetectRTC.js"></script>
<script>
// OR otherwise use NPM
var DetectRTC = require('detectrtc');
</script>
DetectRTC.load(function() {
    DetectRTC.hasWebcam (has webcam device!)
    DetectRTC.hasMicrophone (has microphone device!)
    DetectRTC.hasSpeakers (has speakers!)
    DetectRTC.isScreenCapturingSupported
    DetectRTC.isSctpDataChannelsSupported
    DetectRTC.isRtpDataChannelsSupported
    DetectRTC.isAudioContextSupported
    DetectRTC.isWebRTCSupported
    DetectRTC.isDesktopCapturingSupported
    DetectRTC.isMobileDevice

    DetectRTC.isWebSocketsSupported
    DetectRTC.isWebSocketsBlocked
    DetectRTC.checkWebSocketsSupport(callback)

    DetectRTC.isWebsiteHasWebcamPermissions        // getUserMedia allowed for HTTPs domain in Chrome?
    DetectRTC.isWebsiteHasMicrophonePermissions    // getUserMedia allowed for HTTPs domain in Chrome?

    DetectRTC.audioInputDevices    // microphones
    DetectRTC.audioOutputDevices   // speakers
    DetectRTC.videoInputDevices    // cameras

    DetectRTC.osName
    DetectRTC.osVersion

    DetectRTC.browser.name === 'Edge' || 'Chrome' || 'Firefox'
    DetectRTC.browser.version
    DetectRTC.browser.isChrome
    DetectRTC.browser.isFirefox
    DetectRTC.browser.isOpera
    DetectRTC.browser.isIE
    DetectRTC.browser.isSafari
    DetectRTC.browser.isEdge

    DetectRTC.browser.isPrivateBrowsing // incognito or private modes

    DetectRTC.isCanvasSupportsStreamCapturing
    DetectRTC.isVideoSupportsStreamCapturing

    DetectRTC.DetectLocalIPAddress(callback)
});
================================================ FILE: DetectRTC/npm-test.js ================================================ // https://tonicdev.com/npm/detectrtc var DetectRTC; try { DetectRTC = require('detectrtc'); } catch(e) { DetectRTC = require('./DetectRTC.js'); } console.log(DetectRTC.browser.name + ' version ' + DetectRTC.browser.version); console.log(DetectRTC.osName + ' version ' + DetectRTC.osVersion); console.log(JSON.stringify(DetectRTC)); console.log(DetectRTC); process.exit() ================================================ FILE: DetectRTC/package.json ================================================ { "name": "detectrtc", "preferGlobal": false, "version": "1.4.0", "author": { "name": "Muaz Khan", "email": "muazkh@gmail.com", "url": "https://muazkhan.com/" }, "description": "A tiny JavaScript library that can be used to detect WebRTC features e.g. system having speakers, microphone or webcam, screen capturing is supported, number of audio/video devices etc.", "repository": { "type": "git", "url": "https://github.com/muaz-khan/DetectRTC.git" }, "scripts": { "start": "node server.js", "test": "./node_modules/.bin/protractor test/browserstack.config.js" }, "main": "DetectRTC.js", "keywords": [ "webrtc", "detectrtc", "webrtc-library", "library", "javascript", "rtcweb", "webrtc-experiment", "javascript-library", "muaz", "muaz-khan" ], "analyze": false, "license": "MIT", "readmeFilename": "README.md", "bugs": { "url": "https://github.com/muaz-khan/DetectRTC/issues", "email": "muazkh@gmail.com" }, "homepage": "https://www.webrtc-experiment.com/DetectRTC/", "tonicExampleFilename": "npm-test.js", "_id": "detectrtc@", "_from": "detectrtc@", "devDependencies": { "grunt": "0.4.5", "grunt-cli": "0.1.13", "load-grunt-tasks": "3.4.0", "grunt-contrib-concat": "0.5.1", "grunt-contrib-csslint": "0.5.0", "grunt-contrib-jshint": "0.11.3", "grunt-contrib-uglify": "0.11.0", "grunt-jsbeautifier": "0.2.10", "grunt-replace": "0.11.0", "grunt-contrib-clean": "0.6.0", "grunt-bump": "0.7.0", "grunt-contrib-watch": "^1.1.0" } } ================================================ FILE: DetectRTC/server.js ================================================ var isUseHTTPs = false; // !(!!process.env.PORT || !!process.env.IP); var server = require(isUseHTTPs ? 'https' : 'http'), url = require('url'), path = require('path'), fs = require('fs'); function serverHandler(request, response) { var uri = url.parse(request.url).pathname, filename = path.join(process.cwd(), uri); var stats; try { stats = fs.lstatSync(filename); } catch (e) { response.writeHead(404, { 'Content-Type': 'text/plain' }); response.write('404 Not Found: ' + path.join('/', uri) + '\n'); response.end(); return; } if (fs.statSync(filename).isDirectory()) { filename += '/index.html'; } fs.readFile(filename, 'binary', function(err, file) { if (err) { response.writeHead(500, { 'Content-Type': 'text/plain' }); response.write('404 Not Found: ' + path.join('/', uri) + '\n'); response.end(); return; } response.writeHead(200); response.write(file, 'binary'); response.end(); }); } var app; if (isUseHTTPs) { var options = { key: fs.readFileSync(path.join(__dirname, 'fake-keys/privatekey.pem')), cert: fs.readFileSync(path.join(__dirname, 'fake-keys/certificate.pem')) }; app = server.createServer(options, serverHandler); } else app = server.createServer(serverHandler); app = app.listen(process.env.PORT || 9001, process.env.IP || "0.0.0.0", function() { var addr = app.address(); console.log("Server listening at", addr.address + ":" + addr.port); }); ================================================ FILE: DetectRTC/simple-demos/select-cameras.html ================================================ Select & Change Cameras using DetectRTC

Select & Change Cameras using DetectRTC


================================================ FILE: DetectRTC/simple-demos/simple-demo.html ================================================ ================================================ FILE: DetectRTC/test/CheckDeviceSupport.js ================================================ describe('DetectRTC', function() { it('dev/CheckDeviceSupport', function() { console.log('------------------------------'); console.log('\x1b[31m%s\x1b[0m ', 'dev/CheckDeviceSupport.js'); browser.driver.get('https://www.webrtc-experiment.com/DetectRTC/tests/CheckDeviceSupport.html').then(function() { var audioInputDevices = 0; var audioOutputDevices = 0; var videoInputDevices = 0; browser.driver.findElement(by.id('audioInputDevices')).getText().then(function(value) { audioInputDevices = value; }); browser.driver.findElement(by.id('audioOutputDevices')).getText().then(function(value) { audioOutputDevices = value; }); browser.driver.findElement(by.id('videoInputDevices')).getText().then(function(value) { videoInputDevices = value; }); browser.wait(function() { console.log('audioInputDevices: ' + audioInputDevices); console.log('audioOutputDevices: ' + audioOutputDevices); console.log('videoInputDevices: ' + videoInputDevices); return true; }, 1000, 'CheckDeviceSupport did not return valid information.'); }); }); }); ================================================ FILE: DetectRTC/test/DetectRTC.js ================================================ describe('DetectRTC', function() { it('DetectRTC compiled using grunt', function() { console.log('------------------------------'); console.log('\x1b[31m%s\x1b[0m ', 'DetectRTC.js'); browser.driver.get('https://www.webrtc-experiment.com/DetectRTC/tests/DetectRTC.html').then(function() { var booleans = {}; var failed = {}; ['hasWebcam', 'hasMicrophone', 'hasSpeakers', 'isApplyConstraintsSupported', 'isAudioContextSupported', 'isCanvasSupportsStreamCapturing', 'isCreateMediaStreamSourceSupported', 'isGetUserMediaSupported', 'isMobileDevice', 'isMultiMonitorScreenCapturingSupported', 'isORTCSupported', 'isPromisesSupported', 'isRTPSenderReplaceTracksSupported', 'isRemoteStreamProcessingSupported', 'isRtpDataChannelsSupported', 'isScreenCapturingSupported', 'isSctpDataChannelsSupported', 'isSetSinkIdSupported', 'isVideoSupportsStreamCapturing', 'isWebRTCSupported', 'isWebSocketsBlocked', 'isWebSocketsSupported', 'isWebsiteHasMicrophonePermissions', 'isWebsiteHasWebcamPermissions' ].forEach(function(prop) { browser.driver.findElement(by.id(prop)).getText().then(function(value) { if (!value.toString().length) { failed[prop] = true; return; } if (typeof value !== 'boolean') { if (value === 'true') { value = true; } else if (value === 'false') { value = false; } else { value = value; } } booleans[prop] = value; }); }); browser.wait(function() { Object.keys(booleans).forEach(function(key) { console.log(key + ': ' + booleans[key]); }); if (Object.keys(failed).length) { Object.keys(failed).forEach(function(key) { console.error(key + ': test failed.'); }); throw new Error(Object.keys(failed).length + ' tests failed.'); } return true; }, 1000, 'DetectRTC did not return valid information.'); }); }); }); ================================================ FILE: DetectRTC/test/browserstack.config.js ================================================ exports.config = { specs: [ './getBrowserInfo.js', './detectOSName.js', './CheckDeviceSupport.js', './DetectRTC.js' ], seleniumAddress: 'http://hub-cloud.browserstack.com/wd/hub', multiCapabilities: [] }; ['Chrome' /*, 'Firefox'*/ ].forEach(function(browserName) { var browserInfo = getDefaultBrowserInfo(browserName); [ /*'OS X',*/ 'Windows'].forEach(function(os) { browserInfo.os = os; exports.config.multiCapabilities.push(browserInfo); }); }); function getDefaultBrowserInfo(browserName) { return { browserName: browserName, build: 'DetectRTC', name: 'DetectRTC_Tests', 'browserstack.user': process.env.BROWSERSTACK_USERNAME, 'browserstack.key': process.env.BROWSERSTACK_KEY, 'browserstack.debug': 'true' }; } // android /* var androidInfo = getDefaultBrowserInfo('android'); androidInfo.device = 'Samsung Galaxy S5'; androidInfo.platform = 'ANDROID'; exports.config.multiCapabilities.push(androidInfo); */ ================================================ FILE: DetectRTC/test/detectOSName.js ================================================ describe('DetectRTC', function() { it('dev/detectOSName + dev/isMobile + dev/detectDesktopOS', function() { console.log('------------------------------'); console.log('\x1b[31m%s\x1b[0m ', 'dev/detectOSName.js'); console.log('\x1b[31m%s\x1b[0m ', 'dev/isMobile.js'); console.log('\x1b[31m%s\x1b[0m ', 'dev/detectDesktopOS.js'); browser.driver.get('https://www.webrtc-experiment.com/DetectRTC/tests/detectOSName.html').then(function() { var osName = ''; var osVersion = 0; browser.driver.findElement(by.id('osName')).getText().then(function(value) { osName = value; }); browser.driver.findElement(by.id('osVersion')).getText().then(function(value) { osVersion = value; }); browser.wait(function() { console.log('osName: ' + osName); console.log('osVersion: ' + osVersion); if (osName === 'Unknown OS') { throw new Error('Invalid OS version: ' + osName); } if (!osVersion.toString().length || osVersion === 'Unknown OS Version') { throw new Error('Invalid OS version: ' + osVersion); } return true; }, 1000, 'detectOSName did not return valid information.'); }); }); }); ================================================ FILE: DetectRTC/test/getBrowserInfo.js ================================================ function isNumeric(n) { return !isNaN(parseFloat(n)) && isFinite(n); } describe('DetectRTC', function() { it('dev/getBrowserInfo + dev/detectPrivateBrowsing', function() { console.log('------------------------------'); console.log('\x1b[31m%s\x1b[0m ', 'dev/getBrowserInfo.js'); console.log('\x1b[31m%s\x1b[0m ', 'dev/detectPrivateBrowsing.js'); browser.driver.get('https://www.webrtc-experiment.com/DetectRTC/tests/getBrowserInfo.html').then(function() { var browserName = ''; var browserVersion = 0; var browserFullVersion = 0; var isPrivateBrowsing = 0; var failed = {}; browser.driver.findElement(by.id('browserName')).getText().then(function(value) { if (!value.toString().length) { failed['browserName'] = true; return; } browserName = value; }); browser.driver.findElement(by.id('browserVersion')).getText().then(function(value) { if (!value.toString().length) { failed['browserVersion'] = true; return; } browserVersion = value; }); browser.driver.findElement(by.id('browserFullVersion')).getText().then(function(value) { if (!value.toString().length) { failed['browserFullVersion'] = true; return; } browserFullVersion = value; }); browser.driver.findElement(by.id('isPrivateBrowsing')).getText().then(function(value) { if (!value.toString().length) { failed['isPrivateBrowsing'] = true; return; } if (typeof value !== 'boolean') { if (value === 'true') { value = true; } else if (value === 'false') { value = false; } else { value = value; } } isPrivateBrowsing = value; }); browser.wait(function() { console.log('browserName: ' + browserName); console.log('browserVersion: ' + browserVersion); console.log('browserFullVersion: ' + browserFullVersion); console.log('isPrivateBrowsing: ' + isPrivateBrowsing); if (Object.keys(failed).length) { Object.keys(failed).forEach(function(key) { console.error(key + ': test failed.'); }); throw new Error(Object.keys(failed).length + ' tests failed.'); } // a few manual validations var browserNameLooksGood = false; var expectedNames = ['Chrome', 'Firefox', 'Opera', 'Edge', 'IE', 'Safari']; for (var i = 0; i < expectedNames.length; i++) { if (browserName === expectedNames[i]) { browserNameLooksGood = true; } } if (browserNameLooksGood === false) { throw new Error('Invalid browser name: ' + browserName); } if (!browserVersion.toString().length || isNumeric(browserVersion) === false) { throw new Error('Invalid browser version: ' + browserVersion); } if (!browserFullVersion.toString().length) { throw new Error('Invalid browser full version: ' + browserFullVersion); } if (typeof isPrivateBrowsing !== 'boolean') { throw new Error('Invalid "isPrivateBrowsing" value: ' + isPrivateBrowsing); } return true; }, 1000, 'dev/getBrowserInfo.js did not return valid information.'); }); }); }); ================================================ FILE: DetectRTC/test/html-test-files/CheckDeviceSupport.html ================================================ audioInputDevices:
audioOutputDevices:
videoInputDevices: ================================================ FILE: DetectRTC/test/html-test-files/DetectRTC.html ================================================ ================================================ FILE: DetectRTC/test/html-test-files/detectOSName.html ================================================ osName:
osVersion: ================================================ FILE: DetectRTC/test/html-test-files/getBrowserInfo.html ================================================ browserName:
browserVersion:
browserFullVersion:
isPrivateBrowsing: ================================================ FILE: FileBufferReader/.gitignore ================================================ node_modules bower_components make-tar.sh *.tar.gz lib-cov .*.swp ._* .DS_Store .git .hg .npmrc .lock-wscript .svn .wafpickle-* config.gypi CVS npm-debug.log rmc-debug.log ================================================ FILE: FileBufferReader/.npmignore ================================================ # ignore everything * # but not these files... !FileBufferReader.js !FileBufferReader.min.js !package.json !bower.json !README.md ================================================ FILE: FileBufferReader/.travis.yml ================================================ language: node_js node_js: - "0.11" install: npm install before_script: - npm install grunt-cli@0.1.13 -g - npm install grunt@0.4.5 - grunt after_failure: npm install && grunt matrix: fast_finish: true ================================================ FILE: FileBufferReader/FileBufferReader.js ================================================ // Last time updated: 2017-08-27 5:48:35 AM UTC // ________________ // FileBufferReader // Open-Sourced: https://github.com/muaz-khan/FileBufferReader // -------------------------------------------------- // Muaz Khan - www.MuazKhan.com // MIT License - www.WebRTC-Experiment.com/licence // -------------------------------------------------- 'use strict'; (function() { function FileBufferReader() { var fbr = this; var fbrHelper = new FileBufferReaderHelper(); fbr.chunks = {}; fbr.users = {}; fbr.readAsArrayBuffer = function(file, callback, extra) { var options = { file: file, earlyCallback: function(chunk) { callback(fbrClone(chunk, { currentPosition: -1 })); }, extra: extra || { userid: 0 } }; if (file.extra && Object.keys(file.extra).length) { Object.keys(file.extra).forEach(function(key) { options.extra[key] = file.extra[key]; }); } fbrHelper.readAsArrayBuffer(fbr, options); }; fbr.getNextChunk = function(fileUUID, callback, userid) { var currentPosition; if (typeof fileUUID.currentPosition !== 'undefined') { currentPosition = fileUUID.currentPosition; fileUUID = fileUUID.uuid; } var allFileChunks = fbr.chunks[fileUUID]; if (!allFileChunks) { return; } if (typeof userid !== 'undefined') { if (!fbr.users[userid + '']) { fbr.users[userid + ''] = { fileUUID: fileUUID, userid: userid, currentPosition: -1 }; } if (typeof currentPosition !== 'undefined') { fbr.users[userid + ''].currentPosition = currentPosition; } fbr.users[userid + ''].currentPosition++; currentPosition = fbr.users[userid + ''].currentPosition; } else { if (typeof currentPosition !== 'undefined') { fbr.chunks[fileUUID].currentPosition = currentPosition; } fbr.chunks[fileUUID].currentPosition++; currentPosition = fbr.chunks[fileUUID].currentPosition; } var nextChunk = allFileChunks[currentPosition]; if (!nextChunk) { delete fbr.chunks[fileUUID]; fbr.convertToArrayBuffer({ chunkMissing: true, currentPosition: currentPosition, uuid: fileUUID }, callback); return; } nextChunk = fbrClone(nextChunk); if (typeof userid !== 'undefined') { nextChunk.remoteUserId = userid + ''; } if (!!nextChunk.start) { fbr.onBegin(nextChunk); } if (!!nextChunk.end) { fbr.onEnd(nextChunk); } fbr.onProgress(nextChunk); fbr.convertToArrayBuffer(nextChunk, function(buffer) { if (nextChunk.currentPosition == nextChunk.maxChunks) { callback(buffer, true); return; } callback(buffer, false); }); }; var fbReceiver = new FileBufferReceiver(fbr); fbr.addChunk = function(chunk, callback) { if (!chunk) { return; } fbReceiver.receive(chunk, function(chunk) { fbr.convertToArrayBuffer({ readyForNextChunk: true, currentPosition: chunk.currentPosition, uuid: chunk.uuid }, callback); }); }; fbr.chunkMissing = function(chunk) { delete fbReceiver.chunks[chunk.uuid]; delete fbReceiver.chunksWaiters[chunk.uuid]; }; fbr.onBegin = function() {}; fbr.onEnd = function() {}; fbr.onProgress = function() {}; fbr.convertToObject = FileConverter.ConvertToObject; fbr.convertToArrayBuffer = FileConverter.ConvertToArrayBuffer // for backward compatibility----it is redundant. fbr.setMultipleUsers = function() {}; // extends 'from' object with members from 'to'. If 'to' is null, a deep clone of 'from' is returned function fbrClone(from, to) { if (from == null || typeof from != "object") return from; if (from.constructor != Object && from.constructor != Array) return from; if (from.constructor == Date || from.constructor == RegExp || from.constructor == Function || from.constructor == String || from.constructor == Number || from.constructor == Boolean) return new from.constructor(from); to = to || new from.constructor(); for (var name in from) { to[name] = typeof to[name] == "undefined" ? fbrClone(from[name], null) : to[name]; } return to; } } function FileBufferReaderHelper() { var fbrHelper = this; function processInWebWorker(_function) { var blob = URL.createObjectURL(new Blob([_function.toString(), 'this.onmessage = function (e) {' + _function.name + '(e.data);}' ], { type: 'application/javascript' })); var worker = new Worker(blob); return worker; } fbrHelper.readAsArrayBuffer = function(fbr, options) { var earlyCallback = options.earlyCallback; delete options.earlyCallback; function processChunk(chunk) { if (!fbr.chunks[chunk.uuid]) { fbr.chunks[chunk.uuid] = { currentPosition: -1 }; } options.extra = options.extra || { userid: 0 }; chunk.userid = options.userid || options.extra.userid || 0; chunk.extra = options.extra; fbr.chunks[chunk.uuid][chunk.currentPosition] = chunk; if (chunk.end && earlyCallback) { earlyCallback(chunk.uuid); earlyCallback = null; } // for huge files if ((chunk.maxChunks > 200 && chunk.currentPosition == 200) && earlyCallback) { earlyCallback(chunk.uuid); earlyCallback = null; } } if (false && typeof Worker !== 'undefined') { var webWorker = processInWebWorker(fileReaderWrapper); webWorker.onmessage = function(event) { processChunk(event.data); }; webWorker.postMessage(options); } else { fileReaderWrapper(options, processChunk); } }; function fileReaderWrapper(options, callback) { callback = callback || function(chunk) { postMessage(chunk); }; var file = options.file; if (!file.uuid) { file.uuid = (Math.random() * 100).toString().replace(/\./g, ''); } var chunkSize = options.chunkSize || 15 * 1000; if (options.extra && options.extra.chunkSize) { chunkSize = options.extra.chunkSize; } var sliceId = 0; var cacheSize = chunkSize; var chunksPerSlice = Math.floor(Math.min(100000000, cacheSize) / chunkSize); var sliceSize = chunksPerSlice * chunkSize; var maxChunks = Math.ceil(file.size / chunkSize); file.maxChunks = maxChunks; var numOfChunksInSlice; var currentPosition = 0; var hasEntireFile; var chunks = []; callback({ currentPosition: currentPosition, uuid: file.uuid, maxChunks: maxChunks, size: file.size, name: file.name, type: file.type, lastModifiedDate: (file.lastModifiedDate || new Date()).toString(), start: true }); var blob, reader = new FileReader(); reader.onloadend = function(evt) { if (evt.target.readyState == FileReader.DONE) { addChunks(file.name, evt.target.result, function() { sliceId++; if ((sliceId + 1) * sliceSize < file.size) { blob = file.slice(sliceId * sliceSize, (sliceId + 1) * sliceSize); reader.readAsArrayBuffer(blob); } else if (sliceId * sliceSize < file.size) { blob = file.slice(sliceId * sliceSize, file.size); reader.readAsArrayBuffer(blob); } else { file.url = URL.createObjectURL(file); callback({ currentPosition: currentPosition, uuid: file.uuid, maxChunks: maxChunks, size: file.size, name: file.name, lastModifiedDate: (file.lastModifiedDate || new Date()).toString(), url: URL.createObjectURL(file), type: file.type, end: true }); } }); } }; currentPosition += 1; blob = file.slice(sliceId * sliceSize, (sliceId + 1) * sliceSize); reader.readAsArrayBuffer(blob); function addChunks(fileName, binarySlice, addChunkCallback) { numOfChunksInSlice = Math.ceil(binarySlice.byteLength / chunkSize); for (var i = 0; i < numOfChunksInSlice; i++) { var start = i * chunkSize; chunks[currentPosition] = binarySlice.slice(start, Math.min(start + chunkSize, binarySlice.byteLength)); callback({ uuid: file.uuid, buffer: chunks[currentPosition], currentPosition: currentPosition, maxChunks: maxChunks, size: file.size, name: file.name, lastModifiedDate: (file.lastModifiedDate || new Date()).toString(), type: file.type }); currentPosition++; } if (currentPosition == maxChunks) { hasEntireFile = true; } addChunkCallback(); } } } function FileSelector() { var selector = this; var noFileSelectedCallback = function() {}; selector.selectSingleFile = function(callback, failure) { if (failure) { noFileSelectedCallback = failure; } selectFile(callback); }; selector.selectMultipleFiles = function(callback, failure) { if (failure) { noFileSelectedCallback = failure; } selectFile(callback, true); }; selector.selectDirectory = function(callback, failure) { if (failure) { noFileSelectedCallback = failure; } selectFile(callback, true, true); }; selector.accept = '*.*'; function selectFile(callback, multiple, directory) { callback = callback || function() {}; var file = document.createElement('input'); file.type = 'file'; if (multiple) { file.multiple = true; } if (directory) { file.webkitdirectory = true; } file.accept = selector.accept; file.onclick = function() { file.clickStarted = true; }; document.body.onfocus = function() { setTimeout(function() { if (!file.clickStarted) return; file.clickStarted = false; if (!file.value) { noFileSelectedCallback(); } }, 500); }; file.onchange = function() { if (multiple) { if (!file.files.length) { console.error('No file selected.'); return; } var arr = []; Array.from(file.files).forEach(function(file) { file.url = file.webkitRelativePath; arr.push(file); }); callback(arr); return; } if (!file.files[0]) { console.error('No file selected.'); return; } callback(file.files[0]); file.parentNode.removeChild(file); }; file.style.display = 'none'; (document.body || document.documentElement).appendChild(file); fireClickEvent(file); } function getValidFileName(fileName) { if (!fileName) { fileName = 'file' + (new Date).toISOString().replace(/:|\.|-/g, '') } var a = fileName; a = a.replace(/^.*[\\\/]([^\\\/]*)$/i, "$1"); a = a.replace(/\s/g, "_"); a = a.replace(/,/g, ''); a = a.toLowerCase(); return a; } function fireClickEvent(element) { if (typeof element.click === 'function') { element.click(); return; } if (typeof element.change === 'function') { element.change(); return; } if (typeof document.createEvent('Event') !== 'undefined') { var event = document.createEvent('Event'); if (typeof event.initEvent === 'function' && typeof element.dispatchEvent === 'function') { event.initEvent('click', true, true); element.dispatchEvent(event); return; } } var event = new MouseEvent('click', { view: window, bubbles: true, cancelable: true }); element.dispatchEvent(event); } } function FileBufferReceiver(fbr) { var fbReceiver = this; fbReceiver.chunks = {}; fbReceiver.chunksWaiters = {}; function receive(chunk, callback) { if (!chunk.uuid) { fbr.convertToObject(chunk, function(object) { receive(object); }); return; } if (chunk.start && !fbReceiver.chunks[chunk.uuid]) { fbReceiver.chunks[chunk.uuid] = {}; if (fbr.onBegin) fbr.onBegin(chunk); } if (!chunk.end && chunk.buffer) { fbReceiver.chunks[chunk.uuid][chunk.currentPosition] = chunk.buffer; } if (chunk.end) { var chunksObject = fbReceiver.chunks[chunk.uuid]; var chunksArray = []; Object.keys(chunksObject).forEach(function(item, idx) { chunksArray.push(chunksObject[item]); }); var blob = new Blob(chunksArray, { type: chunk.type }); blob = merge(blob, chunk); blob.url = URL.createObjectURL(blob); blob.uuid = chunk.uuid; if (!blob.size) console.error('Something went wrong. Blob Size is 0.'); if (fbr.onEnd) fbr.onEnd(blob); // clear system memory delete fbReceiver.chunks[chunk.uuid]; delete fbReceiver.chunksWaiters[chunk.uuid]; } if (chunk.buffer && fbr.onProgress) fbr.onProgress(chunk); if (!chunk.end) { callback(chunk); fbReceiver.chunksWaiters[chunk.uuid] = function() { function looper() { if (!chunk.buffer) { return; } if (!fbReceiver.chunks[chunk.uuid]) { return; } if (chunk.currentPosition != chunk.maxChunks && !fbReceiver.chunks[chunk.uuid][chunk.currentPosition]) { callback(chunk); setTimeout(looper, 5000); } } setTimeout(looper, 5000); }; fbReceiver.chunksWaiters[chunk.uuid](); } } fbReceiver.receive = receive; } var FileConverter = { ConvertToArrayBuffer: function(object, callback) { binarize.pack(object, function(dataView) { callback(dataView.buffer); }); }, ConvertToObject: function(buffer, callback) { binarize.unpack(buffer, callback); } }; function merge(mergein, mergeto) { if (!mergein) mergein = {}; if (!mergeto) return mergein; for (var item in mergeto) { try { mergein[item] = mergeto[item]; } catch (e) {} } return mergein; } var debug = false; var BIG_ENDIAN = false, LITTLE_ENDIAN = true, TYPE_LENGTH = Uint8Array.BYTES_PER_ELEMENT, LENGTH_LENGTH = Uint16Array.BYTES_PER_ELEMENT, BYTES_LENGTH = Uint32Array.BYTES_PER_ELEMENT; var Types = { NULL: 0, UNDEFINED: 1, STRING: 2, NUMBER: 3, BOOLEAN: 4, ARRAY: 5, OBJECT: 6, INT8ARRAY: 7, INT16ARRAY: 8, INT32ARRAY: 9, UINT8ARRAY: 10, UINT16ARRAY: 11, UINT32ARRAY: 12, FLOAT32ARRAY: 13, FLOAT64ARRAY: 14, ARRAYBUFFER: 15, BLOB: 16, FILE: 16, BUFFER: 17 // Special type for node.js }; if (debug) { var TypeNames = [ 'NULL', 'UNDEFINED', 'STRING', 'NUMBER', 'BOOLEAN', 'ARRAY', 'OBJECT', 'INT8ARRAY', 'INT16ARRAY', 'INT32ARRAY', 'UINT8ARRAY', 'UINT16ARRAY', 'UINT32ARRAY', 'FLOAT32ARRAY', 'FLOAT64ARRAY', 'ARRAYBUFFER', 'BLOB', 'BUFFER' ]; } var Length = [ null, // Types.NULL null, // Types.UNDEFINED 'Uint16', // Types.STRING 'Float64', // Types.NUMBER 'Uint8', // Types.BOOLEAN null, // Types.ARRAY null, // Types.OBJECT 'Int8', // Types.INT8ARRAY 'Int16', // Types.INT16ARRAY 'Int32', // Types.INT32ARRAY 'Uint8', // Types.UINT8ARRAY 'Uint16', // Types.UINT16ARRAY 'Uint32', // Types.UINT32ARRAY 'Float32', // Types.FLOAT32ARRAY 'Float64', // Types.FLOAT64ARRAY 'Uint8', // Types.ARRAYBUFFER 'Uint8', // Types.BLOB, Types.FILE 'Uint8' // Types.BUFFER ]; var binary_dump = function(view, start, length) { var table = [], endianness = BIG_ENDIAN, ROW_LENGTH = 40; table[0] = []; for (var i = 0; i < ROW_LENGTH; i++) { table[0][i] = i < 10 ? '0' + i.toString(10) : i.toString(10); } for (i = 0; i < length; i++) { var code = view.getUint8(start + i, endianness); var index = ~~(i / ROW_LENGTH) + 1; if (typeof table[index] === 'undefined') table[index] = []; table[index][i % ROW_LENGTH] = code < 16 ? '0' + code.toString(16) : code.toString(16); } console.log('%c' + table[0].join(' '), 'font-weight: bold;'); for (i = 1; i < table.length; i++) { console.log(table[i].join(' ')); } }; var find_type = function(obj) { var type = undefined; if (obj === undefined) { type = Types.UNDEFINED; } else if (obj === null) { type = Types.NULL; } else { var const_name = obj.constructor.name; var const_name_reflection = obj.constructor.toString().match(/\w+/g)[1]; if (const_name !== undefined && Types[const_name.toUpperCase()] !== undefined) { // return type by .constructor.name if possible type = Types[const_name.toUpperCase()]; } else if (const_name_reflection !== undefined && Types[const_name_reflection.toUpperCase()] !== undefined) { type = Types[const_name_reflection.toUpperCase()]; } else { // Work around when constructor.name is not defined switch (typeof obj) { case 'string': type = Types.STRING; break; case 'number': type = Types.NUMBER; break; case 'boolean': type = Types.BOOLEAN; break; case 'object': if (obj instanceof Array) { type = Types.ARRAY; } else if (obj instanceof Int8Array) { type = Types.INT8ARRAY; } else if (obj instanceof Int16Array) { type = Types.INT16ARRAY; } else if (obj instanceof Int32Array) { type = Types.INT32ARRAY; } else if (obj instanceof Uint8Array) { type = Types.UINT8ARRAY; } else if (obj instanceof Uint16Array) { type = Types.UINT16ARRAY; } else if (obj instanceof Uint32Array) { type = Types.UINT32ARRAY; } else if (obj instanceof Float32Array) { type = Types.FLOAT32ARRAY; } else if (obj instanceof Float64Array) { type = Types.FLOAT64ARRAY; } else if (obj instanceof ArrayBuffer) { type = Types.ARRAYBUFFER; } else if (obj instanceof Blob) { // including File type = Types.BLOB; } else if (obj instanceof Buffer) { // node.js only type = Types.BUFFER; } else if (obj instanceof Object) { type = Types.OBJECT; } break; default: break; } } } return type; }; var utf16_utf8 = function(string) { return unescape(encodeURIComponent(string)); }; var utf8_utf16 = function(bytes) { return decodeURIComponent(escape(bytes)); }; /** * packs seriarized elements array into a packed ArrayBuffer * @param {Array} serialized Serialized array of elements. * @return {DataView} view of packed binary */ var pack = function(serialized) { var cursor = 0, i = 0, j = 0, endianness = BIG_ENDIAN; var ab = new ArrayBuffer(serialized[0].byte_length + serialized[0].header_size); var view = new DataView(ab); for (i = 0; i < serialized.length; i++) { var start = cursor, header_size = serialized[i].header_size, type = serialized[i].type, length = serialized[i].length, value = serialized[i].value, byte_length = serialized[i].byte_length, type_name = Length[type], unit = type_name === null ? 0 : window[type_name + 'Array'].BYTES_PER_ELEMENT; // Set type if (type === Types.BUFFER) { // on node.js Blob is emulated using Buffer type view.setUint8(cursor, Types.BLOB, endianness); } else { view.setUint8(cursor, type, endianness); } cursor += TYPE_LENGTH; if (debug) { console.info('Packing', type, TypeNames[type]); } // Set length if required if (type === Types.ARRAY || type === Types.OBJECT) { view.setUint16(cursor, length, endianness); cursor += LENGTH_LENGTH; if (debug) { console.info('Content Length', length); } } // Set byte length view.setUint32(cursor, byte_length, endianness); cursor += BYTES_LENGTH; if (debug) { console.info('Header Size', header_size, 'bytes'); console.info('Byte Length', byte_length, 'bytes'); } switch (type) { case Types.NULL: case Types.UNDEFINED: // NULL and UNDEFINED doesn't have any payload break; case Types.STRING: if (debug) { console.info('Actual Content %c"' + value + '"', 'font-weight:bold;'); } for (j = 0; j < length; j++, cursor += unit) { view.setUint16(cursor, value.charCodeAt(j), endianness); } break; case Types.NUMBER: case Types.BOOLEAN: if (debug) { console.info('%c' + value.toString(), 'font-weight:bold;'); } view['set' + type_name](cursor, value, endianness); cursor += unit; break; case Types.INT8ARRAY: case Types.INT16ARRAY: case Types.INT32ARRAY: case Types.UINT8ARRAY: case Types.UINT16ARRAY: case Types.UINT32ARRAY: case Types.FLOAT32ARRAY: case Types.FLOAT64ARRAY: var _view = new Uint8Array(view.buffer, cursor, byte_length); _view.set(new Uint8Array(value.buffer)); cursor += byte_length; break; case Types.ARRAYBUFFER: case Types.BUFFER: var _view = new Uint8Array(view.buffer, cursor, byte_length); _view.set(new Uint8Array(value)); cursor += byte_length; break; case Types.BLOB: case Types.ARRAY: case Types.OBJECT: break; default: throw 'TypeError: Unexpected type found.'; } if (debug) { binary_dump(view, start, cursor - start); } } return view; }; /** * Unpack binary data into an object with value and cursor * @param {DataView} view [description] * @param {Number} cursor [description] * @return {Object} */ var unpack = function(view, cursor) { var i = 0, endianness = BIG_ENDIAN, start = cursor; var type, length, byte_length, value, elem; // Retrieve "type" type = view.getUint8(cursor, endianness); cursor += TYPE_LENGTH; if (debug) { console.info('Unpacking', type, TypeNames[type]); } // Retrieve "length" if (type === Types.ARRAY || type === Types.OBJECT) { length = view.getUint16(cursor, endianness); cursor += LENGTH_LENGTH; if (debug) { console.info('Content Length', length); } } // Retrieve "byte_length" byte_length = view.getUint32(cursor, endianness); cursor += BYTES_LENGTH; if (debug) { console.info('Byte Length', byte_length, 'bytes'); } var type_name = Length[type]; var unit = type_name === null ? 0 : window[type_name + 'Array'].BYTES_PER_ELEMENT; switch (type) { case Types.NULL: case Types.UNDEFINED: if (debug) { binary_dump(view, start, cursor - start); } // NULL and UNDEFINED doesn't have any octet value = null; break; case Types.STRING: length = byte_length / unit; var string = []; for (i = 0; i < length; i++) { var code = view.getUint16(cursor, endianness); cursor += unit; string.push(String.fromCharCode(code)); } value = string.join(''); if (debug) { console.info('Actual Content %c"' + value + '"', 'font-weight:bold;'); binary_dump(view, start, cursor - start); } break; case Types.NUMBER: value = view.getFloat64(cursor, endianness); cursor += unit; if (debug) { console.info('Actual Content %c"' + value.toString() + '"', 'font-weight:bold;'); binary_dump(view, start, cursor - start); } break; case Types.BOOLEAN: value = view.getUint8(cursor, endianness) === 1 ? true : false; cursor += unit; if (debug) { console.info('Actual Content %c"' + value.toString() + '"', 'font-weight:bold;'); binary_dump(view, start, cursor - start); } break; case Types.INT8ARRAY: case Types.INT16ARRAY: case Types.INT32ARRAY: case Types.UINT8ARRAY: case Types.UINT16ARRAY: case Types.UINT32ARRAY: case Types.FLOAT32ARRAY: case Types.FLOAT64ARRAY: case Types.ARRAYBUFFER: elem = view.buffer.slice(cursor, cursor + byte_length); cursor += byte_length; // If ArrayBuffer if (type === Types.ARRAYBUFFER) { value = elem; // If other TypedArray } else { value = new window[type_name + 'Array'](elem); } if (debug) { binary_dump(view, start, cursor - start); } break; case Types.BLOB: if (debug) { binary_dump(view, start, cursor - start); } // If Blob is available (on browser) if (window.Blob) { var mime = unpack(view, cursor); var buffer = unpack(view, mime.cursor); cursor = buffer.cursor; value = new Blob([buffer.value], { type: mime.value }); } else { // node.js implementation goes here elem = view.buffer.slice(cursor, cursor + byte_length); cursor += byte_length; // node.js implementatino uses Buffer to help Blob value = new Buffer(elem); } break; case Types.ARRAY: if (debug) { binary_dump(view, start, cursor - start); } value = []; for (i = 0; i < length; i++) { // Retrieve array element elem = unpack(view, cursor); cursor = elem.cursor; value.push(elem.value); } break; case Types.OBJECT: if (debug) { binary_dump(view, start, cursor - start); } value = {}; for (i = 0; i < length; i++) { // Retrieve object key and value in sequence var key = unpack(view, cursor); var val = unpack(view, key.cursor); cursor = val.cursor; value[key.value] = val.value; } break; default: throw 'TypeError: Type not supported.'; } return { value: value, cursor: cursor }; }; /** * deferred function to process multiple serialization in order * @param {array} array [description] * @param {Function} callback [description] * @return {void} no return value */ var deferredSerialize = function(array, callback) { var length = array.length, results = [], count = 0, byte_length = 0; for (var i = 0; i < array.length; i++) { (function(index) { serialize(array[index], function(result) { // store results in order results[index] = result; // count byte length byte_length += result[0].header_size + result[0].byte_length; // when all results are on table if (++count === length) { // finally concatenate all reuslts into a single array in order var array = []; for (var j = 0; j < results.length; j++) { array = array.concat(results[j]); } callback(array, byte_length); } }); })(i); } }; /** * Serializes object and return byte_length * @param {mixed} obj JavaScript object you want to serialize * @return {Array} Serialized array object */ var serialize = function(obj, callback) { var subarray = [], unit = 1, header_size = TYPE_LENGTH + BYTES_LENGTH, type, byte_length = 0, length = 0, value = obj; type = find_type(obj); unit = Length[type] === undefined || Length[type] === null ? 0 : window[Length[type] + 'Array'].BYTES_PER_ELEMENT; switch (type) { case Types.UNDEFINED: case Types.NULL: break; case Types.NUMBER: case Types.BOOLEAN: byte_length = unit; break; case Types.STRING: length = obj.length; byte_length += length * unit; break; case Types.INT8ARRAY: case Types.INT16ARRAY: case Types.INT32ARRAY: case Types.UINT8ARRAY: case Types.UINT16ARRAY: case Types.UINT32ARRAY: case Types.FLOAT32ARRAY: case Types.FLOAT64ARRAY: length = obj.length; byte_length += length * unit; break; case Types.ARRAY: deferredSerialize(obj, function(subarray, byte_length) { callback([{ type: type, length: obj.length, header_size: header_size + LENGTH_LENGTH, byte_length: byte_length, value: null }].concat(subarray)); }); return; case Types.OBJECT: var deferred = []; for (var key in obj) { if (obj.hasOwnProperty(key)) { deferred.push(key); deferred.push(obj[key]); length++; } } deferredSerialize(deferred, function(subarray, byte_length) { callback([{ type: type, length: length, header_size: header_size + LENGTH_LENGTH, byte_length: byte_length, value: null }].concat(subarray)); }); return; case Types.ARRAYBUFFER: byte_length += obj.byteLength; break; case Types.BLOB: var mime_type = obj.type; var reader = new FileReader(); reader.onload = function(e) { deferredSerialize([mime_type, e.target.result], function(subarray, byte_length) { callback([{ type: type, length: length, header_size: header_size, byte_length: byte_length, value: null }].concat(subarray)); }); }; reader.onerror = function(e) { throw 'FileReader Error: ' + e; }; reader.readAsArrayBuffer(obj); return; case Types.BUFFER: byte_length += obj.length; break; default: throw 'TypeError: Type "' + obj.constructor.name + '" not supported.'; } callback([{ type: type, length: length, header_size: header_size, byte_length: byte_length, value: value }].concat(subarray)); }; /** * Deserialize binary and return JavaScript object * @param ArrayBuffer buffer ArrayBuffer you want to deserialize * @return mixed Retrieved JavaScript object */ var deserialize = function(buffer, callback) { var view = buffer instanceof DataView ? buffer : new DataView(buffer); var result = unpack(view, 0); return result.value; }; if (debug) { window.Test = { BIG_ENDIAN: BIG_ENDIAN, LITTLE_ENDIAN: LITTLE_ENDIAN, Types: Types, pack: pack, unpack: unpack, serialize: serialize, deserialize: deserialize }; } var binarize = { pack: function(obj, callback) { try { if (debug) console.info('%cPacking Start', 'font-weight: bold; color: red;', obj); serialize(obj, function(array) { if (debug) console.info('Serialized Object', array); callback(pack(array)); }); } catch (e) { throw e; } }, unpack: function(buffer, callback) { try { if (debug) console.info('%cUnpacking Start', 'font-weight: bold; color: red;', buffer); var result = deserialize(buffer); if (debug) console.info('Deserialized Object', result); callback(result); } catch (e) { throw e; } } }; window.FileConverter = FileConverter; window.FileSelector = FileSelector; window.FileBufferReader = FileBufferReader; })(); ================================================ FILE: FileBufferReader/Gruntfile.js ================================================ 'use strict'; module.exports = function(grunt) { require('load-grunt-tasks')(grunt, { pattern: 'grunt-*', config: 'package.json', scope: 'devDependencies' }); var banner = '// Last time updated: <%= grunt.template.today("UTC:yyyy-mm-dd h:MM:ss TT Z") %>\n\n'; banner += '// ________________\n'; banner += '// FileBufferReader\n\n'; banner += '// Open-Sourced: https://github.com/muaz-khan/FileBufferReader\n\n'; banner += '// --------------------------------------------------\n'; banner += '// Muaz Khan - www.MuazKhan.com\n'; banner += '// MIT License - www.WebRTC-Experiment.com/licence\n'; banner += '// --------------------------------------------------\n\n'; banner += '\'use strict\';\n\n'; // configure project grunt.initConfig({ // make node configurations available pkg: grunt.file.readJSON('package.json'), concat: { options: { stripBanners: true, separator: '\n', banner: banner }, dist: { src: [ 'dev/head.js', 'dev/FileBufferReader.js', 'dev/FileBufferReaderHelper.js', 'dev/FileSelector.js', 'dev/FileBufferReceiver.js', 'dev/FileConverter.js', 'dev/common.js', 'dev/binarize.js', 'dev/tail.js' ], dest: 'FileBufferReader.js', }, }, uglify: { options: { mangle: false, banner: banner }, my_target: { files: { 'FileBufferReader.min.js': ['FileBufferReader.js'] } } }, jsbeautifier: { files: ['dev/*.js', 'demo/*.js', 'FileBufferReader.js'], options: { js: { braceStyle: "collapse", breakChainedMethods: false, e4x: false, evalCode: false, indentChar: " ", indentLevel: 0, indentSize: 4, indentWithTabs: false, jslintHappy: false, keepArrayIndentation: false, keepFunctionIndentation: false, maxPreserveNewlines: 10, preserveNewlines: true, spaceBeforeConditional: true, spaceInParen: false, unescapeStrings: false, wrapLineLength: 0 }, html: { braceStyle: "collapse", indentChar: " ", indentScripts: "keep", indentSize: 4, maxPreserveNewlines: 10, preserveNewlines: true, unformatted: ["a", "sub", "sup", "b", "i", "u"], wrapLineLength: 0 }, css: { indentChar: " ", indentSize: 4 } } }, bump: { options: { files: ['package.json', 'bower.json'], updateConfigs: [], commit: true, commitMessage: 'v%VERSION%', commitFiles: ['package.json', 'bower.json'], createTag: true, tagName: '%VERSION%', tagMessage: '%VERSION%', push: false, pushTo: 'upstream', gitDescribeOptions: '--tags --always --abbrev=1 --dirty=-d' } } }); // enable plugins // set default tasks to run when grunt is called without parameters // http://gruntjs.com/api/grunt.task grunt.registerTask('default', ['concat', 'jsbeautifier', 'uglify']); }; ================================================ FILE: FileBufferReader/LICENSE ================================================ The MIT License (MIT) Copyright (c) 2017 [Muaz Khan](https://github.com/muaz-khan) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: FileBufferReader/README.md ================================================ # [FileBufferReader.js](https://github.com/muaz-khan/FileBufferReader) / [Demo](https://www.WebRTC-Experiment.com/FileBufferReader/) / [Watch a YouTube video](https://www.youtube.com/watch?v=gv8xpdGdS4o) [![npm](https://img.shields.io/npm/v/fbr.svg)](https://npmjs.org/package/fbr) [![downloads](https://img.shields.io/npm/dm/fbr.svg)](https://npmjs.org/package/fbr) [![Build Status: Linux](https://travis-ci.org/muaz-khan/FileBufferReader.png?branch=master)](https://travis-ci.org/muaz-khan/FileBufferReader) All released versions: https://github.com/muaz-khan/FileBufferReader/releases Using FileBufferReader.js, you can: 1. Get list of array-buffers with each specific chunkSize 2. Chunks can be step-by-step shared with remote peers, or instantly shared using for-loop You can easily implement retransmission of chunks as well. You need to set `binaryType` to `arraybuffer`: ```javascript WebRTC_Data_Channel.binaryType = 'arraybuffer'; ``` ## A few points: 1. FileBufferReader itself doesn't do anything except reading the file(s) 2. You need to manually share chunks using your preferred medium or gateway 3. FileBufferReader currently uses memory to store chunks; which has storage limits. So, you may not be able to use FileBufferReader to read/share file with 1GB size or more. 4. FileBufferReader is added to support controlled-buffers transmissions whilst following Skype's file sharing style. It is MIT Licenced, which means that you can use it in any commercial/non-commercial product, free of cost. ```sh npm install fbr --production # or using bower bower install fbr ``` To use it: ```html ``` Or run localhost server: ```sh node server.js ``` Then open: `http://localhost:9001/` or `http://127.0.0.1:9001/`. ## [fbr-client](https://github.com/muaz-khan/FileBufferReader/tree/master/fbr-client) You can even try [socket.io file sharing client](https://github.com/muaz-khan/FileBufferReader/tree/master/fbr-client): ```sh npm install fbr-client ``` Then run the server: ```sh cd ./node_modules/fbr-client node server.js port=9001 ``` Then open: `http://localhost:9001/` or `http://127.0.0.1:9001/`. > You can modify development files from the `dev` directory; and use `grunt` tool to recompile into `FileBufferReader.js`. ## FileBufferReader API 1. `chunks` object. It contains multiple files' chunks. Even if you received chunks from remote peer, and invoked `addChunk` method; all chunks will be stored in same `chunks` object. `var fileChunks = fileBufferReader.chunks['file-uuid']`. 2. `readAsArrayBuffer` method. It reads entire file and stores chunkified buffers in `chunks` object. 3. `getNextChunk` method. It simply reads `last-position` and returns next available array-buffer chunk. 4. `onBegin`, `onEnd` and `onProgress` events. These are added only to support file progress bars. 5. `addChunk` method. It allows you store all received chunks in an array until entire file is received. 6. `convertToObject` method. FileBufferReader assumes that you're sending ArrayBuffer using WebRTC data channels. It means that you'll be getting ArrayBuffer type in the `onmessage` event. `convertToObject` method allows you convert ArrayBuffer into JavaScript object type, which is helpful to check type of message. 7. `convertToArrayBuffer` method. You can pass javascript object or any data-type, and this method will return `ArrayBuffer`. ## 1. Link The Library ``` https://cdn.webrtc-experiment.com/FileBufferReader.js # or https://cdn.rawgit.com/muaz-khan/FileBufferReader/master/FileBufferReader.js ``` ## 2. Select File (optional step) You can use `input[type=file].onchange` instead, which is **strongly recommended** over using `FileSelecter` because `FileSelector` object is incapable to handle failures or situations where browser doesn't fires `onchange` event. ```javascript var fileSelector = new FileSelector(); // *.png, *.jpeg, *.mp4, etc. fileSelector.accept = '*.*'; var btnSelectFile = document.getElementById('select-file'); btnSelectFile.onclick = function() { fileSelector.selectSingleFile(function(file) { // file == input[type=file] }); }; ``` ## 3. Read Buffers ```javascript var fileBufferReader = new FileBufferReader(); fileBufferReader.readAsArrayBuffer(file, function(fileUUID) { // var file = fileBufferReader.chunks[fileUUID]; // var listOfChunks = file.listOfChunks; // get first chunk, and send using WebRTC data channels // NEVER send chunks in loop; otherwise you'll face issues in slow networks // remote peer should notify if it is ready for next chunk fileBufferReader.getNextChunk(fileUUID, function(nextChunk, isLastChunk) { if(isLastChunk) { alert('File Successfully sent.'); } // sending using WebRTC data channels datachannel.send(nextChunk); }); }); ``` `readAsArrayBuffer` takes 3rd argument as well; where you can pass `chunkSize`, and your custom data. ```javascript var extra = { chunkSize: 15 * 1000, // Firefox' receiving limit is 16k userid: 'sender-userid' // MOST USEFUL object }; fileBufferReader.readAsArrayBuffer(file, callback, extra); ``` ## 4. When remote peer receives a chunk ```javascript datachannel.onmessage = function(event) { var chunk = event.data; if (chunk instanceof ArrayBuffer || chunk instanceof DataView) { // array buffers are passed using WebRTC data channels // need to convert data back into JavaScript objects fileBufferReader.convertToObject(chunk, function(object) { datachannel.onmessage({ data: object }); }); return; } // if you passed "extra-data", you can access it here: // chunk.extra.senderUserName or whatever else // if target peer requested next chunk if(chunk.readyForNextChunk) { fileBufferReader.getNextChunk(chunk.uuid, function(nextChunk, isLastChunk) { if(isLastChunk) { alert('File Successfully sent.'); } // sending using WebRTC data channels datachannel.send(nextChunk); }); return; } // if chunk is received fileBufferReader.addChunk(chunk, function(promptNextChunk) { // request next chunk datachannel.send(promptNextChunk); }); }; ``` ## 5. File progress helpers Link this script: ``` https://cdn.webrtc-experiment.com/FileProgressBarHandler.js # or https://cdn.rawgit.com/muaz-khan/FileBufferReader/master/fbr.0/dev/FileProgressBarHandler.js ``` Add a files-div: ```html
``` Add following code: ```javascript // this line is optional // however it allows you set the
for progress-bars and files-preview fileBufferReader.filesContainer = document.getElementById('files-container'); // this line sets "onFileStart", "onFileProgress" and "onFileEnd" events (see below lines) FileProgressBarHandler.handle(fileBufferReader); fileBufferReader.onBegin = fileBufferReader.onFileStart; fileBufferReader.onProgress = fileBufferReader.onFileProgress; fileBufferReader.onEnd = fileBufferReader.onFileEnd; ``` Above snippet can be written as following: ```javascript var options = {}; // this line is optional // however it allows you set the
for progress-bars and files-preview options.filesContainer = document.getElementById('files-container'); // this line sets "onFileStart", "onFileProgress" and "onFileEnd" events (see below lines) FileProgressBarHandler.handle(options); fileBufferReader.onBegin = options.onFileStart; fileBufferReader.onProgress = options.onFileProgress; fileBufferReader.onEnd = options.onFileEnd; ``` If you're **NOT interested** in above `FileProgressBarHandler.js`: ```javascript var progressHelper = {}; var outputPanel = document.body; var FileHelper = { onBegin: function(file) { // if you passed "extra-data", you can access it here: // file.extra.senderUserName or whatever else var li = document.createElement('li'); li.title = file.name; li.innerHTML = ' '; outputPanel.insertBefore(li, outputPanel.firstChild); progressHelper[file.uuid] = { li: li, progress: li.querySelector('progress'), label: li.querySelector('label') }; progressHelper[file.uuid].progress.max = file.maxChunks; }, onEnd: function(file) { // if you passed "extra-data", you can access it here: // file.extra.senderUserName or whatever else progressHelper[file.uuid].li.innerHTML = '' + file.name + ''; }, onProgress: function(chunk) { // if you passed "extra-data", you can access it here: // chunk.extra.senderUserName or whatever else var helper = progressHelper[chunk.uuid]; helper.progress.value = chunk.currentPosition || chunk.maxChunks || helper.progress.max; updateLabel(helper.progress, helper.label); } }; function updateLabel(progress, label) { if (progress.position == -1) return; var position = +progress.position.toFixed(2).split('.')[1] || 100; label.innerHTML = position + '%'; } fileBufferReader.onBegin = FileHelper.onBegin; fileBufferReader.onProgress = FileHelper.onProgress; fileBufferReader.onEnd = FileHelper.onEnd; ``` ## Sharing with multiple users? ```javascript fbr.readAsArrayBuffer(file, function(fileUUID) { ['first-user', 'second-user', 'third-user'].forEach(function(userid) { fbr.getNextChunk(fileUUID, function(nextChunk, isLastChunk) { specific_datachannel.send(nextChunk); }, userid); }); }); datachannel.onmessage = function(event) { fbr.getNextChunk(message.uuid, function(nextChunk, isLastChunk) { specific_datachannel.send(nextChunk); }, specific_userid); }; ``` > Pass specific-userid as 3rd argument over `getNextChunk` method. To uniquely identify progress-bars for each user, watch for `remoteUserId` object: ```javascript FileHelper.onBegin = function(file) { if(file.remoteUserId) { // file is being shared with multiple users } }; FileHelper.onEnd = function(file) { if(file.remoteUserId) { // file is being shared with multiple users } }; FileHelper.onProgress = function(chunk) { if(chunk.remoteUserId) { // file is being shared with multiple users } }; ``` ## Advance Usages ```javascript var fbr = new FileBufferReader(); fbr.readAsArrayBuffer(file, function(fileUUID) { // don't call "getNextChunk" // instead, try to process/use/acccess chunks yourself var thisFileChunks = fbr.chunks[fileUUID]; var numberOfFileChunks = thisFileChunks[0].maxChunks; var arrayOfRealBuffers = []; for(var i = 1; i < numberOfFileChunks; i++) { var fileChunk = thisFileChunks[i]; var realArrayBufferObject = fileChunk.buffer; arrayOfRealBuffers.push(realArrayBufferObject); } var fileBlob = new Blob(realArrayBufferObject, { type: thisFileChunks[0].type }); var blobURL = URL.createObjectURL(fileBlob); document.write(''); }); ``` The structure of `fileBufferReader.chunks` object looks like this: ```javascript fileBufferReader.chunks = { // "4152661527041346" is file-uuid "4152661527041346":{ // "0" index helps firing "onStart" event. "0":{ "currentPosition":0, "uuid":"4152661527041346", "maxChunks":20, "size":298540, "name":"WebRTC.png", "type":"image/png", "lastModifiedDate":"Wed Oct 14 2015 15:51:26 GMT+0500 (PKT)", "start":true, "userid":0, "extra":{ "userid":0 } }, // index "1" to "maxChunks" are the real file-chunks "1":{ "uuid":"4152661527041346", // this is the real ArrayBuffer object "buffer":{}, "currentPosition":1, "maxChunks":20, "size":298540, "name":"WebRTC.png", "lastModifiedDate":"Wed Oct 14 2015 15:51:26 GMT+0500 (PKT)", "type":"image/png", "userid":0, "extra":{ "userid":0 } }, .... .... // this is the last file-chunk // here "currentPosition===maxChunks" "20":{ "uuid":"4152661527041346", "buffer":{}, "currentPosition":20, "maxChunks":20, "size":298540, "name":"WebRTC.png", "lastModifiedDate":"Wed Oct 14 2015 15:51:26 GMT+0500 (PKT)", "type":"image/png", "userid":0, "extra":{ "userid":0 } }, // this one helps firing "onEnd" event "21":{ "currentPosition":21, "uuid":"4152661527041346", "maxChunks":20, "size":298540, "name":"WebRTC.png", "lastModifiedDate":"Wed Oct 14 2015 15:51:26 GMT+0500 (PKT)", "url":"blob:http%3A//domain/fe5a20d0-cdb4-4f4e-9de7-1a14340fc402", "type":"image/png", "end":true, "userid":0, "extra":{ "userid":0 } }, // this is optionally used to detect which chunk is being shared // you should skip it. "currentPosition":0 } } ``` You can see that real file-chunks starts from `1` and ends before `length-1`. E.g. ```javascript var fileChunks = fbr.chunks['file-uuid']; var allFileIndices = Object.keys(fileChunks); var allFileBuffers = []; for(var chunkIndex = 1; chunkIndex < allFileIndices.length; i++) { var chunk = fileChunks[chunkIndex]; allFileBuffers.push(chunk.buffer); } ``` # `FileSelector` Provides methods to select single file, multiple files or entire directory. **Select single file:** ```javascript var selector = new FileSelector(); selector.accept = '*.png'; selector.selectSingleFile(function(file) { alert(file.name); }, function() { alert('User did not select any file.'); }); ``` **Select multiple files:** ```javascript var selector = new FileSelector(); selector.accept = '*.png'; selector.selectMultipleFiles(function(files) { files.forEach(function(file) { alert(file.name); }); }, function() { alert('User did not select any file.'); }); ``` **Select entire directory:** ```javascript var selector = new FileSelector(); selector.accept = '*.png'; selector.selectDirectory(function(files) { files.forEach(function(file) { alert(file.webkitRelativePath); }); }, function() { alert('User did not select any file.'); }); ``` # `FileConverter` This global object exposes two methods: 1. `ConvertToArrayBuffer` 2. `ConvertToObject` Here is how to use these methods: ```javascript var yourObject = { x: 0, y: 1, str: 'string', bool: true }; FileConverter.ConvertToArrayBuffer(yourObject, function(arrayBuffer) { alert(arrayBuffer.byteLength); // to convert back to "object" FileConverter.ConvertToObject(arrayBuffer, function(yourObject) { alert( JSON.stringify(yourObject) ); }); }); ``` When you call `getNextChunk`, the `FileBufferReader` instance checks for `currentPosition` and returns buffer using following snippet: ```javascript // this method explains insights of FileBufferReader fbr.getNextChunk = function(fileUUId, callback) { var fileChunks = fbr.chunks[fileUUID]; var currentPosition = fileChunks.currentPosition; var nextChunk = fileChunks[currentPosition]; FileConverter.ConvertToArrayBuffer(nextChunk, function(buffer) { // you can see that "callback" is passed two arguments // 1) the converted buffer // 2) "isLastChunk" boolean callback(buffer, currentPosition == nextChunk.maxChunks); }); }; // and your code calls above method as following; fbr.getNextChunks('file-uuid', function(buffer) { webrtc.channel.send(buffer); }); ``` ## Applications using FileBufferReader 1. [RTCMultiConnection.js](https://githbu.com/RTCMultiConnection) ## RTCMultiConnection FileBufferReader Demos 1. https://rtcmulticonnection.herokuapp.com/demos/Audio+Video+TextChat+FileSharing.html 2. https://rtcmulticonnection.herokuapp.com/demos/TextChat+FileSharing.html More demos here: https://rtcmulticonnection.herokuapp.com/demos/ ## License [FileBufferReader.js](https://github.com/muaz-khan/FileBufferReader) is released under [MIT licence](https://www.webrtc-experiment.com/licence/) . Copyright (c) [Muaz Khan](http://www.muazkhan.com/). ================================================ FILE: FileBufferReader/bower.json ================================================ { "name": "fbr", "preferGlobal": true, "version": "2.0.8", "author": { "name": "Muaz Khan", "email": "muazkh@gmail.com", "url": "http://www.muazkhan.com/" }, "description": "FileBufferReader is a JavaScript library reads file and returns chunkified array-buffers. The resulting buffers can be shared using WebRTC data channels or socket.io. Share files same as Skype do!", "scripts": { "start": "node FileBufferReader.js" }, "main": "./FileBufferReader.js", "repository": { "type": "git", "url": "https://github.com/muaz-khan/FileBufferReader.git" }, "keywords": [ "webrtc", "file", "file-sharing", "datachannel", "data-channels", "file-reading", "file-buffer", "buffer", "arraybuffer", "buffers", "chunkifier", "fragmenter", "library", "javascript", "webrtc-experiment", "javascript-library", "muaz", "muaz-khan" ], "analyze": false, "license": "MIT", "readmeFilename": "README.md", "bugs": { "url": "https://github.com/muaz-khan/FileBufferReader/issues", "email": "muazkh@gmail.com" }, "homepage": "https://www.WebRTC-Experiment.com/FileBufferReader/", "_id": "fbr@", "_from": "fbr@", "devDependencies": { "grunt": "0.4.5", "grunt-bump": "0.7.0", "grunt-cli": "0.1.13", "grunt-contrib-clean": "0.6.0", "grunt-contrib-concat": "0.5.1", "grunt-contrib-copy": "0.8.2", "grunt-contrib-uglify": "0.11.0", "grunt-jsbeautifier": "0.2.10", "grunt-replace": "0.11.0", "load-grunt-tasks": "3.4.0" } } ================================================ FILE: FileBufferReader/demo/IceServersHandler.js ================================================ // IceServersHandler.js var IceServersHandler = (function() { function getIceServers(connection) { // resiprocate: 3344+4433 // pions: 7575 var iceServers = [{ 'urls': [ 'stun:webrtcweb.com:7788', // coTURN 'stun:webrtcweb.com:7788?transport=udp', // coTURN ], 'username': 'muazkh', 'credential': 'muazkh' }, { 'urls': [ 'turn:webrtcweb.com:7788', // coTURN 7788+8877 'turn:webrtcweb.com:4455?transport=udp', // restund udp 'turn:webrtcweb.com:8877?transport=udp', // coTURN udp 'turn:webrtcweb.com:8877?transport=tcp', // coTURN tcp ], 'username': 'muazkh', 'credential': 'muazkh' }, { 'urls': [ 'stun:stun.l.google.com:19302', 'stun:stun.l.google.com:19302?transport=udp', ] } ]; if (typeof window.InstallTrigger !== 'undefined') { iceServers[0].urls = iceServers[0].urls.pop(); iceServers[1].urls = iceServers[1].urls.pop(); } return iceServers; } return { getIceServers: getIceServers }; })(); ================================================ FILE: FileBufferReader/demo/PeerConnection.js ================================================ // Last time updated at November 17, 2018 // Muaz Khan - www.MuazKhan.com // MIT License - www.WebRTC-Experiment.com/licence // Documentation - https://github.com/muaz-khan/FileBufferReader.js // _________________ // PeerConnection.js (function() { window.PeerConnection = function(socketURL, userid) { this.userid = userid || getToken(); this.peers = {}; if (!socketURL) throw 'Socket-URL is required.'; var signaler = new Signaler(this, socketURL); var that = this; this.send = function(data) { var channel = answererDataChannel || offererDataChannel; if (channel.readyState != 'open') { return setTimeout(function() { that.send(data); }, 1000); } channel.send(data); }; signaler.ondata = function(data) { if (that.ondata) { that.ondata(data); } }; this.onopen = function() { console.log('DataChannel Opened.'); }; }; function Signaler(root, socketURL) { var self = this; root.startBroadcasting = function() { (function transmit() { socket.send({ userid: root.userid, broadcasting: true }); !self.participantFound && !self.stopBroadcasting && setTimeout(transmit, 3000); })(); }; root.sendParticipationRequest = function(userid) { socket.send({ participationRequest: true, userid: root.userid, to: userid }); }; // if someone shared SDP this.onsdp = function(message) { var sdp = message.sdp; if (sdp.type == 'offer') { root.peers[message.userid] = Answer.createAnswer(merge(options, { sdp: sdp })); } if (sdp.type == 'answer') { root.peers[message.userid].setRemoteDescription(sdp); } }; root.acceptRequest = function(userid) { root.peers[userid] = Offer.createOffer(options); }; // it is passed over Offer/Answer objects for reusability var options = { onsdp: function(sdp) { socket.send({ userid: root.userid, sdp: sdp, to: root.participant }); }, onicecandidate: function(candidate) { socket.send({ userid: root.userid, candidate: candidate, to: root.participant }); }, ondata: function(data) { self.ondata(data); }, onopen: function() { root.onopen(); }, onclose: function(e) { if (root.onclose) root.onclose(e); }, onerror: function(e) { if (root.onerror) root.onerror(e); } }; function closePeerConnections() { self.stopBroadcasting = true; for (var userid in root.peers) { if(root.peers[userid] && root.peers[userid].peer) { root.peers[userid].peer.close(); } } root.peers = {}; } root.close = function() { socket.send({ userLeft: true, userid: root.userid, to: root.participant }); closePeerConnections(); }; window.onbeforeunload = function() { root.close(); }; window.onkeyup = function(e) { if (e.keyCode == 116) { root.close(); } }; // users who broadcasts themselves var invokers = {}, peer; function onmessage(e) { var message = JSON.parse(e.data); if (message.userid == root.userid) return; root.participant = message.userid; // for pretty logging message.sdp && console.debug(JSON.stringify(message, function(key, value) { console.log(value.sdp.type, '---', value.sdp.sdp); }, '---')); // if someone shared SDP if (message.sdp && message.to == root.userid) { self.onsdp(message); } // if someone shared ICE if (message.candidate && message.to == root.userid) { peer = root.peers[message.userid]; if (peer) peer.addIceCandidate(message.candidate); } // if someone sent participation request if (message.participationRequest && message.to == root.userid) { self.participantFound = true; if (root.onParticipationRequest) { root.onParticipationRequest(message.userid); } else root.acceptRequest(message.userid); } // if someone is broadcasting himself! if (message.broadcasting) { if (!invokers[message.userid]) { invokers[message.userid] = message; if (root.onuserfound) root.onuserfound(message.userid); else root.sendParticipationRequest(message.userid); } } if (message.userLeft && message.to == root.userid) { closePeerConnections(); } } var socket = socketURL; if (typeof socketURL == 'string') { socket = new WebSocket(socketURL); socket.push = socket.send; socket.send = function(data) { if (socket.readyState != 1) return setTimeout(function() { socket.send(data); }, 1000); socket.push(JSON.stringify(data)); }; socket.onopen = function() { console.log('websocket connection opened.'); }; } socket.onmessage = onmessage; } var RTCPeerConnection; if (typeof window.RTCPeerConnection !== 'undefined') { RTCPeerConnection = window.RTCPeerConnection; } else if (typeof mozRTCPeerConnection !== 'undefined') { RTCPeerConnection = mozRTCPeerConnection; } else if (typeof webkitRTCPeerConnection !== 'undefined') { RTCPeerConnection = webkitRTCPeerConnection; } var RTCSessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription; var RTCIceCandidate = window.RTCIceCandidate || window.mozRTCIceCandidate; var isChrome = !!navigator.webkitGetUserMedia; var iceServers = { iceServers: IceServersHandler.getIceServers(), iceTransportPolicy: 'all' }; var optionalArgument = { optional: [{ DtlsSrtpKeyAgreement: true }, { googImprovedWifiBwe: true }, { googScreencastMinBitrate: 300 }, { googIPv6: true }, { googDscp: true }, { googCpuUnderuseThreshold: 55 }, { googCpuOveruseThreshold: 85 }, { googSuspendBelowMinBitrate: true }, { googCpuOveruseDetection: true }], mandatory: {} }; var offerAnswerConstraints = { OfferToReceiveAudio: false, OfferToReceiveVideo: false, optional: [], mandatory: { OfferToReceiveAudio: false, OfferToReceiveVideo: false } }; function getToken() { if (window.crypto && window.crypto.getRandomValues && navigator.userAgent.indexOf('Safari') === -1) { var a = window.crypto.getRandomValues(new Uint32Array(3)), token = ''; for (var i = 0, l = a.length; i < l; i++) { token += a[i].toString(36); } return token; } else { return (Math.random() * new Date().getTime()).toString(36).replace(/\./g, ''); } } function setChannelEvents(channel, config) { channel.binaryType = 'arraybuffer'; channel.onmessage = function(event) { config.ondata(event.data); }; channel.onopen = function() { config.onopen(); }; channel.onerror = function(e) { config.onerror(e); }; channel.onclose = function(e) { config.onclose(e); }; } var dataChannelDict = {}; var offererDataChannel; var Offer = { createOffer: function(config) { var peer = new RTCPeerConnection(iceServers, optionalArgument); var self = this; self.config = config; peer.onicecandidate = function(event) { if (event.candidate && event.candidate.candidate) { config.onicecandidate(event.candidate); } }; peer.onsignalingstatechange = function() { console.log('onsignalingstatechange:', JSON.stringify({ iceGatheringState: peer.iceGatheringState, signalingState: peer.signalingState, iceConnectionState: peer.iceConnectionState })); if (peer.iceConnectionState.search(/closed|failed/gi) !== -1) { config.onclose(); } }; peer.oniceconnectionstatechange = function() { console.log('oniceconnectionstatechange:', JSON.stringify({ iceGatheringState: peer.iceGatheringState, signalingState: peer.signalingState, iceConnectionState: peer.iceConnectionState })); if (peer.iceConnectionState.search(/closed|failed/gi) !== -1) { config.onclose(); } }; this.createDataChannel(peer); window.peer = peer; peer.createOffer(offerAnswerConstraints).then(function(sdp) { peer.setLocalDescription(sdp).then(function() { config.onsdp(sdp); }); }).catch(onSdpError); this.peer = peer; return this; }, setRemoteDescription: function(sdp) { this.peer.setRemoteDescription(new RTCSessionDescription(sdp)); }, addIceCandidate: function(candidate) { this.peer.addIceCandidate(new RTCIceCandidate({ sdpMLineIndex: candidate.sdpMLineIndex, candidate: candidate.candidate })); }, createDataChannel: function(peer) { offererDataChannel = (this.peer || peer).createDataChannel('channel', dataChannelDict); setChannelEvents(offererDataChannel, this.config); } }; var answererDataChannel; var Answer = { createAnswer: function(config) { var peer = new RTCPeerConnection(iceServers, optionalArgument); var self = this; self.config = config; peer.ondatachannel = function(event) { answererDataChannel = event.channel; setChannelEvents(answererDataChannel, config); }; peer.onicecandidate = function(event) { if (event.candidate) config.onicecandidate(event.candidate); }; peer.onsignalingstatechange = function() { console.log('onsignalingstatechange:', JSON.stringify({ iceGatheringState: peer.iceGatheringState, signalingState: peer.signalingState })); }; peer.oniceconnectionstatechange = function() { console.log('oniceconnectionstatechange:', JSON.stringify({ iceGatheringState: peer.iceGatheringState, signalingState: peer.signalingState })); }; peer.setRemoteDescription(new RTCSessionDescription(config.sdp)).then(function() { peer.createAnswer(offerAnswerConstraints).then(function(sdp) { peer.setLocalDescription(sdp).then(function() { config.onsdp(sdp); }); }).catch(onSdpError); }); this.peer = peer; return self; }, addIceCandidate: function(candidate) { this.peer.addIceCandidate(new RTCIceCandidate({ sdpMLineIndex: candidate.sdpMLineIndex, candidate: candidate.candidate })); }, createDataChannel: function(peer) { answererDataChannel = (this.peer || peer).createDataChannel('channel', dataChannelDict); setChannelEvents(answererDataChannel, this.config); } }; function merge(mergein, mergeto) { for (var t in mergeto) { mergein[t] = mergeto[t]; } return mergein; } function useless() {} function onSdpError(e) { console.error(e); } })(); ================================================ FILE: FileBufferReader/demo/PeerUI.js ================================================ // Muaz Khan - www.MuazKhan.com // MIT License - www.WebRTC-Experiment.com/licence // Source Code - https://github.com/muaz-khan/FileBufferReader // _________ // PeerUI.js window.addEventListener('load', function() { var setupOffer = document.getElementById('setup-offer'), innerHTML; var SIGNALING_URI = 'wss://websocket-over-nodejs.herokuapp.com:443/'; var SIGNALING_URI = 'wss://webrtcweb.com:9449/'; var channel = location.href.replace(/\/|:|#|%|\.|\[|\]/g, ''); if (location.hash && location.hash.length > 2) { channel = location.hash.replace('#', ''); } var websocket = new WebSocket(SIGNALING_URI); websocket.onopen = function() { var innerHTML = 'Setup WebRTC Connection'; setupOffer.innerHTML = innerHTML; setupOffer.disabled = false; websocket.push(JSON.stringify({ open: true, channel: channel })); var info = document.getElementById('info'); if (location.hash.length > 2) { document.getElementById('share-this-link').innerHTML = 'Share this link with other users!'; info.innerHTML = 'Your UNIQUE room-id is: ' + location.hash.replace('#', '') + '.
Open same URL on a new window or tab.'; info.style.display = 'block'; } }; websocket.push = websocket.send; websocket.send = function(data) { if (websocket.readyState != 1) { console.warn('websocket connection is not opened yet.'); return setTimeout(function() { websocket.send(data); }, 1000); } websocket.push(JSON.stringify({ data: data, channel: channel })); }; var progressHelper = {}; var outputPanel = document.querySelector('.output-panel'); function previewFile(file) { try { file.url = URL.createObjectURL(fileSelector.lastSelectedFile || file); } catch (e) { return; } var fname = ''; if (file.extra && file.extra.webkitRelativePath) { fname = file.extra.webkitRelativePath; } else { fname = file.name; } var html = 'Download ' + fname + ' on your Disk!'; html += '
Download

' + fname + '

' + bytesToSize(file.size) + '

'; if (file.name.match(/\.jpg|\.png|\.jpeg|\.gif/gi)) { html += ''; } else if (file.name.match(/\.wav|\.mp3/gi)) { html += ''; } else if (file.name.match(/\.webm|\.flv|\.mp4/gi)) { html += ''; } else if (file.name.match(/\.js|\.txt|\.sh/gi)) { html += ''; html += '
'; } progressHelper[file.uuid].div.innerHTML = html; fileSelector.lastSelectedFile = false; } var FileHelper = { onBegin: function(file) { var div = document.createElement('div'); var fName = ''; if (file.extra && file.extra.webkitRelativePath) { fName = file.extra.webkitRelativePath; } else { fName = file.name; } var html = '
'; html += '
'; html += '
'; html += '
1% complete
'; html += '
'; html += ''; html += ''; html += ''; html += '
'; div.innerHTML = html; outputPanel.insertBefore(div, outputPanel.firstChild); progressHelper[file.uuid] = { div: div, file: file }; btnSelectFile.disabled = true; btnSelectDirectory.disabled = true; btnSelectMultiple.disabled = true; if (fileSelector.lastSelectedFile) { if (filesRemaining.files) { if (filesRemaining.directory) { btnSelectDirectory.innerHTML = 'File Sending In-Progress...'; } else { btnSelectMultiple.innerHTML = 'File Sending In-Progress...'; } } else { btnSelectFile.innerHTML = 'File Sending In-Progress...'; } } else { if (filesRemaining.files) { if (filesRemaining.directory) { btnSelectDirectory.innerHTML = 'File Receiving In-Progress...'; } else { btnSelectMultiple.innerHTML = 'File Receiving In-Progress...'; } } else { btnSelectFile.innerHTML = 'File Receiving In-Progress...'; } } resetTimeCalculator(); timeCalculator(div.querySelector('progress')); progressHelper.lastFileUUID = file.uuid; div.querySelector('.btn-close').onclick = function() { peerConnection.stopCallback = function() { div.parentNode.removeChild(div); peerConnection.send('stopped:::' + file.uuid); isStoppedTimer = true; }; }; var paused = false; div.querySelector('.btn-pause').onclick = function() { var btn = div.querySelector('.btn-pause'); if(paused) { paused = false; isPausedTimer = false; btn.style.backgroundImage = 'url(https://cdn.webrtc-experiment.com/FileBufferReader/icons/pause-icon.png)'; if(peerConnection.resumeCallback) { peerConnection.resumeCallback(); } peerConnection.paused = false; peerConnection.send('resumed:::' + file.uuid); return; } paused = true; isPausedTimer = true; btn.style.backgroundImage = 'url(https://cdn.webrtc-experiment.com/FileBufferReader/icons/resume-icon.png)'; peerConnection.resumeCallback = null; peerConnection.paused = true; peerConnection.send('paused:::' + file.uuid); }; }, onEnd: function(file) { previewFile(file); btnSelectFile.innerHTML = 'Single'; btnSelectDirectory.innerHTML = 'Directory'; btnSelectMultiple.innerHTML = 'Multiple'; if (peerConnection.isOpened) { btnSelectFile.disabled = false; btnSelectDirectory.disabled = false; btnSelectMultiple.disabled = false; } progressHelper.lastFileUUID = null; if (filesRemaining.files) { filesRemaining.idx++; sendEntireDirectory(); } }, onProgress: function(chunk) { var helper = progressHelper[chunk.uuid]; if(!helper) return; var div = helper.div; var file = helper.file; var progress = div.querySelector('progress'); var percentComplete = div.querySelector('.percent-complete'); var itemsRemaining = div.querySelector('.items-remaining'); var sizeRemaining = div.querySelector('.size-remaining'); var timeRemaining = div.querySelector('.time-remaining'); if(!progress) return; progress.value = chunk.currentPosition || chunk.maxChunks || progress.max; if (progress.position > 0) { var position = +progress.position.toFixed(2).split('.')[1] || 100; percentComplete.innerHTML = position + '% complete'; } if (chunk.currentPosition + 2 != chunk.maxChunks) { progressHelper[chunk.uuid].lastChunk = chunk; progressHelper.callback = function(tRemaining) { var lastChunk = progressHelper[chunk.uuid].lastChunk; var singleChunkSize = chunk.size / lastChunk.maxChunks; var endedAt = (new Date).getTime(); var timeElapsed = endedAt - (progressHelper.startedAt || (new Date).getTime()); progressHelper.latencies.push(timeElapsed); var avg = calculateAverage(progressHelper.latencies); // html += '
Latency in millseconds: ' + timeElapsed + ' (Average): ' + avg + ''; var remainingFileSize = singleChunkSize * (lastChunk.maxChunks - lastChunk.currentPosition); progressHelper.startedAt = (new Date).getTime(); itemsRemaining.innerHTML = (lastChunk.maxChunks - lastChunk.currentPosition); sizeRemaining.innerHTML = bytesToSize(remainingFileSize); timeRemaining.innerHTML = tRemaining; }; } else { btnSelectFile.innerHTML = 'Single'; btnSelectDirectory.innerHTML = 'Directory'; btnSelectMultiple.innerHTML = 'Multiple'; if (peerConnection.isOpened) { btnSelectFile.disabled = false; btnSelectDirectory.disabled = false; btnSelectMultiple.disabled = false; } } } }; // RTCPeerConection // ---------------- var peerConnection = new PeerConnection(websocket); peerConnection.onuserfound = function(userid) { setupOffer.innerHTML = 'Detecting other users...'; setupOffer.disabled = true; peerConnection.sendParticipationRequest(userid); }; peerConnection.onopen = function() { peerConnection.isOpened = true; innerHTML = 'PeerConnection is established.'; setupOffer.innerHTML = innerHTML; setupOffer.disabled = true; btnSelectFile.disabled = false; btnSelectFile.innerHTML = 'Single'; btnSelectDirectory.disabled = false; btnSelectDirectory.innerHTML = 'Directory'; btnSelectMultiple.disabled = false; btnSelectMultiple.innerHTML = 'Multiple'; }; peerConnection.onclose = function() { onCloseOrOnError('PeerConnection is closed.'); resetButtons(); peerConnection.isOpened = false; var helper = progressHelper[progressHelper.lastFileUUID]; if (helper && helper.div && helper.div.parentNode) { isStoppedTimer = true; helper.div.parentNode.removeChild(helper.div); } }; function resetButtons() { btnSelectFile.innerHTML = 'Single'; btnSelectFile.disabled = true; btnSelectDirectory.innerHTML = 'Directory'; btnSelectDirectory.disabled = true; btnSelectMultiple.innerHTML = 'Multiple'; btnSelectMultiple.disabled = true; setupOffer.disabled = false; setupOffer.innerHTML = 'Setup WebRTC Connection'; } peerConnection.onerror = function() { onCloseOrOnError('Something went wrong.'); resetButtons(); }; // getNextChunkCallback gets next available buffer // you need to send that buffer using WebRTC data channels function getNextChunkCallback(nextChunk, isLastChunk) { if (isLastChunk) { // alert('File Successfully sent.'); } // sending using WebRTC data channels peerConnection.send(nextChunk); }; peerConnection.ondata = function(chunk) { if(typeof chunk === 'string' && chunk.indexOf('stopped:::') !== -1) { var div = document.getElementById(chunk.split('stopped:::')[1]); if(div && div.parentNode) { div.parentNode.removeChild(div); } isStoppedTimer = true; return; } if(typeof chunk === 'string' && chunk.indexOf('paused:::') !== -1) { var div = document.getElementById(chunk.split('paused:::')[1]); if(div && div.querySelector('.btn-pause')) { div.querySelector('.btn-pause').style.backgroundImage = 'url(https://cdn.webrtc-experiment.com/FileBufferReader/icons/resume-icon.png)'; } isPausedTimer = true; return; } if(typeof chunk === 'string' && chunk.indexOf('resumed:::') !== -1) { var div = document.getElementById(chunk.split('resumed:::')[1]); if(div && div.querySelector('.btn-pause')) { div.querySelector('.btn-pause').style.backgroundImage = 'url(https://cdn.webrtc-experiment.com/FileBufferReader/icons/pause-icon.png)'; } isPausedTimer = false; return; } if (chunk instanceof ArrayBuffer || chunk instanceof DataView) { // array buffers are passed using WebRTC data channels // need to convert data back into JavaScript objects fileBufferReader.convertToObject(chunk, function(object) { peerConnection.ondata(object); }); return; } // if target user requested next chunk if (chunk.readyForNextChunk) { if(peerConnection.paused) { peerConnection.resumeCallback = function() { fileBufferReader.getNextChunk(chunk, getNextChunkCallback); }; return; } if(peerConnection.stopCallback) { peerConnection.stopCallback(); return; } fileBufferReader.getNextChunk(chunk /*aka metadata*/ , getNextChunkCallback); return; } // if any of the chunks missed if (chunk.chunkMissing) { fileBufferReader.chunkMissing(chunk); return; } // if chunk is received fileBufferReader.addChunk(chunk, function(promptNextChunk) { // request next chunk if(peerConnection.paused) { peerConnection.resumeCallback = function() { peerConnection.send(promptNextChunk); }; return; } if(peerConnection.stopCallback) { peerConnection.stopCallback(); return; } peerConnection.send(promptNextChunk); }); }; var progressIterations = 0; var ONE_SECOND = 1000; function resetTimeCalculator() { progressIterations = 0; isStoppedTimer = false; progressHelper.callback = function() {}; progressHelper.latencies = []; } function calculateAverage(arr) { var sum = 0; for (var i = 0; i < arr.length; i++) { sum += parseInt(arr[i], 10); //don't forget to add the base } var avg = sum / arr.length; return avg.toFixed(1); } var isStoppedTimer = false; var isPausedTimer = false; // https://github.com/23/resumable.js/issues/168#issuecomment-65297110 function timeCalculator(progress, selfInvoker) { if (isStoppedTimer) return; var step = 1; var remainingProgress = 1.0 - progress.position; var estimatedCompletionTime = Math.round((remainingProgress / progress.position) * progressIterations); var estimatedHours, estimatedMinutes, estimatedSeconds, displayHours, displayMinutes, displaySeconds; progressIterations += step; if (progress.position < 1.0) { if (isFinite(estimatedCompletionTime)) { estimatedHours = Math.floor(estimatedCompletionTime / 3600); displayHours = estimatedHours > 9 ? estimatedHours : '0' + estimatedHours; estimatedMinutes = Math.floor((estimatedCompletionTime / 60) % 60); displayMinutes = estimatedMinutes > 9 ? estimatedMinutes : '0' + estimatedMinutes; estimatedSeconds = estimatedCompletionTime % 60; displaySeconds = estimatedSeconds > 9 ? estimatedSeconds : '0' + estimatedSeconds; } var output = ''; if (displayHours > 0) { output += displayHours + ' hours '; } if (displayMinutes > 0) { output += displayMinutes + ' minutes '; } if (displaySeconds > 0) { output += displaySeconds + ' seconds '; } if (output.length && !isPausedTimer) { progressHelper.callback(output); } } setTimeout(function() { timeCalculator(progress, true); }, step * ONE_SECOND); } // ------------------------- // using FileBufferReader.js var fileSelector = new FileSelector(); // you can force specific files e.g. // image/png, image/*, image/jpeg, video/webm, audio/ogg etc. fileSelector.accept = '*.*'; var fileBufferReader = new FileBufferReader(); fileBufferReader.chunkSize = 60 * 1000; // 60k fileBufferReader.onBegin = FileHelper.onBegin; fileBufferReader.onProgress = FileHelper.onProgress; fileBufferReader.onEnd = FileHelper.onEnd; function onFileSelected(file) { fileSelector.lastSelectedFile = file; if (filesRemaining.files) { if (filesRemaining.directory) { btnSelectDirectory.innerHTML = 'Please wait..'; btnSelectDirectory.disabled = true; } else { btnSelectMultiple.innerHTML = 'Please wait..'; btnSelectMultiple.disabled = true; } } else { btnSelectFile.innerHTML = 'Please wait..';; btnSelectFile.disabled = true; } fileBufferReader.readAsArrayBuffer(file, function(metadata) { fileBufferReader.getNextChunk(metadata, getNextChunkCallback); }, { chunkSize: fileBufferReader.chunkSize, webkitRelativePath: file.webkitRelativePath || file.name }); setTimeout(function() { if (fileSelector.lastSelectedFile) return; btnSelectFile.innerHTML = 'Single'; btnSelectFile.disabled = false; btnSelectDirectory.innerHTML = 'Directory'; btnSelectDirectory.disabled = false; btnSelectMultiple.innerHTML = 'Multiple'; btnSelectMultiple.disabled = false; }, 5000); } var btnSelectFile = document.getElementById('select-file'); btnSelectFile.onclick = function() { btnSelectFile.disabled = true; fileSelector.selectSingleFile(function(file) { onFileSelected(file); }, onNoFileSelected); }; var btnSelectMultiple = document.getElementById('select-multiple'); btnSelectMultiple.onclick = function() { btnSelectMultiple.disabled = true; fileSelector.selectMultipleFiles(function(files) { filesRemaining = { files: files, idx: 0 }; sendEntireDirectory(); }, onNoFileSelected); }; var btnSelectDirectory = document.getElementById('select-directory'); btnSelectDirectory.onclick = function() { btnSelectDirectory.disabled = true; fileSelector.selectDirectory(function(files) { filesRemaining = { files: files, idx: 0, directory: true }; sendEntireDirectory(); }, onNoFileSelected); }; function onNoFileSelected() { peerConnection.onopen(); } var filesRemaining = 'none'; function sendEntireDirectory() { if (filesRemaining === 'none') return; if (!filesRemaining.files[filesRemaining.idx]) { filesRemaining = 'none'; return; } onFileSelected(filesRemaining.files[filesRemaining.idx]); } // drag-drop support function onDragOver() { mainContainer.style.border = '7px solid #98a90f'; mainContainer.style.background = '#ffff13'; mainContainer.style.borderRadisu = '16px'; } function onDragLeave() { mainContainer.style.border = '1px solid rgb(189, 189, 189)'; mainContainer.style.background = 'transparent'; mainContainer.style.borderRadisu = 0; } var mainContainer = document.getElementById('main-container'); document.addEventListener('dragenter', function(e) { e.preventDefault(); e.stopPropagation(); if (!peerConnection || !peerConnection.isOpened) return; e.dataTransfer.dropEffect = 'copy'; onDragOver(); }, false); document.addEventListener('dragleave', function(e) { e.preventDefault(); e.stopPropagation(); if (!peerConnection || !peerConnection.isOpened) return; e.dataTransfer.dropEffect = 'copy'; onDragLeave(); }, false); document.addEventListener('dragover', function(e) { e.preventDefault(); e.stopPropagation(); if (!peerConnection || !peerConnection.isOpened) return; e.dataTransfer.dropEffect = 'copy'; onDragOver(); }, false); document.addEventListener('drop', function(e) { e.preventDefault(); e.stopPropagation(); if (!peerConnection || !peerConnection.isOpened) return; onDragLeave(); if (!e.dataTransfer.files || !e.dataTransfer.files.length) { return; } var file = e.dataTransfer.files[0]; filesRemaining = { files: e.dataTransfer.files, idx: 0 }; if (!peerConnection || !peerConnection.isOpened) { alert('Pleas setup WebRTC connection before sharing this file.'); return; } onFileSelected(file); }, false); // -------------------------------------------------------- setupOffer.onclick = function(event) { if (event !== true) { peerConnection.startBroadcasting(); } setupOffer.innerHTML = 'Detecting other users in this room...'; setupOffer.disabled = true; setTimeout(function() { if (!peerConnection.isOpened) { var innerHTML = 'I am alone in this room.'; setupOffer.innerHTML = innerHTML; setTimeout(function() { setupOffer.onclick(true); }, 2000); } }, 5 * 1000); }; function onCloseOrOnError(_innerHTML) { innerHTML = _innerHTML; setupOffer.innerHTML = innerHTML; setupOffer.disabled = false; setupOffer.className = 'button'; setTimeout(function() { innerHTML = 'Setup WebRTC Connection'; setupOffer.innerHTML = innerHTML; setupOffer.disabled = false; }, 1000); btnSelectFile.disabled = true; btnSelectDirectory.disabled = true; btnSelectMultiple.disabled = true; } function millsecondsToSeconds(millis) { var seconds = ((millis % 60000) / 1000).toFixed(1); return seconds; } function bytesToSize(bytes) { var k = 1000; var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; if (bytes === 0) { return '0 Bytes'; } var i = parseInt(Math.floor(Math.log(bytes) / Math.log(k)), 10); return (bytes / Math.pow(k, i)).toPrecision(3) + ' ' + sizes[i]; } function getToken() { if (window.crypto && window.crypto.getRandomValues && navigator.userAgent.indexOf('Safari') === -1) { var a = window.crypto.getRandomValues(new Uint32Array(3)), token = ''; for (var i = 0, l = a.length; i < l; i++) { token += a[i].toString(36); } return token; } else { return (Math.random() * new Date().getTime()).toString(36).replace(/\./g, ''); } } }, false); ================================================ FILE: FileBufferReader/demo/adapter-latest.js ================================================ (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.adapter = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i= 14393 && url.indexOf('?transport=udp') === -1; }); delete server.url; server.urls = isString ? urls[0] : urls; return !!urls.length; } }); } // Determines the intersection of local and remote capabilities. function getCommonCapabilities(localCapabilities, remoteCapabilities) { var commonCapabilities = { codecs: [], headerExtensions: [], fecMechanisms: [] }; var findCodecByPayloadType = function(pt, codecs) { pt = parseInt(pt, 10); for (var i = 0; i < codecs.length; i++) { if (codecs[i].payloadType === pt || codecs[i].preferredPayloadType === pt) { return codecs[i]; } } }; var rtxCapabilityMatches = function(lRtx, rRtx, lCodecs, rCodecs) { var lCodec = findCodecByPayloadType(lRtx.parameters.apt, lCodecs); var rCodec = findCodecByPayloadType(rRtx.parameters.apt, rCodecs); return lCodec && rCodec && lCodec.name.toLowerCase() === rCodec.name.toLowerCase(); }; localCapabilities.codecs.forEach(function(lCodec) { for (var i = 0; i < remoteCapabilities.codecs.length; i++) { var rCodec = remoteCapabilities.codecs[i]; if (lCodec.name.toLowerCase() === rCodec.name.toLowerCase() && lCodec.clockRate === rCodec.clockRate) { if (lCodec.name.toLowerCase() === 'rtx' && lCodec.parameters && rCodec.parameters.apt) { // for RTX we need to find the local rtx that has a apt // which points to the same local codec as the remote one. if (!rtxCapabilityMatches(lCodec, rCodec, localCapabilities.codecs, remoteCapabilities.codecs)) { continue; } } rCodec = JSON.parse(JSON.stringify(rCodec)); // deepcopy // number of channels is the highest common number of channels rCodec.numChannels = Math.min(lCodec.numChannels, rCodec.numChannels); // push rCodec so we reply with offerer payload type commonCapabilities.codecs.push(rCodec); // determine common feedback mechanisms rCodec.rtcpFeedback = rCodec.rtcpFeedback.filter(function(fb) { for (var j = 0; j < lCodec.rtcpFeedback.length; j++) { if (lCodec.rtcpFeedback[j].type === fb.type && lCodec.rtcpFeedback[j].parameter === fb.parameter) { return true; } } return false; }); // FIXME: also need to determine .parameters // see https://github.com/openpeer/ortc/issues/569 break; } } }); localCapabilities.headerExtensions.forEach(function(lHeaderExtension) { for (var i = 0; i < remoteCapabilities.headerExtensions.length; i++) { var rHeaderExtension = remoteCapabilities.headerExtensions[i]; if (lHeaderExtension.uri === rHeaderExtension.uri) { commonCapabilities.headerExtensions.push(rHeaderExtension); break; } } }); // FIXME: fecMechanisms return commonCapabilities; } // is action=setLocalDescription with type allowed in signalingState function isActionAllowedInSignalingState(action, type, signalingState) { return { offer: { setLocalDescription: ['stable', 'have-local-offer'], setRemoteDescription: ['stable', 'have-remote-offer'] }, answer: { setLocalDescription: ['have-remote-offer', 'have-local-pranswer'], setRemoteDescription: ['have-local-offer', 'have-remote-pranswer'] } }[type][action].indexOf(signalingState) !== -1; } function maybeAddCandidate(iceTransport, candidate) { // Edge's internal representation adds some fields therefore // not all fieldѕ are taken into account. var alreadyAdded = iceTransport.getRemoteCandidates() .find(function(remoteCandidate) { return candidate.foundation === remoteCandidate.foundation && candidate.ip === remoteCandidate.ip && candidate.port === remoteCandidate.port && candidate.priority === remoteCandidate.priority && candidate.protocol === remoteCandidate.protocol && candidate.type === remoteCandidate.type; }); if (!alreadyAdded) { iceTransport.addRemoteCandidate(candidate); } return !alreadyAdded; } function makeError(name, description) { var e = new Error(description); e.name = name; // legacy error codes from https://heycam.github.io/webidl/#idl-DOMException-error-names e.code = { NotSupportedError: 9, InvalidStateError: 11, InvalidAccessError: 15, TypeError: undefined, OperationError: undefined }[name]; return e; } module.exports = function(window, edgeVersion) { // https://w3c.github.io/mediacapture-main/#mediastream // Helper function to add the track to the stream and // dispatch the event ourselves. function addTrackToStreamAndFireEvent(track, stream) { stream.addTrack(track); stream.dispatchEvent(new window.MediaStreamTrackEvent('addtrack', {track: track})); } function removeTrackFromStreamAndFireEvent(track, stream) { stream.removeTrack(track); stream.dispatchEvent(new window.MediaStreamTrackEvent('removetrack', {track: track})); } function fireAddTrack(pc, track, receiver, streams) { var trackEvent = new Event('track'); trackEvent.track = track; trackEvent.receiver = receiver; trackEvent.transceiver = {receiver: receiver}; trackEvent.streams = streams; window.setTimeout(function() { pc._dispatchEvent('track', trackEvent); }); } var RTCPeerConnection = function(config) { var pc = this; var _eventTarget = document.createDocumentFragment(); ['addEventListener', 'removeEventListener', 'dispatchEvent'] .forEach(function(method) { pc[method] = _eventTarget[method].bind(_eventTarget); }); this.canTrickleIceCandidates = null; this.needNegotiation = false; this.localStreams = []; this.remoteStreams = []; this._localDescription = null; this._remoteDescription = null; this.signalingState = 'stable'; this.iceConnectionState = 'new'; this.connectionState = 'new'; this.iceGatheringState = 'new'; config = JSON.parse(JSON.stringify(config || {})); this.usingBundle = config.bundlePolicy === 'max-bundle'; if (config.rtcpMuxPolicy === 'negotiate') { throw(makeError('NotSupportedError', 'rtcpMuxPolicy \'negotiate\' is not supported')); } else if (!config.rtcpMuxPolicy) { config.rtcpMuxPolicy = 'require'; } switch (config.iceTransportPolicy) { case 'all': case 'relay': break; default: config.iceTransportPolicy = 'all'; break; } switch (config.bundlePolicy) { case 'balanced': case 'max-compat': case 'max-bundle': break; default: config.bundlePolicy = 'balanced'; break; } config.iceServers = filterIceServers(config.iceServers || [], edgeVersion); this._iceGatherers = []; if (config.iceCandidatePoolSize) { for (var i = config.iceCandidatePoolSize; i > 0; i--) { this._iceGatherers.push(new window.RTCIceGatherer({ iceServers: config.iceServers, gatherPolicy: config.iceTransportPolicy })); } } else { config.iceCandidatePoolSize = 0; } this._config = config; // per-track iceGathers, iceTransports, dtlsTransports, rtpSenders, ... // everything that is needed to describe a SDP m-line. this.transceivers = []; this._sdpSessionId = SDPUtils.generateSessionId(); this._sdpSessionVersion = 0; this._dtlsRole = undefined; // role for a=setup to use in answers. this._isClosed = false; }; Object.defineProperty(RTCPeerConnection.prototype, 'localDescription', { configurable: true, get: function() { return this._localDescription; } }); Object.defineProperty(RTCPeerConnection.prototype, 'remoteDescription', { configurable: true, get: function() { return this._remoteDescription; } }); // set up event handlers on prototype RTCPeerConnection.prototype.onicecandidate = null; RTCPeerConnection.prototype.onaddstream = null; RTCPeerConnection.prototype.ontrack = null; RTCPeerConnection.prototype.onremovestream = null; RTCPeerConnection.prototype.onsignalingstatechange = null; RTCPeerConnection.prototype.oniceconnectionstatechange = null; RTCPeerConnection.prototype.onconnectionstatechange = null; RTCPeerConnection.prototype.onicegatheringstatechange = null; RTCPeerConnection.prototype.onnegotiationneeded = null; RTCPeerConnection.prototype.ondatachannel = null; RTCPeerConnection.prototype._dispatchEvent = function(name, event) { if (this._isClosed) { return; } this.dispatchEvent(event); if (typeof this['on' + name] === 'function') { this['on' + name](event); } }; RTCPeerConnection.prototype._emitGatheringStateChange = function() { var event = new Event('icegatheringstatechange'); this._dispatchEvent('icegatheringstatechange', event); }; RTCPeerConnection.prototype.getConfiguration = function() { return this._config; }; RTCPeerConnection.prototype.getLocalStreams = function() { return this.localStreams; }; RTCPeerConnection.prototype.getRemoteStreams = function() { return this.remoteStreams; }; // internal helper to create a transceiver object. // (which is not yet the same as the WebRTC 1.0 transceiver) RTCPeerConnection.prototype._createTransceiver = function(kind, doNotAdd) { var hasBundleTransport = this.transceivers.length > 0; var transceiver = { track: null, iceGatherer: null, iceTransport: null, dtlsTransport: null, localCapabilities: null, remoteCapabilities: null, rtpSender: null, rtpReceiver: null, kind: kind, mid: null, sendEncodingParameters: null, recvEncodingParameters: null, stream: null, associatedRemoteMediaStreams: [], wantReceive: true }; if (this.usingBundle && hasBundleTransport) { transceiver.iceTransport = this.transceivers[0].iceTransport; transceiver.dtlsTransport = this.transceivers[0].dtlsTransport; } else { var transports = this._createIceAndDtlsTransports(); transceiver.iceTransport = transports.iceTransport; transceiver.dtlsTransport = transports.dtlsTransport; } if (!doNotAdd) { this.transceivers.push(transceiver); } return transceiver; }; RTCPeerConnection.prototype.addTrack = function(track, stream) { if (this._isClosed) { throw makeError('InvalidStateError', 'Attempted to call addTrack on a closed peerconnection.'); } var alreadyExists = this.transceivers.find(function(s) { return s.track === track; }); if (alreadyExists) { throw makeError('InvalidAccessError', 'Track already exists.'); } var transceiver; for (var i = 0; i < this.transceivers.length; i++) { if (!this.transceivers[i].track && this.transceivers[i].kind === track.kind) { transceiver = this.transceivers[i]; } } if (!transceiver) { transceiver = this._createTransceiver(track.kind); } this._maybeFireNegotiationNeeded(); if (this.localStreams.indexOf(stream) === -1) { this.localStreams.push(stream); } transceiver.track = track; transceiver.stream = stream; transceiver.rtpSender = new window.RTCRtpSender(track, transceiver.dtlsTransport); return transceiver.rtpSender; }; RTCPeerConnection.prototype.addStream = function(stream) { var pc = this; if (edgeVersion >= 15025) { stream.getTracks().forEach(function(track) { pc.addTrack(track, stream); }); } else { // Clone is necessary for local demos mostly, attaching directly // to two different senders does not work (build 10547). // Fixed in 15025 (or earlier) var clonedStream = stream.clone(); stream.getTracks().forEach(function(track, idx) { var clonedTrack = clonedStream.getTracks()[idx]; track.addEventListener('enabled', function(event) { clonedTrack.enabled = event.enabled; }); }); clonedStream.getTracks().forEach(function(track) { pc.addTrack(track, clonedStream); }); } }; RTCPeerConnection.prototype.removeTrack = function(sender) { if (this._isClosed) { throw makeError('InvalidStateError', 'Attempted to call removeTrack on a closed peerconnection.'); } if (!(sender instanceof window.RTCRtpSender)) { throw new TypeError('Argument 1 of RTCPeerConnection.removeTrack ' + 'does not implement interface RTCRtpSender.'); } var transceiver = this.transceivers.find(function(t) { return t.rtpSender === sender; }); if (!transceiver) { throw makeError('InvalidAccessError', 'Sender was not created by this connection.'); } var stream = transceiver.stream; transceiver.rtpSender.stop(); transceiver.rtpSender = null; transceiver.track = null; transceiver.stream = null; // remove the stream from the set of local streams var localStreams = this.transceivers.map(function(t) { return t.stream; }); if (localStreams.indexOf(stream) === -1 && this.localStreams.indexOf(stream) > -1) { this.localStreams.splice(this.localStreams.indexOf(stream), 1); } this._maybeFireNegotiationNeeded(); }; RTCPeerConnection.prototype.removeStream = function(stream) { var pc = this; stream.getTracks().forEach(function(track) { var sender = pc.getSenders().find(function(s) { return s.track === track; }); if (sender) { pc.removeTrack(sender); } }); }; RTCPeerConnection.prototype.getSenders = function() { return this.transceivers.filter(function(transceiver) { return !!transceiver.rtpSender; }) .map(function(transceiver) { return transceiver.rtpSender; }); }; RTCPeerConnection.prototype.getReceivers = function() { return this.transceivers.filter(function(transceiver) { return !!transceiver.rtpReceiver; }) .map(function(transceiver) { return transceiver.rtpReceiver; }); }; RTCPeerConnection.prototype._createIceGatherer = function(sdpMLineIndex, usingBundle) { var pc = this; if (usingBundle && sdpMLineIndex > 0) { return this.transceivers[0].iceGatherer; } else if (this._iceGatherers.length) { return this._iceGatherers.shift(); } var iceGatherer = new window.RTCIceGatherer({ iceServers: this._config.iceServers, gatherPolicy: this._config.iceTransportPolicy }); Object.defineProperty(iceGatherer, 'state', {value: 'new', writable: true} ); this.transceivers[sdpMLineIndex].bufferedCandidateEvents = []; this.transceivers[sdpMLineIndex].bufferCandidates = function(event) { var end = !event.candidate || Object.keys(event.candidate).length === 0; // polyfill since RTCIceGatherer.state is not implemented in // Edge 10547 yet. iceGatherer.state = end ? 'completed' : 'gathering'; if (pc.transceivers[sdpMLineIndex].bufferedCandidateEvents !== null) { pc.transceivers[sdpMLineIndex].bufferedCandidateEvents.push(event); } }; iceGatherer.addEventListener('localcandidate', this.transceivers[sdpMLineIndex].bufferCandidates); return iceGatherer; }; // start gathering from an RTCIceGatherer. RTCPeerConnection.prototype._gather = function(mid, sdpMLineIndex) { var pc = this; var iceGatherer = this.transceivers[sdpMLineIndex].iceGatherer; if (iceGatherer.onlocalcandidate) { return; } var bufferedCandidateEvents = this.transceivers[sdpMLineIndex].bufferedCandidateEvents; this.transceivers[sdpMLineIndex].bufferedCandidateEvents = null; iceGatherer.removeEventListener('localcandidate', this.transceivers[sdpMLineIndex].bufferCandidates); iceGatherer.onlocalcandidate = function(evt) { if (pc.usingBundle && sdpMLineIndex > 0) { // if we know that we use bundle we can drop candidates with // ѕdpMLineIndex > 0. If we don't do this then our state gets // confused since we dispose the extra ice gatherer. return; } var event = new Event('icecandidate'); event.candidate = {sdpMid: mid, sdpMLineIndex: sdpMLineIndex}; var cand = evt.candidate; // Edge emits an empty object for RTCIceCandidateComplete‥ var end = !cand || Object.keys(cand).length === 0; if (end) { // polyfill since RTCIceGatherer.state is not implemented in // Edge 10547 yet. if (iceGatherer.state === 'new' || iceGatherer.state === 'gathering') { iceGatherer.state = 'completed'; } } else { if (iceGatherer.state === 'new') { iceGatherer.state = 'gathering'; } // RTCIceCandidate doesn't have a component, needs to be added cand.component = 1; // also the usernameFragment. TODO: update SDP to take both variants. cand.ufrag = iceGatherer.getLocalParameters().usernameFragment; var serializedCandidate = SDPUtils.writeCandidate(cand); event.candidate = Object.assign(event.candidate, SDPUtils.parseCandidate(serializedCandidate)); event.candidate.candidate = serializedCandidate; event.candidate.toJSON = function() { return { candidate: event.candidate.candidate, sdpMid: event.candidate.sdpMid, sdpMLineIndex: event.candidate.sdpMLineIndex, usernameFragment: event.candidate.usernameFragment }; }; } // update local description. var sections = SDPUtils.getMediaSections(pc._localDescription.sdp); if (!end) { sections[event.candidate.sdpMLineIndex] += 'a=' + event.candidate.candidate + '\r\n'; } else { sections[event.candidate.sdpMLineIndex] += 'a=end-of-candidates\r\n'; } pc._localDescription.sdp = SDPUtils.getDescription(pc._localDescription.sdp) + sections.join(''); var complete = pc.transceivers.every(function(transceiver) { return transceiver.iceGatherer && transceiver.iceGatherer.state === 'completed'; }); if (pc.iceGatheringState !== 'gathering') { pc.iceGatheringState = 'gathering'; pc._emitGatheringStateChange(); } // Emit candidate. Also emit null candidate when all gatherers are // complete. if (!end) { pc._dispatchEvent('icecandidate', event); } if (complete) { pc._dispatchEvent('icecandidate', new Event('icecandidate')); pc.iceGatheringState = 'complete'; pc._emitGatheringStateChange(); } }; // emit already gathered candidates. window.setTimeout(function() { bufferedCandidateEvents.forEach(function(e) { iceGatherer.onlocalcandidate(e); }); }, 0); }; // Create ICE transport and DTLS transport. RTCPeerConnection.prototype._createIceAndDtlsTransports = function() { var pc = this; var iceTransport = new window.RTCIceTransport(null); iceTransport.onicestatechange = function() { pc._updateIceConnectionState(); pc._updateConnectionState(); }; var dtlsTransport = new window.RTCDtlsTransport(iceTransport); dtlsTransport.ondtlsstatechange = function() { pc._updateConnectionState(); }; dtlsTransport.onerror = function() { // onerror does not set state to failed by itself. Object.defineProperty(dtlsTransport, 'state', {value: 'failed', writable: true}); pc._updateConnectionState(); }; return { iceTransport: iceTransport, dtlsTransport: dtlsTransport }; }; // Destroy ICE gatherer, ICE transport and DTLS transport. // Without triggering the callbacks. RTCPeerConnection.prototype._disposeIceAndDtlsTransports = function( sdpMLineIndex) { var iceGatherer = this.transceivers[sdpMLineIndex].iceGatherer; if (iceGatherer) { delete iceGatherer.onlocalcandidate; delete this.transceivers[sdpMLineIndex].iceGatherer; } var iceTransport = this.transceivers[sdpMLineIndex].iceTransport; if (iceTransport) { delete iceTransport.onicestatechange; delete this.transceivers[sdpMLineIndex].iceTransport; } var dtlsTransport = this.transceivers[sdpMLineIndex].dtlsTransport; if (dtlsTransport) { delete dtlsTransport.ondtlsstatechange; delete dtlsTransport.onerror; delete this.transceivers[sdpMLineIndex].dtlsTransport; } }; // Start the RTP Sender and Receiver for a transceiver. RTCPeerConnection.prototype._transceive = function(transceiver, send, recv) { var params = getCommonCapabilities(transceiver.localCapabilities, transceiver.remoteCapabilities); if (send && transceiver.rtpSender) { params.encodings = transceiver.sendEncodingParameters; params.rtcp = { cname: SDPUtils.localCName, compound: transceiver.rtcpParameters.compound }; if (transceiver.recvEncodingParameters.length) { params.rtcp.ssrc = transceiver.recvEncodingParameters[0].ssrc; } transceiver.rtpSender.send(params); } if (recv && transceiver.rtpReceiver && params.codecs.length > 0) { // remove RTX field in Edge 14942 if (transceiver.kind === 'video' && transceiver.recvEncodingParameters && edgeVersion < 15019) { transceiver.recvEncodingParameters.forEach(function(p) { delete p.rtx; }); } if (transceiver.recvEncodingParameters.length) { params.encodings = transceiver.recvEncodingParameters; } else { params.encodings = [{}]; } params.rtcp = { compound: transceiver.rtcpParameters.compound }; if (transceiver.rtcpParameters.cname) { params.rtcp.cname = transceiver.rtcpParameters.cname; } if (transceiver.sendEncodingParameters.length) { params.rtcp.ssrc = transceiver.sendEncodingParameters[0].ssrc; } transceiver.rtpReceiver.receive(params); } }; RTCPeerConnection.prototype.setLocalDescription = function(description) { var pc = this; // Note: pranswer is not supported. if (['offer', 'answer'].indexOf(description.type) === -1) { return Promise.reject(makeError('TypeError', 'Unsupported type "' + description.type + '"')); } if (!isActionAllowedInSignalingState('setLocalDescription', description.type, pc.signalingState) || pc._isClosed) { return Promise.reject(makeError('InvalidStateError', 'Can not set local ' + description.type + ' in state ' + pc.signalingState)); } var sections; var sessionpart; if (description.type === 'offer') { // VERY limited support for SDP munging. Limited to: // * changing the order of codecs sections = SDPUtils.splitSections(description.sdp); sessionpart = sections.shift(); sections.forEach(function(mediaSection, sdpMLineIndex) { var caps = SDPUtils.parseRtpParameters(mediaSection); pc.transceivers[sdpMLineIndex].localCapabilities = caps; }); pc.transceivers.forEach(function(transceiver, sdpMLineIndex) { pc._gather(transceiver.mid, sdpMLineIndex); }); } else if (description.type === 'answer') { sections = SDPUtils.splitSections(pc._remoteDescription.sdp); sessionpart = sections.shift(); var isIceLite = SDPUtils.matchPrefix(sessionpart, 'a=ice-lite').length > 0; sections.forEach(function(mediaSection, sdpMLineIndex) { var transceiver = pc.transceivers[sdpMLineIndex]; var iceGatherer = transceiver.iceGatherer; var iceTransport = transceiver.iceTransport; var dtlsTransport = transceiver.dtlsTransport; var localCapabilities = transceiver.localCapabilities; var remoteCapabilities = transceiver.remoteCapabilities; // treat bundle-only as not-rejected. var rejected = SDPUtils.isRejected(mediaSection) && SDPUtils.matchPrefix(mediaSection, 'a=bundle-only').length === 0; if (!rejected && !transceiver.rejected) { var remoteIceParameters = SDPUtils.getIceParameters( mediaSection, sessionpart); var remoteDtlsParameters = SDPUtils.getDtlsParameters( mediaSection, sessionpart); if (isIceLite) { remoteDtlsParameters.role = 'server'; } if (!pc.usingBundle || sdpMLineIndex === 0) { pc._gather(transceiver.mid, sdpMLineIndex); if (iceTransport.state === 'new') { iceTransport.start(iceGatherer, remoteIceParameters, isIceLite ? 'controlling' : 'controlled'); } if (dtlsTransport.state === 'new') { dtlsTransport.start(remoteDtlsParameters); } } // Calculate intersection of capabilities. var params = getCommonCapabilities(localCapabilities, remoteCapabilities); // Start the RTCRtpSender. The RTCRtpReceiver for this // transceiver has already been started in setRemoteDescription. pc._transceive(transceiver, params.codecs.length > 0, false); } }); } pc._localDescription = { type: description.type, sdp: description.sdp }; if (description.type === 'offer') { pc._updateSignalingState('have-local-offer'); } else { pc._updateSignalingState('stable'); } return Promise.resolve(); }; RTCPeerConnection.prototype.setRemoteDescription = function(description) { var pc = this; // Note: pranswer is not supported. if (['offer', 'answer'].indexOf(description.type) === -1) { return Promise.reject(makeError('TypeError', 'Unsupported type "' + description.type + '"')); } if (!isActionAllowedInSignalingState('setRemoteDescription', description.type, pc.signalingState) || pc._isClosed) { return Promise.reject(makeError('InvalidStateError', 'Can not set remote ' + description.type + ' in state ' + pc.signalingState)); } var streams = {}; pc.remoteStreams.forEach(function(stream) { streams[stream.id] = stream; }); var receiverList = []; var sections = SDPUtils.splitSections(description.sdp); var sessionpart = sections.shift(); var isIceLite = SDPUtils.matchPrefix(sessionpart, 'a=ice-lite').length > 0; var usingBundle = SDPUtils.matchPrefix(sessionpart, 'a=group:BUNDLE ').length > 0; pc.usingBundle = usingBundle; var iceOptions = SDPUtils.matchPrefix(sessionpart, 'a=ice-options:')[0]; if (iceOptions) { pc.canTrickleIceCandidates = iceOptions.substr(14).split(' ') .indexOf('trickle') >= 0; } else { pc.canTrickleIceCandidates = false; } sections.forEach(function(mediaSection, sdpMLineIndex) { var lines = SDPUtils.splitLines(mediaSection); var kind = SDPUtils.getKind(mediaSection); // treat bundle-only as not-rejected. var rejected = SDPUtils.isRejected(mediaSection) && SDPUtils.matchPrefix(mediaSection, 'a=bundle-only').length === 0; var protocol = lines[0].substr(2).split(' ')[2]; var direction = SDPUtils.getDirection(mediaSection, sessionpart); var remoteMsid = SDPUtils.parseMsid(mediaSection); var mid = SDPUtils.getMid(mediaSection) || SDPUtils.generateIdentifier(); // Reject datachannels which are not implemented yet. if (rejected || (kind === 'application' && (protocol === 'DTLS/SCTP' || protocol === 'UDP/DTLS/SCTP'))) { // TODO: this is dangerous in the case where a non-rejected m-line // becomes rejected. pc.transceivers[sdpMLineIndex] = { mid: mid, kind: kind, protocol: protocol, rejected: true }; return; } if (!rejected && pc.transceivers[sdpMLineIndex] && pc.transceivers[sdpMLineIndex].rejected) { // recycle a rejected transceiver. pc.transceivers[sdpMLineIndex] = pc._createTransceiver(kind, true); } var transceiver; var iceGatherer; var iceTransport; var dtlsTransport; var rtpReceiver; var sendEncodingParameters; var recvEncodingParameters; var localCapabilities; var track; // FIXME: ensure the mediaSection has rtcp-mux set. var remoteCapabilities = SDPUtils.parseRtpParameters(mediaSection); var remoteIceParameters; var remoteDtlsParameters; if (!rejected) { remoteIceParameters = SDPUtils.getIceParameters(mediaSection, sessionpart); remoteDtlsParameters = SDPUtils.getDtlsParameters(mediaSection, sessionpart); remoteDtlsParameters.role = 'client'; } recvEncodingParameters = SDPUtils.parseRtpEncodingParameters(mediaSection); var rtcpParameters = SDPUtils.parseRtcpParameters(mediaSection); var isComplete = SDPUtils.matchPrefix(mediaSection, 'a=end-of-candidates', sessionpart).length > 0; var cands = SDPUtils.matchPrefix(mediaSection, 'a=candidate:') .map(function(cand) { return SDPUtils.parseCandidate(cand); }) .filter(function(cand) { return cand.component === 1; }); // Check if we can use BUNDLE and dispose transports. if ((description.type === 'offer' || description.type === 'answer') && !rejected && usingBundle && sdpMLineIndex > 0 && pc.transceivers[sdpMLineIndex]) { pc._disposeIceAndDtlsTransports(sdpMLineIndex); pc.transceivers[sdpMLineIndex].iceGatherer = pc.transceivers[0].iceGatherer; pc.transceivers[sdpMLineIndex].iceTransport = pc.transceivers[0].iceTransport; pc.transceivers[sdpMLineIndex].dtlsTransport = pc.transceivers[0].dtlsTransport; if (pc.transceivers[sdpMLineIndex].rtpSender) { pc.transceivers[sdpMLineIndex].rtpSender.setTransport( pc.transceivers[0].dtlsTransport); } if (pc.transceivers[sdpMLineIndex].rtpReceiver) { pc.transceivers[sdpMLineIndex].rtpReceiver.setTransport( pc.transceivers[0].dtlsTransport); } } if (description.type === 'offer' && !rejected) { transceiver = pc.transceivers[sdpMLineIndex] || pc._createTransceiver(kind); transceiver.mid = mid; if (!transceiver.iceGatherer) { transceiver.iceGatherer = pc._createIceGatherer(sdpMLineIndex, usingBundle); } if (cands.length && transceiver.iceTransport.state === 'new') { if (isComplete && (!usingBundle || sdpMLineIndex === 0)) { transceiver.iceTransport.setRemoteCandidates(cands); } else { cands.forEach(function(candidate) { maybeAddCandidate(transceiver.iceTransport, candidate); }); } } localCapabilities = window.RTCRtpReceiver.getCapabilities(kind); // filter RTX until additional stuff needed for RTX is implemented // in adapter.js if (edgeVersion < 15019) { localCapabilities.codecs = localCapabilities.codecs.filter( function(codec) { return codec.name !== 'rtx'; }); } sendEncodingParameters = transceiver.sendEncodingParameters || [{ ssrc: (2 * sdpMLineIndex + 2) * 1001 }]; // TODO: rewrite to use http://w3c.github.io/webrtc-pc/#set-associated-remote-streams var isNewTrack = false; if (direction === 'sendrecv' || direction === 'sendonly') { isNewTrack = !transceiver.rtpReceiver; rtpReceiver = transceiver.rtpReceiver || new window.RTCRtpReceiver(transceiver.dtlsTransport, kind); if (isNewTrack) { var stream; track = rtpReceiver.track; // FIXME: does not work with Plan B. if (remoteMsid && remoteMsid.stream === '-') { // no-op. a stream id of '-' means: no associated stream. } else if (remoteMsid) { if (!streams[remoteMsid.stream]) { streams[remoteMsid.stream] = new window.MediaStream(); Object.defineProperty(streams[remoteMsid.stream], 'id', { get: function() { return remoteMsid.stream; } }); } Object.defineProperty(track, 'id', { get: function() { return remoteMsid.track; } }); stream = streams[remoteMsid.stream]; } else { if (!streams.default) { streams.default = new window.MediaStream(); } stream = streams.default; } if (stream) { addTrackToStreamAndFireEvent(track, stream); transceiver.associatedRemoteMediaStreams.push(stream); } receiverList.push([track, rtpReceiver, stream]); } } else if (transceiver.rtpReceiver && transceiver.rtpReceiver.track) { transceiver.associatedRemoteMediaStreams.forEach(function(s) { var nativeTrack = s.getTracks().find(function(t) { return t.id === transceiver.rtpReceiver.track.id; }); if (nativeTrack) { removeTrackFromStreamAndFireEvent(nativeTrack, s); } }); transceiver.associatedRemoteMediaStreams = []; } transceiver.localCapabilities = localCapabilities; transceiver.remoteCapabilities = remoteCapabilities; transceiver.rtpReceiver = rtpReceiver; transceiver.rtcpParameters = rtcpParameters; transceiver.sendEncodingParameters = sendEncodingParameters; transceiver.recvEncodingParameters = recvEncodingParameters; // Start the RTCRtpReceiver now. The RTPSender is started in // setLocalDescription. pc._transceive(pc.transceivers[sdpMLineIndex], false, isNewTrack); } else if (description.type === 'answer' && !rejected) { transceiver = pc.transceivers[sdpMLineIndex]; iceGatherer = transceiver.iceGatherer; iceTransport = transceiver.iceTransport; dtlsTransport = transceiver.dtlsTransport; rtpReceiver = transceiver.rtpReceiver; sendEncodingParameters = transceiver.sendEncodingParameters; localCapabilities = transceiver.localCapabilities; pc.transceivers[sdpMLineIndex].recvEncodingParameters = recvEncodingParameters; pc.transceivers[sdpMLineIndex].remoteCapabilities = remoteCapabilities; pc.transceivers[sdpMLineIndex].rtcpParameters = rtcpParameters; if (cands.length && iceTransport.state === 'new') { if ((isIceLite || isComplete) && (!usingBundle || sdpMLineIndex === 0)) { iceTransport.setRemoteCandidates(cands); } else { cands.forEach(function(candidate) { maybeAddCandidate(transceiver.iceTransport, candidate); }); } } if (!usingBundle || sdpMLineIndex === 0) { if (iceTransport.state === 'new') { iceTransport.start(iceGatherer, remoteIceParameters, 'controlling'); } if (dtlsTransport.state === 'new') { dtlsTransport.start(remoteDtlsParameters); } } // If the offer contained RTX but the answer did not, // remove RTX from sendEncodingParameters. var commonCapabilities = getCommonCapabilities( transceiver.localCapabilities, transceiver.remoteCapabilities); var hasRtx = commonCapabilities.codecs.filter(function(c) { return c.name.toLowerCase() === 'rtx'; }).length; if (!hasRtx && transceiver.sendEncodingParameters[0].rtx) { delete transceiver.sendEncodingParameters[0].rtx; } pc._transceive(transceiver, direction === 'sendrecv' || direction === 'recvonly', direction === 'sendrecv' || direction === 'sendonly'); // TODO: rewrite to use http://w3c.github.io/webrtc-pc/#set-associated-remote-streams if (rtpReceiver && (direction === 'sendrecv' || direction === 'sendonly')) { track = rtpReceiver.track; if (remoteMsid) { if (!streams[remoteMsid.stream]) { streams[remoteMsid.stream] = new window.MediaStream(); } addTrackToStreamAndFireEvent(track, streams[remoteMsid.stream]); receiverList.push([track, rtpReceiver, streams[remoteMsid.stream]]); } else { if (!streams.default) { streams.default = new window.MediaStream(); } addTrackToStreamAndFireEvent(track, streams.default); receiverList.push([track, rtpReceiver, streams.default]); } } else { // FIXME: actually the receiver should be created later. delete transceiver.rtpReceiver; } } }); if (pc._dtlsRole === undefined) { pc._dtlsRole = description.type === 'offer' ? 'active' : 'passive'; } pc._remoteDescription = { type: description.type, sdp: description.sdp }; if (description.type === 'offer') { pc._updateSignalingState('have-remote-offer'); } else { pc._updateSignalingState('stable'); } Object.keys(streams).forEach(function(sid) { var stream = streams[sid]; if (stream.getTracks().length) { if (pc.remoteStreams.indexOf(stream) === -1) { pc.remoteStreams.push(stream); var event = new Event('addstream'); event.stream = stream; window.setTimeout(function() { pc._dispatchEvent('addstream', event); }); } receiverList.forEach(function(item) { var track = item[0]; var receiver = item[1]; if (stream.id !== item[2].id) { return; } fireAddTrack(pc, track, receiver, [stream]); }); } }); receiverList.forEach(function(item) { if (item[2]) { return; } fireAddTrack(pc, item[0], item[1], []); }); // check whether addIceCandidate({}) was called within four seconds after // setRemoteDescription. window.setTimeout(function() { if (!(pc && pc.transceivers)) { return; } pc.transceivers.forEach(function(transceiver) { if (transceiver.iceTransport && transceiver.iceTransport.state === 'new' && transceiver.iceTransport.getRemoteCandidates().length > 0) { console.warn('Timeout for addRemoteCandidate. Consider sending ' + 'an end-of-candidates notification'); transceiver.iceTransport.addRemoteCandidate({}); } }); }, 4000); return Promise.resolve(); }; RTCPeerConnection.prototype.close = function() { this.transceivers.forEach(function(transceiver) { /* not yet if (transceiver.iceGatherer) { transceiver.iceGatherer.close(); } */ if (transceiver.iceTransport) { transceiver.iceTransport.stop(); } if (transceiver.dtlsTransport) { transceiver.dtlsTransport.stop(); } if (transceiver.rtpSender) { transceiver.rtpSender.stop(); } if (transceiver.rtpReceiver) { transceiver.rtpReceiver.stop(); } }); // FIXME: clean up tracks, local streams, remote streams, etc this._isClosed = true; this._updateSignalingState('closed'); }; // Update the signaling state. RTCPeerConnection.prototype._updateSignalingState = function(newState) { this.signalingState = newState; var event = new Event('signalingstatechange'); this._dispatchEvent('signalingstatechange', event); }; // Determine whether to fire the negotiationneeded event. RTCPeerConnection.prototype._maybeFireNegotiationNeeded = function() { var pc = this; if (this.signalingState !== 'stable' || this.needNegotiation === true) { return; } this.needNegotiation = true; window.setTimeout(function() { if (pc.needNegotiation) { pc.needNegotiation = false; var event = new Event('negotiationneeded'); pc._dispatchEvent('negotiationneeded', event); } }, 0); }; // Update the ice connection state. RTCPeerConnection.prototype._updateIceConnectionState = function() { var newState; var states = { 'new': 0, closed: 0, checking: 0, connected: 0, completed: 0, disconnected: 0, failed: 0 }; this.transceivers.forEach(function(transceiver) { states[transceiver.iceTransport.state]++; }); newState = 'new'; if (states.failed > 0) { newState = 'failed'; } else if (states.checking > 0) { newState = 'checking'; } else if (states.disconnected > 0) { newState = 'disconnected'; } else if (states.new > 0) { newState = 'new'; } else if (states.connected > 0) { newState = 'connected'; } else if (states.completed > 0) { newState = 'completed'; } if (newState !== this.iceConnectionState) { this.iceConnectionState = newState; var event = new Event('iceconnectionstatechange'); this._dispatchEvent('iceconnectionstatechange', event); } }; // Update the connection state. RTCPeerConnection.prototype._updateConnectionState = function() { var newState; var states = { 'new': 0, closed: 0, connecting: 0, connected: 0, completed: 0, disconnected: 0, failed: 0 }; this.transceivers.forEach(function(transceiver) { states[transceiver.iceTransport.state]++; states[transceiver.dtlsTransport.state]++; }); // ICETransport.completed and connected are the same for this purpose. states.connected += states.completed; newState = 'new'; if (states.failed > 0) { newState = 'failed'; } else if (states.connecting > 0) { newState = 'connecting'; } else if (states.disconnected > 0) { newState = 'disconnected'; } else if (states.new > 0) { newState = 'new'; } else if (states.connected > 0) { newState = 'connected'; } if (newState !== this.connectionState) { this.connectionState = newState; var event = new Event('connectionstatechange'); this._dispatchEvent('connectionstatechange', event); } }; RTCPeerConnection.prototype.createOffer = function() { var pc = this; if (pc._isClosed) { return Promise.reject(makeError('InvalidStateError', 'Can not call createOffer after close')); } var numAudioTracks = pc.transceivers.filter(function(t) { return t.kind === 'audio'; }).length; var numVideoTracks = pc.transceivers.filter(function(t) { return t.kind === 'video'; }).length; // Determine number of audio and video tracks we need to send/recv. var offerOptions = arguments[0]; if (offerOptions) { // Reject Chrome legacy constraints. if (offerOptions.mandatory || offerOptions.optional) { throw new TypeError( 'Legacy mandatory/optional constraints not supported.'); } if (offerOptions.offerToReceiveAudio !== undefined) { if (offerOptions.offerToReceiveAudio === true) { numAudioTracks = 1; } else if (offerOptions.offerToReceiveAudio === false) { numAudioTracks = 0; } else { numAudioTracks = offerOptions.offerToReceiveAudio; } } if (offerOptions.offerToReceiveVideo !== undefined) { if (offerOptions.offerToReceiveVideo === true) { numVideoTracks = 1; } else if (offerOptions.offerToReceiveVideo === false) { numVideoTracks = 0; } else { numVideoTracks = offerOptions.offerToReceiveVideo; } } } pc.transceivers.forEach(function(transceiver) { if (transceiver.kind === 'audio') { numAudioTracks--; if (numAudioTracks < 0) { transceiver.wantReceive = false; } } else if (transceiver.kind === 'video') { numVideoTracks--; if (numVideoTracks < 0) { transceiver.wantReceive = false; } } }); // Create M-lines for recvonly streams. while (numAudioTracks > 0 || numVideoTracks > 0) { if (numAudioTracks > 0) { pc._createTransceiver('audio'); numAudioTracks--; } if (numVideoTracks > 0) { pc._createTransceiver('video'); numVideoTracks--; } } var sdp = SDPUtils.writeSessionBoilerplate(pc._sdpSessionId, pc._sdpSessionVersion++); pc.transceivers.forEach(function(transceiver, sdpMLineIndex) { // For each track, create an ice gatherer, ice transport, // dtls transport, potentially rtpsender and rtpreceiver. var track = transceiver.track; var kind = transceiver.kind; var mid = transceiver.mid || SDPUtils.generateIdentifier(); transceiver.mid = mid; if (!transceiver.iceGatherer) { transceiver.iceGatherer = pc._createIceGatherer(sdpMLineIndex, pc.usingBundle); } var localCapabilities = window.RTCRtpSender.getCapabilities(kind); // filter RTX until additional stuff needed for RTX is implemented // in adapter.js if (edgeVersion < 15019) { localCapabilities.codecs = localCapabilities.codecs.filter( function(codec) { return codec.name !== 'rtx'; }); } localCapabilities.codecs.forEach(function(codec) { // work around https://bugs.chromium.org/p/webrtc/issues/detail?id=6552 // by adding level-asymmetry-allowed=1 if (codec.name === 'H264' && codec.parameters['level-asymmetry-allowed'] === undefined) { codec.parameters['level-asymmetry-allowed'] = '1'; } // for subsequent offers, we might have to re-use the payload // type of the last offer. if (transceiver.remoteCapabilities && transceiver.remoteCapabilities.codecs) { transceiver.remoteCapabilities.codecs.forEach(function(remoteCodec) { if (codec.name.toLowerCase() === remoteCodec.name.toLowerCase() && codec.clockRate === remoteCodec.clockRate) { codec.preferredPayloadType = remoteCodec.payloadType; } }); } }); localCapabilities.headerExtensions.forEach(function(hdrExt) { var remoteExtensions = transceiver.remoteCapabilities && transceiver.remoteCapabilities.headerExtensions || []; remoteExtensions.forEach(function(rHdrExt) { if (hdrExt.uri === rHdrExt.uri) { hdrExt.id = rHdrExt.id; } }); }); // generate an ssrc now, to be used later in rtpSender.send var sendEncodingParameters = transceiver.sendEncodingParameters || [{ ssrc: (2 * sdpMLineIndex + 1) * 1001 }]; if (track) { // add RTX if (edgeVersion >= 15019 && kind === 'video' && !sendEncodingParameters[0].rtx) { sendEncodingParameters[0].rtx = { ssrc: sendEncodingParameters[0].ssrc + 1 }; } } if (transceiver.wantReceive) { transceiver.rtpReceiver = new window.RTCRtpReceiver( transceiver.dtlsTransport, kind); } transceiver.localCapabilities = localCapabilities; transceiver.sendEncodingParameters = sendEncodingParameters; }); // always offer BUNDLE and dispose on return if not supported. if (pc._config.bundlePolicy !== 'max-compat') { sdp += 'a=group:BUNDLE ' + pc.transceivers.map(function(t) { return t.mid; }).join(' ') + '\r\n'; } sdp += 'a=ice-options:trickle\r\n'; pc.transceivers.forEach(function(transceiver, sdpMLineIndex) { sdp += writeMediaSection(transceiver, transceiver.localCapabilities, 'offer', transceiver.stream, pc._dtlsRole); sdp += 'a=rtcp-rsize\r\n'; if (transceiver.iceGatherer && pc.iceGatheringState !== 'new' && (sdpMLineIndex === 0 || !pc.usingBundle)) { transceiver.iceGatherer.getLocalCandidates().forEach(function(cand) { cand.component = 1; sdp += 'a=' + SDPUtils.writeCandidate(cand) + '\r\n'; }); if (transceiver.iceGatherer.state === 'completed') { sdp += 'a=end-of-candidates\r\n'; } } }); var desc = new window.RTCSessionDescription({ type: 'offer', sdp: sdp }); return Promise.resolve(desc); }; RTCPeerConnection.prototype.createAnswer = function() { var pc = this; if (pc._isClosed) { return Promise.reject(makeError('InvalidStateError', 'Can not call createAnswer after close')); } if (!(pc.signalingState === 'have-remote-offer' || pc.signalingState === 'have-local-pranswer')) { return Promise.reject(makeError('InvalidStateError', 'Can not call createAnswer in signalingState ' + pc.signalingState)); } var sdp = SDPUtils.writeSessionBoilerplate(pc._sdpSessionId, pc._sdpSessionVersion++); if (pc.usingBundle) { sdp += 'a=group:BUNDLE ' + pc.transceivers.map(function(t) { return t.mid; }).join(' ') + '\r\n'; } sdp += 'a=ice-options:trickle\r\n'; var mediaSectionsInOffer = SDPUtils.getMediaSections( pc._remoteDescription.sdp).length; pc.transceivers.forEach(function(transceiver, sdpMLineIndex) { if (sdpMLineIndex + 1 > mediaSectionsInOffer) { return; } if (transceiver.rejected) { if (transceiver.kind === 'application') { if (transceiver.protocol === 'DTLS/SCTP') { // legacy fmt sdp += 'm=application 0 DTLS/SCTP 5000\r\n'; } else { sdp += 'm=application 0 ' + transceiver.protocol + ' webrtc-datachannel\r\n'; } } else if (transceiver.kind === 'audio') { sdp += 'm=audio 0 UDP/TLS/RTP/SAVPF 0\r\n' + 'a=rtpmap:0 PCMU/8000\r\n'; } else if (transceiver.kind === 'video') { sdp += 'm=video 0 UDP/TLS/RTP/SAVPF 120\r\n' + 'a=rtpmap:120 VP8/90000\r\n'; } sdp += 'c=IN IP4 0.0.0.0\r\n' + 'a=inactive\r\n' + 'a=mid:' + transceiver.mid + '\r\n'; return; } // FIXME: look at direction. if (transceiver.stream) { var localTrack; if (transceiver.kind === 'audio') { localTrack = transceiver.stream.getAudioTracks()[0]; } else if (transceiver.kind === 'video') { localTrack = transceiver.stream.getVideoTracks()[0]; } if (localTrack) { // add RTX if (edgeVersion >= 15019 && transceiver.kind === 'video' && !transceiver.sendEncodingParameters[0].rtx) { transceiver.sendEncodingParameters[0].rtx = { ssrc: transceiver.sendEncodingParameters[0].ssrc + 1 }; } } } // Calculate intersection of capabilities. var commonCapabilities = getCommonCapabilities( transceiver.localCapabilities, transceiver.remoteCapabilities); var hasRtx = commonCapabilities.codecs.filter(function(c) { return c.name.toLowerCase() === 'rtx'; }).length; if (!hasRtx && transceiver.sendEncodingParameters[0].rtx) { delete transceiver.sendEncodingParameters[0].rtx; } sdp += writeMediaSection(transceiver, commonCapabilities, 'answer', transceiver.stream, pc._dtlsRole); if (transceiver.rtcpParameters && transceiver.rtcpParameters.reducedSize) { sdp += 'a=rtcp-rsize\r\n'; } }); var desc = new window.RTCSessionDescription({ type: 'answer', sdp: sdp }); return Promise.resolve(desc); }; RTCPeerConnection.prototype.addIceCandidate = function(candidate) { var pc = this; var sections; if (candidate && !(candidate.sdpMLineIndex !== undefined || candidate.sdpMid)) { return Promise.reject(new TypeError('sdpMLineIndex or sdpMid required')); } // TODO: needs to go into ops queue. return new Promise(function(resolve, reject) { if (!pc._remoteDescription) { return reject(makeError('InvalidStateError', 'Can not add ICE candidate without a remote description')); } else if (!candidate || candidate.candidate === '') { for (var j = 0; j < pc.transceivers.length; j++) { if (pc.transceivers[j].rejected) { continue; } pc.transceivers[j].iceTransport.addRemoteCandidate({}); sections = SDPUtils.getMediaSections(pc._remoteDescription.sdp); sections[j] += 'a=end-of-candidates\r\n'; pc._remoteDescription.sdp = SDPUtils.getDescription(pc._remoteDescription.sdp) + sections.join(''); if (pc.usingBundle) { break; } } } else { var sdpMLineIndex = candidate.sdpMLineIndex; if (candidate.sdpMid) { for (var i = 0; i < pc.transceivers.length; i++) { if (pc.transceivers[i].mid === candidate.sdpMid) { sdpMLineIndex = i; break; } } } var transceiver = pc.transceivers[sdpMLineIndex]; if (transceiver) { if (transceiver.rejected) { return resolve(); } var cand = Object.keys(candidate.candidate).length > 0 ? SDPUtils.parseCandidate(candidate.candidate) : {}; // Ignore Chrome's invalid candidates since Edge does not like them. if (cand.protocol === 'tcp' && (cand.port === 0 || cand.port === 9)) { return resolve(); } // Ignore RTCP candidates, we assume RTCP-MUX. if (cand.component && cand.component !== 1) { return resolve(); } // when using bundle, avoid adding candidates to the wrong // ice transport. And avoid adding candidates added in the SDP. if (sdpMLineIndex === 0 || (sdpMLineIndex > 0 && transceiver.iceTransport !== pc.transceivers[0].iceTransport)) { if (!maybeAddCandidate(transceiver.iceTransport, cand)) { return reject(makeError('OperationError', 'Can not add ICE candidate')); } } // update the remoteDescription. var candidateString = candidate.candidate.trim(); if (candidateString.indexOf('a=') === 0) { candidateString = candidateString.substr(2); } sections = SDPUtils.getMediaSections(pc._remoteDescription.sdp); sections[sdpMLineIndex] += 'a=' + (cand.type ? candidateString : 'end-of-candidates') + '\r\n'; pc._remoteDescription.sdp = SDPUtils.getDescription(pc._remoteDescription.sdp) + sections.join(''); } else { return reject(makeError('OperationError', 'Can not add ICE candidate')); } } resolve(); }); }; RTCPeerConnection.prototype.getStats = function(selector) { if (selector && selector instanceof window.MediaStreamTrack) { var senderOrReceiver = null; this.transceivers.forEach(function(transceiver) { if (transceiver.rtpSender && transceiver.rtpSender.track === selector) { senderOrReceiver = transceiver.rtpSender; } else if (transceiver.rtpReceiver && transceiver.rtpReceiver.track === selector) { senderOrReceiver = transceiver.rtpReceiver; } }); if (!senderOrReceiver) { throw makeError('InvalidAccessError', 'Invalid selector.'); } return senderOrReceiver.getStats(); } var promises = []; this.transceivers.forEach(function(transceiver) { ['rtpSender', 'rtpReceiver', 'iceGatherer', 'iceTransport', 'dtlsTransport'].forEach(function(method) { if (transceiver[method]) { promises.push(transceiver[method].getStats()); } }); }); return Promise.all(promises).then(function(allStats) { var results = new Map(); allStats.forEach(function(stats) { stats.forEach(function(stat) { results.set(stat.id, stat); }); }); return results; }); }; // fix low-level stat names and return Map instead of object. var ortcObjects = ['RTCRtpSender', 'RTCRtpReceiver', 'RTCIceGatherer', 'RTCIceTransport', 'RTCDtlsTransport']; ortcObjects.forEach(function(ortcObjectName) { var obj = window[ortcObjectName]; if (obj && obj.prototype && obj.prototype.getStats) { var nativeGetstats = obj.prototype.getStats; obj.prototype.getStats = function() { return nativeGetstats.apply(this) .then(function(nativeStats) { var mapStats = new Map(); Object.keys(nativeStats).forEach(function(id) { nativeStats[id].type = fixStatsType(nativeStats[id]); mapStats.set(id, nativeStats[id]); }); return mapStats; }); }; } }); // legacy callback shims. Should be moved to adapter.js some days. var methods = ['createOffer', 'createAnswer']; methods.forEach(function(method) { var nativeMethod = RTCPeerConnection.prototype[method]; RTCPeerConnection.prototype[method] = function() { var args = arguments; if (typeof args[0] === 'function' || typeof args[1] === 'function') { // legacy return nativeMethod.apply(this, [arguments[2]]) .then(function(description) { if (typeof args[0] === 'function') { args[0].apply(null, [description]); } }, function(error) { if (typeof args[1] === 'function') { args[1].apply(null, [error]); } }); } return nativeMethod.apply(this, arguments); }; }); methods = ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate']; methods.forEach(function(method) { var nativeMethod = RTCPeerConnection.prototype[method]; RTCPeerConnection.prototype[method] = function() { var args = arguments; if (typeof args[1] === 'function' || typeof args[2] === 'function') { // legacy return nativeMethod.apply(this, arguments) .then(function() { if (typeof args[1] === 'function') { args[1].apply(null); } }, function(error) { if (typeof args[2] === 'function') { args[2].apply(null, [error]); } }); } return nativeMethod.apply(this, arguments); }; }); // getStats is special. It doesn't have a spec legacy method yet we support // getStats(something, cb) without error callbacks. ['getStats'].forEach(function(method) { var nativeMethod = RTCPeerConnection.prototype[method]; RTCPeerConnection.prototype[method] = function() { var args = arguments; if (typeof args[1] === 'function') { return nativeMethod.apply(this, arguments) .then(function() { if (typeof args[1] === 'function') { args[1].apply(null); } }); } return nativeMethod.apply(this, arguments); }; }); return RTCPeerConnection; }; },{"sdp":2}],2:[function(require,module,exports){ /* eslint-env node */ 'use strict'; // SDP helpers. var SDPUtils = {}; // Generate an alphanumeric identifier for cname or mids. // TODO: use UUIDs instead? https://gist.github.com/jed/982883 SDPUtils.generateIdentifier = function() { return Math.random().toString(36).substr(2, 10); }; // The RTCP CNAME used by all peerconnections from the same JS. SDPUtils.localCName = SDPUtils.generateIdentifier(); // Splits SDP into lines, dealing with both CRLF and LF. SDPUtils.splitLines = function(blob) { return blob.trim().split('\n').map(function(line) { return line.trim(); }); }; // Splits SDP into sessionpart and mediasections. Ensures CRLF. SDPUtils.splitSections = function(blob) { var parts = blob.split('\nm='); return parts.map(function(part, index) { return (index > 0 ? 'm=' + part : part).trim() + '\r\n'; }); }; // returns the session description. SDPUtils.getDescription = function(blob) { var sections = SDPUtils.splitSections(blob); return sections && sections[0]; }; // returns the individual media sections. SDPUtils.getMediaSections = function(blob) { var sections = SDPUtils.splitSections(blob); sections.shift(); return sections; }; // Returns lines that start with a certain prefix. SDPUtils.matchPrefix = function(blob, prefix) { return SDPUtils.splitLines(blob).filter(function(line) { return line.indexOf(prefix) === 0; }); }; // Parses an ICE candidate line. Sample input: // candidate:702786350 2 udp 41819902 8.8.8.8 60769 typ relay raddr 8.8.8.8 // rport 55996" SDPUtils.parseCandidate = function(line) { var parts; // Parse both variants. if (line.indexOf('a=candidate:') === 0) { parts = line.substring(12).split(' '); } else { parts = line.substring(10).split(' '); } var candidate = { foundation: parts[0], component: parseInt(parts[1], 10), protocol: parts[2].toLowerCase(), priority: parseInt(parts[3], 10), ip: parts[4], address: parts[4], // address is an alias for ip. port: parseInt(parts[5], 10), // skip parts[6] == 'typ' type: parts[7] }; for (var i = 8; i < parts.length; i += 2) { switch (parts[i]) { case 'raddr': candidate.relatedAddress = parts[i + 1]; break; case 'rport': candidate.relatedPort = parseInt(parts[i + 1], 10); break; case 'tcptype': candidate.tcpType = parts[i + 1]; break; case 'ufrag': candidate.ufrag = parts[i + 1]; // for backward compability. candidate.usernameFragment = parts[i + 1]; break; default: // extension handling, in particular ufrag candidate[parts[i]] = parts[i + 1]; break; } } return candidate; }; // Translates a candidate object into SDP candidate attribute. SDPUtils.writeCandidate = function(candidate) { var sdp = []; sdp.push(candidate.foundation); sdp.push(candidate.component); sdp.push(candidate.protocol.toUpperCase()); sdp.push(candidate.priority); sdp.push(candidate.address || candidate.ip); sdp.push(candidate.port); var type = candidate.type; sdp.push('typ'); sdp.push(type); if (type !== 'host' && candidate.relatedAddress && candidate.relatedPort) { sdp.push('raddr'); sdp.push(candidate.relatedAddress); sdp.push('rport'); sdp.push(candidate.relatedPort); } if (candidate.tcpType && candidate.protocol.toLowerCase() === 'tcp') { sdp.push('tcptype'); sdp.push(candidate.tcpType); } if (candidate.usernameFragment || candidate.ufrag) { sdp.push('ufrag'); sdp.push(candidate.usernameFragment || candidate.ufrag); } return 'candidate:' + sdp.join(' '); }; // Parses an ice-options line, returns an array of option tags. // a=ice-options:foo bar SDPUtils.parseIceOptions = function(line) { return line.substr(14).split(' '); }; // Parses an rtpmap line, returns RTCRtpCoddecParameters. Sample input: // a=rtpmap:111 opus/48000/2 SDPUtils.parseRtpMap = function(line) { var parts = line.substr(9).split(' '); var parsed = { payloadType: parseInt(parts.shift(), 10) // was: id }; parts = parts[0].split('/'); parsed.name = parts[0]; parsed.clockRate = parseInt(parts[1], 10); // was: clockrate parsed.channels = parts.length === 3 ? parseInt(parts[2], 10) : 1; // legacy alias, got renamed back to channels in ORTC. parsed.numChannels = parsed.channels; return parsed; }; // Generate an a=rtpmap line from RTCRtpCodecCapability or // RTCRtpCodecParameters. SDPUtils.writeRtpMap = function(codec) { var pt = codec.payloadType; if (codec.preferredPayloadType !== undefined) { pt = codec.preferredPayloadType; } var channels = codec.channels || codec.numChannels || 1; return 'a=rtpmap:' + pt + ' ' + codec.name + '/' + codec.clockRate + (channels !== 1 ? '/' + channels : '') + '\r\n'; }; // Parses an a=extmap line (headerextension from RFC 5285). Sample input: // a=extmap:2 urn:ietf:params:rtp-hdrext:toffset // a=extmap:2/sendonly urn:ietf:params:rtp-hdrext:toffset SDPUtils.parseExtmap = function(line) { var parts = line.substr(9).split(' '); return { id: parseInt(parts[0], 10), direction: parts[0].indexOf('/') > 0 ? parts[0].split('/')[1] : 'sendrecv', uri: parts[1] }; }; // Generates a=extmap line from RTCRtpHeaderExtensionParameters or // RTCRtpHeaderExtension. SDPUtils.writeExtmap = function(headerExtension) { return 'a=extmap:' + (headerExtension.id || headerExtension.preferredId) + (headerExtension.direction && headerExtension.direction !== 'sendrecv' ? '/' + headerExtension.direction : '') + ' ' + headerExtension.uri + '\r\n'; }; // Parses an ftmp line, returns dictionary. Sample input: // a=fmtp:96 vbr=on;cng=on // Also deals with vbr=on; cng=on SDPUtils.parseFmtp = function(line) { var parsed = {}; var kv; var parts = line.substr(line.indexOf(' ') + 1).split(';'); for (var j = 0; j < parts.length; j++) { kv = parts[j].trim().split('='); parsed[kv[0].trim()] = kv[1]; } return parsed; }; // Generates an a=ftmp line from RTCRtpCodecCapability or RTCRtpCodecParameters. SDPUtils.writeFmtp = function(codec) { var line = ''; var pt = codec.payloadType; if (codec.preferredPayloadType !== undefined) { pt = codec.preferredPayloadType; } if (codec.parameters && Object.keys(codec.parameters).length) { var params = []; Object.keys(codec.parameters).forEach(function(param) { if (codec.parameters[param]) { params.push(param + '=' + codec.parameters[param]); } else { params.push(param); } }); line += 'a=fmtp:' + pt + ' ' + params.join(';') + '\r\n'; } return line; }; // Parses an rtcp-fb line, returns RTCPRtcpFeedback object. Sample input: // a=rtcp-fb:98 nack rpsi SDPUtils.parseRtcpFb = function(line) { var parts = line.substr(line.indexOf(' ') + 1).split(' '); return { type: parts.shift(), parameter: parts.join(' ') }; }; // Generate a=rtcp-fb lines from RTCRtpCodecCapability or RTCRtpCodecParameters. SDPUtils.writeRtcpFb = function(codec) { var lines = ''; var pt = codec.payloadType; if (codec.preferredPayloadType !== undefined) { pt = codec.preferredPayloadType; } if (codec.rtcpFeedback && codec.rtcpFeedback.length) { // FIXME: special handling for trr-int? codec.rtcpFeedback.forEach(function(fb) { lines += 'a=rtcp-fb:' + pt + ' ' + fb.type + (fb.parameter && fb.parameter.length ? ' ' + fb.parameter : '') + '\r\n'; }); } return lines; }; // Parses an RFC 5576 ssrc media attribute. Sample input: // a=ssrc:3735928559 cname:something SDPUtils.parseSsrcMedia = function(line) { var sp = line.indexOf(' '); var parts = { ssrc: parseInt(line.substr(7, sp - 7), 10) }; var colon = line.indexOf(':', sp); if (colon > -1) { parts.attribute = line.substr(sp + 1, colon - sp - 1); parts.value = line.substr(colon + 1); } else { parts.attribute = line.substr(sp + 1); } return parts; }; SDPUtils.parseSsrcGroup = function(line) { var parts = line.substr(13).split(' '); return { semantics: parts.shift(), ssrcs: parts.map(function(ssrc) { return parseInt(ssrc, 10); }) }; }; // Extracts the MID (RFC 5888) from a media section. // returns the MID or undefined if no mid line was found. SDPUtils.getMid = function(mediaSection) { var mid = SDPUtils.matchPrefix(mediaSection, 'a=mid:')[0]; if (mid) { return mid.substr(6); } }; SDPUtils.parseFingerprint = function(line) { var parts = line.substr(14).split(' '); return { algorithm: parts[0].toLowerCase(), // algorithm is case-sensitive in Edge. value: parts[1] }; }; // Extracts DTLS parameters from SDP media section or sessionpart. // FIXME: for consistency with other functions this should only // get the fingerprint line as input. See also getIceParameters. SDPUtils.getDtlsParameters = function(mediaSection, sessionpart) { var lines = SDPUtils.matchPrefix(mediaSection + sessionpart, 'a=fingerprint:'); // Note: a=setup line is ignored since we use the 'auto' role. // Note2: 'algorithm' is not case sensitive except in Edge. return { role: 'auto', fingerprints: lines.map(SDPUtils.parseFingerprint) }; }; // Serializes DTLS parameters to SDP. SDPUtils.writeDtlsParameters = function(params, setupType) { var sdp = 'a=setup:' + setupType + '\r\n'; params.fingerprints.forEach(function(fp) { sdp += 'a=fingerprint:' + fp.algorithm + ' ' + fp.value + '\r\n'; }); return sdp; }; // Parses ICE information from SDP media section or sessionpart. // FIXME: for consistency with other functions this should only // get the ice-ufrag and ice-pwd lines as input. SDPUtils.getIceParameters = function(mediaSection, sessionpart) { var lines = SDPUtils.splitLines(mediaSection); // Search in session part, too. lines = lines.concat(SDPUtils.splitLines(sessionpart)); var iceParameters = { usernameFragment: lines.filter(function(line) { return line.indexOf('a=ice-ufrag:') === 0; })[0].substr(12), password: lines.filter(function(line) { return line.indexOf('a=ice-pwd:') === 0; })[0].substr(10) }; return iceParameters; }; // Serializes ICE parameters to SDP. SDPUtils.writeIceParameters = function(params) { return 'a=ice-ufrag:' + params.usernameFragment + '\r\n' + 'a=ice-pwd:' + params.password + '\r\n'; }; // Parses the SDP media section and returns RTCRtpParameters. SDPUtils.parseRtpParameters = function(mediaSection) { var description = { codecs: [], headerExtensions: [], fecMechanisms: [], rtcp: [] }; var lines = SDPUtils.splitLines(mediaSection); var mline = lines[0].split(' '); for (var i = 3; i < mline.length; i++) { // find all codecs from mline[3..] var pt = mline[i]; var rtpmapline = SDPUtils.matchPrefix( mediaSection, 'a=rtpmap:' + pt + ' ')[0]; if (rtpmapline) { var codec = SDPUtils.parseRtpMap(rtpmapline); var fmtps = SDPUtils.matchPrefix( mediaSection, 'a=fmtp:' + pt + ' '); // Only the first a=fmtp: is considered. codec.parameters = fmtps.length ? SDPUtils.parseFmtp(fmtps[0]) : {}; codec.rtcpFeedback = SDPUtils.matchPrefix( mediaSection, 'a=rtcp-fb:' + pt + ' ') .map(SDPUtils.parseRtcpFb); description.codecs.push(codec); // parse FEC mechanisms from rtpmap lines. switch (codec.name.toUpperCase()) { case 'RED': case 'ULPFEC': description.fecMechanisms.push(codec.name.toUpperCase()); break; default: // only RED and ULPFEC are recognized as FEC mechanisms. break; } } } SDPUtils.matchPrefix(mediaSection, 'a=extmap:').forEach(function(line) { description.headerExtensions.push(SDPUtils.parseExtmap(line)); }); // FIXME: parse rtcp. return description; }; // Generates parts of the SDP media section describing the capabilities / // parameters. SDPUtils.writeRtpDescription = function(kind, caps) { var sdp = ''; // Build the mline. sdp += 'm=' + kind + ' '; sdp += caps.codecs.length > 0 ? '9' : '0'; // reject if no codecs. sdp += ' UDP/TLS/RTP/SAVPF '; sdp += caps.codecs.map(function(codec) { if (codec.preferredPayloadType !== undefined) { return codec.preferredPayloadType; } return codec.payloadType; }).join(' ') + '\r\n'; sdp += 'c=IN IP4 0.0.0.0\r\n'; sdp += 'a=rtcp:9 IN IP4 0.0.0.0\r\n'; // Add a=rtpmap lines for each codec. Also fmtp and rtcp-fb. caps.codecs.forEach(function(codec) { sdp += SDPUtils.writeRtpMap(codec); sdp += SDPUtils.writeFmtp(codec); sdp += SDPUtils.writeRtcpFb(codec); }); var maxptime = 0; caps.codecs.forEach(function(codec) { if (codec.maxptime > maxptime) { maxptime = codec.maxptime; } }); if (maxptime > 0) { sdp += 'a=maxptime:' + maxptime + '\r\n'; } sdp += 'a=rtcp-mux\r\n'; if (caps.headerExtensions) { caps.headerExtensions.forEach(function(extension) { sdp += SDPUtils.writeExtmap(extension); }); } // FIXME: write fecMechanisms. return sdp; }; // Parses the SDP media section and returns an array of // RTCRtpEncodingParameters. SDPUtils.parseRtpEncodingParameters = function(mediaSection) { var encodingParameters = []; var description = SDPUtils.parseRtpParameters(mediaSection); var hasRed = description.fecMechanisms.indexOf('RED') !== -1; var hasUlpfec = description.fecMechanisms.indexOf('ULPFEC') !== -1; // filter a=ssrc:... cname:, ignore PlanB-msid var ssrcs = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:') .map(function(line) { return SDPUtils.parseSsrcMedia(line); }) .filter(function(parts) { return parts.attribute === 'cname'; }); var primarySsrc = ssrcs.length > 0 && ssrcs[0].ssrc; var secondarySsrc; var flows = SDPUtils.matchPrefix(mediaSection, 'a=ssrc-group:FID') .map(function(line) { var parts = line.substr(17).split(' '); return parts.map(function(part) { return parseInt(part, 10); }); }); if (flows.length > 0 && flows[0].length > 1 && flows[0][0] === primarySsrc) { secondarySsrc = flows[0][1]; } description.codecs.forEach(function(codec) { if (codec.name.toUpperCase() === 'RTX' && codec.parameters.apt) { var encParam = { ssrc: primarySsrc, codecPayloadType: parseInt(codec.parameters.apt, 10) }; if (primarySsrc && secondarySsrc) { encParam.rtx = {ssrc: secondarySsrc}; } encodingParameters.push(encParam); if (hasRed) { encParam = JSON.parse(JSON.stringify(encParam)); encParam.fec = { ssrc: primarySsrc, mechanism: hasUlpfec ? 'red+ulpfec' : 'red' }; encodingParameters.push(encParam); } } }); if (encodingParameters.length === 0 && primarySsrc) { encodingParameters.push({ ssrc: primarySsrc }); } // we support both b=AS and b=TIAS but interpret AS as TIAS. var bandwidth = SDPUtils.matchPrefix(mediaSection, 'b='); if (bandwidth.length) { if (bandwidth[0].indexOf('b=TIAS:') === 0) { bandwidth = parseInt(bandwidth[0].substr(7), 10); } else if (bandwidth[0].indexOf('b=AS:') === 0) { // use formula from JSEP to convert b=AS to TIAS value. bandwidth = parseInt(bandwidth[0].substr(5), 10) * 1000 * 0.95 - (50 * 40 * 8); } else { bandwidth = undefined; } encodingParameters.forEach(function(params) { params.maxBitrate = bandwidth; }); } return encodingParameters; }; // parses http://draft.ortc.org/#rtcrtcpparameters* SDPUtils.parseRtcpParameters = function(mediaSection) { var rtcpParameters = {}; // Gets the first SSRC. Note tha with RTX there might be multiple // SSRCs. var remoteSsrc = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:') .map(function(line) { return SDPUtils.parseSsrcMedia(line); }) .filter(function(obj) { return obj.attribute === 'cname'; })[0]; if (remoteSsrc) { rtcpParameters.cname = remoteSsrc.value; rtcpParameters.ssrc = remoteSsrc.ssrc; } // Edge uses the compound attribute instead of reducedSize // compound is !reducedSize var rsize = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-rsize'); rtcpParameters.reducedSize = rsize.length > 0; rtcpParameters.compound = rsize.length === 0; // parses the rtcp-mux attrіbute. // Note that Edge does not support unmuxed RTCP. var mux = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-mux'); rtcpParameters.mux = mux.length > 0; return rtcpParameters; }; // parses either a=msid: or a=ssrc:... msid lines and returns // the id of the MediaStream and MediaStreamTrack. SDPUtils.parseMsid = function(mediaSection) { var parts; var spec = SDPUtils.matchPrefix(mediaSection, 'a=msid:'); if (spec.length === 1) { parts = spec[0].substr(7).split(' '); return {stream: parts[0], track: parts[1]}; } var planB = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:') .map(function(line) { return SDPUtils.parseSsrcMedia(line); }) .filter(function(msidParts) { return msidParts.attribute === 'msid'; }); if (planB.length > 0) { parts = planB[0].value.split(' '); return {stream: parts[0], track: parts[1]}; } }; // Generate a session ID for SDP. // https://tools.ietf.org/html/draft-ietf-rtcweb-jsep-20#section-5.2.1 // recommends using a cryptographically random +ve 64-bit value // but right now this should be acceptable and within the right range SDPUtils.generateSessionId = function() { return Math.random().toString().substr(2, 21); }; // Write boilder plate for start of SDP // sessId argument is optional - if not supplied it will // be generated randomly // sessVersion is optional and defaults to 2 // sessUser is optional and defaults to 'thisisadapterortc' SDPUtils.writeSessionBoilerplate = function(sessId, sessVer, sessUser) { var sessionId; var version = sessVer !== undefined ? sessVer : 2; if (sessId) { sessionId = sessId; } else { sessionId = SDPUtils.generateSessionId(); } var user = sessUser || 'thisisadapterortc'; // FIXME: sess-id should be an NTP timestamp. return 'v=0\r\n' + 'o=' + user + ' ' + sessionId + ' ' + version + ' IN IP4 127.0.0.1\r\n' + 's=-\r\n' + 't=0 0\r\n'; }; SDPUtils.writeMediaSection = function(transceiver, caps, type, stream) { var sdp = SDPUtils.writeRtpDescription(transceiver.kind, caps); // Map ICE parameters (ufrag, pwd) to SDP. sdp += SDPUtils.writeIceParameters( transceiver.iceGatherer.getLocalParameters()); // Map DTLS parameters to SDP. sdp += SDPUtils.writeDtlsParameters( transceiver.dtlsTransport.getLocalParameters(), type === 'offer' ? 'actpass' : 'active'); sdp += 'a=mid:' + transceiver.mid + '\r\n'; if (transceiver.direction) { sdp += 'a=' + transceiver.direction + '\r\n'; } else if (transceiver.rtpSender && transceiver.rtpReceiver) { sdp += 'a=sendrecv\r\n'; } else if (transceiver.rtpSender) { sdp += 'a=sendonly\r\n'; } else if (transceiver.rtpReceiver) { sdp += 'a=recvonly\r\n'; } else { sdp += 'a=inactive\r\n'; } if (transceiver.rtpSender) { // spec. var msid = 'msid:' + stream.id + ' ' + transceiver.rtpSender.track.id + '\r\n'; sdp += 'a=' + msid; // for Chrome. sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc + ' ' + msid; if (transceiver.sendEncodingParameters[0].rtx) { sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc + ' ' + msid; sdp += 'a=ssrc-group:FID ' + transceiver.sendEncodingParameters[0].ssrc + ' ' + transceiver.sendEncodingParameters[0].rtx.ssrc + '\r\n'; } } // FIXME: this should be written by writeRtpDescription. sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc + ' cname:' + SDPUtils.localCName + '\r\n'; if (transceiver.rtpSender && transceiver.sendEncodingParameters[0].rtx) { sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc + ' cname:' + SDPUtils.localCName + '\r\n'; } return sdp; }; // Gets the direction from the mediaSection or the sessionpart. SDPUtils.getDirection = function(mediaSection, sessionpart) { // Look for sendrecv, sendonly, recvonly, inactive, default to sendrecv. var lines = SDPUtils.splitLines(mediaSection); for (var i = 0; i < lines.length; i++) { switch (lines[i]) { case 'a=sendrecv': case 'a=sendonly': case 'a=recvonly': case 'a=inactive': return lines[i].substr(2); default: // FIXME: What should happen here? } } if (sessionpart) { return SDPUtils.getDirection(sessionpart); } return 'sendrecv'; }; SDPUtils.getKind = function(mediaSection) { var lines = SDPUtils.splitLines(mediaSection); var mline = lines[0].split(' '); return mline[0].substr(2); }; SDPUtils.isRejected = function(mediaSection) { return mediaSection.split(' ', 2)[1] === '0'; }; SDPUtils.parseMLine = function(mediaSection) { var lines = SDPUtils.splitLines(mediaSection); var parts = lines[0].substr(2).split(' '); return { kind: parts[0], port: parseInt(parts[1], 10), protocol: parts[2], fmt: parts.slice(3).join(' ') }; }; SDPUtils.parseOLine = function(mediaSection) { var line = SDPUtils.matchPrefix(mediaSection, 'o=')[0]; var parts = line.substr(2).split(' '); return { username: parts[0], sessionId: parts[1], sessionVersion: parseInt(parts[2], 10), netType: parts[3], addressType: parts[4], address: parts[5] }; }; // a very naive interpretation of a valid SDP. SDPUtils.isValidSDP = function(blob) { if (typeof blob !== 'string' || blob.length === 0) { return false; } var lines = SDPUtils.splitLines(blob); for (var i = 0; i < lines.length; i++) { if (lines[i].length < 2 || lines[i].charAt(1) !== '=') { return false; } // TODO: check the modifier a bit more. } return true; }; // Expose public methods. if (typeof module === 'object') { module.exports = SDPUtils; } },{}],3:[function(require,module,exports){ (function (global){ /* * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ /* eslint-env node */ 'use strict'; var adapterFactory = require('./adapter_factory.js'); module.exports = adapterFactory({window: global.window}); }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) },{"./adapter_factory.js":4}],4:[function(require,module,exports){ /* * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ /* eslint-env node */ 'use strict'; var utils = require('./utils'); // Shimming starts here. module.exports = function(dependencies, opts) { var window = dependencies && dependencies.window; var options = { shimChrome: true, shimFirefox: true, shimEdge: true, shimSafari: true, }; for (var key in opts) { if (hasOwnProperty.call(opts, key)) { options[key] = opts[key]; } } // Utils. var logging = utils.log; var browserDetails = utils.detectBrowser(window); // Uncomment the line below if you want logging to occur, including logging // for the switch statement below. Can also be turned on in the browser via // adapter.disableLog(false), but then logging from the switch statement below // will not appear. // require('./utils').disableLog(false); // Browser shims. var chromeShim = require('./chrome/chrome_shim') || null; var edgeShim = require('./edge/edge_shim') || null; var firefoxShim = require('./firefox/firefox_shim') || null; var safariShim = require('./safari/safari_shim') || null; var commonShim = require('./common_shim') || null; // Export to the adapter global object visible in the browser. var adapter = { browserDetails: browserDetails, commonShim: commonShim, extractVersion: utils.extractVersion, disableLog: utils.disableLog, disableWarnings: utils.disableWarnings }; // Shim browser if found. switch (browserDetails.browser) { case 'chrome': if (!chromeShim || !chromeShim.shimPeerConnection || !options.shimChrome) { logging('Chrome shim is not included in this adapter release.'); return adapter; } logging('adapter.js shimming chrome.'); // Export to the adapter global object visible in the browser. adapter.browserShim = chromeShim; commonShim.shimCreateObjectURL(window); chromeShim.shimGetUserMedia(window); chromeShim.shimMediaStream(window); chromeShim.shimSourceObject(window); chromeShim.shimPeerConnection(window); chromeShim.shimOnTrack(window); chromeShim.shimAddTrackRemoveTrack(window); chromeShim.shimGetSendersWithDtmf(window); chromeShim.shimSenderReceiverGetStats(window); chromeShim.fixNegotiationNeeded(window); commonShim.shimRTCIceCandidate(window); commonShim.shimMaxMessageSize(window); commonShim.shimSendThrowTypeError(window); break; case 'firefox': if (!firefoxShim || !firefoxShim.shimPeerConnection || !options.shimFirefox) { logging('Firefox shim is not included in this adapter release.'); return adapter; } logging('adapter.js shimming firefox.'); // Export to the adapter global object visible in the browser. adapter.browserShim = firefoxShim; commonShim.shimCreateObjectURL(window); firefoxShim.shimGetUserMedia(window); firefoxShim.shimSourceObject(window); firefoxShim.shimPeerConnection(window); firefoxShim.shimOnTrack(window); firefoxShim.shimRemoveStream(window); firefoxShim.shimSenderGetStats(window); firefoxShim.shimReceiverGetStats(window); firefoxShim.shimRTCDataChannel(window); commonShim.shimRTCIceCandidate(window); commonShim.shimMaxMessageSize(window); commonShim.shimSendThrowTypeError(window); break; case 'edge': if (!edgeShim || !edgeShim.shimPeerConnection || !options.shimEdge) { logging('MS edge shim is not included in this adapter release.'); return adapter; } logging('adapter.js shimming edge.'); // Export to the adapter global object visible in the browser. adapter.browserShim = edgeShim; commonShim.shimCreateObjectURL(window); edgeShim.shimGetUserMedia(window); edgeShim.shimPeerConnection(window); edgeShim.shimReplaceTrack(window); edgeShim.shimGetDisplayMedia(window); // the edge shim implements the full RTCIceCandidate object. commonShim.shimMaxMessageSize(window); commonShim.shimSendThrowTypeError(window); break; case 'safari': if (!safariShim || !options.shimSafari) { logging('Safari shim is not included in this adapter release.'); return adapter; } logging('adapter.js shimming safari.'); // Export to the adapter global object visible in the browser. adapter.browserShim = safariShim; commonShim.shimCreateObjectURL(window); safariShim.shimRTCIceServerUrls(window); safariShim.shimCreateOfferLegacy(window); safariShim.shimCallbacksAPI(window); safariShim.shimLocalStreamsAPI(window); safariShim.shimRemoteStreamsAPI(window); safariShim.shimTrackEventTransceiver(window); safariShim.shimGetUserMedia(window); commonShim.shimRTCIceCandidate(window); commonShim.shimMaxMessageSize(window); commonShim.shimSendThrowTypeError(window); break; default: logging('Unsupported browser!'); break; } return adapter; }; },{"./chrome/chrome_shim":5,"./common_shim":7,"./edge/edge_shim":8,"./firefox/firefox_shim":11,"./safari/safari_shim":13,"./utils":14}],5:[function(require,module,exports){ /* * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ /* eslint-env node */ 'use strict'; var utils = require('../utils.js'); var logging = utils.log; /* iterates the stats graph recursively. */ function walkStats(stats, base, resultSet) { if (!base || resultSet.has(base.id)) { return; } resultSet.set(base.id, base); Object.keys(base).forEach(function(name) { if (name.endsWith('Id')) { walkStats(stats, stats.get(base[name]), resultSet); } else if (name.endsWith('Ids')) { base[name].forEach(function(id) { walkStats(stats, stats.get(id), resultSet); }); } }); } /* filter getStats for a sender/receiver track. */ function filterStats(result, track, outbound) { var streamStatsType = outbound ? 'outbound-rtp' : 'inbound-rtp'; var filteredResult = new Map(); if (track === null) { return filteredResult; } var trackStats = []; result.forEach(function(value) { if (value.type === 'track' && value.trackIdentifier === track.id) { trackStats.push(value); } }); trackStats.forEach(function(trackStat) { result.forEach(function(stats) { if (stats.type === streamStatsType && stats.trackId === trackStat.id) { walkStats(result, stats, filteredResult); } }); }); return filteredResult; } module.exports = { shimGetUserMedia: require('./getusermedia'), shimMediaStream: function(window) { window.MediaStream = window.MediaStream || window.webkitMediaStream; }, shimOnTrack: function(window) { if (typeof window === 'object' && window.RTCPeerConnection && !('ontrack' in window.RTCPeerConnection.prototype)) { Object.defineProperty(window.RTCPeerConnection.prototype, 'ontrack', { get: function() { return this._ontrack; }, set: function(f) { if (this._ontrack) { this.removeEventListener('track', this._ontrack); } this.addEventListener('track', this._ontrack = f); }, enumerable: true, configurable: true }); var origSetRemoteDescription = window.RTCPeerConnection.prototype.setRemoteDescription; window.RTCPeerConnection.prototype.setRemoteDescription = function() { var pc = this; if (!pc._ontrackpoly) { pc._ontrackpoly = function(e) { // onaddstream does not fire when a track is added to an existing // stream. But stream.onaddtrack is implemented so we use that. e.stream.addEventListener('addtrack', function(te) { var receiver; if (window.RTCPeerConnection.prototype.getReceivers) { receiver = pc.getReceivers().find(function(r) { return r.track && r.track.id === te.track.id; }); } else { receiver = {track: te.track}; } var event = new Event('track'); event.track = te.track; event.receiver = receiver; event.transceiver = {receiver: receiver}; event.streams = [e.stream]; pc.dispatchEvent(event); }); e.stream.getTracks().forEach(function(track) { var receiver; if (window.RTCPeerConnection.prototype.getReceivers) { receiver = pc.getReceivers().find(function(r) { return r.track && r.track.id === track.id; }); } else { receiver = {track: track}; } var event = new Event('track'); event.track = track; event.receiver = receiver; event.transceiver = {receiver: receiver}; event.streams = [e.stream]; pc.dispatchEvent(event); }); }; pc.addEventListener('addstream', pc._ontrackpoly); } return origSetRemoteDescription.apply(pc, arguments); }; } else { // even if RTCRtpTransceiver is in window, it is only used and // emitted in unified-plan. Unfortunately this means we need // to unconditionally wrap the event. utils.wrapPeerConnectionEvent(window, 'track', function(e) { if (!e.transceiver) { Object.defineProperty(e, 'transceiver', {value: {receiver: e.receiver}}); } return e; }); } }, shimGetSendersWithDtmf: function(window) { // Overrides addTrack/removeTrack, depends on shimAddTrackRemoveTrack. if (typeof window === 'object' && window.RTCPeerConnection && !('getSenders' in window.RTCPeerConnection.prototype) && 'createDTMFSender' in window.RTCPeerConnection.prototype) { var shimSenderWithDtmf = function(pc, track) { return { track: track, get dtmf() { if (this._dtmf === undefined) { if (track.kind === 'audio') { this._dtmf = pc.createDTMFSender(track); } else { this._dtmf = null; } } return this._dtmf; }, _pc: pc }; }; // augment addTrack when getSenders is not available. if (!window.RTCPeerConnection.prototype.getSenders) { window.RTCPeerConnection.prototype.getSenders = function() { this._senders = this._senders || []; return this._senders.slice(); // return a copy of the internal state. }; var origAddTrack = window.RTCPeerConnection.prototype.addTrack; window.RTCPeerConnection.prototype.addTrack = function(track, stream) { var pc = this; var sender = origAddTrack.apply(pc, arguments); if (!sender) { sender = shimSenderWithDtmf(pc, track); pc._senders.push(sender); } return sender; }; var origRemoveTrack = window.RTCPeerConnection.prototype.removeTrack; window.RTCPeerConnection.prototype.removeTrack = function(sender) { var pc = this; origRemoveTrack.apply(pc, arguments); var idx = pc._senders.indexOf(sender); if (idx !== -1) { pc._senders.splice(idx, 1); } }; } var origAddStream = window.RTCPeerConnection.prototype.addStream; window.RTCPeerConnection.prototype.addStream = function(stream) { var pc = this; pc._senders = pc._senders || []; origAddStream.apply(pc, [stream]); stream.getTracks().forEach(function(track) { pc._senders.push(shimSenderWithDtmf(pc, track)); }); }; var origRemoveStream = window.RTCPeerConnection.prototype.removeStream; window.RTCPeerConnection.prototype.removeStream = function(stream) { var pc = this; pc._senders = pc._senders || []; origRemoveStream.apply(pc, [stream]); stream.getTracks().forEach(function(track) { var sender = pc._senders.find(function(s) { return s.track === track; }); if (sender) { pc._senders.splice(pc._senders.indexOf(sender), 1); // remove sender } }); }; } else if (typeof window === 'object' && window.RTCPeerConnection && 'getSenders' in window.RTCPeerConnection.prototype && 'createDTMFSender' in window.RTCPeerConnection.prototype && window.RTCRtpSender && !('dtmf' in window.RTCRtpSender.prototype)) { var origGetSenders = window.RTCPeerConnection.prototype.getSenders; window.RTCPeerConnection.prototype.getSenders = function() { var pc = this; var senders = origGetSenders.apply(pc, []); senders.forEach(function(sender) { sender._pc = pc; }); return senders; }; Object.defineProperty(window.RTCRtpSender.prototype, 'dtmf', { get: function() { if (this._dtmf === undefined) { if (this.track.kind === 'audio') { this._dtmf = this._pc.createDTMFSender(this.track); } else { this._dtmf = null; } } return this._dtmf; } }); } }, shimSenderReceiverGetStats: function(window) { if (!(typeof window === 'object' && window.RTCPeerConnection && window.RTCRtpSender && window.RTCRtpReceiver)) { return; } // shim sender stats. if (!('getStats' in window.RTCRtpSender.prototype)) { var origGetSenders = window.RTCPeerConnection.prototype.getSenders; if (origGetSenders) { window.RTCPeerConnection.prototype.getSenders = function() { var pc = this; var senders = origGetSenders.apply(pc, []); senders.forEach(function(sender) { sender._pc = pc; }); return senders; }; } var origAddTrack = window.RTCPeerConnection.prototype.addTrack; if (origAddTrack) { window.RTCPeerConnection.prototype.addTrack = function() { var sender = origAddTrack.apply(this, arguments); sender._pc = this; return sender; }; } window.RTCRtpSender.prototype.getStats = function() { var sender = this; return this._pc.getStats().then(function(result) { /* Note: this will include stats of all senders that * send a track with the same id as sender.track as * it is not possible to identify the RTCRtpSender. */ return filterStats(result, sender.track, true); }); }; } // shim receiver stats. if (!('getStats' in window.RTCRtpReceiver.prototype)) { var origGetReceivers = window.RTCPeerConnection.prototype.getReceivers; if (origGetReceivers) { window.RTCPeerConnection.prototype.getReceivers = function() { var pc = this; var receivers = origGetReceivers.apply(pc, []); receivers.forEach(function(receiver) { receiver._pc = pc; }); return receivers; }; } utils.wrapPeerConnectionEvent(window, 'track', function(e) { e.receiver._pc = e.srcElement; return e; }); window.RTCRtpReceiver.prototype.getStats = function() { var receiver = this; return this._pc.getStats().then(function(result) { return filterStats(result, receiver.track, false); }); }; } if (!('getStats' in window.RTCRtpSender.prototype && 'getStats' in window.RTCRtpReceiver.prototype)) { return; } // shim RTCPeerConnection.getStats(track). var origGetStats = window.RTCPeerConnection.prototype.getStats; window.RTCPeerConnection.prototype.getStats = function() { var pc = this; if (arguments.length > 0 && arguments[0] instanceof window.MediaStreamTrack) { var track = arguments[0]; var sender; var receiver; var err; pc.getSenders().forEach(function(s) { if (s.track === track) { if (sender) { err = true; } else { sender = s; } } }); pc.getReceivers().forEach(function(r) { if (r.track === track) { if (receiver) { err = true; } else { receiver = r; } } return r.track === track; }); if (err || (sender && receiver)) { return Promise.reject(new DOMException( 'There are more than one sender or receiver for the track.', 'InvalidAccessError')); } else if (sender) { return sender.getStats(); } else if (receiver) { return receiver.getStats(); } return Promise.reject(new DOMException( 'There is no sender or receiver for the track.', 'InvalidAccessError')); } return origGetStats.apply(pc, arguments); }; }, shimSourceObject: function(window) { var URL = window && window.URL; if (typeof window === 'object') { if (window.HTMLMediaElement && !('srcObject' in window.HTMLMediaElement.prototype)) { // Shim the srcObject property, once, when HTMLMediaElement is found. Object.defineProperty(window.HTMLMediaElement.prototype, 'srcObject', { get: function() { return this._srcObject; }, set: function(stream) { var self = this; // Use _srcObject as a private property for this shim this._srcObject = stream; if (this.src) { URL.revokeObjectURL(this.src); } if (!stream) { this.src = ''; return undefined; } this.src = URL.createObjectURL(stream); // We need to recreate the blob url when a track is added or // removed. Doing it manually since we want to avoid a recursion. stream.addEventListener('addtrack', function() { if (self.src) { URL.revokeObjectURL(self.src); } self.src = URL.createObjectURL(stream); }); stream.addEventListener('removetrack', function() { if (self.src) { URL.revokeObjectURL(self.src); } self.src = URL.createObjectURL(stream); }); } }); } } }, shimAddTrackRemoveTrackWithNative: function(window) { // shim addTrack/removeTrack with native variants in order to make // the interactions with legacy getLocalStreams behave as in other browsers. // Keeps a mapping stream.id => [stream, rtpsenders...] window.RTCPeerConnection.prototype.getLocalStreams = function() { var pc = this; this._shimmedLocalStreams = this._shimmedLocalStreams || {}; return Object.keys(this._shimmedLocalStreams).map(function(streamId) { return pc._shimmedLocalStreams[streamId][0]; }); }; var origAddTrack = window.RTCPeerConnection.prototype.addTrack; window.RTCPeerConnection.prototype.addTrack = function(track, stream) { if (!stream) { return origAddTrack.apply(this, arguments); } this._shimmedLocalStreams = this._shimmedLocalStreams || {}; var sender = origAddTrack.apply(this, arguments); if (!this._shimmedLocalStreams[stream.id]) { this._shimmedLocalStreams[stream.id] = [stream, sender]; } else if (this._shimmedLocalStreams[stream.id].indexOf(sender) === -1) { this._shimmedLocalStreams[stream.id].push(sender); } return sender; }; var origAddStream = window.RTCPeerConnection.prototype.addStream; window.RTCPeerConnection.prototype.addStream = function(stream) { var pc = this; this._shimmedLocalStreams = this._shimmedLocalStreams || {}; stream.getTracks().forEach(function(track) { var alreadyExists = pc.getSenders().find(function(s) { return s.track === track; }); if (alreadyExists) { throw new DOMException('Track already exists.', 'InvalidAccessError'); } }); var existingSenders = pc.getSenders(); origAddStream.apply(this, arguments); var newSenders = pc.getSenders().filter(function(newSender) { return existingSenders.indexOf(newSender) === -1; }); this._shimmedLocalStreams[stream.id] = [stream].concat(newSenders); }; var origRemoveStream = window.RTCPeerConnection.prototype.removeStream; window.RTCPeerConnection.prototype.removeStream = function(stream) { this._shimmedLocalStreams = this._shimmedLocalStreams || {}; delete this._shimmedLocalStreams[stream.id]; return origRemoveStream.apply(this, arguments); }; var origRemoveTrack = window.RTCPeerConnection.prototype.removeTrack; window.RTCPeerConnection.prototype.removeTrack = function(sender) { var pc = this; this._shimmedLocalStreams = this._shimmedLocalStreams || {}; if (sender) { Object.keys(this._shimmedLocalStreams).forEach(function(streamId) { var idx = pc._shimmedLocalStreams[streamId].indexOf(sender); if (idx !== -1) { pc._shimmedLocalStreams[streamId].splice(idx, 1); } if (pc._shimmedLocalStreams[streamId].length === 1) { delete pc._shimmedLocalStreams[streamId]; } }); } return origRemoveTrack.apply(this, arguments); }; }, shimAddTrackRemoveTrack: function(window) { var browserDetails = utils.detectBrowser(window); // shim addTrack and removeTrack. if (window.RTCPeerConnection.prototype.addTrack && browserDetails.version >= 65) { return this.shimAddTrackRemoveTrackWithNative(window); } // also shim pc.getLocalStreams when addTrack is shimmed // to return the original streams. var origGetLocalStreams = window.RTCPeerConnection.prototype .getLocalStreams; window.RTCPeerConnection.prototype.getLocalStreams = function() { var pc = this; var nativeStreams = origGetLocalStreams.apply(this); pc._reverseStreams = pc._reverseStreams || {}; return nativeStreams.map(function(stream) { return pc._reverseStreams[stream.id]; }); }; var origAddStream = window.RTCPeerConnection.prototype.addStream; window.RTCPeerConnection.prototype.addStream = function(stream) { var pc = this; pc._streams = pc._streams || {}; pc._reverseStreams = pc._reverseStreams || {}; stream.getTracks().forEach(function(track) { var alreadyExists = pc.getSenders().find(function(s) { return s.track === track; }); if (alreadyExists) { throw new DOMException('Track already exists.', 'InvalidAccessError'); } }); // Add identity mapping for consistency with addTrack. // Unless this is being used with a stream from addTrack. if (!pc._reverseStreams[stream.id]) { var newStream = new window.MediaStream(stream.getTracks()); pc._streams[stream.id] = newStream; pc._reverseStreams[newStream.id] = stream; stream = newStream; } origAddStream.apply(pc, [stream]); }; var origRemoveStream = window.RTCPeerConnection.prototype.removeStream; window.RTCPeerConnection.prototype.removeStream = function(stream) { var pc = this; pc._streams = pc._streams || {}; pc._reverseStreams = pc._reverseStreams || {}; origRemoveStream.apply(pc, [(pc._streams[stream.id] || stream)]); delete pc._reverseStreams[(pc._streams[stream.id] ? pc._streams[stream.id].id : stream.id)]; delete pc._streams[stream.id]; }; window.RTCPeerConnection.prototype.addTrack = function(track, stream) { var pc = this; if (pc.signalingState === 'closed') { throw new DOMException( 'The RTCPeerConnection\'s signalingState is \'closed\'.', 'InvalidStateError'); } var streams = [].slice.call(arguments, 1); if (streams.length !== 1 || !streams[0].getTracks().find(function(t) { return t === track; })) { // this is not fully correct but all we can manage without // [[associated MediaStreams]] internal slot. throw new DOMException( 'The adapter.js addTrack polyfill only supports a single ' + ' stream which is associated with the specified track.', 'NotSupportedError'); } var alreadyExists = pc.getSenders().find(function(s) { return s.track === track; }); if (alreadyExists) { throw new DOMException('Track already exists.', 'InvalidAccessError'); } pc._streams = pc._streams || {}; pc._reverseStreams = pc._reverseStreams || {}; var oldStream = pc._streams[stream.id]; if (oldStream) { // this is using odd Chrome behaviour, use with caution: // https://bugs.chromium.org/p/webrtc/issues/detail?id=7815 // Note: we rely on the high-level addTrack/dtmf shim to // create the sender with a dtmf sender. oldStream.addTrack(track); // Trigger ONN async. Promise.resolve().then(function() { pc.dispatchEvent(new Event('negotiationneeded')); }); } else { var newStream = new window.MediaStream([track]); pc._streams[stream.id] = newStream; pc._reverseStreams[newStream.id] = stream; pc.addStream(newStream); } return pc.getSenders().find(function(s) { return s.track === track; }); }; // replace the internal stream id with the external one and // vice versa. function replaceInternalStreamId(pc, description) { var sdp = description.sdp; Object.keys(pc._reverseStreams || []).forEach(function(internalId) { var externalStream = pc._reverseStreams[internalId]; var internalStream = pc._streams[externalStream.id]; sdp = sdp.replace(new RegExp(internalStream.id, 'g'), externalStream.id); }); return new RTCSessionDescription({ type: description.type, sdp: sdp }); } function replaceExternalStreamId(pc, description) { var sdp = description.sdp; Object.keys(pc._reverseStreams || []).forEach(function(internalId) { var externalStream = pc._reverseStreams[internalId]; var internalStream = pc._streams[externalStream.id]; sdp = sdp.replace(new RegExp(externalStream.id, 'g'), internalStream.id); }); return new RTCSessionDescription({ type: description.type, sdp: sdp }); } ['createOffer', 'createAnswer'].forEach(function(method) { var nativeMethod = window.RTCPeerConnection.prototype[method]; window.RTCPeerConnection.prototype[method] = function() { var pc = this; var args = arguments; var isLegacyCall = arguments.length && typeof arguments[0] === 'function'; if (isLegacyCall) { return nativeMethod.apply(pc, [ function(description) { var desc = replaceInternalStreamId(pc, description); args[0].apply(null, [desc]); }, function(err) { if (args[1]) { args[1].apply(null, err); } }, arguments[2] ]); } return nativeMethod.apply(pc, arguments) .then(function(description) { return replaceInternalStreamId(pc, description); }); }; }); var origSetLocalDescription = window.RTCPeerConnection.prototype.setLocalDescription; window.RTCPeerConnection.prototype.setLocalDescription = function() { var pc = this; if (!arguments.length || !arguments[0].type) { return origSetLocalDescription.apply(pc, arguments); } arguments[0] = replaceExternalStreamId(pc, arguments[0]); return origSetLocalDescription.apply(pc, arguments); }; // TODO: mangle getStats: https://w3c.github.io/webrtc-stats/#dom-rtcmediastreamstats-streamidentifier var origLocalDescription = Object.getOwnPropertyDescriptor( window.RTCPeerConnection.prototype, 'localDescription'); Object.defineProperty(window.RTCPeerConnection.prototype, 'localDescription', { get: function() { var pc = this; var description = origLocalDescription.get.apply(this); if (description.type === '') { return description; } return replaceInternalStreamId(pc, description); } }); window.RTCPeerConnection.prototype.removeTrack = function(sender) { var pc = this; if (pc.signalingState === 'closed') { throw new DOMException( 'The RTCPeerConnection\'s signalingState is \'closed\'.', 'InvalidStateError'); } // We can not yet check for sender instanceof RTCRtpSender // since we shim RTPSender. So we check if sender._pc is set. if (!sender._pc) { throw new DOMException('Argument 1 of RTCPeerConnection.removeTrack ' + 'does not implement interface RTCRtpSender.', 'TypeError'); } var isLocal = sender._pc === pc; if (!isLocal) { throw new DOMException('Sender was not created by this connection.', 'InvalidAccessError'); } // Search for the native stream the senders track belongs to. pc._streams = pc._streams || {}; var stream; Object.keys(pc._streams).forEach(function(streamid) { var hasTrack = pc._streams[streamid].getTracks().find(function(track) { return sender.track === track; }); if (hasTrack) { stream = pc._streams[streamid]; } }); if (stream) { if (stream.getTracks().length === 1) { // if this is the last track of the stream, remove the stream. This // takes care of any shimmed _senders. pc.removeStream(pc._reverseStreams[stream.id]); } else { // relying on the same odd chrome behaviour as above. stream.removeTrack(sender.track); } pc.dispatchEvent(new Event('negotiationneeded')); } }; }, shimPeerConnection: function(window) { var browserDetails = utils.detectBrowser(window); // The RTCPeerConnection object. if (!window.RTCPeerConnection && window.webkitRTCPeerConnection) { window.RTCPeerConnection = function(pcConfig, pcConstraints) { // Translate iceTransportPolicy to iceTransports, // see https://code.google.com/p/webrtc/issues/detail?id=4869 // this was fixed in M56 along with unprefixing RTCPeerConnection. logging('PeerConnection'); if (pcConfig && pcConfig.iceTransportPolicy) { pcConfig.iceTransports = pcConfig.iceTransportPolicy; } return new window.webkitRTCPeerConnection(pcConfig, pcConstraints); }; window.RTCPeerConnection.prototype = window.webkitRTCPeerConnection.prototype; // wrap static methods. Currently just generateCertificate. if (window.webkitRTCPeerConnection.generateCertificate) { Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', { get: function() { return window.webkitRTCPeerConnection.generateCertificate; } }); } } var origGetStats = window.RTCPeerConnection.prototype.getStats; window.RTCPeerConnection.prototype.getStats = function(selector, successCallback, errorCallback) { var pc = this; var args = arguments; // If selector is a function then we are in the old style stats so just // pass back the original getStats format to avoid breaking old users. if (arguments.length > 0 && typeof selector === 'function') { return origGetStats.apply(this, arguments); } // When spec-style getStats is supported, return those when called with // either no arguments or the selector argument is null. if (origGetStats.length === 0 && (arguments.length === 0 || typeof arguments[0] !== 'function')) { return origGetStats.apply(this, []); } var fixChromeStats_ = function(response) { var standardReport = {}; var reports = response.result(); reports.forEach(function(report) { var standardStats = { id: report.id, timestamp: report.timestamp, type: { localcandidate: 'local-candidate', remotecandidate: 'remote-candidate' }[report.type] || report.type }; report.names().forEach(function(name) { standardStats[name] = report.stat(name); }); standardReport[standardStats.id] = standardStats; }); return standardReport; }; // shim getStats with maplike support var makeMapStats = function(stats) { return new Map(Object.keys(stats).map(function(key) { return [key, stats[key]]; })); }; if (arguments.length >= 2) { var successCallbackWrapper_ = function(response) { args[1](makeMapStats(fixChromeStats_(response))); }; return origGetStats.apply(this, [successCallbackWrapper_, arguments[0]]); } // promise-support return new Promise(function(resolve, reject) { origGetStats.apply(pc, [ function(response) { resolve(makeMapStats(fixChromeStats_(response))); }, reject]); }).then(successCallback, errorCallback); }; // add promise support -- natively available in Chrome 51 if (browserDetails.version < 51) { ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate'] .forEach(function(method) { var nativeMethod = window.RTCPeerConnection.prototype[method]; window.RTCPeerConnection.prototype[method] = function() { var args = arguments; var pc = this; var promise = new Promise(function(resolve, reject) { nativeMethod.apply(pc, [args[0], resolve, reject]); }); if (args.length < 2) { return promise; } return promise.then(function() { args[1].apply(null, []); }, function(err) { if (args.length >= 3) { args[2].apply(null, [err]); } }); }; }); } // promise support for createOffer and createAnswer. Available (without // bugs) since M52: crbug/619289 if (browserDetails.version < 52) { ['createOffer', 'createAnswer'].forEach(function(method) { var nativeMethod = window.RTCPeerConnection.prototype[method]; window.RTCPeerConnection.prototype[method] = function() { var pc = this; if (arguments.length < 1 || (arguments.length === 1 && typeof arguments[0] === 'object')) { var opts = arguments.length === 1 ? arguments[0] : undefined; return new Promise(function(resolve, reject) { nativeMethod.apply(pc, [resolve, reject, opts]); }); } return nativeMethod.apply(this, arguments); }; }); } // shim implicit creation of RTCSessionDescription/RTCIceCandidate ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate'] .forEach(function(method) { var nativeMethod = window.RTCPeerConnection.prototype[method]; window.RTCPeerConnection.prototype[method] = function() { arguments[0] = new ((method === 'addIceCandidate') ? window.RTCIceCandidate : window.RTCSessionDescription)(arguments[0]); return nativeMethod.apply(this, arguments); }; }); // support for addIceCandidate(null or undefined) var nativeAddIceCandidate = window.RTCPeerConnection.prototype.addIceCandidate; window.RTCPeerConnection.prototype.addIceCandidate = function() { if (!arguments[0]) { if (arguments[1]) { arguments[1].apply(null); } return Promise.resolve(); } return nativeAddIceCandidate.apply(this, arguments); }; }, fixNegotiationNeeded: function(window) { utils.wrapPeerConnectionEvent(window, 'negotiationneeded', function(e) { var pc = e.target; if (pc.signalingState !== 'stable') { return; } return e; }); }, shimGetDisplayMedia: function(window, getSourceId) { if (!window.navigator || !window.navigator.mediaDevices || 'getDisplayMedia' in window.navigator.mediaDevices) { return; } // getSourceId is a function that returns a promise resolving with // the sourceId of the screen/window/tab to be shared. if (typeof getSourceId !== 'function') { console.error('shimGetDisplayMedia: getSourceId argument is not ' + 'a function'); return; } window.navigator.mediaDevices.getDisplayMedia = function(constraints) { return getSourceId(constraints) .then(function(sourceId) { var widthSpecified = constraints.video && constraints.video.width; var heightSpecified = constraints.video && constraints.video.height; var frameRateSpecified = constraints.video && constraints.video.frameRate; constraints.video = { mandatory: { chromeMediaSource: 'desktop', chromeMediaSourceId: sourceId, maxFrameRate: frameRateSpecified || 3 } }; if (widthSpecified) { constraints.video.mandatory.maxWidth = widthSpecified; } if (heightSpecified) { constraints.video.mandatory.maxHeight = heightSpecified; } return window.navigator.mediaDevices.getUserMedia(constraints); }); }; window.navigator.getDisplayMedia = function(constraints) { utils.deprecated('navigator.getDisplayMedia', 'navigator.mediaDevices.getDisplayMedia'); return window.navigator.mediaDevices.getDisplayMedia(constraints); }; } }; },{"../utils.js":14,"./getusermedia":6}],6:[function(require,module,exports){ /* * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ /* eslint-env node */ 'use strict'; var utils = require('../utils.js'); var logging = utils.log; // Expose public methods. module.exports = function(window) { var browserDetails = utils.detectBrowser(window); var navigator = window && window.navigator; var constraintsToChrome_ = function(c) { if (typeof c !== 'object' || c.mandatory || c.optional) { return c; } var cc = {}; Object.keys(c).forEach(function(key) { if (key === 'require' || key === 'advanced' || key === 'mediaSource') { return; } var r = (typeof c[key] === 'object') ? c[key] : {ideal: c[key]}; if (r.exact !== undefined && typeof r.exact === 'number') { r.min = r.max = r.exact; } var oldname_ = function(prefix, name) { if (prefix) { return prefix + name.charAt(0).toUpperCase() + name.slice(1); } return (name === 'deviceId') ? 'sourceId' : name; }; if (r.ideal !== undefined) { cc.optional = cc.optional || []; var oc = {}; if (typeof r.ideal === 'number') { oc[oldname_('min', key)] = r.ideal; cc.optional.push(oc); oc = {}; oc[oldname_('max', key)] = r.ideal; cc.optional.push(oc); } else { oc[oldname_('', key)] = r.ideal; cc.optional.push(oc); } } if (r.exact !== undefined && typeof r.exact !== 'number') { cc.mandatory = cc.mandatory || {}; cc.mandatory[oldname_('', key)] = r.exact; } else { ['min', 'max'].forEach(function(mix) { if (r[mix] !== undefined) { cc.mandatory = cc.mandatory || {}; cc.mandatory[oldname_(mix, key)] = r[mix]; } }); } }); if (c.advanced) { cc.optional = (cc.optional || []).concat(c.advanced); } return cc; }; var shimConstraints_ = function(constraints, func) { if (browserDetails.version >= 61) { return func(constraints); } constraints = JSON.parse(JSON.stringify(constraints)); if (constraints && typeof constraints.audio === 'object') { var remap = function(obj, a, b) { if (a in obj && !(b in obj)) { obj[b] = obj[a]; delete obj[a]; } }; constraints = JSON.parse(JSON.stringify(constraints)); remap(constraints.audio, 'autoGainControl', 'googAutoGainControl'); remap(constraints.audio, 'noiseSuppression', 'googNoiseSuppression'); constraints.audio = constraintsToChrome_(constraints.audio); } if (constraints && typeof constraints.video === 'object') { // Shim facingMode for mobile & surface pro. var face = constraints.video.facingMode; face = face && ((typeof face === 'object') ? face : {ideal: face}); var getSupportedFacingModeLies = browserDetails.version < 66; if ((face && (face.exact === 'user' || face.exact === 'environment' || face.ideal === 'user' || face.ideal === 'environment')) && !(navigator.mediaDevices.getSupportedConstraints && navigator.mediaDevices.getSupportedConstraints().facingMode && !getSupportedFacingModeLies)) { delete constraints.video.facingMode; var matches; if (face.exact === 'environment' || face.ideal === 'environment') { matches = ['back', 'rear']; } else if (face.exact === 'user' || face.ideal === 'user') { matches = ['front']; } if (matches) { // Look for matches in label, or use last cam for back (typical). return navigator.mediaDevices.enumerateDevices() .then(function(devices) { devices = devices.filter(function(d) { return d.kind === 'videoinput'; }); var dev = devices.find(function(d) { return matches.some(function(match) { return d.label.toLowerCase().indexOf(match) !== -1; }); }); if (!dev && devices.length && matches.indexOf('back') !== -1) { dev = devices[devices.length - 1]; // more likely the back cam } if (dev) { constraints.video.deviceId = face.exact ? {exact: dev.deviceId} : {ideal: dev.deviceId}; } constraints.video = constraintsToChrome_(constraints.video); logging('chrome: ' + JSON.stringify(constraints)); return func(constraints); }); } } constraints.video = constraintsToChrome_(constraints.video); } logging('chrome: ' + JSON.stringify(constraints)); return func(constraints); }; var shimError_ = function(e) { if (browserDetails.version >= 64) { return e; } return { name: { PermissionDeniedError: 'NotAllowedError', PermissionDismissedError: 'NotAllowedError', InvalidStateError: 'NotAllowedError', DevicesNotFoundError: 'NotFoundError', ConstraintNotSatisfiedError: 'OverconstrainedError', TrackStartError: 'NotReadableError', MediaDeviceFailedDueToShutdown: 'NotAllowedError', MediaDeviceKillSwitchOn: 'NotAllowedError', TabCaptureError: 'AbortError', ScreenCaptureError: 'AbortError', DeviceCaptureError: 'AbortError' }[e.name] || e.name, message: e.message, constraint: e.constraint || e.constraintName, toString: function() { return this.name + (this.message && ': ') + this.message; } }; }; var getUserMedia_ = function(constraints, onSuccess, onError) { shimConstraints_(constraints, function(c) { navigator.webkitGetUserMedia(c, onSuccess, function(e) { if (onError) { onError(shimError_(e)); } }); }); }; navigator.getUserMedia = getUserMedia_; // Returns the result of getUserMedia as a Promise. var getUserMediaPromise_ = function(constraints) { return new Promise(function(resolve, reject) { navigator.getUserMedia(constraints, resolve, reject); }); }; if (!navigator.mediaDevices) { navigator.mediaDevices = { getUserMedia: getUserMediaPromise_, enumerateDevices: function() { return new Promise(function(resolve) { var kinds = {audio: 'audioinput', video: 'videoinput'}; return window.MediaStreamTrack.getSources(function(devices) { resolve(devices.map(function(device) { return {label: device.label, kind: kinds[device.kind], deviceId: device.id, groupId: ''}; })); }); }); }, getSupportedConstraints: function() { return { deviceId: true, echoCancellation: true, facingMode: true, frameRate: true, height: true, width: true }; } }; } // A shim for getUserMedia method on the mediaDevices object. // TODO(KaptenJansson) remove once implemented in Chrome stable. if (!navigator.mediaDevices.getUserMedia) { navigator.mediaDevices.getUserMedia = function(constraints) { return getUserMediaPromise_(constraints); }; } else { // Even though Chrome 45 has navigator.mediaDevices and a getUserMedia // function which returns a Promise, it does not accept spec-style // constraints. var origGetUserMedia = navigator.mediaDevices.getUserMedia. bind(navigator.mediaDevices); navigator.mediaDevices.getUserMedia = function(cs) { return shimConstraints_(cs, function(c) { return origGetUserMedia(c).then(function(stream) { if (c.audio && !stream.getAudioTracks().length || c.video && !stream.getVideoTracks().length) { stream.getTracks().forEach(function(track) { track.stop(); }); throw new DOMException('', 'NotFoundError'); } return stream; }, function(e) { return Promise.reject(shimError_(e)); }); }); }; } // Dummy devicechange event methods. // TODO(KaptenJansson) remove once implemented in Chrome stable. if (typeof navigator.mediaDevices.addEventListener === 'undefined') { navigator.mediaDevices.addEventListener = function() { logging('Dummy mediaDevices.addEventListener called.'); }; } if (typeof navigator.mediaDevices.removeEventListener === 'undefined') { navigator.mediaDevices.removeEventListener = function() { logging('Dummy mediaDevices.removeEventListener called.'); }; } }; },{"../utils.js":14}],7:[function(require,module,exports){ /* * Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ /* eslint-env node */ 'use strict'; var SDPUtils = require('sdp'); var utils = require('./utils'); module.exports = { shimRTCIceCandidate: function(window) { // foundation is arbitrarily chosen as an indicator for full support for // https://w3c.github.io/webrtc-pc/#rtcicecandidate-interface if (!window.RTCIceCandidate || (window.RTCIceCandidate && 'foundation' in window.RTCIceCandidate.prototype)) { return; } var NativeRTCIceCandidate = window.RTCIceCandidate; window.RTCIceCandidate = function(args) { // Remove the a= which shouldn't be part of the candidate string. if (typeof args === 'object' && args.candidate && args.candidate.indexOf('a=') === 0) { args = JSON.parse(JSON.stringify(args)); args.candidate = args.candidate.substr(2); } if (args.candidate && args.candidate.length) { // Augment the native candidate with the parsed fields. var nativeCandidate = new NativeRTCIceCandidate(args); var parsedCandidate = SDPUtils.parseCandidate(args.candidate); var augmentedCandidate = Object.assign(nativeCandidate, parsedCandidate); // Add a serializer that does not serialize the extra attributes. augmentedCandidate.toJSON = function() { return { candidate: augmentedCandidate.candidate, sdpMid: augmentedCandidate.sdpMid, sdpMLineIndex: augmentedCandidate.sdpMLineIndex, usernameFragment: augmentedCandidate.usernameFragment, }; }; return augmentedCandidate; } return new NativeRTCIceCandidate(args); }; window.RTCIceCandidate.prototype = NativeRTCIceCandidate.prototype; // Hook up the augmented candidate in onicecandidate and // addEventListener('icecandidate', ...) utils.wrapPeerConnectionEvent(window, 'icecandidate', function(e) { if (e.candidate) { Object.defineProperty(e, 'candidate', { value: new window.RTCIceCandidate(e.candidate), writable: 'false' }); } return e; }); }, // shimCreateObjectURL must be called before shimSourceObject to avoid loop. shimCreateObjectURL: function(window) { var URL = window && window.URL; if (!(typeof window === 'object' && window.HTMLMediaElement && 'srcObject' in window.HTMLMediaElement.prototype && URL.createObjectURL && URL.revokeObjectURL)) { // Only shim CreateObjectURL using srcObject if srcObject exists. return undefined; } var nativeCreateObjectURL = URL.createObjectURL.bind(URL); var nativeRevokeObjectURL = URL.revokeObjectURL.bind(URL); var streams = new Map(), newId = 0; URL.createObjectURL = function(stream) { if ('getTracks' in stream) { var url = 'polyblob:' + (++newId); streams.set(url, stream); utils.deprecated('URL.createObjectURL(stream)', 'elem.srcObject = stream'); return url; } return nativeCreateObjectURL(stream); }; URL.revokeObjectURL = function(url) { nativeRevokeObjectURL(url); streams.delete(url); }; var dsc = Object.getOwnPropertyDescriptor(window.HTMLMediaElement.prototype, 'src'); Object.defineProperty(window.HTMLMediaElement.prototype, 'src', { get: function() { return dsc.get.apply(this); }, set: function(url) { this.srcObject = streams.get(url) || null; return dsc.set.apply(this, [url]); } }); var nativeSetAttribute = window.HTMLMediaElement.prototype.setAttribute; window.HTMLMediaElement.prototype.setAttribute = function() { if (arguments.length === 2 && ('' + arguments[0]).toLowerCase() === 'src') { this.srcObject = streams.get(arguments[1]) || null; } return nativeSetAttribute.apply(this, arguments); }; }, shimMaxMessageSize: function(window) { if (window.RTCSctpTransport || !window.RTCPeerConnection) { return; } var browserDetails = utils.detectBrowser(window); if (!('sctp' in window.RTCPeerConnection.prototype)) { Object.defineProperty(window.RTCPeerConnection.prototype, 'sctp', { get: function() { return typeof this._sctp === 'undefined' ? null : this._sctp; } }); } var sctpInDescription = function(description) { var sections = SDPUtils.splitSections(description.sdp); sections.shift(); return sections.some(function(mediaSection) { var mLine = SDPUtils.parseMLine(mediaSection); return mLine && mLine.kind === 'application' && mLine.protocol.indexOf('SCTP') !== -1; }); }; var getRemoteFirefoxVersion = function(description) { // TODO: Is there a better solution for detecting Firefox? var match = description.sdp.match(/mozilla...THIS_IS_SDPARTA-(\d+)/); if (match === null || match.length < 2) { return -1; } var version = parseInt(match[1], 10); // Test for NaN (yes, this is ugly) return version !== version ? -1 : version; }; var getCanSendMaxMessageSize = function(remoteIsFirefox) { // Every implementation we know can send at least 64 KiB. // Note: Although Chrome is technically able to send up to 256 KiB, the // data does not reach the other peer reliably. // See: https://bugs.chromium.org/p/webrtc/issues/detail?id=8419 var canSendMaxMessageSize = 65536; if (browserDetails.browser === 'firefox') { if (browserDetails.version < 57) { if (remoteIsFirefox === -1) { // FF < 57 will send in 16 KiB chunks using the deprecated PPID // fragmentation. canSendMaxMessageSize = 16384; } else { // However, other FF (and RAWRTC) can reassemble PPID-fragmented // messages. Thus, supporting ~2 GiB when sending. canSendMaxMessageSize = 2147483637; } } else if (browserDetails.version < 60) { // Currently, all FF >= 57 will reset the remote maximum message size // to the default value when a data channel is created at a later // stage. :( // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1426831 canSendMaxMessageSize = browserDetails.version === 57 ? 65535 : 65536; } else { // FF >= 60 supports sending ~2 GiB canSendMaxMessageSize = 2147483637; } } return canSendMaxMessageSize; }; var getMaxMessageSize = function(description, remoteIsFirefox) { // Note: 65536 bytes is the default value from the SDP spec. Also, // every implementation we know supports receiving 65536 bytes. var maxMessageSize = 65536; // FF 57 has a slightly incorrect default remote max message size, so // we need to adjust it here to avoid a failure when sending. // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1425697 if (browserDetails.browser === 'firefox' && browserDetails.version === 57) { maxMessageSize = 65535; } var match = SDPUtils.matchPrefix(description.sdp, 'a=max-message-size:'); if (match.length > 0) { maxMessageSize = parseInt(match[0].substr(19), 10); } else if (browserDetails.browser === 'firefox' && remoteIsFirefox !== -1) { // If the maximum message size is not present in the remote SDP and // both local and remote are Firefox, the remote peer can receive // ~2 GiB. maxMessageSize = 2147483637; } return maxMessageSize; }; var origSetRemoteDescription = window.RTCPeerConnection.prototype.setRemoteDescription; window.RTCPeerConnection.prototype.setRemoteDescription = function() { var pc = this; pc._sctp = null; if (sctpInDescription(arguments[0])) { // Check if the remote is FF. var isFirefox = getRemoteFirefoxVersion(arguments[0]); // Get the maximum message size the local peer is capable of sending var canSendMMS = getCanSendMaxMessageSize(isFirefox); // Get the maximum message size of the remote peer. var remoteMMS = getMaxMessageSize(arguments[0], isFirefox); // Determine final maximum message size var maxMessageSize; if (canSendMMS === 0 && remoteMMS === 0) { maxMessageSize = Number.POSITIVE_INFINITY; } else if (canSendMMS === 0 || remoteMMS === 0) { maxMessageSize = Math.max(canSendMMS, remoteMMS); } else { maxMessageSize = Math.min(canSendMMS, remoteMMS); } // Create a dummy RTCSctpTransport object and the 'maxMessageSize' // attribute. var sctp = {}; Object.defineProperty(sctp, 'maxMessageSize', { get: function() { return maxMessageSize; } }); pc._sctp = sctp; } return origSetRemoteDescription.apply(pc, arguments); }; }, shimSendThrowTypeError: function(window) { if (!(window.RTCPeerConnection && 'createDataChannel' in window.RTCPeerConnection.prototype)) { return; } // Note: Although Firefox >= 57 has a native implementation, the maximum // message size can be reset for all data channels at a later stage. // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1426831 function wrapDcSend(dc, pc) { var origDataChannelSend = dc.send; dc.send = function() { var data = arguments[0]; var length = data.length || data.size || data.byteLength; if (dc.readyState === 'open' && pc.sctp && length > pc.sctp.maxMessageSize) { throw new TypeError('Message too large (can send a maximum of ' + pc.sctp.maxMessageSize + ' bytes)'); } return origDataChannelSend.apply(dc, arguments); }; } var origCreateDataChannel = window.RTCPeerConnection.prototype.createDataChannel; window.RTCPeerConnection.prototype.createDataChannel = function() { var pc = this; var dataChannel = origCreateDataChannel.apply(pc, arguments); wrapDcSend(dataChannel, pc); return dataChannel; }; utils.wrapPeerConnectionEvent(window, 'datachannel', function(e) { wrapDcSend(e.channel, e.target); return e; }); } }; },{"./utils":14,"sdp":2}],8:[function(require,module,exports){ /* * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ /* eslint-env node */ 'use strict'; var utils = require('../utils'); var filterIceServers = require('./filtericeservers'); var shimRTCPeerConnection = require('rtcpeerconnection-shim'); module.exports = { shimGetUserMedia: require('./getusermedia'), shimPeerConnection: function(window) { var browserDetails = utils.detectBrowser(window); if (window.RTCIceGatherer) { if (!window.RTCIceCandidate) { window.RTCIceCandidate = function(args) { return args; }; } if (!window.RTCSessionDescription) { window.RTCSessionDescription = function(args) { return args; }; } // this adds an additional event listener to MediaStrackTrack that signals // when a tracks enabled property was changed. Workaround for a bug in // addStream, see below. No longer required in 15025+ if (browserDetails.version < 15025) { var origMSTEnabled = Object.getOwnPropertyDescriptor( window.MediaStreamTrack.prototype, 'enabled'); Object.defineProperty(window.MediaStreamTrack.prototype, 'enabled', { set: function(value) { origMSTEnabled.set.call(this, value); var ev = new Event('enabled'); ev.enabled = value; this.dispatchEvent(ev); } }); } } // ORTC defines the DTMF sender a bit different. // https://github.com/w3c/ortc/issues/714 if (window.RTCRtpSender && !('dtmf' in window.RTCRtpSender.prototype)) { Object.defineProperty(window.RTCRtpSender.prototype, 'dtmf', { get: function() { if (this._dtmf === undefined) { if (this.track.kind === 'audio') { this._dtmf = new window.RTCDtmfSender(this); } else if (this.track.kind === 'video') { this._dtmf = null; } } return this._dtmf; } }); } // Edge currently only implements the RTCDtmfSender, not the // RTCDTMFSender alias. See http://draft.ortc.org/#rtcdtmfsender2* if (window.RTCDtmfSender && !window.RTCDTMFSender) { window.RTCDTMFSender = window.RTCDtmfSender; } var RTCPeerConnectionShim = shimRTCPeerConnection(window, browserDetails.version); window.RTCPeerConnection = function(config) { if (config && config.iceServers) { config.iceServers = filterIceServers(config.iceServers); } return new RTCPeerConnectionShim(config); }; window.RTCPeerConnection.prototype = RTCPeerConnectionShim.prototype; }, shimReplaceTrack: function(window) { // ORTC has replaceTrack -- https://github.com/w3c/ortc/issues/614 if (window.RTCRtpSender && !('replaceTrack' in window.RTCRtpSender.prototype)) { window.RTCRtpSender.prototype.replaceTrack = window.RTCRtpSender.prototype.setTrack; } }, shimGetDisplayMedia: function(window, preferredMediaSource) { if (!('getDisplayMedia' in window.navigator) || !window.navigator.mediaDevices || 'getDisplayMedia' in window.navigator.mediaDevices) { return; } var origGetDisplayMedia = window.navigator.getDisplayMedia; window.navigator.mediaDevices.getDisplayMedia = function(constraints) { return origGetDisplayMedia(constraints); }; window.navigator.getDisplayMedia = function(constraints) { utils.deprecated('navigator.getDisplayMedia', 'navigator.mediaDevices.getDisplayMedia'); return origGetDisplayMedia(constraints); }; } }; },{"../utils":14,"./filtericeservers":9,"./getusermedia":10,"rtcpeerconnection-shim":1}],9:[function(require,module,exports){ /* * Copyright (c) 2018 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ /* eslint-env node */ 'use strict'; var utils = require('../utils'); // Edge does not like // 1) stun: filtered after 14393 unless ?transport=udp is present // 2) turn: that does not have all of turn:host:port?transport=udp // 3) turn: with ipv6 addresses // 4) turn: occurring muliple times module.exports = function(iceServers, edgeVersion) { var hasTurn = false; iceServers = JSON.parse(JSON.stringify(iceServers)); return iceServers.filter(function(server) { if (server && (server.urls || server.url)) { var urls = server.urls || server.url; if (server.url && !server.urls) { utils.deprecated('RTCIceServer.url', 'RTCIceServer.urls'); } var isString = typeof urls === 'string'; if (isString) { urls = [urls]; } urls = urls.filter(function(url) { var validTurn = url.indexOf('turn:') === 0 && url.indexOf('transport=udp') !== -1 && url.indexOf('turn:[') === -1 && !hasTurn; if (validTurn) { hasTurn = true; return true; } return url.indexOf('stun:') === 0 && edgeVersion >= 14393 && url.indexOf('?transport=udp') === -1; }); delete server.url; server.urls = isString ? urls[0] : urls; return !!urls.length; } }); }; },{"../utils":14}],10:[function(require,module,exports){ /* * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ /* eslint-env node */ 'use strict'; // Expose public methods. module.exports = function(window) { var navigator = window && window.navigator; var shimError_ = function(e) { return { name: {PermissionDeniedError: 'NotAllowedError'}[e.name] || e.name, message: e.message, constraint: e.constraint, toString: function() { return this.name; } }; }; // getUserMedia error shim. var origGetUserMedia = navigator.mediaDevices.getUserMedia. bind(navigator.mediaDevices); navigator.mediaDevices.getUserMedia = function(c) { return origGetUserMedia(c).catch(function(e) { return Promise.reject(shimError_(e)); }); }; }; },{}],11:[function(require,module,exports){ /* * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ /* eslint-env node */ 'use strict'; var utils = require('../utils'); module.exports = { shimGetUserMedia: require('./getusermedia'), shimOnTrack: function(window) { if (typeof window === 'object' && window.RTCPeerConnection && !('ontrack' in window.RTCPeerConnection.prototype)) { Object.defineProperty(window.RTCPeerConnection.prototype, 'ontrack', { get: function() { return this._ontrack; }, set: function(f) { if (this._ontrack) { this.removeEventListener('track', this._ontrack); this.removeEventListener('addstream', this._ontrackpoly); } this.addEventListener('track', this._ontrack = f); this.addEventListener('addstream', this._ontrackpoly = function(e) { e.stream.getTracks().forEach(function(track) { var event = new Event('track'); event.track = track; event.receiver = {track: track}; event.transceiver = {receiver: event.receiver}; event.streams = [e.stream]; this.dispatchEvent(event); }.bind(this)); }.bind(this)); }, enumerable: true, configurable: true }); } if (typeof window === 'object' && window.RTCTrackEvent && ('receiver' in window.RTCTrackEvent.prototype) && !('transceiver' in window.RTCTrackEvent.prototype)) { Object.defineProperty(window.RTCTrackEvent.prototype, 'transceiver', { get: function() { return {receiver: this.receiver}; } }); } }, shimSourceObject: function(window) { // Firefox has supported mozSrcObject since FF22, unprefixed in 42. if (typeof window === 'object') { if (window.HTMLMediaElement && !('srcObject' in window.HTMLMediaElement.prototype)) { // Shim the srcObject property, once, when HTMLMediaElement is found. Object.defineProperty(window.HTMLMediaElement.prototype, 'srcObject', { get: function() { return this.mozSrcObject; }, set: function(stream) { this.mozSrcObject = stream; } }); } } }, shimPeerConnection: function(window) { var browserDetails = utils.detectBrowser(window); if (typeof window !== 'object' || !(window.RTCPeerConnection || window.mozRTCPeerConnection)) { return; // probably media.peerconnection.enabled=false in about:config } // The RTCPeerConnection object. if (!window.RTCPeerConnection) { window.RTCPeerConnection = function(pcConfig, pcConstraints) { if (browserDetails.version < 38) { // .urls is not supported in FF < 38. // create RTCIceServers with a single url. if (pcConfig && pcConfig.iceServers) { var newIceServers = []; for (var i = 0; i < pcConfig.iceServers.length; i++) { var server = pcConfig.iceServers[i]; if (server.hasOwnProperty('urls')) { for (var j = 0; j < server.urls.length; j++) { var newServer = { url: server.urls[j] }; if (server.urls[j].indexOf('turn') === 0) { newServer.username = server.username; newServer.credential = server.credential; } newIceServers.push(newServer); } } else { newIceServers.push(pcConfig.iceServers[i]); } } pcConfig.iceServers = newIceServers; } } return new window.mozRTCPeerConnection(pcConfig, pcConstraints); }; window.RTCPeerConnection.prototype = window.mozRTCPeerConnection.prototype; // wrap static methods. Currently just generateCertificate. if (window.mozRTCPeerConnection.generateCertificate) { Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', { get: function() { return window.mozRTCPeerConnection.generateCertificate; } }); } window.RTCSessionDescription = window.mozRTCSessionDescription; window.RTCIceCandidate = window.mozRTCIceCandidate; } // shim away need for obsolete RTCIceCandidate/RTCSessionDescription. ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate'] .forEach(function(method) { var nativeMethod = window.RTCPeerConnection.prototype[method]; window.RTCPeerConnection.prototype[method] = function() { arguments[0] = new ((method === 'addIceCandidate') ? window.RTCIceCandidate : window.RTCSessionDescription)(arguments[0]); return nativeMethod.apply(this, arguments); }; }); // support for addIceCandidate(null or undefined) var nativeAddIceCandidate = window.RTCPeerConnection.prototype.addIceCandidate; window.RTCPeerConnection.prototype.addIceCandidate = function() { if (!arguments[0]) { if (arguments[1]) { arguments[1].apply(null); } return Promise.resolve(); } return nativeAddIceCandidate.apply(this, arguments); }; // shim getStats with maplike support var makeMapStats = function(stats) { var map = new Map(); Object.keys(stats).forEach(function(key) { map.set(key, stats[key]); map[key] = stats[key]; }); return map; }; var modernStatsTypes = { inboundrtp: 'inbound-rtp', outboundrtp: 'outbound-rtp', candidatepair: 'candidate-pair', localcandidate: 'local-candidate', remotecandidate: 'remote-candidate' }; var nativeGetStats = window.RTCPeerConnection.prototype.getStats; window.RTCPeerConnection.prototype.getStats = function( selector, onSucc, onErr ) { return nativeGetStats.apply(this, [selector || null]) .then(function(stats) { if (browserDetails.version < 48) { stats = makeMapStats(stats); } if (browserDetails.version < 53 && !onSucc) { // Shim only promise getStats with spec-hyphens in type names // Leave callback version alone; misc old uses of forEach before Map try { stats.forEach(function(stat) { stat.type = modernStatsTypes[stat.type] || stat.type; }); } catch (e) { if (e.name !== 'TypeError') { throw e; } // Avoid TypeError: "type" is read-only, in old versions. 34-43ish stats.forEach(function(stat, i) { stats.set(i, Object.assign({}, stat, { type: modernStatsTypes[stat.type] || stat.type })); }); } } return stats; }) .then(onSucc, onErr); }; }, shimSenderGetStats: function(window) { if (!(typeof window === 'object' && window.RTCPeerConnection && window.RTCRtpSender)) { return; } if (window.RTCRtpSender && 'getStats' in window.RTCRtpSender.prototype) { return; } var origGetSenders = window.RTCPeerConnection.prototype.getSenders; if (origGetSenders) { window.RTCPeerConnection.prototype.getSenders = function() { var pc = this; var senders = origGetSenders.apply(pc, []); senders.forEach(function(sender) { sender._pc = pc; }); return senders; }; } var origAddTrack = window.RTCPeerConnection.prototype.addTrack; if (origAddTrack) { window.RTCPeerConnection.prototype.addTrack = function() { var sender = origAddTrack.apply(this, arguments); sender._pc = this; return sender; }; } window.RTCRtpSender.prototype.getStats = function() { return this.track ? this._pc.getStats(this.track) : Promise.resolve(new Map()); }; }, shimReceiverGetStats: function(window) { if (!(typeof window === 'object' && window.RTCPeerConnection && window.RTCRtpSender)) { return; } if (window.RTCRtpSender && 'getStats' in window.RTCRtpReceiver.prototype) { return; } var origGetReceivers = window.RTCPeerConnection.prototype.getReceivers; if (origGetReceivers) { window.RTCPeerConnection.prototype.getReceivers = function() { var pc = this; var receivers = origGetReceivers.apply(pc, []); receivers.forEach(function(receiver) { receiver._pc = pc; }); return receivers; }; } utils.wrapPeerConnectionEvent(window, 'track', function(e) { e.receiver._pc = e.srcElement; return e; }); window.RTCRtpReceiver.prototype.getStats = function() { return this._pc.getStats(this.track); }; }, shimRemoveStream: function(window) { if (!window.RTCPeerConnection || 'removeStream' in window.RTCPeerConnection.prototype) { return; } window.RTCPeerConnection.prototype.removeStream = function(stream) { var pc = this; utils.deprecated('removeStream', 'removeTrack'); this.getSenders().forEach(function(sender) { if (sender.track && stream.getTracks().indexOf(sender.track) !== -1) { pc.removeTrack(sender); } }); }; }, shimRTCDataChannel: function(window) { // rename DataChannel to RTCDataChannel (native fix in FF60): // https://bugzilla.mozilla.org/show_bug.cgi?id=1173851 if (window.DataChannel && !window.RTCDataChannel) { window.RTCDataChannel = window.DataChannel; } }, shimGetDisplayMedia: function(window, preferredMediaSource) { if (!window.navigator || !window.navigator.mediaDevices || 'getDisplayMedia' in window.navigator.mediaDevices) { return; } window.navigator.mediaDevices.getDisplayMedia = function(constraints) { if (!(constraints && constraints.video)) { var err = new DOMException('getDisplayMedia without video ' + 'constraints is undefined'); err.name = 'NotFoundError'; // from https://heycam.github.io/webidl/#idl-DOMException-error-names err.code = 8; return Promise.reject(err); } if (constraints.video === true) { constraints.video = {mediaSource: preferredMediaSource}; } else { constraints.video.mediaSource = preferredMediaSource; } return window.navigator.mediaDevices.getUserMedia(constraints); }; window.navigator.getDisplayMedia = function(constraints) { utils.deprecated('navigator.getDisplayMedia', 'navigator.mediaDevices.getDisplayMedia'); return window.navigator.mediaDevices.getDisplayMedia(constraints); }; } }; },{"../utils":14,"./getusermedia":12}],12:[function(require,module,exports){ /* * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ /* eslint-env node */ 'use strict'; var utils = require('../utils'); var logging = utils.log; // Expose public methods. module.exports = function(window) { var browserDetails = utils.detectBrowser(window); var navigator = window && window.navigator; var MediaStreamTrack = window && window.MediaStreamTrack; var shimError_ = function(e) { return { name: { InternalError: 'NotReadableError', NotSupportedError: 'TypeError', PermissionDeniedError: 'NotAllowedError', SecurityError: 'NotAllowedError' }[e.name] || e.name, message: { 'The operation is insecure.': 'The request is not allowed by the ' + 'user agent or the platform in the current context.' }[e.message] || e.message, constraint: e.constraint, toString: function() { return this.name + (this.message && ': ') + this.message; } }; }; // getUserMedia constraints shim. var getUserMedia_ = function(constraints, onSuccess, onError) { var constraintsToFF37_ = function(c) { if (typeof c !== 'object' || c.require) { return c; } var require = []; Object.keys(c).forEach(function(key) { if (key === 'require' || key === 'advanced' || key === 'mediaSource') { return; } var r = c[key] = (typeof c[key] === 'object') ? c[key] : {ideal: c[key]}; if (r.min !== undefined || r.max !== undefined || r.exact !== undefined) { require.push(key); } if (r.exact !== undefined) { if (typeof r.exact === 'number') { r. min = r.max = r.exact; } else { c[key] = r.exact; } delete r.exact; } if (r.ideal !== undefined) { c.advanced = c.advanced || []; var oc = {}; if (typeof r.ideal === 'number') { oc[key] = {min: r.ideal, max: r.ideal}; } else { oc[key] = r.ideal; } c.advanced.push(oc); delete r.ideal; if (!Object.keys(r).length) { delete c[key]; } } }); if (require.length) { c.require = require; } return c; }; constraints = JSON.parse(JSON.stringify(constraints)); if (browserDetails.version < 38) { logging('spec: ' + JSON.stringify(constraints)); if (constraints.audio) { constraints.audio = constraintsToFF37_(constraints.audio); } if (constraints.video) { constraints.video = constraintsToFF37_(constraints.video); } logging('ff37: ' + JSON.stringify(constraints)); } return navigator.mozGetUserMedia(constraints, onSuccess, function(e) { onError(shimError_(e)); }); }; // Returns the result of getUserMedia as a Promise. var getUserMediaPromise_ = function(constraints) { return new Promise(function(resolve, reject) { getUserMedia_(constraints, resolve, reject); }); }; // Shim for mediaDevices on older versions. if (!navigator.mediaDevices) { navigator.mediaDevices = {getUserMedia: getUserMediaPromise_, addEventListener: function() { }, removeEventListener: function() { } }; } navigator.mediaDevices.enumerateDevices = navigator.mediaDevices.enumerateDevices || function() { return new Promise(function(resolve) { var infos = [ {kind: 'audioinput', deviceId: 'default', label: '', groupId: ''}, {kind: 'videoinput', deviceId: 'default', label: '', groupId: ''} ]; resolve(infos); }); }; if (browserDetails.version < 41) { // Work around http://bugzil.la/1169665 var orgEnumerateDevices = navigator.mediaDevices.enumerateDevices.bind(navigator.mediaDevices); navigator.mediaDevices.enumerateDevices = function() { return orgEnumerateDevices().then(undefined, function(e) { if (e.name === 'NotFoundError') { return []; } throw e; }); }; } if (browserDetails.version < 49) { var origGetUserMedia = navigator.mediaDevices.getUserMedia. bind(navigator.mediaDevices); navigator.mediaDevices.getUserMedia = function(c) { return origGetUserMedia(c).then(function(stream) { // Work around https://bugzil.la/802326 if (c.audio && !stream.getAudioTracks().length || c.video && !stream.getVideoTracks().length) { stream.getTracks().forEach(function(track) { track.stop(); }); throw new DOMException('The object can not be found here.', 'NotFoundError'); } return stream; }, function(e) { return Promise.reject(shimError_(e)); }); }; } if (!(browserDetails.version > 55 && 'autoGainControl' in navigator.mediaDevices.getSupportedConstraints())) { var remap = function(obj, a, b) { if (a in obj && !(b in obj)) { obj[b] = obj[a]; delete obj[a]; } }; var nativeGetUserMedia = navigator.mediaDevices.getUserMedia. bind(navigator.mediaDevices); navigator.mediaDevices.getUserMedia = function(c) { if (typeof c === 'object' && typeof c.audio === 'object') { c = JSON.parse(JSON.stringify(c)); remap(c.audio, 'autoGainControl', 'mozAutoGainControl'); remap(c.audio, 'noiseSuppression', 'mozNoiseSuppression'); } return nativeGetUserMedia(c); }; if (MediaStreamTrack && MediaStreamTrack.prototype.getSettings) { var nativeGetSettings = MediaStreamTrack.prototype.getSettings; MediaStreamTrack.prototype.getSettings = function() { var obj = nativeGetSettings.apply(this, arguments); remap(obj, 'mozAutoGainControl', 'autoGainControl'); remap(obj, 'mozNoiseSuppression', 'noiseSuppression'); return obj; }; } if (MediaStreamTrack && MediaStreamTrack.prototype.applyConstraints) { var nativeApplyConstraints = MediaStreamTrack.prototype.applyConstraints; MediaStreamTrack.prototype.applyConstraints = function(c) { if (this.kind === 'audio' && typeof c === 'object') { c = JSON.parse(JSON.stringify(c)); remap(c, 'autoGainControl', 'mozAutoGainControl'); remap(c, 'noiseSuppression', 'mozNoiseSuppression'); } return nativeApplyConstraints.apply(this, [c]); }; } } navigator.getUserMedia = function(constraints, onSuccess, onError) { if (browserDetails.version < 44) { return getUserMedia_(constraints, onSuccess, onError); } // Replace Firefox 44+'s deprecation warning with unprefixed version. utils.deprecated('navigator.getUserMedia', 'navigator.mediaDevices.getUserMedia'); navigator.mediaDevices.getUserMedia(constraints).then(onSuccess, onError); }; }; },{"../utils":14}],13:[function(require,module,exports){ /* * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ 'use strict'; var utils = require('../utils'); module.exports = { shimLocalStreamsAPI: function(window) { if (typeof window !== 'object' || !window.RTCPeerConnection) { return; } if (!('getLocalStreams' in window.RTCPeerConnection.prototype)) { window.RTCPeerConnection.prototype.getLocalStreams = function() { if (!this._localStreams) { this._localStreams = []; } return this._localStreams; }; } if (!('getStreamById' in window.RTCPeerConnection.prototype)) { window.RTCPeerConnection.prototype.getStreamById = function(id) { var result = null; if (this._localStreams) { this._localStreams.forEach(function(stream) { if (stream.id === id) { result = stream; } }); } if (this._remoteStreams) { this._remoteStreams.forEach(function(stream) { if (stream.id === id) { result = stream; } }); } return result; }; } if (!('addStream' in window.RTCPeerConnection.prototype)) { var _addTrack = window.RTCPeerConnection.prototype.addTrack; window.RTCPeerConnection.prototype.addStream = function(stream) { if (!this._localStreams) { this._localStreams = []; } if (this._localStreams.indexOf(stream) === -1) { this._localStreams.push(stream); } var pc = this; stream.getTracks().forEach(function(track) { _addTrack.call(pc, track, stream); }); }; window.RTCPeerConnection.prototype.addTrack = function(track, stream) { if (stream) { if (!this._localStreams) { this._localStreams = [stream]; } else if (this._localStreams.indexOf(stream) === -1) { this._localStreams.push(stream); } } return _addTrack.call(this, track, stream); }; } if (!('removeStream' in window.RTCPeerConnection.prototype)) { window.RTCPeerConnection.prototype.removeStream = function(stream) { if (!this._localStreams) { this._localStreams = []; } var index = this._localStreams.indexOf(stream); if (index === -1) { return; } this._localStreams.splice(index, 1); var pc = this; var tracks = stream.getTracks(); this.getSenders().forEach(function(sender) { if (tracks.indexOf(sender.track) !== -1) { pc.removeTrack(sender); } }); }; } }, shimRemoteStreamsAPI: function(window) { if (typeof window !== 'object' || !window.RTCPeerConnection) { return; } if (!('getRemoteStreams' in window.RTCPeerConnection.prototype)) { window.RTCPeerConnection.prototype.getRemoteStreams = function() { return this._remoteStreams ? this._remoteStreams : []; }; } if (!('onaddstream' in window.RTCPeerConnection.prototype)) { Object.defineProperty(window.RTCPeerConnection.prototype, 'onaddstream', { get: function() { return this._onaddstream; }, set: function(f) { if (this._onaddstream) { this.removeEventListener('addstream', this._onaddstream); } this.addEventListener('addstream', this._onaddstream = f); } }); var origSetRemoteDescription = window.RTCPeerConnection.prototype.setRemoteDescription; window.RTCPeerConnection.prototype.setRemoteDescription = function() { var pc = this; if (!this._onaddstreampoly) { this.addEventListener('track', this._onaddstreampoly = function(e) { e.streams.forEach(function(stream) { if (!pc._remoteStreams) { pc._remoteStreams = []; } if (pc._remoteStreams.indexOf(stream) >= 0) { return; } pc._remoteStreams.push(stream); var event = new Event('addstream'); event.stream = stream; pc.dispatchEvent(event); }); }); } return origSetRemoteDescription.apply(pc, arguments); }; } }, shimCallbacksAPI: function(window) { if (typeof window !== 'object' || !window.RTCPeerConnection) { return; } var prototype = window.RTCPeerConnection.prototype; var createOffer = prototype.createOffer; var createAnswer = prototype.createAnswer; var setLocalDescription = prototype.setLocalDescription; var setRemoteDescription = prototype.setRemoteDescription; var addIceCandidate = prototype.addIceCandidate; prototype.createOffer = function(successCallback, failureCallback) { var options = (arguments.length >= 2) ? arguments[2] : arguments[0]; var promise = createOffer.apply(this, [options]); if (!failureCallback) { return promise; } promise.then(successCallback, failureCallback); return Promise.resolve(); }; prototype.createAnswer = function(successCallback, failureCallback) { var options = (arguments.length >= 2) ? arguments[2] : arguments[0]; var promise = createAnswer.apply(this, [options]); if (!failureCallback) { return promise; } promise.then(successCallback, failureCallback); return Promise.resolve(); }; var withCallback = function(description, successCallback, failureCallback) { var promise = setLocalDescription.apply(this, [description]); if (!failureCallback) { return promise; } promise.then(successCallback, failureCallback); return Promise.resolve(); }; prototype.setLocalDescription = withCallback; withCallback = function(description, successCallback, failureCallback) { var promise = setRemoteDescription.apply(this, [description]); if (!failureCallback) { return promise; } promise.then(successCallback, failureCallback); return Promise.resolve(); }; prototype.setRemoteDescription = withCallback; withCallback = function(candidate, successCallback, failureCallback) { var promise = addIceCandidate.apply(this, [candidate]); if (!failureCallback) { return promise; } promise.then(successCallback, failureCallback); return Promise.resolve(); }; prototype.addIceCandidate = withCallback; }, shimGetUserMedia: function(window) { var navigator = window && window.navigator; if (!navigator.getUserMedia) { if (navigator.webkitGetUserMedia) { navigator.getUserMedia = navigator.webkitGetUserMedia.bind(navigator); } else if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { navigator.getUserMedia = function(constraints, cb, errcb) { navigator.mediaDevices.getUserMedia(constraints) .then(cb, errcb); }.bind(navigator); } } }, shimRTCIceServerUrls: function(window) { // migrate from non-spec RTCIceServer.url to RTCIceServer.urls var OrigPeerConnection = window.RTCPeerConnection; window.RTCPeerConnection = function(pcConfig, pcConstraints) { if (pcConfig && pcConfig.iceServers) { var newIceServers = []; for (var i = 0; i < pcConfig.iceServers.length; i++) { var server = pcConfig.iceServers[i]; if (!server.hasOwnProperty('urls') && server.hasOwnProperty('url')) { utils.deprecated('RTCIceServer.url', 'RTCIceServer.urls'); server = JSON.parse(JSON.stringify(server)); server.urls = server.url; delete server.url; newIceServers.push(server); } else { newIceServers.push(pcConfig.iceServers[i]); } } pcConfig.iceServers = newIceServers; } return new OrigPeerConnection(pcConfig, pcConstraints); }; window.RTCPeerConnection.prototype = OrigPeerConnection.prototype; // wrap static methods. Currently just generateCertificate. if ('generateCertificate' in window.RTCPeerConnection) { Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', { get: function() { return OrigPeerConnection.generateCertificate; } }); } }, shimTrackEventTransceiver: function(window) { // Add event.transceiver member over deprecated event.receiver if (typeof window === 'object' && window.RTCPeerConnection && ('receiver' in window.RTCTrackEvent.prototype) && // can't check 'transceiver' in window.RTCTrackEvent.prototype, as it is // defined for some reason even when window.RTCTransceiver is not. !window.RTCTransceiver) { Object.defineProperty(window.RTCTrackEvent.prototype, 'transceiver', { get: function() { return {receiver: this.receiver}; } }); } }, shimCreateOfferLegacy: function(window) { var origCreateOffer = window.RTCPeerConnection.prototype.createOffer; window.RTCPeerConnection.prototype.createOffer = function(offerOptions) { var pc = this; if (offerOptions) { if (typeof offerOptions.offerToReceiveAudio !== 'undefined') { // support bit values offerOptions.offerToReceiveAudio = !!offerOptions.offerToReceiveAudio; } var audioTransceiver = pc.getTransceivers().find(function(transceiver) { return transceiver.sender.track && transceiver.sender.track.kind === 'audio'; }); if (offerOptions.offerToReceiveAudio === false && audioTransceiver) { if (audioTransceiver.direction === 'sendrecv') { if (audioTransceiver.setDirection) { audioTransceiver.setDirection('sendonly'); } else { audioTransceiver.direction = 'sendonly'; } } else if (audioTransceiver.direction === 'recvonly') { if (audioTransceiver.setDirection) { audioTransceiver.setDirection('inactive'); } else { audioTransceiver.direction = 'inactive'; } } } else if (offerOptions.offerToReceiveAudio === true && !audioTransceiver) { pc.addTransceiver('audio'); } if (typeof offerOptions.offerToReceiveVideo !== 'undefined') { // support bit values offerOptions.offerToReceiveVideo = !!offerOptions.offerToReceiveVideo; } var videoTransceiver = pc.getTransceivers().find(function(transceiver) { return transceiver.sender.track && transceiver.sender.track.kind === 'video'; }); if (offerOptions.offerToReceiveVideo === false && videoTransceiver) { if (videoTransceiver.direction === 'sendrecv') { videoTransceiver.setDirection('sendonly'); } else if (videoTransceiver.direction === 'recvonly') { videoTransceiver.setDirection('inactive'); } } else if (offerOptions.offerToReceiveVideo === true && !videoTransceiver) { pc.addTransceiver('video'); } } return origCreateOffer.apply(pc, arguments); }; } }; },{"../utils":14}],14:[function(require,module,exports){ /* * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. */ /* eslint-env node */ 'use strict'; var logDisabled_ = true; var deprecationWarnings_ = true; /** * Extract browser version out of the provided user agent string. * * @param {!string} uastring userAgent string. * @param {!string} expr Regular expression used as match criteria. * @param {!number} pos position in the version string to be returned. * @return {!number} browser version. */ function extractVersion(uastring, expr, pos) { var match = uastring.match(expr); return match && match.length >= pos && parseInt(match[pos], 10); } // Wraps the peerconnection event eventNameToWrap in a function // which returns the modified event object (or false to prevent // the event). function wrapPeerConnectionEvent(window, eventNameToWrap, wrapper) { if (!window.RTCPeerConnection) { return; } var proto = window.RTCPeerConnection.prototype; var nativeAddEventListener = proto.addEventListener; proto.addEventListener = function(nativeEventName, cb) { if (nativeEventName !== eventNameToWrap) { return nativeAddEventListener.apply(this, arguments); } var wrappedCallback = function(e) { var modifiedEvent = wrapper(e); if (modifiedEvent) { cb(modifiedEvent); } }; this._eventMap = this._eventMap || {}; this._eventMap[cb] = wrappedCallback; return nativeAddEventListener.apply(this, [nativeEventName, wrappedCallback]); }; var nativeRemoveEventListener = proto.removeEventListener; proto.removeEventListener = function(nativeEventName, cb) { if (nativeEventName !== eventNameToWrap || !this._eventMap || !this._eventMap[cb]) { return nativeRemoveEventListener.apply(this, arguments); } var unwrappedCb = this._eventMap[cb]; delete this._eventMap[cb]; return nativeRemoveEventListener.apply(this, [nativeEventName, unwrappedCb]); }; Object.defineProperty(proto, 'on' + eventNameToWrap, { get: function() { return this['_on' + eventNameToWrap]; }, set: function(cb) { if (this['_on' + eventNameToWrap]) { this.removeEventListener(eventNameToWrap, this['_on' + eventNameToWrap]); delete this['_on' + eventNameToWrap]; } if (cb) { this.addEventListener(eventNameToWrap, this['_on' + eventNameToWrap] = cb); } }, enumerable: true, configurable: true }); } // Utility methods. module.exports = { extractVersion: extractVersion, wrapPeerConnectionEvent: wrapPeerConnectionEvent, disableLog: function(bool) { if (typeof bool !== 'boolean') { return new Error('Argument type: ' + typeof bool + '. Please use a boolean.'); } logDisabled_ = bool; return (bool) ? 'adapter.js logging disabled' : 'adapter.js logging enabled'; }, /** * Disable or enable deprecation warnings * @param {!boolean} bool set to true to disable warnings. */ disableWarnings: function(bool) { if (typeof bool !== 'boolean') { return new Error('Argument type: ' + typeof bool + '. Please use a boolean.'); } deprecationWarnings_ = !bool; return 'adapter.js deprecation warnings ' + (bool ? 'disabled' : 'enabled'); }, log: function() { if (typeof window === 'object') { if (logDisabled_) { return; } if (typeof console !== 'undefined' && typeof console.log === 'function') { console.log.apply(console, arguments); } } }, /** * Shows a deprecation warning suggesting the modern and spec-compatible API. */ deprecated: function(oldMethod, newMethod) { if (!deprecationWarnings_) { return; } console.warn(oldMethod + ' is deprecated, please use ' + newMethod + ' instead.'); }, /** * Browser detector. * * @return {object} result containing browser and version * properties. */ detectBrowser: function(window) { var navigator = window && window.navigator; // Returned result object. var result = {}; result.browser = null; result.version = null; // Fail early if it's not a browser if (typeof window === 'undefined' || !window.navigator) { result.browser = 'Not a browser.'; return result; } if (navigator.mozGetUserMedia) { // Firefox. result.browser = 'firefox'; result.version = extractVersion(navigator.userAgent, /Firefox\/(\d+)\./, 1); } else if (navigator.webkitGetUserMedia) { // Chrome, Chromium, Webview, Opera. // Version matches Chrome/WebRTC version. result.browser = 'chrome'; result.version = extractVersion(navigator.userAgent, /Chrom(e|ium)\/(\d+)\./, 2); } else if (navigator.mediaDevices && navigator.userAgent.match(/Edge\/(\d+).(\d+)$/)) { // Edge. result.browser = 'edge'; result.version = extractVersion(navigator.userAgent, /Edge\/(\d+).(\d+)$/, 2); } else if (window.RTCPeerConnection && navigator.userAgent.match(/AppleWebKit\/(\d+)\./)) { // Safari. result.browser = 'safari'; result.version = extractVersion(navigator.userAgent, /AppleWebKit\/(\d+)\./, 1); } else { // Default fallthrough: not supported. result.browser = 'Not a supported browser.'; return result; } return result; } }; },{}]},{},[3])(3) }); ================================================ FILE: FileBufferReader/demo/index.html ================================================ WebRTC File Sharing | FileBufferReader

WebRTC File Sharing | FileBufferReader

Github Source Codes | What's New?

Watch a YouTube video to understand how this demo works.




FileBufferReader is a JavaScript library reads file and returns chunkified array-buffers. The resulting buffers can be shared using WebRTC data channels or socket.io. It is Open Sourced on Github
================================================ FILE: FileBufferReader/dev/FileBufferReader.js ================================================ function FileBufferReader() { var fbr = this; var fbrHelper = new FileBufferReaderHelper(); fbr.chunks = {}; fbr.users = {}; fbr.readAsArrayBuffer = function(file, callback, extra) { var options = { file: file, earlyCallback: function(chunk) { callback(fbrClone(chunk, { currentPosition: -1 })); }, extra: extra || { userid: 0 } }; if (file.extra && Object.keys(file.extra).length) { Object.keys(file.extra).forEach(function(key) { options.extra[key] = file.extra[key]; }); } fbrHelper.readAsArrayBuffer(fbr, options); }; fbr.getNextChunk = function(fileUUID, callback, userid) { var currentPosition; if (typeof fileUUID.currentPosition !== 'undefined') { currentPosition = fileUUID.currentPosition; fileUUID = fileUUID.uuid; } var allFileChunks = fbr.chunks[fileUUID]; if (!allFileChunks) { return; } if (typeof userid !== 'undefined') { if (!fbr.users[userid + '']) { fbr.users[userid + ''] = { fileUUID: fileUUID, userid: userid, currentPosition: -1 }; } if (typeof currentPosition !== 'undefined') { fbr.users[userid + ''].currentPosition = currentPosition; } fbr.users[userid + ''].currentPosition++; currentPosition = fbr.users[userid + ''].currentPosition; } else { if (typeof currentPosition !== 'undefined') { fbr.chunks[fileUUID].currentPosition = currentPosition; } fbr.chunks[fileUUID].currentPosition++; currentPosition = fbr.chunks[fileUUID].currentPosition; } var nextChunk = allFileChunks[currentPosition]; if (!nextChunk) { delete fbr.chunks[fileUUID]; fbr.convertToArrayBuffer({ chunkMissing: true, currentPosition: currentPosition, uuid: fileUUID }, callback); return; } nextChunk = fbrClone(nextChunk); if (typeof userid !== 'undefined') { nextChunk.remoteUserId = userid + ''; } if (!!nextChunk.start) { fbr.onBegin(nextChunk); } if (!!nextChunk.end) { fbr.onEnd(nextChunk); } fbr.onProgress(nextChunk); fbr.convertToArrayBuffer(nextChunk, function(buffer) { if (nextChunk.currentPosition == nextChunk.maxChunks) { callback(buffer, true); return; } callback(buffer, false); }); }; var fbReceiver = new FileBufferReceiver(fbr); fbr.addChunk = function(chunk, callback) { if (!chunk) { return; } fbReceiver.receive(chunk, function(chunk) { fbr.convertToArrayBuffer({ readyForNextChunk: true, currentPosition: chunk.currentPosition, uuid: chunk.uuid }, callback); }); }; fbr.chunkMissing = function(chunk) { delete fbReceiver.chunks[chunk.uuid]; delete fbReceiver.chunksWaiters[chunk.uuid]; }; fbr.onBegin = function() {}; fbr.onEnd = function() {}; fbr.onProgress = function() {}; fbr.convertToObject = FileConverter.ConvertToObject; fbr.convertToArrayBuffer = FileConverter.ConvertToArrayBuffer // for backward compatibility----it is redundant. fbr.setMultipleUsers = function() {}; // extends 'from' object with members from 'to'. If 'to' is null, a deep clone of 'from' is returned function fbrClone(from, to) { if (from == null || typeof from != "object") return from; if (from.constructor != Object && from.constructor != Array) return from; if (from.constructor == Date || from.constructor == RegExp || from.constructor == Function || from.constructor == String || from.constructor == Number || from.constructor == Boolean) return new from.constructor(from); to = to || new from.constructor(); for (var name in from) { to[name] = typeof to[name] == "undefined" ? fbrClone(from[name], null) : to[name]; } return to; } } ================================================ FILE: FileBufferReader/dev/FileBufferReaderHelper.js ================================================ function FileBufferReaderHelper() { var fbrHelper = this; function processInWebWorker(_function) { var blob = URL.createObjectURL(new Blob([_function.toString(), 'this.onmessage = function (e) {' + _function.name + '(e.data);}' ], { type: 'application/javascript' })); var worker = new Worker(blob); return worker; } fbrHelper.readAsArrayBuffer = function(fbr, options) { var earlyCallback = options.earlyCallback; delete options.earlyCallback; function processChunk(chunk) { if (!fbr.chunks[chunk.uuid]) { fbr.chunks[chunk.uuid] = { currentPosition: -1 }; } options.extra = options.extra || { userid: 0 }; chunk.userid = options.userid || options.extra.userid || 0; chunk.extra = options.extra; fbr.chunks[chunk.uuid][chunk.currentPosition] = chunk; if (chunk.end && earlyCallback) { earlyCallback(chunk.uuid); earlyCallback = null; } // for huge files if ((chunk.maxChunks > 200 && chunk.currentPosition == 200) && earlyCallback) { earlyCallback(chunk.uuid); earlyCallback = null; } } if (false && typeof Worker !== 'undefined') { var webWorker = processInWebWorker(fileReaderWrapper); webWorker.onmessage = function(event) { processChunk(event.data); }; webWorker.postMessage(options); } else { fileReaderWrapper(options, processChunk); } }; function fileReaderWrapper(options, callback) { callback = callback || function(chunk) { postMessage(chunk); }; var file = options.file; if (!file.uuid) { file.uuid = (Math.random() * 100).toString().replace(/\./g, ''); } var chunkSize = options.chunkSize || 15 * 1000; if (options.extra && options.extra.chunkSize) { chunkSize = options.extra.chunkSize; } var sliceId = 0; var cacheSize = chunkSize; var chunksPerSlice = Math.floor(Math.min(100000000, cacheSize) / chunkSize); var sliceSize = chunksPerSlice * chunkSize; var maxChunks = Math.ceil(file.size / chunkSize); file.maxChunks = maxChunks; var numOfChunksInSlice; var currentPosition = 0; var hasEntireFile; var chunks = []; callback({ currentPosition: currentPosition, uuid: file.uuid, maxChunks: maxChunks, size: file.size, name: file.name, type: file.type, lastModifiedDate: (file.lastModifiedDate || new Date()).toString(), start: true }); var blob, reader = new FileReader(); reader.onloadend = function(evt) { if (evt.target.readyState == FileReader.DONE) { addChunks(file.name, evt.target.result, function() { sliceId++; if ((sliceId + 1) * sliceSize < file.size) { blob = file.slice(sliceId * sliceSize, (sliceId + 1) * sliceSize); reader.readAsArrayBuffer(blob); } else if (sliceId * sliceSize < file.size) { blob = file.slice(sliceId * sliceSize, file.size); reader.readAsArrayBuffer(blob); } else { file.url = URL.createObjectURL(file); callback({ currentPosition: currentPosition, uuid: file.uuid, maxChunks: maxChunks, size: file.size, name: file.name, lastModifiedDate: (file.lastModifiedDate || new Date()).toString(), url: URL.createObjectURL(file), type: file.type, end: true }); } }); } }; currentPosition += 1; blob = file.slice(sliceId * sliceSize, (sliceId + 1) * sliceSize); reader.readAsArrayBuffer(blob); function addChunks(fileName, binarySlice, addChunkCallback) { numOfChunksInSlice = Math.ceil(binarySlice.byteLength / chunkSize); for (var i = 0; i < numOfChunksInSlice; i++) { var start = i * chunkSize; chunks[currentPosition] = binarySlice.slice(start, Math.min(start + chunkSize, binarySlice.byteLength)); callback({ uuid: file.uuid, buffer: chunks[currentPosition], currentPosition: currentPosition, maxChunks: maxChunks, size: file.size, name: file.name, lastModifiedDate: (file.lastModifiedDate || new Date()).toString(), type: file.type }); currentPosition++; } if (currentPosition == maxChunks) { hasEntireFile = true; } addChunkCallback(); } } } ================================================ FILE: FileBufferReader/dev/FileBufferReceiver.js ================================================ function FileBufferReceiver(fbr) { var fbReceiver = this; fbReceiver.chunks = {}; fbReceiver.chunksWaiters = {}; function receive(chunk, callback) { if (!chunk.uuid) { fbr.convertToObject(chunk, function(object) { receive(object); }); return; } if (chunk.start && !fbReceiver.chunks[chunk.uuid]) { fbReceiver.chunks[chunk.uuid] = {}; if (fbr.onBegin) fbr.onBegin(chunk); } if (!chunk.end && chunk.buffer) { fbReceiver.chunks[chunk.uuid][chunk.currentPosition] = chunk.buffer; } if (chunk.end) { var chunksObject = fbReceiver.chunks[chunk.uuid]; var chunksArray = []; Object.keys(chunksObject).forEach(function(item, idx) { chunksArray.push(chunksObject[item]); }); var blob = new Blob(chunksArray, { type: chunk.type }); blob = merge(blob, chunk); blob.url = URL.createObjectURL(blob); blob.uuid = chunk.uuid; if (!blob.size) console.error('Something went wrong. Blob Size is 0.'); if (fbr.onEnd) fbr.onEnd(blob); // clear system memory delete fbReceiver.chunks[chunk.uuid]; delete fbReceiver.chunksWaiters[chunk.uuid]; } if (chunk.buffer && fbr.onProgress) fbr.onProgress(chunk); if (!chunk.end) { callback(chunk); fbReceiver.chunksWaiters[chunk.uuid] = function() { function looper() { if (!chunk.buffer) { return; } if (!fbReceiver.chunks[chunk.uuid]) { return; } if (chunk.currentPosition != chunk.maxChunks && !fbReceiver.chunks[chunk.uuid][chunk.currentPosition]) { callback(chunk); setTimeout(looper, 5000); } } setTimeout(looper, 5000); }; fbReceiver.chunksWaiters[chunk.uuid](); } } fbReceiver.receive = receive; } ================================================ FILE: FileBufferReader/dev/FileConverter.js ================================================ var FileConverter = { ConvertToArrayBuffer: function(object, callback) { binarize.pack(object, function(dataView) { callback(dataView.buffer); }); }, ConvertToObject: function(buffer, callback) { binarize.unpack(buffer, callback); } }; ================================================ FILE: FileBufferReader/dev/FileSelector.js ================================================ function FileSelector() { var selector = this; var noFileSelectedCallback = function() {}; selector.selectSingleFile = function(callback, failure) { if (failure) { noFileSelectedCallback = failure; } selectFile(callback); }; selector.selectMultipleFiles = function(callback, failure) { if (failure) { noFileSelectedCallback = failure; } selectFile(callback, true); }; selector.selectDirectory = function(callback, failure) { if (failure) { noFileSelectedCallback = failure; } selectFile(callback, true, true); }; selector.accept = '*.*'; function selectFile(callback, multiple, directory) { callback = callback || function() {}; var file = document.createElement('input'); file.type = 'file'; if (multiple) { file.multiple = true; } if (directory) { file.webkitdirectory = true; } file.accept = selector.accept; file.onclick = function() { file.clickStarted = true; }; document.body.onfocus = function() { setTimeout(function() { if (!file.clickStarted) return; file.clickStarted = false; if (!file.value) { noFileSelectedCallback(); } }, 500); }; file.onchange = function() { if (multiple) { if (!file.files.length) { console.error('No file selected.'); return; } var arr = []; Array.from(file.files).forEach(function(file) { file.url = file.webkitRelativePath; arr.push(file); }); callback(arr); return; } if (!file.files[0]) { console.error('No file selected.'); return; } callback(file.files[0]); file.parentNode.removeChild(file); }; file.style.display = 'none'; (document.body || document.documentElement).appendChild(file); fireClickEvent(file); } function getValidFileName(fileName) { if (!fileName) { fileName = 'file' + (new Date).toISOString().replace(/:|\.|-/g, '') } var a = fileName; a = a.replace(/^.*[\\\/]([^\\\/]*)$/i, "$1"); a = a.replace(/\s/g, "_"); a = a.replace(/,/g, ''); a = a.toLowerCase(); return a; } function fireClickEvent(element) { if (typeof element.click === 'function') { element.click(); return; } if (typeof element.change === 'function') { element.change(); return; } if (typeof document.createEvent('Event') !== 'undefined') { var event = document.createEvent('Event'); if (typeof event.initEvent === 'function' && typeof element.dispatchEvent === 'function') { event.initEvent('click', true, true); element.dispatchEvent(event); return; } } var event = new MouseEvent('click', { view: window, bubbles: true, cancelable: true }); element.dispatchEvent(event); } } ================================================ FILE: FileBufferReader/dev/binarize.js ================================================ /* Copyright 2013 Eiji Kitamura Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Author: Eiji Kitamura (agektmr@gmail.com) */ var debug = false; var BIG_ENDIAN = false, LITTLE_ENDIAN = true, TYPE_LENGTH = Uint8Array.BYTES_PER_ELEMENT, LENGTH_LENGTH = Uint16Array.BYTES_PER_ELEMENT, BYTES_LENGTH = Uint32Array.BYTES_PER_ELEMENT; var Types = { NULL: 0, UNDEFINED: 1, STRING: 2, NUMBER: 3, BOOLEAN: 4, ARRAY: 5, OBJECT: 6, INT8ARRAY: 7, INT16ARRAY: 8, INT32ARRAY: 9, UINT8ARRAY: 10, UINT16ARRAY: 11, UINT32ARRAY: 12, FLOAT32ARRAY: 13, FLOAT64ARRAY: 14, ARRAYBUFFER: 15, BLOB: 16, FILE: 16, BUFFER: 17 // Special type for node.js }; if (debug) { var TypeNames = [ 'NULL', 'UNDEFINED', 'STRING', 'NUMBER', 'BOOLEAN', 'ARRAY', 'OBJECT', 'INT8ARRAY', 'INT16ARRAY', 'INT32ARRAY', 'UINT8ARRAY', 'UINT16ARRAY', 'UINT32ARRAY', 'FLOAT32ARRAY', 'FLOAT64ARRAY', 'ARRAYBUFFER', 'BLOB', 'BUFFER' ]; } var Length = [ null, // Types.NULL null, // Types.UNDEFINED 'Uint16', // Types.STRING 'Float64', // Types.NUMBER 'Uint8', // Types.BOOLEAN null, // Types.ARRAY null, // Types.OBJECT 'Int8', // Types.INT8ARRAY 'Int16', // Types.INT16ARRAY 'Int32', // Types.INT32ARRAY 'Uint8', // Types.UINT8ARRAY 'Uint16', // Types.UINT16ARRAY 'Uint32', // Types.UINT32ARRAY 'Float32', // Types.FLOAT32ARRAY 'Float64', // Types.FLOAT64ARRAY 'Uint8', // Types.ARRAYBUFFER 'Uint8', // Types.BLOB, Types.FILE 'Uint8' // Types.BUFFER ]; var binary_dump = function(view, start, length) { var table = [], endianness = BIG_ENDIAN, ROW_LENGTH = 40; table[0] = []; for (var i = 0; i < ROW_LENGTH; i++) { table[0][i] = i < 10 ? '0' + i.toString(10) : i.toString(10); } for (i = 0; i < length; i++) { var code = view.getUint8(start + i, endianness); var index = ~~(i / ROW_LENGTH) + 1; if (typeof table[index] === 'undefined') table[index] = []; table[index][i % ROW_LENGTH] = code < 16 ? '0' + code.toString(16) : code.toString(16); } console.log('%c' + table[0].join(' '), 'font-weight: bold;'); for (i = 1; i < table.length; i++) { console.log(table[i].join(' ')); } }; var find_type = function(obj) { var type = undefined; if (obj === undefined) { type = Types.UNDEFINED; } else if (obj === null) { type = Types.NULL; } else { var const_name = obj.constructor.name; var const_name_reflection = obj.constructor.toString().match(/\w+/g)[1]; if (const_name !== undefined && Types[const_name.toUpperCase()] !== undefined) { // return type by .constructor.name if possible type = Types[const_name.toUpperCase()]; } else if (const_name_reflection !== undefined && Types[const_name_reflection.toUpperCase()] !== undefined) { type = Types[const_name_reflection.toUpperCase()]; } else { // Work around when constructor.name is not defined switch (typeof obj) { case 'string': type = Types.STRING; break; case 'number': type = Types.NUMBER; break; case 'boolean': type = Types.BOOLEAN; break; case 'object': if (obj instanceof Array) { type = Types.ARRAY; } else if (obj instanceof Int8Array) { type = Types.INT8ARRAY; } else if (obj instanceof Int16Array) { type = Types.INT16ARRAY; } else if (obj instanceof Int32Array) { type = Types.INT32ARRAY; } else if (obj instanceof Uint8Array) { type = Types.UINT8ARRAY; } else if (obj instanceof Uint16Array) { type = Types.UINT16ARRAY; } else if (obj instanceof Uint32Array) { type = Types.UINT32ARRAY; } else if (obj instanceof Float32Array) { type = Types.FLOAT32ARRAY; } else if (obj instanceof Float64Array) { type = Types.FLOAT64ARRAY; } else if (obj instanceof ArrayBuffer) { type = Types.ARRAYBUFFER; } else if (obj instanceof Blob) { // including File type = Types.BLOB; } else if (obj instanceof Buffer) { // node.js only type = Types.BUFFER; } else if (obj instanceof Object) { type = Types.OBJECT; } break; default: break; } } } return type; }; var utf16_utf8 = function(string) { return unescape(encodeURIComponent(string)); }; var utf8_utf16 = function(bytes) { return decodeURIComponent(escape(bytes)); }; /** * packs seriarized elements array into a packed ArrayBuffer * @param {Array} serialized Serialized array of elements. * @return {DataView} view of packed binary */ var pack = function(serialized) { var cursor = 0, i = 0, j = 0, endianness = BIG_ENDIAN; var ab = new ArrayBuffer(serialized[0].byte_length + serialized[0].header_size); var view = new DataView(ab); for (i = 0; i < serialized.length; i++) { var start = cursor, header_size = serialized[i].header_size, type = serialized[i].type, length = serialized[i].length, value = serialized[i].value, byte_length = serialized[i].byte_length, type_name = Length[type], unit = type_name === null ? 0 : window[type_name + 'Array'].BYTES_PER_ELEMENT; // Set type if (type === Types.BUFFER) { // on node.js Blob is emulated using Buffer type view.setUint8(cursor, Types.BLOB, endianness); } else { view.setUint8(cursor, type, endianness); } cursor += TYPE_LENGTH; if (debug) { console.info('Packing', type, TypeNames[type]); } // Set length if required if (type === Types.ARRAY || type === Types.OBJECT) { view.setUint16(cursor, length, endianness); cursor += LENGTH_LENGTH; if (debug) { console.info('Content Length', length); } } // Set byte length view.setUint32(cursor, byte_length, endianness); cursor += BYTES_LENGTH; if (debug) { console.info('Header Size', header_size, 'bytes'); console.info('Byte Length', byte_length, 'bytes'); } switch (type) { case Types.NULL: case Types.UNDEFINED: // NULL and UNDEFINED doesn't have any payload break; case Types.STRING: if (debug) { console.info('Actual Content %c"' + value + '"', 'font-weight:bold;'); } for (j = 0; j < length; j++, cursor += unit) { view.setUint16(cursor, value.charCodeAt(j), endianness); } break; case Types.NUMBER: case Types.BOOLEAN: if (debug) { console.info('%c' + value.toString(), 'font-weight:bold;'); } view['set' + type_name](cursor, value, endianness); cursor += unit; break; case Types.INT8ARRAY: case Types.INT16ARRAY: case Types.INT32ARRAY: case Types.UINT8ARRAY: case Types.UINT16ARRAY: case Types.UINT32ARRAY: case Types.FLOAT32ARRAY: case Types.FLOAT64ARRAY: var _view = new Uint8Array(view.buffer, cursor, byte_length); _view.set(new Uint8Array(value.buffer)); cursor += byte_length; break; case Types.ARRAYBUFFER: case Types.BUFFER: var _view = new Uint8Array(view.buffer, cursor, byte_length); _view.set(new Uint8Array(value)); cursor += byte_length; break; case Types.BLOB: case Types.ARRAY: case Types.OBJECT: break; default: throw 'TypeError: Unexpected type found.'; } if (debug) { binary_dump(view, start, cursor - start); } } return view; }; /** * Unpack binary data into an object with value and cursor * @param {DataView} view [description] * @param {Number} cursor [description] * @return {Object} */ var unpack = function(view, cursor) { var i = 0, endianness = BIG_ENDIAN, start = cursor; var type, length, byte_length, value, elem; // Retrieve "type" type = view.getUint8(cursor, endianness); cursor += TYPE_LENGTH; if (debug) { console.info('Unpacking', type, TypeNames[type]); } // Retrieve "length" if (type === Types.ARRAY || type === Types.OBJECT) { length = view.getUint16(cursor, endianness); cursor += LENGTH_LENGTH; if (debug) { console.info('Content Length', length); } } // Retrieve "byte_length" byte_length = view.getUint32(cursor, endianness); cursor += BYTES_LENGTH; if (debug) { console.info('Byte Length', byte_length, 'bytes'); } var type_name = Length[type]; var unit = type_name === null ? 0 : window[type_name + 'Array'].BYTES_PER_ELEMENT; switch (type) { case Types.NULL: case Types.UNDEFINED: if (debug) { binary_dump(view, start, cursor - start); } // NULL and UNDEFINED doesn't have any octet value = null; break; case Types.STRING: length = byte_length / unit; var string = []; for (i = 0; i < length; i++) { var code = view.getUint16(cursor, endianness); cursor += unit; string.push(String.fromCharCode(code)); } value = string.join(''); if (debug) { console.info('Actual Content %c"' + value + '"', 'font-weight:bold;'); binary_dump(view, start, cursor - start); } break; case Types.NUMBER: value = view.getFloat64(cursor, endianness); cursor += unit; if (debug) { console.info('Actual Content %c"' + value.toString() + '"', 'font-weight:bold;'); binary_dump(view, start, cursor - start); } break; case Types.BOOLEAN: value = view.getUint8(cursor, endianness) === 1 ? true : false; cursor += unit; if (debug) { console.info('Actual Content %c"' + value.toString() + '"', 'font-weight:bold;'); binary_dump(view, start, cursor - start); } break; case Types.INT8ARRAY: case Types.INT16ARRAY: case Types.INT32ARRAY: case Types.UINT8ARRAY: case Types.UINT16ARRAY: case Types.UINT32ARRAY: case Types.FLOAT32ARRAY: case Types.FLOAT64ARRAY: case Types.ARRAYBUFFER: elem = view.buffer.slice(cursor, cursor + byte_length); cursor += byte_length; // If ArrayBuffer if (type === Types.ARRAYBUFFER) { value = elem; // If other TypedArray } else { value = new window[type_name + 'Array'](elem); } if (debug) { binary_dump(view, start, cursor - start); } break; case Types.BLOB: if (debug) { binary_dump(view, start, cursor - start); } // If Blob is available (on browser) if (window.Blob) { var mime = unpack(view, cursor); var buffer = unpack(view, mime.cursor); cursor = buffer.cursor; value = new Blob([buffer.value], { type: mime.value }); } else { // node.js implementation goes here elem = view.buffer.slice(cursor, cursor + byte_length); cursor += byte_length; // node.js implementatino uses Buffer to help Blob value = new Buffer(elem); } break; case Types.ARRAY: if (debug) { binary_dump(view, start, cursor - start); } value = []; for (i = 0; i < length; i++) { // Retrieve array element elem = unpack(view, cursor); cursor = elem.cursor; value.push(elem.value); } break; case Types.OBJECT: if (debug) { binary_dump(view, start, cursor - start); } value = {}; for (i = 0; i < length; i++) { // Retrieve object key and value in sequence var key = unpack(view, cursor); var val = unpack(view, key.cursor); cursor = val.cursor; value[key.value] = val.value; } break; default: throw 'TypeError: Type not supported.'; } return { value: value, cursor: cursor }; }; /** * deferred function to process multiple serialization in order * @param {array} array [description] * @param {Function} callback [description] * @return {void} no return value */ var deferredSerialize = function(array, callback) { var length = array.length, results = [], count = 0, byte_length = 0; for (var i = 0; i < array.length; i++) { (function(index) { serialize(array[index], function(result) { // store results in order results[index] = result; // count byte length byte_length += result[0].header_size + result[0].byte_length; // when all results are on table if (++count === length) { // finally concatenate all reuslts into a single array in order var array = []; for (var j = 0; j < results.length; j++) { array = array.concat(results[j]); } callback(array, byte_length); } }); })(i); } }; /** * Serializes object and return byte_length * @param {mixed} obj JavaScript object you want to serialize * @return {Array} Serialized array object */ var serialize = function(obj, callback) { var subarray = [], unit = 1, header_size = TYPE_LENGTH + BYTES_LENGTH, type, byte_length = 0, length = 0, value = obj; type = find_type(obj); unit = Length[type] === undefined || Length[type] === null ? 0 : window[Length[type] + 'Array'].BYTES_PER_ELEMENT; switch (type) { case Types.UNDEFINED: case Types.NULL: break; case Types.NUMBER: case Types.BOOLEAN: byte_length = unit; break; case Types.STRING: length = obj.length; byte_length += length * unit; break; case Types.INT8ARRAY: case Types.INT16ARRAY: case Types.INT32ARRAY: case Types.UINT8ARRAY: case Types.UINT16ARRAY: case Types.UINT32ARRAY: case Types.FLOAT32ARRAY: case Types.FLOAT64ARRAY: length = obj.length; byte_length += length * unit; break; case Types.ARRAY: deferredSerialize(obj, function(subarray, byte_length) { callback([{ type: type, length: obj.length, header_size: header_size + LENGTH_LENGTH, byte_length: byte_length, value: null }].concat(subarray)); }); return; case Types.OBJECT: var deferred = []; for (var key in obj) { if (obj.hasOwnProperty(key)) { deferred.push(key); deferred.push(obj[key]); length++; } } deferredSerialize(deferred, function(subarray, byte_length) { callback([{ type: type, length: length, header_size: header_size + LENGTH_LENGTH, byte_length: byte_length, value: null }].concat(subarray)); }); return; case Types.ARRAYBUFFER: byte_length += obj.byteLength; break; case Types.BLOB: var mime_type = obj.type; var reader = new FileReader(); reader.onload = function(e) { deferredSerialize([mime_type, e.target.result], function(subarray, byte_length) { callback([{ type: type, length: length, header_size: header_size, byte_length: byte_length, value: null }].concat(subarray)); }); }; reader.onerror = function(e) { throw 'FileReader Error: ' + e; }; reader.readAsArrayBuffer(obj); return; case Types.BUFFER: byte_length += obj.length; break; default: throw 'TypeError: Type "' + obj.constructor.name + '" not supported.'; } callback([{ type: type, length: length, header_size: header_size, byte_length: byte_length, value: value }].concat(subarray)); }; /** * Deserialize binary and return JavaScript object * @param ArrayBuffer buffer ArrayBuffer you want to deserialize * @return mixed Retrieved JavaScript object */ var deserialize = function(buffer, callback) { var view = buffer instanceof DataView ? buffer : new DataView(buffer); var result = unpack(view, 0); return result.value; }; if (debug) { window.Test = { BIG_ENDIAN: BIG_ENDIAN, LITTLE_ENDIAN: LITTLE_ENDIAN, Types: Types, pack: pack, unpack: unpack, serialize: serialize, deserialize: deserialize }; } var binarize = { pack: function(obj, callback) { try { if (debug) console.info('%cPacking Start', 'font-weight: bold; color: red;', obj); serialize(obj, function(array) { if (debug) console.info('Serialized Object', array); callback(pack(array)); }); } catch (e) { throw e; } }, unpack: function(buffer, callback) { try { if (debug) console.info('%cUnpacking Start', 'font-weight: bold; color: red;', buffer); var result = deserialize(buffer); if (debug) console.info('Deserialized Object', result); callback(result); } catch (e) { throw e; } } }; ================================================ FILE: FileBufferReader/dev/common.js ================================================ function merge(mergein, mergeto) { if (!mergein) mergein = {}; if (!mergeto) return mergein; for (var item in mergeto) { try { mergein[item] = mergeto[item]; } catch (e) {} } return mergein; } ================================================ FILE: FileBufferReader/dev/head.js ================================================ (function() { ================================================ FILE: FileBufferReader/dev/tail.js ================================================ window.FileConverter = FileConverter; window.FileSelector = FileSelector; window.FileBufferReader = FileBufferReader; })(); ================================================ FILE: FileBufferReader/fbr-client/.gitignore ================================================ node_modules bower_components make-tar.sh *.tar.gz lib-cov .*.swp ._* .DS_Store .git .hg .npmrc .lock-wscript .svn .wafpickle-* config.gypi CVS npm-debug.log rmc-debug.log ================================================ FILE: FileBufferReader/fbr-client/.npmignore ================================================ node_modules lib-cov npm-debug.log ================================================ FILE: FileBufferReader/fbr-client/README.md ================================================ ## fbr-client [![npm](https://img.shields.io/npm/v/fbr-client.svg)](https://npmjs.org/package/fbr-client) [![downloads](https://img.shields.io/npm/dm/fbr-client.svg)](https://npmjs.org/package/fbr-client) Socket.io client for [FileBufferReader.js](https://github.com/muaz-khan/FileBufferReader). ```sh npm install fbr-client ``` Then run the server: ```sh cd ./node_modules/fbr-client/ node server.js ``` Then open: `http://localhost:9001/` or `http://local-ip:9001/`. # Change Port ```sh node server.js port=80 # or node server.js port=8888 ``` ## Credits [Muaz Khan](https://github.com/muaz-khan): 1. Personal Webpage: http://www.muazkhan.com 2. Email: muazkh@gmail.com 3. Twitter: https://twitter.com/muazkh and https://twitter.com/WebRTCWeb 4. Google+: https://plus.google.com/+WebRTC-Experiment 5. Facebook: https://www.facebook.com/WebRTC ## License [FileBufferReader.js](https://github.com/muaz-khan/FileBufferReader) is released under [MIT licence](https://www.webrtc-experiment.com/licence/) . Copyright (c) [Muaz Khan](https://plus.google.com/+MuazKhan). ================================================ FILE: FileBufferReader/fbr-client/demo/circular-progress-bar.css ================================================ .rect-auto, .c100.p51 .slice, .c100.p52 .slice, .c100.p53 .slice, .c100.p54 .slice, .c100.p55 .slice, .c100.p56 .slice, .c100.p57 .slice, .c100.p58 .slice, .c100.p59 .slice, .c100.p60 .slice, .c100.p61 .slice, .c100.p62 .slice, .c100.p63 .slice, .c100.p64 .slice, .c100.p65 .slice, .c100.p66 .slice, .c100.p67 .slice, .c100.p68 .slice, .c100.p69 .slice, .c100.p70 .slice, .c100.p71 .slice, .c100.p72 .slice, .c100.p73 .slice, .c100.p74 .slice, .c100.p75 .slice, .c100.p76 .slice, .c100.p77 .slice, .c100.p78 .slice, .c100.p79 .slice, .c100.p80 .slice, .c100.p81 .slice, .c100.p82 .slice, .c100.p83 .slice, .c100.p84 .slice, .c100.p85 .slice, .c100.p86 .slice, .c100.p87 .slice, .c100.p88 .slice, .c100.p89 .slice, .c100.p90 .slice, .c100.p91 .slice, .c100.p92 .slice, .c100.p93 .slice, .c100.p94 .slice, .c100.p95 .slice, .c100.p96 .slice, .c100.p97 .slice, .c100.p98 .slice, .c100.p99 .slice, .c100.p100 .slice { clip: rect(auto, auto, auto, auto); } .pie, .c100 .bar, .c100.p51 .fill, .c100.p52 .fill, .c100.p53 .fill, .c100.p54 .fill, .c100.p55 .fill, .c100.p56 .fill, .c100.p57 .fill, .c100.p58 .fill, .c100.p59 .fill, .c100.p60 .fill, .c100.p61 .fill, .c100.p62 .fill, .c100.p63 .fill, .c100.p64 .fill, .c100.p65 .fill, .c100.p66 .fill, .c100.p67 .fill, .c100.p68 .fill, .c100.p69 .fill, .c100.p70 .fill, .c100.p71 .fill, .c100.p72 .fill, .c100.p73 .fill, .c100.p74 .fill, .c100.p75 .fill, .c100.p76 .fill, .c100.p77 .fill, .c100.p78 .fill, .c100.p79 .fill, .c100.p80 .fill, .c100.p81 .fill, .c100.p82 .fill, .c100.p83 .fill, .c100.p84 .fill, .c100.p85 .fill, .c100.p86 .fill, .c100.p87 .fill, .c100.p88 .fill, .c100.p89 .fill, .c100.p90 .fill, .c100.p91 .fill, .c100.p92 .fill, .c100.p93 .fill, .c100.p94 .fill, .c100.p95 .fill, .c100.p96 .fill, .c100.p97 .fill, .c100.p98 .fill, .c100.p99 .fill, .c100.p100 .fill { position: absolute; border: 0.08em solid #307bbb; width: 0.84em; height: 0.84em; clip: rect(0em, 0.5em, 1em, 0em); border-radius: 50%; -webkit-transform: rotate(0deg); -moz-transform: rotate(0deg); -ms-transform: rotate(0deg); -o-transform: rotate(0deg); transform: rotate(0deg); } .pie-fill, .c100.p51 .bar:after, .c100.p51 .fill, .c100.p52 .bar:after, .c100.p52 .fill, .c100.p53 .bar:after, .c100.p53 .fill, .c100.p54 .bar:after, .c100.p54 .fill, .c100.p55 .bar:after, .c100.p55 .fill, .c100.p56 .bar:after, .c100.p56 .fill, .c100.p57 .bar:after, .c100.p57 .fill, .c100.p58 .bar:after, .c100.p58 .fill, .c100.p59 .bar:after, .c100.p59 .fill, .c100.p60 .bar:after, .c100.p60 .fill, .c100.p61 .bar:after, .c100.p61 .fill, .c100.p62 .bar:after, .c100.p62 .fill, .c100.p63 .bar:after, .c100.p63 .fill, .c100.p64 .bar:after, .c100.p64 .fill, .c100.p65 .bar:after, .c100.p65 .fill, .c100.p66 .bar:after, .c100.p66 .fill, .c100.p67 .bar:after, .c100.p67 .fill, .c100.p68 .bar:after, .c100.p68 .fill, .c100.p69 .bar:after, .c100.p69 .fill, .c100.p70 .bar:after, .c100.p70 .fill, .c100.p71 .bar:after, .c100.p71 .fill, .c100.p72 .bar:after, .c100.p72 .fill, .c100.p73 .bar:after, .c100.p73 .fill, .c100.p74 .bar:after, .c100.p74 .fill, .c100.p75 .bar:after, .c100.p75 .fill, .c100.p76 .bar:after, .c100.p76 .fill, .c100.p77 .bar:after, .c100.p77 .fill, .c100.p78 .bar:after, .c100.p78 .fill, .c100.p79 .bar:after, .c100.p79 .fill, .c100.p80 .bar:after, .c100.p80 .fill, .c100.p81 .bar:after, .c100.p81 .fill, .c100.p82 .bar:after, .c100.p82 .fill, .c100.p83 .bar:after, .c100.p83 .fill, .c100.p84 .bar:after, .c100.p84 .fill, .c100.p85 .bar:after, .c100.p85 .fill, .c100.p86 .bar:after, .c100.p86 .fill, .c100.p87 .bar:after, .c100.p87 .fill, .c100.p88 .bar:after, .c100.p88 .fill, .c100.p89 .bar:after, .c100.p89 .fill, .c100.p90 .bar:after, .c100.p90 .fill, .c100.p91 .bar:after, .c100.p91 .fill, .c100.p92 .bar:after, .c100.p92 .fill, .c100.p93 .bar:after, .c100.p93 .fill, .c100.p94 .bar:after, .c100.p94 .fill, .c100.p95 .bar:after, .c100.p95 .fill, .c100.p96 .bar:after, .c100.p96 .fill, .c100.p97 .bar:after, .c100.p97 .fill, .c100.p98 .bar:after, .c100.p98 .fill, .c100.p99 .bar:after, .c100.p99 .fill, .c100.p100 .bar:after, .c100.p100 .fill { -webkit-transform: rotate(180deg); -moz-transform: rotate(180deg); -ms-transform: rotate(180deg); -o-transform: rotate(180deg); transform: rotate(180deg); } .c100 { position: relative; font-size: 120px; width: 1em; height: 1em; border-radius: 50%; float: left; margin: 0 0.1em 0.1em 0; background-color: #cccccc; } .c100 *, .c100 *:before, .c100 *:after { -webkit-box-sizing: content-box; -moz-box-sizing: content-box; box-sizing: content-box; } .c100.center { float: none; margin: 0 auto; } .c100.big { font-size: 240px; } .c100.small { font-size: 80px; } .c100 > span { position: absolute; width: 100%; z-index: 1; left: 0; top: 0; width: 5em; line-height: 5em; font-size: 0.2em; color: #cccccc; display: block; text-align: center; white-space: nowrap; -webkit-transition-property: all; -moz-transition-property: all; -o-transition-property: all; transition-property: all; -webkit-transition-duration: 0.2s; -moz-transition-duration: 0.2s; -o-transition-duration: 0.2s; transition-duration: 0.2s; -webkit-transition-timing-function: ease-out; -moz-transition-timing-function: ease-out; -o-transition-timing-function: ease-out; transition-timing-function: ease-out; } .c100:after { position: absolute; top: 0.08em; left: 0.08em; display: block; content: " "; border-radius: 50%; background-color: #f5f5f5; width: 0.84em; height: 0.84em; -webkit-transition-property: all; -moz-transition-property: all; -o-transition-property: all; transition-property: all; -webkit-transition-duration: 0.2s; -moz-transition-duration: 0.2s; -o-transition-duration: 0.2s; transition-duration: 0.2s; -webkit-transition-timing-function: ease-in; -moz-transition-timing-function: ease-in; -o-transition-timing-function: ease-in; transition-timing-function: ease-in; } .c100 .slice { position: absolute; width: 1em; height: 1em; clip: rect(0em, 1em, 1em, 0.5em); } .c100.p1 .bar { -webkit-transform: rotate(3.6deg); -moz-transform: rotate(3.6deg); -ms-transform: rotate(3.6deg); -o-transform: rotate(3.6deg); transform: rotate(3.6deg); } .c100.p2 .bar { -webkit-transform: rotate(7.2deg); -moz-transform: rotate(7.2deg); -ms-transform: rotate(7.2deg); -o-transform: rotate(7.2deg); transform: rotate(7.2deg); } .c100.p3 .bar { -webkit-transform: rotate(10.8deg); -moz-transform: rotate(10.8deg); -ms-transform: rotate(10.8deg); -o-transform: rotate(10.8deg); transform: rotate(10.8deg); } .c100.p4 .bar { -webkit-transform: rotate(14.4deg); -moz-transform: rotate(14.4deg); -ms-transform: rotate(14.4deg); -o-transform: rotate(14.4deg); transform: rotate(14.4deg); } .c100.p5 .bar { -webkit-transform: rotate(18deg); -moz-transform: rotate(18deg); -ms-transform: rotate(18deg); -o-transform: rotate(18deg); transform: rotate(18deg); } .c100.p6 .bar { -webkit-transform: rotate(21.6deg); -moz-transform: rotate(21.6deg); -ms-transform: rotate(21.6deg); -o-transform: rotate(21.6deg); transform: rotate(21.6deg); } .c100.p7 .bar { -webkit-transform: rotate(25.2deg); -moz-transform: rotate(25.2deg); -ms-transform: rotate(25.2deg); -o-transform: rotate(25.2deg); transform: rotate(25.2deg); } .c100.p8 .bar { -webkit-transform: rotate(28.8deg); -moz-transform: rotate(28.8deg); -ms-transform: rotate(28.8deg); -o-transform: rotate(28.8deg); transform: rotate(28.8deg); } .c100.p9 .bar { -webkit-transform: rotate(32.4deg); -moz-transform: rotate(32.4deg); -ms-transform: rotate(32.4deg); -o-transform: rotate(32.4deg); transform: rotate(32.4deg); } .c100.p10 .bar { -webkit-transform: rotate(36deg); -moz-transform: rotate(36deg); -ms-transform: rotate(36deg); -o-transform: rotate(36deg); transform: rotate(36deg); } .c100.p11 .bar { -webkit-transform: rotate(39.6deg); -moz-transform: rotate(39.6deg); -ms-transform: rotate(39.6deg); -o-transform: rotate(39.6deg); transform: rotate(39.6deg); } .c100.p12 .bar { -webkit-transform: rotate(43.2deg); -moz-transform: rotate(43.2deg); -ms-transform: rotate(43.2deg); -o-transform: rotate(43.2deg); transform: rotate(43.2deg); } .c100.p13 .bar { -webkit-transform: rotate(46.800000000000004deg); -moz-transform: rotate(46.800000000000004deg); -ms-transform: rotate(46.800000000000004deg); -o-transform: rotate(46.800000000000004deg); transform: rotate(46.800000000000004deg); } .c100.p14 .bar { -webkit-transform: rotate(50.4deg); -moz-transform: rotate(50.4deg); -ms-transform: rotate(50.4deg); -o-transform: rotate(50.4deg); transform: rotate(50.4deg); } .c100.p15 .bar { -webkit-transform: rotate(54deg); -moz-transform: rotate(54deg); -ms-transform: rotate(54deg); -o-transform: rotate(54deg); transform: rotate(54deg); } .c100.p16 .bar { -webkit-transform: rotate(57.6deg); -moz-transform: rotate(57.6deg); -ms-transform: rotate(57.6deg); -o-transform: rotate(57.6deg); transform: rotate(57.6deg); } .c100.p17 .bar { -webkit-transform: rotate(61.2deg); -moz-transform: rotate(61.2deg); -ms-transform: rotate(61.2deg); -o-transform: rotate(61.2deg); transform: rotate(61.2deg); } .c100.p18 .bar { -webkit-transform: rotate(64.8deg); -moz-transform: rotate(64.8deg); -ms-transform: rotate(64.8deg); -o-transform: rotate(64.8deg); transform: rotate(64.8deg); } .c100.p19 .bar { -webkit-transform: rotate(68.4deg); -moz-transform: rotate(68.4deg); -ms-transform: rotate(68.4deg); -o-transform: rotate(68.4deg); transform: rotate(68.4deg); } .c100.p20 .bar { -webkit-transform: rotate(72deg); -moz-transform: rotate(72deg); -ms-transform: rotate(72deg); -o-transform: rotate(72deg); transform: rotate(72deg); } .c100.p21 .bar { -webkit-transform: rotate(75.60000000000001deg); -moz-transform: rotate(75.60000000000001deg); -ms-transform: rotate(75.60000000000001deg); -o-transform: rotate(75.60000000000001deg); transform: rotate(75.60000000000001deg); } .c100.p22 .bar { -webkit-transform: rotate(79.2deg); -moz-transform: rotate(79.2deg); -ms-transform: rotate(79.2deg); -o-transform: rotate(79.2deg); transform: rotate(79.2deg); } .c100.p23 .bar { -webkit-transform: rotate(82.8deg); -moz-transform: rotate(82.8deg); -ms-transform: rotate(82.8deg); -o-transform: rotate(82.8deg); transform: rotate(82.8deg); } .c100.p24 .bar { -webkit-transform: rotate(86.4deg); -moz-transform: rotate(86.4deg); -ms-transform: rotate(86.4deg); -o-transform: rotate(86.4deg); transform: rotate(86.4deg); } .c100.p25 .bar { -webkit-transform: rotate(90deg); -moz-transform: rotate(90deg); -ms-transform: rotate(90deg); -o-transform: rotate(90deg); transform: rotate(90deg); } .c100.p26 .bar { -webkit-transform: rotate(93.60000000000001deg); -moz-transform: rotate(93.60000000000001deg); -ms-transform: rotate(93.60000000000001deg); -o-transform: rotate(93.60000000000001deg); transform: rotate(93.60000000000001deg); } .c100.p27 .bar { -webkit-transform: rotate(97.2deg); -moz-transform: rotate(97.2deg); -ms-transform: rotate(97.2deg); -o-transform: rotate(97.2deg); transform: rotate(97.2deg); } .c100.p28 .bar { -webkit-transform: rotate(100.8deg); -moz-transform: rotate(100.8deg); -ms-transform: rotate(100.8deg); -o-transform: rotate(100.8deg); transform: rotate(100.8deg); } .c100.p29 .bar { -webkit-transform: rotate(104.4deg); -moz-transform: rotate(104.4deg); -ms-transform: rotate(104.4deg); -o-transform: rotate(104.4deg); transform: rotate(104.4deg); } .c100.p30 .bar { -webkit-transform: rotate(108deg); -moz-transform: rotate(108deg); -ms-transform: rotate(108deg); -o-transform: rotate(108deg); transform: rotate(108deg); } .c100.p31 .bar { -webkit-transform: rotate(111.60000000000001deg); -moz-transform: rotate(111.60000000000001deg); -ms-transform: rotate(111.60000000000001deg); -o-transform: rotate(111.60000000000001deg); transform: rotate(111.60000000000001deg); } .c100.p32 .bar { -webkit-transform: rotate(115.2deg); -moz-transform: rotate(115.2deg); -ms-transform: rotate(115.2deg); -o-transform: rotate(115.2deg); transform: rotate(115.2deg); } .c100.p33 .bar { -webkit-transform: rotate(118.8deg); -moz-transform: rotate(118.8deg); -ms-transform: rotate(118.8deg); -o-transform: rotate(118.8deg); transform: rotate(118.8deg); } .c100.p34 .bar { -webkit-transform: rotate(122.4deg); -moz-transform: rotate(122.4deg); -ms-transform: rotate(122.4deg); -o-transform: rotate(122.4deg); transform: rotate(122.4deg); } .c100.p35 .bar { -webkit-transform: rotate(126deg); -moz-transform: rotate(126deg); -ms-transform: rotate(126deg); -o-transform: rotate(126deg); transform: rotate(126deg); } .c100.p36 .bar { -webkit-transform: rotate(129.6deg); -moz-transform: rotate(129.6deg); -ms-transform: rotate(129.6deg); -o-transform: rotate(129.6deg); transform: rotate(129.6deg); } .c100.p37 .bar { -webkit-transform: rotate(133.20000000000002deg); -moz-transform: rotate(133.20000000000002deg); -ms-transform: rotate(133.20000000000002deg); -o-transform: rotate(133.20000000000002deg); transform: rotate(133.20000000000002deg); } .c100.p38 .bar { -webkit-transform: rotate(136.8deg); -moz-transform: rotate(136.8deg); -ms-transform: rotate(136.8deg); -o-transform: rotate(136.8deg); transform: rotate(136.8deg); } .c100.p39 .bar { -webkit-transform: rotate(140.4deg); -moz-transform: rotate(140.4deg); -ms-transform: rotate(140.4deg); -o-transform: rotate(140.4deg); transform: rotate(140.4deg); } .c100.p40 .bar { -webkit-transform: rotate(144deg); -moz-transform: rotate(144deg); -ms-transform: rotate(144deg); -o-transform: rotate(144deg); transform: rotate(144deg); } .c100.p41 .bar { -webkit-transform: rotate(147.6deg); -moz-transform: rotate(147.6deg); -ms-transform: rotate(147.6deg); -o-transform: rotate(147.6deg); transform: rotate(147.6deg); } .c100.p42 .bar { -webkit-transform: rotate(151.20000000000002deg); -moz-transform: rotate(151.20000000000002deg); -ms-transform: rotate(151.20000000000002deg); -o-transform: rotate(151.20000000000002deg); transform: rotate(151.20000000000002deg); } .c100.p43 .bar { -webkit-transform: rotate(154.8deg); -moz-transform: rotate(154.8deg); -ms-transform: rotate(154.8deg); -o-transform: rotate(154.8deg); transform: rotate(154.8deg); } .c100.p44 .bar { -webkit-transform: rotate(158.4deg); -moz-transform: rotate(158.4deg); -ms-transform: rotate(158.4deg); -o-transform: rotate(158.4deg); transform: rotate(158.4deg); } .c100.p45 .bar { -webkit-transform: rotate(162deg); -moz-transform: rotate(162deg); -ms-transform: rotate(162deg); -o-transform: rotate(162deg); transform: rotate(162deg); } .c100.p46 .bar { -webkit-transform: rotate(165.6deg); -moz-transform: rotate(165.6deg); -ms-transform: rotate(165.6deg); -o-transform: rotate(165.6deg); transform: rotate(165.6deg); } .c100.p47 .bar { -webkit-transform: rotate(169.20000000000002deg); -moz-transform: rotate(169.20000000000002deg); -ms-transform: rotate(169.20000000000002deg); -o-transform: rotate(169.20000000000002deg); transform: rotate(169.20000000000002deg); } .c100.p48 .bar { -webkit-transform: rotate(172.8deg); -moz-transform: rotate(172.8deg); -ms-transform: rotate(172.8deg); -o-transform: rotate(172.8deg); transform: rotate(172.8deg); } .c100.p49 .bar { -webkit-transform: rotate(176.4deg); -moz-transform: rotate(176.4deg); -ms-transform: rotate(176.4deg); -o-transform: rotate(176.4deg); transform: rotate(176.4deg); } .c100.p50 .bar { -webkit-transform: rotate(180deg); -moz-transform: rotate(180deg); -ms-transform: rotate(180deg); -o-transform: rotate(180deg); transform: rotate(180deg); } .c100.p51 .bar { -webkit-transform: rotate(183.6deg); -moz-transform: rotate(183.6deg); -ms-transform: rotate(183.6deg); -o-transform: rotate(183.6deg); transform: rotate(183.6deg); } .c100.p52 .bar { -webkit-transform: rotate(187.20000000000002deg); -moz-transform: rotate(187.20000000000002deg); -ms-transform: rotate(187.20000000000002deg); -o-transform: rotate(187.20000000000002deg); transform: rotate(187.20000000000002deg); } .c100.p53 .bar { -webkit-transform: rotate(190.8deg); -moz-transform: rotate(190.8deg); -ms-transform: rotate(190.8deg); -o-transform: rotate(190.8deg); transform: rotate(190.8deg); } .c100.p54 .bar { -webkit-transform: rotate(194.4deg); -moz-transform: rotate(194.4deg); -ms-transform: rotate(194.4deg); -o-transform: rotate(194.4deg); transform: rotate(194.4deg); } .c100.p55 .bar { -webkit-transform: rotate(198deg); -moz-transform: rotate(198deg); -ms-transform: rotate(198deg); -o-transform: rotate(198deg); transform: rotate(198deg); } .c100.p56 .bar { -webkit-transform: rotate(201.6deg); -moz-transform: rotate(201.6deg); -ms-transform: rotate(201.6deg); -o-transform: rotate(201.6deg); transform: rotate(201.6deg); } .c100.p57 .bar { -webkit-transform: rotate(205.20000000000002deg); -moz-transform: rotate(205.20000000000002deg); -ms-transform: rotate(205.20000000000002deg); -o-transform: rotate(205.20000000000002deg); transform: rotate(205.20000000000002deg); } .c100.p58 .bar { -webkit-transform: rotate(208.8deg); -moz-transform: rotate(208.8deg); -ms-transform: rotate(208.8deg); -o-transform: rotate(208.8deg); transform: rotate(208.8deg); } .c100.p59 .bar { -webkit-transform: rotate(212.4deg); -moz-transform: rotate(212.4deg); -ms-transform: rotate(212.4deg); -o-transform: rotate(212.4deg); transform: rotate(212.4deg); } .c100.p60 .bar { -webkit-transform: rotate(216deg); -moz-transform: rotate(216deg); -ms-transform: rotate(216deg); -o-transform: rotate(216deg); transform: rotate(216deg); } .c100.p61 .bar { -webkit-transform: rotate(219.6deg); -moz-transform: rotate(219.6deg); -ms-transform: rotate(219.6deg); -o-transform: rotate(219.6deg); transform: rotate(219.6deg); } .c100.p62 .bar { -webkit-transform: rotate(223.20000000000002deg); -moz-transform: rotate(223.20000000000002deg); -ms-transform: rotate(223.20000000000002deg); -o-transform: rotate(223.20000000000002deg); transform: rotate(223.20000000000002deg); } .c100.p63 .bar { -webkit-transform: rotate(226.8deg); -moz-transform: rotate(226.8deg); -ms-transform: rotate(226.8deg); -o-transform: rotate(226.8deg); transform: rotate(226.8deg); } .c100.p64 .bar { -webkit-transform: rotate(230.4deg); -moz-transform: rotate(230.4deg); -ms-transform: rotate(230.4deg); -o-transform: rotate(230.4deg); transform: rotate(230.4deg); } .c100.p65 .bar { -webkit-transform: rotate(234deg); -moz-transform: rotate(234deg); -ms-transform: rotate(234deg); -o-transform: rotate(234deg); transform: rotate(234deg); } .c100.p66 .bar { -webkit-transform: rotate(237.6deg); -moz-transform: rotate(237.6deg); -ms-transform: rotate(237.6deg); -o-transform: rotate(237.6deg); transform: rotate(237.6deg); } .c100.p67 .bar { -webkit-transform: rotate(241.20000000000002deg); -moz-transform: rotate(241.20000000000002deg); -ms-transform: rotate(241.20000000000002deg); -o-transform: rotate(241.20000000000002deg); transform: rotate(241.20000000000002deg); } .c100.p68 .bar { -webkit-transform: rotate(244.8deg); -moz-transform: rotate(244.8deg); -ms-transform: rotate(244.8deg); -o-transform: rotate(244.8deg); transform: rotate(244.8deg); } .c100.p69 .bar { -webkit-transform: rotate(248.4deg); -moz-transform: rotate(248.4deg); -ms-transform: rotate(248.4deg); -o-transform: rotate(248.4deg); transform: rotate(248.4deg); } .c100.p70 .bar { -webkit-transform: rotate(252deg); -moz-transform: rotate(252deg); -ms-transform: rotate(252deg); -o-transform: rotate(252deg); transform: rotate(252deg); } .c100.p71 .bar { -webkit-transform: rotate(255.6deg); -moz-transform: rotate(255.6deg); -ms-transform: rotate(255.6deg); -o-transform: rotate(255.6deg); transform: rotate(255.6deg); } .c100.p72 .bar { -webkit-transform: rotate(259.2deg); -moz-transform: rotate(259.2deg); -ms-transform: rotate(259.2deg); -o-transform: rotate(259.2deg); transform: rotate(259.2deg); } .c100.p73 .bar { -webkit-transform: rotate(262.8deg); -moz-transform: rotate(262.8deg); -ms-transform: rotate(262.8deg); -o-transform: rotate(262.8deg); transform: rotate(262.8deg); } .c100.p74 .bar { -webkit-transform: rotate(266.40000000000003deg); -moz-transform: rotate(266.40000000000003deg); -ms-transform: rotate(266.40000000000003deg); -o-transform: rotate(266.40000000000003deg); transform: rotate(266.40000000000003deg); } .c100.p75 .bar { -webkit-transform: rotate(270deg); -moz-transform: rotate(270deg); -ms-transform: rotate(270deg); -o-transform: rotate(270deg); transform: rotate(270deg); } .c100.p76 .bar { -webkit-transform: rotate(273.6deg); -moz-transform: rotate(273.6deg); -ms-transform: rotate(273.6deg); -o-transform: rotate(273.6deg); transform: rotate(273.6deg); } .c100.p77 .bar { -webkit-transform: rotate(277.2deg); -moz-transform: rotate(277.2deg); -ms-transform: rotate(277.2deg); -o-transform: rotate(277.2deg); transform: rotate(277.2deg); } .c100.p78 .bar { -webkit-transform: rotate(280.8deg); -moz-transform: rotate(280.8deg); -ms-transform: rotate(280.8deg); -o-transform: rotate(280.8deg); transform: rotate(280.8deg); } .c100.p79 .bar { -webkit-transform: rotate(284.40000000000003deg); -moz-transform: rotate(284.40000000000003deg); -ms-transform: rotate(284.40000000000003deg); -o-transform: rotate(284.40000000000003deg); transform: rotate(284.40000000000003deg); } .c100.p80 .bar { -webkit-transform: rotate(288deg); -moz-transform: rotate(288deg); -ms-transform: rotate(288deg); -o-transform: rotate(288deg); transform: rotate(288deg); } .c100.p81 .bar { -webkit-transform: rotate(291.6deg); -moz-transform: rotate(291.6deg); -ms-transform: rotate(291.6deg); -o-transform: rotate(291.6deg); transform: rotate(291.6deg); } .c100.p82 .bar { -webkit-transform: rotate(295.2deg); -moz-transform: rotate(295.2deg); -ms-transform: rotate(295.2deg); -o-transform: rotate(295.2deg); transform: rotate(295.2deg); } .c100.p83 .bar { -webkit-transform: rotate(298.8deg); -moz-transform: rotate(298.8deg); -ms-transform: rotate(298.8deg); -o-transform: rotate(298.8deg); transform: rotate(298.8deg); } .c100.p84 .bar { -webkit-transform: rotate(302.40000000000003deg); -moz-transform: rotate(302.40000000000003deg); -ms-transform: rotate(302.40000000000003deg); -o-transform: rotate(302.40000000000003deg); transform: rotate(302.40000000000003deg); } .c100.p85 .bar { -webkit-transform: rotate(306deg); -moz-transform: rotate(306deg); -ms-transform: rotate(306deg); -o-transform: rotate(306deg); transform: rotate(306deg); } .c100.p86 .bar { -webkit-transform: rotate(309.6deg); -moz-transform: rotate(309.6deg); -ms-transform: rotate(309.6deg); -o-transform: rotate(309.6deg); transform: rotate(309.6deg); } .c100.p87 .bar { -webkit-transform: rotate(313.2deg); -moz-transform: rotate(313.2deg); -ms-transform: rotate(313.2deg); -o-transform: rotate(313.2deg); transform: rotate(313.2deg); } .c100.p88 .bar { -webkit-transform: rotate(316.8deg); -moz-transform: rotate(316.8deg); -ms-transform: rotate(316.8deg); -o-transform: rotate(316.8deg); transform: rotate(316.8deg); } .c100.p89 .bar { -webkit-transform: rotate(320.40000000000003deg); -moz-transform: rotate(320.40000000000003deg); -ms-transform: rotate(320.40000000000003deg); -o-transform: rotate(320.40000000000003deg); transform: rotate(320.40000000000003deg); } .c100.p90 .bar { -webkit-transform: rotate(324deg); -moz-transform: rotate(324deg); -ms-transform: rotate(324deg); -o-transform: rotate(324deg); transform: rotate(324deg); } .c100.p91 .bar { -webkit-transform: rotate(327.6deg); -moz-transform: rotate(327.6deg); -ms-transform: rotate(327.6deg); -o-transform: rotate(327.6deg); transform: rotate(327.6deg); } .c100.p92 .bar { -webkit-transform: rotate(331.2deg); -moz-transform: rotate(331.2deg); -ms-transform: rotate(331.2deg); -o-transform: rotate(331.2deg); transform: rotate(331.2deg); } .c100.p93 .bar { -webkit-transform: rotate(334.8deg); -moz-transform: rotate(334.8deg); -ms-transform: rotate(334.8deg); -o-transform: rotate(334.8deg); transform: rotate(334.8deg); } .c100.p94 .bar { -webkit-transform: rotate(338.40000000000003deg); -moz-transform: rotate(338.40000000000003deg); -ms-transform: rotate(338.40000000000003deg); -o-transform: rotate(338.40000000000003deg); transform: rotate(338.40000000000003deg); } .c100.p95 .bar { -webkit-transform: rotate(342deg); -moz-transform: rotate(342deg); -ms-transform: rotate(342deg); -o-transform: rotate(342deg); transform: rotate(342deg); } .c100.p96 .bar { -webkit-transform: rotate(345.6deg); -moz-transform: rotate(345.6deg); -ms-transform: rotate(345.6deg); -o-transform: rotate(345.6deg); transform: rotate(345.6deg); } .c100.p97 .bar { -webkit-transform: rotate(349.2deg); -moz-transform: rotate(349.2deg); -ms-transform: rotate(349.2deg); -o-transform: rotate(349.2deg); transform: rotate(349.2deg); } .c100.p98 .bar { -webkit-transform: rotate(352.8deg); -moz-transform: rotate(352.8deg); -ms-transform: rotate(352.8deg); -o-transform: rotate(352.8deg); transform: rotate(352.8deg); } .c100.p99 .bar { -webkit-transform: rotate(356.40000000000003deg); -moz-transform: rotate(356.40000000000003deg); -ms-transform: rotate(356.40000000000003deg); -o-transform: rotate(356.40000000000003deg); transform: rotate(356.40000000000003deg); } .c100.p100 .bar { -webkit-transform: rotate(360deg); -moz-transform: rotate(360deg); -ms-transform: rotate(360deg); -o-transform: rotate(360deg); transform: rotate(360deg); } .c100:hover { cursor: default; } .c100:hover > span { width: 3.33em; line-height: 3.33em; font-size: 0.3em; color: #307bbb; } .c100:hover:after { top: 0.04em; left: 0.04em; width: 0.92em; height: 0.92em; } .c100.dark { background-color: #777777; } .c100.dark .bar, .c100.dark .fill { border-color: #c6ff00 !important; } .c100.dark > span { color: #777777; } .c100.dark:after { background-color: #666666; } .c100.dark:hover > span { color: #c6ff00; } .c100.green .bar, .c100.green .fill { border-color: #4db53c !important; } .c100.green:hover > span { color: #4db53c; } .c100.green.dark .bar, .c100.green.dark .fill { border-color: #5fd400 !important; } .c100.green.dark:hover > span { color: #5fd400; } .c100.orange .bar, .c100.orange .fill { border-color: #dd9d22 !important; } .c100.orange:hover > span { color: #dd9d22; } .c100.orange.dark .bar, .c100.orange.dark .fill { border-color: #e08833 !important; } .c100.orange.dark:hover > span { color: #e08833; } ================================================ FILE: FileBufferReader/fbr-client/demo/style.css ================================================ @import url(https://fonts.googleapis.com/css?family=Open+Sans);h1,h2,strong{font-weight:400}.gh-btn,.gh-count,.td-style2,a,footer a{text-decoration:none}html{background:#eee}body{font-family:'-apple-system',BlinkMacSystemFont,'Segoe UI','Open Sans',Myriad,Arial;font-size:1.2em;line-height:1.5em;margin:0}article,footer{display:block;max-width:900px;min-width:360px;width:80%}article{background:#fff;border:1px solid;border-color:#ddd #aaa #aaa #ddd;margin:2.5em auto 0;padding:2em}h1{margin-top:0}article p:first-of-type{margin-top:1.6em}article p:last-child{margin-bottom:0}footer{margin:0 auto 2em;text-align:center}footer a{color:#666;font-size:inherit;padding:1em;text-shadow:0 1px 1px #fff}footer a:focus,footer a:hover{color:#111}h1,h2{border-bottom:1px solid #bdbdbd;display:inline;line-height:36px;padding:0 0 3px}a{color:#2844FA}a:focus,a:hover{color:#1B29A4}a:active{color:#000}:-moz-any-link:focus{border:0;color:#000}::selection{background:#ccc}::-moz-selection{background:#ccc}button,input[type=button]{-moz-border-radius:3px;-moz-transition:none;-webkit-transition:none;background:#0370ea;background:-moz-linear-gradient(top,#008dfd 0,#0370ea 100%);background:-webkit-linear-gradient(top,#008dfd 0,#0370ea 100%);border:1px solid #076bd2;border-radius:3px;color:#fff;display:inline-block;font-family:inherit;line-height:1.3;padding:5px 12px;text-align:center;text-shadow:1px 1px 1px #076bd2;font-size:1.5em}button:hover,input[type=button]:hover{background:#0993f0}button:active,input[type=button]:active{background:#0a76be}button[disabled],input[type=button][disabled]{background:0 0;border:1px solid #bbb5b5;color:gray;text-shadow:none}strong{color:#cc0e0e;font-family:inherit}td,th,tr{vertical-align:top;padding:.7em 1.4em;border-top:1px dotted #BBA9A9;border-right:1px dotted #BBA9A9}.gh-ico,.gravatar{vertical-align:middle}.table-style,.tr-style{border-top-style:solid;border-top-color:#ccc}table,tbody,td,tr{width:100%!important}.table-style{border-collapse:collapse;border-spacing:0;margin-top:0;margin-bottom:16px;display:block;width:728px;overflow:auto;word-break:normal;color:#333;font-family:'Helvetica Neue',Helvetica,'Segoe UI',Arial,freesans,sans-serif;font-size:16px;line-height:25.6px}.tr-style{border-top-width:1px;background-color:#f8f8f8}.td-style{padding:6px 13px;border:1px solid #ddd}.td-style2{color:#4183c4;background:0 0}.logo img{border-radius:50%;box-shadow:0 0 5px #000,0 0 5px #000,0 0 5px #000,0 0 5px #000,0 0 5px #000}.experiment{border:1px solid #bdbdbd;margin:1em 3em;border-radius:.2em;text-align:left}.experiment .header{padding:.2em .4em}.experiment .description{padding:.8em 1.4em}ol{margin-left:1em}pre{border-left:2px solid red;margin-left:2em;padding-left:1em;overflow:auto}.commit{font-size:.8em;margin:1em .6em;padding:8px 8px 0;background:#e6f1f6;border:1px solid #c5d5dd;border-radius:4px}.commit-desc{display:block;margin:-5px 0 10px}.commit-desc img{max-width:100%}.commit-meta{margin-left:-8px;width:100%;padding:8px;background:#fff;border-top:1px solid #d8e6ec;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.fork-left,.fork-right,.gh-btn,.gh-ico{background-repeat:no-repeat}.authorship{margin-top:-2px;margin-left:-4px;margin-bottom:-4px;font-size:14px;color:#999}.gravatar{margin-top:-2px;margin-right:3px;border-radius:3px}.author-name{color:#444}.commit-url{float:right;margin-left:15px;color:#888;font-size:12px}.dim{color:#dfdfdf}.roshan{color:red}.github-stargazers{position:absolute;right:14%;top:8%;font:700 11px/14px "Helvetica Neue",Helvetica,Arial,sans-serif;text-rendering:optimizeLegibility;overflow:hidden}.github-btn{height:20px;overflow:hidden}.gh-btn,.gh-count,.gh-ico{float:left;margin-left:5px}.gh-btn,.gh-count{padding:2px 5px 2px 4px;color:#555;text-shadow:0 1px 0 #fff;white-space:nowrap;cursor:pointer;border-radius:3px}.gh-btn{background-color:#e6e6e6;background-image:-webkit-gradient(linear,0 0,0 100%,from(#fafafa),to(#eaeaea));background-image:-webkit-linear-gradient(#fafafa,#eaeaea);background-image:-moz-linear-gradient(top,#fafafa,#eaeaea);background-image:-ms-linear-gradient(#fafafa,#eaeaea);background-image:-o-linear-gradient(#fafafa,#eaeaea);background-image:linear-gradient(#fafafa,#eaeaea);border:1px solid #d4d4d4;border-bottom-color:#bcbcbc}.gh-btn:active,.gh-btn:focus,.gh-btn:hover{color:#fff;text-decoration:none;text-shadow:0 -1px 0 rgba(0,0,0,.25);border-color:#518cc6 #518cc6 #2a65a0;background-color:#3072b3}.gh-btn:focus,.gh-btn:hover{background-image:-webkit-gradient(linear,0 0,0 100%,from(#599bdc),to(#3072b3));background-image:-webkit-linear-gradient(#599bdc,#3072b3);background-image:-moz-linear-gradient(top,#599bdc,#3072b3);background-image:-ms-linear-gradient(#599bdc,#3072b3);background-image:-o-linear-gradient(#599bdc,#3072b3);background-image:linear-gradient(#599bdc,#3072b3)}.gh-btn:active{background-image:none;-webkit-box-shadow:inset 0 2px 5px rgba(0,0,0,.1);-moz-box-shadow:inset 0 2px 5px rgba(0,0,0,.1);box-shadow:inset 0 2px 5px rgba(0,0,0,.1)}.gh-ico{width:14px;height:15px;margin-top:-1px;margin-right:4px;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAtCAQAAABGtvB0AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAB7RJREFUWMPt12tQVPcZx/HHGw0VG6yo1Y42YGIbjamT6JhEbc1AUodaJNbnsNwsFRQUsUSQQUEUNILGotFITTA2olVCI7FoiLdquOgEcFBAQS5Z5bLcXFZcdvfs7ZxfX+yqoLvQ6btO+5w3e3bOdz87+9/5n12i/3RGkSfNoV/RQppDnjTq3yjYg9O4kg2s50pOY48hg/E+v63NNtXIomww1dRmey+hCUMRywVthDKntKy8rDynNEIp9LEwaDAhL0XWohzRWIRFiEa53HdqK00cjBAEU16N9RD8MRuz4W899GWNYOQgp4FLfopsvJs4Zj79jKbRdPIas6AxURYLUukHzoiJAfqz1bsPsoq38G4+xLu4a+en528GiDzFcfGnuZIOIU0Jorr8SM3JhoKqk6YH9akQJEPSAifIij9vuo930rMYT46kfCxK7g77i+Oi7oh4hejqLvSb6uM0QrxQf8IJsrItv4AorLk/ojDx6NOnwrocF1qlOoRIq+yPWI07x/cK+lYniEI6H0IkSP0RRuys4uWC7LiQzcWvkYtsxYCp/GXhDFlyiuxcwhPDjQORfd7JvoGSM+SCb+lUa8dA5M6cc0slkxMkWpewJXNWfkWA/IRI78z2iUuP0jkujA1l2xqn1W+ApZ9xHL+4mWFUOkH2V0eVn5iR9mlb6VGlAEaK+kalnIypa69n1jouTLs7r6bNbN72/rs1ByEDPUV4C8PIo/Oqcb8TpCE+0LQ6cveRkMKIpmBrhBh7DzMxjP0VlltbHBeYJOvO7mhJMp7VVUl6Y8fD74ho4snNsogXnCAYd/amYMrMunhsW/06bXxXch0RBwni11X4CTlrgmXjhV3HVnec6WvqrWj/hl4vSJUNCCbnA5/CqgDxD5XrGyO061VRbVwRYCysgg8N1gRCpy/vKTO0aaq0tWI19AiiwQfeqiuZFZH3Ay2BlqiefTdU38KbhmqmIB3V0EOPaqRjylDXExEmYBU+wzmcw2dYhaF21P/P//yMpMn0Cr1BC2khvUGv0GQaOUTBY3kNn2Yl93EfK/k0r+Gxg1w+nDzn+17cqyo1tFsNVoOhXVV6ce98X/Kk4c4AV94u6GwbZKg51Gx7JOh4B7s6DFynL6jMsRrsG6QGGvudxXDj2PQF5KhhL+EWQyHtaS+pNhSjAAW64pLqPe0KiSHU8ovPEpHLtUoAJhyGL0YTEcENvsiGCdDeixaeYfhFoYuRrL5Xio2Yh+eIiOCKeYhvKU1RM4Tup5jhsctMPYBcmDv3qTUY+de51q8BkyZ2GY0Y8EEp6hkHWjs/ilvFPxqAu69f27I/q4WhaGK3J8/P/7n2HoB9yS/nprz2G3qBvGgGzaTp5PXm4q+2fzAbHwK6Fp9Z/V4qKJWxo0uOWb2aIfRyCqfzCc7jTzhDeMhYvQFRGR2MoI8eB6OuHwbkPAyrXwdY+iqOVP2t+VLrlYYzVScsOqAxkUjKAW5/QS6P3u04hRhmup+OYemZA2/BtmNHNlF36gpzgJkn2Yq4GVa9VQ13ojsJcDA3dxHBXdJIpqQ5diQ8hnHkNtyI0g47QqLLieD2+W3Gym22omwroN9KRCOufewIUZXSWCIxCajea0eiyhgVG4jYTWFwhDDYm+hmjICoGlvRVQJgGlHCZIseDudyEBGmQlZX2JGVPREiJhNFejsh8H4WESZEGlbobYW+1dhBRHR7MZzMvUwiIrHVpLEjgZZYNRHRvnBnyNYzRERxnQxbIYnaKiKidqdI18dERL0VsBekkGNVRESn/ZwhmV8QEW1ofoTIFk0ljSWPU3OdId+nkgd5qMsfI+HGMB37sH9CeJjJMZJ2nP3Y748Pw+w/3cxdolrpZ30P/nK3EyURfr2/N3Ra1HZkcwfj89AHb2PBtZIQy7NERgeC8NbVpQI2dtsK3T+B/CVwoR+3L0avA+IoEVHaXMj6a3bk6DnG+j0YyYvzlnVezPk+URNqp9bqMzqLq7GJiChiK+NQsX3h1wLlWTSy9b3EgMJp2CRftvTZXt3UiBwsISKiEWUHAHGzHakNDrIG9fLzuUEK5fb5CNYcXCnakEM3sAlvEhHxmBCNQrq9xlZggqw3ad6dh1fNyoRQennhr433bUjN4z8bb78uqmUzJttP4Z7dyAjMg1fud0IvHxduBJsZa/UrzBF3HyWBxxj7mzHu0bmUBjRfIi8pUuptL9TeseoAUWl9oK2zX+Cp/AaQnmxEROqoGB2Ddxn9Dt+JUkU+SOpmJLYmd0T1EBHxME5jROvUcU8KuMk1QNXJsa+atuG6pV5TAmiK1N/qG4nIxWVW5VFAqsWYfghclXlhJobwj4YYfHLxUnwTI74prnGNhogn8VeMMFPTKfyw//4MT7kbUJX+bim9VBSuKQI0RZqiviZ6yd9fVQLI3Xj6HoRJzedj+hiCng/E5mxsYCTWxTeGGvmAoGOs0929gJ/S042nXA1Yxbr8qhPtpUDblY5r5od1+VYDIN/CNHp2MEl3NKsl0MpgCDIj2L74gVJWi/bY4wUc2IzGh7DdfiXAorV/gUXsgRs5HjyHKPXl3MbknpVGAYIcbkzuyW1UX8EauJLTwXjEohAqyJDQhkLEYjwNPnDHcmTgS1zGZfwdGVgOd/pvmX8Bbv8r+TZ9z+kAAAAASUVORK5CYII=);background-position:0 0}.gh-btn:active .gh-ico,.gh-btn:focus .gh-ico,.gh-btn:hover .gh-ico{background-position:-25px 0}.gh-count{position:relative;margin-left:0;background-color:#fafafa;border:1px solid #d4d4d4}.gh-count:focus,.gh-count:hover{color:#4183C4}.gh-count:after,.gh-count:before{content:' ';position:absolute;display:inline-block;width:0;height:0;border-color:transparent;border-style:solid}.gh-count:before{top:50%;left:-3px;margin-top:-4px;border-width:4px 4px 4px 0;border-right-color:#fafafa}.gh-count:after{top:50%;left:-4px;z-index:-1;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#d4d4d4}.github-btn-large{height:30px}.github-btn-large .gh-btn,.github-btn-large .gh-count{padding:3px 10px 3px 8px;font-size:16px;line-height:22px;border-radius:4px}.github-btn-large .gh-ico{width:22px;height:23px;background-position:0 -20px}.github-btn-large .gh-btn:active .gh-ico,.github-btn-large .gh-btn:focus .gh-ico,.github-btn-large .gh-btn:hover .gh-ico{background-position:-25px -20px}.github-btn-large .gh-count{margin-left:6px}.github-btn-large .gh-count:before{left:-5px;margin-top:-6px;border-width:6px 6px 6px 0}.github-btn-large .gh-count:after{left:-6px;margin-top:-7px;border-width:7px 7px 7px 0}@media (-moz-min-device-pixel-ratio:2),(-o-min-device-pixel-ratio:2/1),(-webkit-min-device-pixel-ratio:2),(min-device-pixel-ratio:2){.gh-ico{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABaCAQAAADkmzsCAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAE81JREFUeNrtnGl0VFW2gHcIIggINLQoaj9bQHmgjUwRBZMK2A4Iora7CAFjGBIRFESZmwZkEgkiAg0oiigIggJhkkGgAjIpgyAkEAhICCGQkEDmoaru937UkKqQhFTwvbd6Lc5dK6tycm/t8917zj57uhH5/2h+Uk+aSGt5UoIkSJ6UVtJY6omf/Ec1P7lPnhBTKUd7afQHwqi//l1n6V69rHa16SXdox9pZ63yB319LWknplqdFgw78V32EdsV7Nhsadm/xn07793qwWKSdlLrj4CoqkP0vFLKcVYHaNWbFnCXBNbpvHNOYQqltIILP86s01kC5c83i/GYHncMO6Rg9JlPT648tSJ+wclRZ0MKnTDHtOVNCWgoQWP655x1jjub1UzkbQYzibXkODvPjO4nQXLXzWD00AJFGXZ5128FO7EUHwU7Y469m6oomq+vVlpAbQn8/n17EYARQ1eqe/6R6nQ3fgKwF64YL4FSu7IYvdSmvFawNRYLFn5gIn14hVfoyxQ2YcGyNbZ3oaI2NVdKQBUJiJ5s2IErW0dIkLSQO0Skhtwp9aSWVJWa8qgEbR7JVTDs302QAKnMqtQ2WqhE5p3fn7onYx5PUM3rblWjw5UFF/ad2x+Zp2iBtq6EiPsnRBpFwBkefOXFNi+ISQKlo4fGChJT+25hr9KEM2AvGhch9/uOcbvGK+FF5/aztu9hten32kz9tLE+oZ21ldbT5rpR7eFxrD+3P6xI0RN6u68q976gnCQglSYiGQcNe9LOt8OqBvcLnTZo3rtjI9p3G/p6yn7DyDwuQhOuQE7ifUE+q2IdppiN/UdYxj3mK4qihXrNQ2PZFMV8jXtZtv+IGUXf9VFEg93zATtPi0jVoqsAdqs1p1hjGXYAa7bUFeFpDPjp31LfN4zbNEWJusga7hXpf7VU5YsSni3CvaydnqLoRb3NFxl/aVGYDnwhIiJ/zU2ijJafKgEiInwJhVf+0tw3kO6K2Ti/jzYiemf/3LJAzIaaRGiTuM+Mol19kbHmPcDOgyIi7TrnpZQFYthnvyM1RWiMAd8P9Qmkx+fKqAxGiIjolLIwFEVPqJ8II4dmKT0W+iLjzHoo2OX4fGQJ5bScxNr1RUSKDkPCWp9AwuKVpQncIyJi/r1cEPRRERotPquExfsiI/M0ZI91fM67SLlt21MiItkTIfOUTyCh+crm1Y7PZnv5ID26iIhs3aiE5vsiw5YLSS87PjuWddkt6RURkaRXwJrj2xpB2T7C8TnkBiDj+omI7PinovgiA2DV03Kn1JXaRmH5IGfNUltqf/cMgM8gS8Icn/vnlw/ydR8RkaWvVwZkyUtyp9SWWrYL5YMc6iS1pdZXL/sM0tuqvDNe22ugthuXWh6G2Vg4QFtr2yETld5WX2TYc+DgVNoTSDvWlcth5yla0/bQh2DP8glkSLbyxpcaoK211br9ZqNskLHp0/poW23Zf5kyJNsXGUXHIHbl+adovTco8Q1s5YBs4mnang04tRaKfvMJZPp5JfIozfkbzZiyKa6XrXSMoZnpP/E3mvJwRKwyI9GnJ/I5pB6SZiJyhwT88h7ZZWD8jMMXaZZ2FPjUJ5Aftihm49tnaDr1tc9G2Xek714VP/5KZL7ZCdDT/nZ2VErMMXsMH9KGh7/uZDaUzZt9WiPdwTAiekldOiV3rx4c0S59aMGm/GQM53wqLDjBIrrjsHjrRvQyDKCbTyB5I/sUKrpYRB/SuMHr+QELlo1xLpDwwkt7sWBhPnVFRHSx0rewYIRPINVIgbObpUPCI8RdWu6weNdOdYEUpQ99yn3y7fLk2c3ARXwyg4QOSxMUNTSYVitD1PranLXDNi3vm6soDnW84BAj6ICfiIgGq6EsS+BJ36xGRgDGnKHyeEIbrGkLvjBv7J+fCmAUASTMcp5YQx6fMxQDGOajYUrVgjUDchVNXRrA4rF71VBDDWVMujL1Ur+CAVlhi9yq+j69rLyZW7AaH/13biceiq6azdIh8ysMDAzI3A1X1hWk5p+9uMzp03d8VYsygJP46iqIEHLsYIhd0VNLA23b5yzvu3HAuhD71EvKzAv988ddGbXNidFYzygh9uMH6eG7Z0U7CiE36fWedTrv/yBvFYvsRWnr4dLy/EsZO5OXSwN5TEz9QvOSgULaVMJ54zaWbIozG4qmL1nCDnawo7d1bJwy4ee+eaOS/rVbRER76lXFbGyJ5WsfZ69LTi/sYM1cNVFMYpKO1pyLmyB5eX5a6u74aDGJadUkWxZgI6SSHjvN+HFrbIhNUfrHbfiqcFSobfRRZdye3kXDTg87rN11p6KE2LYd50ceqmz8gR4UAFw9snB4nc62gnPbID7ampOyN3HH0n9m/OpwSqh8gEOEp9kRe3BglnPXuKYMuGBm2OEe9ogrrp1kUNaJA2yn081EhGjNcafKzYLMExiJOwxr3ln3TnKMx24yqkUwW4t2rjzdJ7u07bBP1venbDFsIehmY3RUYzDnS90OExnEzQcBRWjKl1hsMXuPfnJ2aGZYvqJGeOGQ1LlJ+4/YYrCwiCZ/TNwUf55hFj+TChhcZi8z6Yz/Hxb3pSqvsMIzOOc+VvDSHyjo/6JRhba8xXzWYGEHa5jLQFpTRW61W+1Wu9VutVvtVvtfbf5SXx6URyVAOkqgBEoHCZBH5EH5k/zH2BJ+0kAekcBSs+4mMUmgtJD6f0juXWtpF/1A1+kJzdBCLdB0jdNonaLPaM2b/vKGEiAmMT3a5cuRR79J2ZuTaM2yW+1FRVk555J3H1m6cPjDz4lJTNLu5rK8VfRFXeXI9JZ65OlK7VrpQoKa0kpM1YOXjEne5cj0lhp2LEyyLB5dPVhM0koqc+PUT3tp3A1SDI7juIao74++kQRWDY6ekpNIBVrWuVUTqwZLoDTyFaOF/lRywD3tkXlDsgdnR+aVErHfqS18WhdNxTS8b/qx6zNvnOEwv3LG4RB7tvSj74aLSZr6sF40Uj1i8q9Zo1I2x17YZ49xeSb2mKR9P8RNT+lt9UDJ1YgKY7QQ09aP7J7JhQwW0ZMHil0FqvBXevMl1zymWcHWGWKS5hVCUX+dXTy8t3I2xRW6aiC2sIzPWMgytrrqITbGDczxgJldofXyUK1OJ6M9IH6jV9kRLKrzmsvHBzgZXauTPFQRjGWuYb1eFH3SHoOF9YygM3fjvg/4cQ9/ZyQbsNhj1sSHFblRvtEb6f17a3VKsrjHlUY/bnh/qUJ/0lyXnLfU6iT33ghknmtIYzLS9mBhEU+XHcGiGs+wGEvanjEZbpR55QqoJYHxxU9jy9Tm0lYelnrlTsT60kLaj3mMLa7LTq29QaWKvukazsxkWwzRvFCBu+VHV9baYmYmu1HeLGdQbbfPcmPMw18ecW57baSuiPhLbakvDaWRNJQGUlP8pI60dZ7REn/muS7dMVvalrlStKVrx5iThIWoAeF6RL/QTuXuM930O02MfIsoLHOTnCAFWlZcqtHYCLvVOZaPREQ2js5MSNj476HOTS/oul3dVD148eikmLzLu6JERIhyLnvruIgyVLH662HHQCZfNiy8RxVd5RzYQQ0U0ZraVrvpaxqpvfRFfVRv00A94jxjE1V4z7BMuez8/XCpK6VK7Q6Zp50Yyx3POiXG8eu1+FmDxfTwc++/8dWYtVO3zoievGTM8L71n/5osOuKtIPO57/c8XvmmXodSq0e0n6OQbyZm7OLt0REwhLck8XQWLWW2DkK1J2i65UmIsKgvF0DXVUTpanihltnODHicO7ReaeLSx6yfi+ZtrYXubInUJDsnMp3EOvo+XGmNLweo6omKIqZw4cZ57hbfa5WaF9HCctx3q1/HTnkzEAmarWSMv7SxpENwU57V19hMhVsRVfFWaZGAHaAvEv3t70eRB1DmnaJr6nh6BuaUlGQwRlunb94uuuqniVEVFszyTmmL919ddOPVBTk2ilp41refO7oi54sJW+X+QdH8vn3/Tzi6puaUFGQ8AK9zymiReK+HoaimEtmGBte+gUAK43dfW3P/FDhJ3Ktp9k1lfgrVoDUgyUml9Yz2xRl7BVGu/sCy0tTX3cccC1vRo5PUxSzXb1qrfq3NwwAY527q/bsd25UzOH1TOIbuOv2jGgAw4jwTv/py47hbDnOfe6+Az5geEwlGm37zdnzD08Z28Y4x+POfNS4P/MUPrUNE92710uOHss/vUB6z3VMrLRZboxHfcTwmEoZMxzPsvd8TxmnvwPAxp2unmXd8LGlHnApXGobVoAzq7xA+u9XlCHZBLtB3vIVJMRdB0Hg0CxF6fOrp4yMIwB5R4t7Tk7yFaQos9iDz/sVIMO7MiI8TVGmpuC2XwbM9RVEUZd6vGNaiqK8fsVTRt5lgGvfFfdcXIDvzW0lZ6wAyE/zAulVoCizDxf3jFlVCRC3Izr3gKKEFnjKsOYCXJxR3JO+sBIg7lud8iGALc9b+RqKMttDYU5e5ztIcaXw3I2ONedlXAKQMKm4J2u67xwea25CyR4RcWj+qJXFPXOW+ooRZi0uEJ/xTVkgh6ZLA2kgDaWh/ClxpK8YthxpIHdJfblL7v55SikgYVZFGe+hAX6Y7CvI0Mziq8evVErWc9lyAI5/KjWlljSQ+lL/QBdfQfKPSSOpL3+WBlL32AIAe64XyBt5ihIZqy/pSxqmofr8x7NCbb6BjErV7mrWLhqi4RGxihLpVfNoTQZIO3S+Z7rZ9hqhPEcfcn0k2UZ3zHQh5FpE6mEA6yUvkDGXFaVvkjbXlvqidtUXJg6efNk3kBlHNVK76qv6sgb1vaAoI7y0VuE+gMzT6zvSkhfpygu8zAofQT4mkm68SvdfXsk8A1D4sxfIxyccc/rzQds1swudeZxns38ckFdxjDHpRNEBE4/TaVcfR3nUTK9yWttcAMP2RS8edDnP1OW0Dxjbi/3VMc87DHybt2O9drVzng+jMU/yBO15ivEpe9/JqhjGiKsZuxlIV54giKcmjHL0Rq/3WuyvOkazcpw4rOu7pJ00TXyQgxXE2EUD95fVcFvS3qU9F4c59FafXdzjqjvgDpbYYtaeHHatfOPxnaz1J+wxRHkYPFsdz/fCKC+Q+o46xot7pJkz/t5cgqT17Nvpxx7KNx4PEe6VHG+WvMfp2Xi/wkTHsVecte9Nnd5JrH6y8iEWYMFyee/6E7OSR5Zws8ZkzL6w4cSFfViw8EmxBaWNHSXQY9MJ9LbjjS0OizUyVO4UoQexyUuDusnD4idCI8Jzvkj7tYRtdShrIeE8UMIhqOMsE4StJSMhtX90WaxLRES0pn6rNv15zJ10YS47sGB5v0QZ7ftphiNs9ynPecZaXHGxLceL4ZxSQp3lyZslQPypxQps1+KaPSuPSUOpJ40kIHmXN0jyrtsfKiWTEnDWFRjqdd1fi6Y7VLAa+qQIJhYPO6RW/VyriFCf56LnXz+pVs/jWe4u4WmaHJ58ZF7R9FKiYOcdz+SDgdJcBD++MWwJG6oHS5AEStDC4dfPqfXX+/7NPxrs9OR/LyXiRtC6E84BxmtNqjMu7adQq9p0p4bq3/XN4ri8R1Rx1nUOc0096fjb2pPFlrSHlAjX+whNnpUmIjQk17CnHVkzacGwHz/OOecOOlx1V8kvLfEVTZs86z7vjdLCbP62ZUNcOmqt+ovwr3nnFLWrVfMc7/OMTe9lU5acUULsY9OVyM3XJSKWO75hSLZteWnlN/hz2FnNtKNqsDQTP6IAu2EzChyqIGe7vQguTAXI3w5p673Cew9XDU7c5sQ4WkY5FM+fPNDTlS6Yr37UK9gyLs1zKn17WlG+ilOU1fHK8AMlMJzh1hD7yQN0KSMu2cqVLohdWTVYWs6rx3qvcq1xABcmApwb7gVSTVpWDT65xnliIa3KDhR/tjrePeyv9TbewLLv13mJ05M++31IlrJoi6LMXKQoK9cro496hZO+cF27Kp7Pyq4kYpD7nYRNdTpLR7nH+gxRfM7k3Fj4fRS4fp5+0w3iJ/dIhzqdEza4iQeVF8VtzJZZxRFcy1tNmOrKiEy9pER9pigffaEos2d4gmgjtbium5XMVo84SWly3BHc1MNms5ikndwtVURSN8CZ0d4glzZKFblbAsTU7R+ph4ujxjcKSHezxUy75Ea5pv0L2jGA4fQbf1r5cL7i+jljigtE/TVC013XTEuxxdD9BlL8XWFPsOZsiqoeLCZ5Sv47aQs4TPvL7wHED4Rz26SjmKoHb55RlOnGWF6B8jfescfMvuCxMo5pmNYQGXXUjTDHBfLeCa2h4Z55xtlJ9hjeuXGmB3/meOQHz6yf+sCzYkrcDo5Y/a6JAGsmQfKeB57dMK1YnwGzK1QARxVGY4k+6WXEZ+s3YdnKrFmK8vV4RZn6kaKGZhafFWpbexILoytaZ0ckeR4uU965bYXpsGEawPz3ADZFAYbV09TPpX+F84f48TaW07+MuC7ya7YrZsITSrO9Rl5N+BkLb+NDdpcW7Lr+5T3AuHbKMEqxuGLw7a1EEV5gs2HZEuuVHyzzeCtna6xhYXNZKrfcm9aTuArZvsfpQWWqH3iAT7DYY2J+m5Ra9utjofbJl3cfNSxY+Jj/qlzVAFXoxvfXJ6PdLY8VdKHyJRz40YnFWLDk7Np99NPECWkDc18vCrWH2sKLBuW8n7bw3N6jebuwYGERwdxkrQi1eJ4PiCaONPLIJZXjrGYyz3DzZSIi+PEkE1zJ6FKOzYwngP+U/5xBDQKIYDKLiWYzm1nDl0ykH229/0PArXarlWz/A3bbfoDcyFIFAAAAAElFTkSuQmCC);background-size:50px 45px}}.plusone-gplus{position:absolute;top:8%}@media all and (max-width:800px){body{font-size:1.1em}article{margin:1.5em auto 0;padding:1.5em}.experiment{margin:1em .2em}}@media all and (max-width:500px){body{font-size:.9em}article{margin:.5em auto 0;padding:.5em}.experiment{margin:1em .1em}}@media all and (max-width:300px){body{font-size:.8em}article{margin:0 auto;padding:0}.experiment{margin:1em 0}}@media all and (min-width:1300px){.latest-commits{position:fixed;left:-3em;bottom:-1em;height:50%;overflow:auto;width:20%;font-size:1em}}.fork-left,.fork-right{position:absolute;top:0}li pre{margin:0}li h2{color:red}li li h2{font-size:1em;color:#0665f3}.commit pre,code{font-size:1.2em}.fork-left,.fork-right{background-position:center center;width:140px;height:140px}.fork-left{left:0;background:url(https://cdn.webrtc-experiment.com/images/fork-left.png)}.fork-right{right:0;background:url(https://cdn.webrtc-experiment.com/images/fork-right.png)}select{border:1px solid #d9d9d9;border-radius:1px;height:50px;margin-left:1em;margin-right:-5px;padding:1.1em;vertical-align:6px}p{padding:0 .8em .4em}li{border-bottom:1px solid #bdbdbd;border-left:1px solid #bdbdbd;padding:.5em}.commit pre{border:1px dotted #000;margin:1em}blockquote{background:#f1f1f1;padding:1em;border:1px dotted gray;margin:0 1em}.answer{border-left:1px dotted gray;margin-left:5em;padding:0 1em}pre a{text-decoration:underline}blockquote.inline{margin:1em;border-radius:3px;box-shadow:2px 2px #b6aaaa} article p:first-of-type { margin-top: inherit; } li { border-bottom: 1px solid rgb(189, 189, 189); border-left: 1px solid rgb(189, 189, 189); padding: .5em; } .image-container { margin: 2em 0; text-align: center; } .image-parent { border: 2px solid black; border-radius: 5px; margin: 0 2em; } .image-parent img { width: 100%; } blockquote { margin: 10px; border-radius: 10px; background: rgb(253, 253, 253); padding: 8px 20px; font-size: 25px; font-weight: 200; line-height: 1.5; word-spacing: 5px; } section blockquote { color: #EC008C; } .specific-h2, h1 { font-size: 35px; font-weight: 200; padding: 5px 10px; border-bottom: 1px dotted #EC008C; } button, .output-panel, .highlighted-name { font-size: 25px; font-weight: 200; line-height: 1.5; word-spacing: 5px; } .highlighted-name { color: #EC008C; } .output-panel { overflow: hidden; margin-left: 0; padding-left: 0; border-left: 0; } .specific-h2 a { color: #EC008C; } h1 { font-size: 40px; color: #EC008C; } .inline-iframe { width: 100%; border: 0; padding: 0; outline: none; } video { width: 100%: } video, img { max-width: 100%; } .single-line-text { white-space: nowrap; display: block; text-overflow: ellipsis; width: 100%; overflow: hidden; } button { text-align: center; display: inline-block; } pre { border: 0; margin-left: 0; font-size: 14px; } .button { width: 200px; margin-top: 30px; margin-bottom: 30px; } .button a { display: block; height: 50px; width: 200px; /*TYPE*/ color: white; font: 17px/50px Helvetica, Verdana, sans-serif; text-decoration: none; text-align: center; text-transform: uppercase; /*GRADIENT*/ background: #00b7ea; /* Old browsers */ background: -moz-linear-gradient(top, #00b7ea 0%, #009ec3 100%); /* FF3.6+ */ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #00b7ea), color-stop(100%, #009ec3)); /* Chrome,Safari4+ */ background: -webkit-linear-gradient(top, #00b7ea 0%, #009ec3 100%); /* Chrome10+,Safari5.1+ */ background: -o-linear-gradient(top, #00b7ea 0%, #009ec3 100%); /* Opera 11.10+ */ background: -ms-linear-gradient(top, #00b7ea 0%, #009ec3 100%); /* IE10+ */ background: linear-gradient(top, #00b7ea 0%, #009ec3 100%); /* W3C */ filter: progid: DXImageTransform.Microsoft.gradient( startColorstr='#00b7ea', endColorstr='#009ec3', GradientType=0); /* IE6-9 */ } .button a, .button p { -webkit-border-radius: 10px; -moz-border-radius: 10px; border-radius: 10px; -webkit-box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.2); -moz-box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.2); box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.2); } .button p { background: #222; display: block; height: 40px; width: 180px; margin: -50px 0 0 10px; /*TYPE*/ text-align: center; font: 12px/45px Helvetica, Verdana, sans-serif; color: #fff; /*POSITION*/ position: absolute; z-index: -1; /*TRANSITION*/ -webkit-transition: margin 0.5s ease; -moz-transition: margin 0.5s ease; -o-transition: margin 0.5s ease; -ms-transition: margin 0.5s ease; transition: margin 0.5s ease; } /*HOVER*/ .button:hover .bottom { margin: -10px 0 0 10px; } .button:hover .top { margin: -80px 0 0 10px; line-height: 35px; } /*ACTIVE*/ .button a:active { background: #00b7ea; /* Old browsers */ background: -moz-linear-gradient(top, #00b7ea 36%, #009ec3 100%); /* FF3.6+ */ background: -webkit-gradient(linear, left top, left bottom, color-stop(36%, #00b7ea), color-stop(100%, #009ec3)); /* Chrome,Safari4+ */ background: -webkit-linear-gradient(top, #00b7ea 36%, #009ec3 100%); /* Chrome10+,Safari5.1+ */ background: -o-linear-gradient(top, #00b7ea 36%, #009ec3 100%); /* Opera 11.10+ */ background: -ms-linear-gradient(top, #00b7ea 36%, #009ec3 100%); /* IE10+ */ background: linear-gradient(top, #00b7ea 36%, #009ec3 100%); /* W3C */ filter: progid: DXImageTransform.Microsoft.gradient( startColorstr='#00b7ea', endColorstr='#009ec3', GradientType=0); /* IE6-9 */ } .button:active .bottom { margin: -20px 0 0 10px; } .button:active .top { margin: -70px 0 0 10px; } .fit-screen { position: fixed; top: -20px; left: -20px; width: 100%; height: 100%; background: white; z-index: 999999999999999; padding: 0; } .fit-screen li { border-left: 0; padding: 0; margin: 0; } ================================================ FILE: FileBufferReader/fbr-client/demo/ui.js ================================================ window.addEventListener('load', function() { var socket = io.connect(); socket.on('connect', function() { console.info('socket.io connection opened.'); btnSelectFile.disabled = false; btnSelectFile.innerHTML = 'Select or Drop a File'; }); socket.on('disconnect', function() { btnSelectFile.disabled = true; resetButtons(); var helper = progressHelper[progressHelper.lastFileUUID]; if (helper && helper.li && helper.li.parentNode) { isStoppedTimer = true; helper.li.parentNode.removeChild(helper.li); } }); socket.on('error', function() { btnSelectFile.disabled = true; resetButtons(); }); var progressHelper = {}; var outputPanel = document.querySelector('.output-panel'); function previewFile(file) { try { file.url = URL.createObjectURL(fileSelector.lastSelectedFile || file); } catch (e) { return; } var html = 'Download ' + file.name + ' on your Disk!'; html += '
Download

' + file.name + '

' + bytesToSize(file.size) + '

'; if (file.name.match(/\.jpg|\.png|\.jpeg|\.gif/gi)) { html += ''; } else if (file.name.match(/\.wav|\.mp3/gi)) { html += ''; } else if (file.name.match(/\.webm|\.flv|\.mp4/gi)) { html += ''; } else if (file.name.match(/\.pdf|\.js|\.txt|\.sh/gi)) { html += ''; html += '
'; } progressHelper[file.uuid].li.innerHTML = html; fileSelector.lastSelectedFile = false; } var FileHelper = { onBegin: function(file) { var li = document.createElement('li'); li.innerHTML = '
' + file.name + '

25%
'; li.style['min-height'] = '350px'; outputPanel.insertBefore(li, outputPanel.firstChild); outputPanel.className = 'fit-screen'; outputPanel.style.height = innerHeight + 'px'; progressHelper[file.uuid] = { li: li, progress: li.querySelector('progress'), label: li.querySelector('label') }; progressHelper[file.uuid].progress.max = file.maxChunks; btnSelectFile.disabled = true; if (fileSelector.lastSelectedFile) { btnSelectFile.innerHTML = 'File Sending In-Progress...'; } else { btnSelectFile.innerHTML = 'File Receiving In-Progress...'; } resetTimeCalculator(); timeCalculator(progressHelper[file.uuid].progress); progressHelper.lastFileUUID = file.uuid; }, onEnd: function(file) { previewFile(file); btnSelectFile.innerHTML = 'Select or Drop a File'; progressHelper.lastFileUUID = null; outputPanel.className = ''; outputPanel.style.height = 'auto'; }, onProgress: function(chunk) { var helper = progressHelper[chunk.uuid]; helper.progress.value = chunk.currentPosition || chunk.maxChunks || helper.progress.max; if (helper.progress.position > 0 && helper.li.querySelector('.circular-progress-bar-percentage')) { var position = +helper.progress.position.toFixed(2).split('.')[1] || 100; helper.li.querySelector('.circular-progress-bar-percentage').innerHTML = position + '%'; helper.li.querySelector('.circular-progress-bar').className = 'circular-progress-bar c100 p' + position; } if (chunk.currentPosition + 2 != chunk.maxChunks && helper.li.querySelector('.file-name')) { progressHelper[chunk.uuid].lastChunk = chunk; progressHelper.callback = function(timeRemaining) { var lastChunk = progressHelper[chunk.uuid].lastChunk; var singleChunkSize = chunk.size / lastChunk.maxChunks; var html = 'File name: ' + lastChunk.name + ' (File size: ' + bytesToSize(chunk.size) + ')'; html += '
Pieces (total/remaining): ' + lastChunk.maxChunks + '/' + lastChunk.currentPosition; html += ' (Single piece size: ' + bytesToSize(singleChunkSize) + ')'; var endedAt = (new Date).getTime(); var timeElapsed = endedAt - (progressHelper.startedAt || (new Date).getTime()); progressHelper.latencies.push(timeElapsed); var avg = calculateAverage(progressHelper.latencies); html += '
Latency in millseconds: ' + timeElapsed + ' (Average): ' + avg + ''; var remainingFileSize = singleChunkSize * (lastChunk.maxChunks - lastChunk.currentPosition); html += '
Remaining (time): ' + timeRemaining + ' (Remaining file size): ' + bytesToSize(remainingFileSize); helper.li.querySelector('.file-name').innerHTML = html; progressHelper.startedAt = (new Date).getTime(); }; } else { btnSelectFile.innerHTML = 'Select or Drop a File'; } } }; // RTCPeerConection // ---------------- function resetButtons() { btnSelectFile.innerHTML = 'Select or Drop a File'; btnSelectFile.disabled = false; } // getNextChunkCallback gets next available buffer // you need to send that buffer using socket.io function getNextChunkCallback(nextChunk, isLastChunk) { if (isLastChunk) { // alert('File Successfully sent.'); } socket.emit('buffer-stream', nextChunk); }; socket.on('buffer-stream', onBufferStream); function onBufferStream(chunk) { if (chunk instanceof ArrayBuffer || chunk instanceof DataView) { // array buffers are passed using socket.io // need to convert data back into JavaScript objects fileBufferReader.convertToObject(chunk, function(object) { onBufferStream(object); }); return; } // if target user requested next chunk if (chunk.readyForNextChunk) { fileBufferReader.getNextChunk(chunk /*aka metadata*/ , getNextChunkCallback); return; } // if any of the chunks missed if (chunk.chunkMissing) { fileBufferReader.chunkMissing(chunk); return; } // if chunk is received fileBufferReader.addChunk(chunk, function(promptNextChunk) { socket.emit('buffer-stream', promptNextChunk); }); }; var progressIterations = 0; var ONE_SECOND = 1000; function resetTimeCalculator() { progressIterations = 0; isStoppedTimer = false; progressHelper.callback = function() {}; progressHelper.latencies = []; } function calculateAverage(arr) { var sum = 0; for (var i = 0; i < arr.length; i++) { sum += parseInt(arr[i], 10); //don't forget to add the base } var avg = sum / arr.length; return avg.toFixed(1); } var isStoppedTimer = false; // https://github.com/23/resumable.js/issues/168#issuecomment-65297110 function timeCalculator(progress, selfInvoker) { if (isStoppedTimer) return; var step = 1; var remainingProgress = 1.0 - progress.position; var estimatedCompletionTime = Math.round((remainingProgress / progress.position) * progressIterations); var estimatedHours, estimatedMinutes, estimatedSeconds, displayHours, displayMinutes, displaySeconds; progressIterations += step; if (progress.position < 1.0) { if (isFinite(estimatedCompletionTime)) { estimatedHours = Math.floor(estimatedCompletionTime / 3600); displayHours = estimatedHours > 9 ? estimatedHours : '0' + estimatedHours; estimatedMinutes = Math.floor((estimatedCompletionTime / 60) % 60); displayMinutes = estimatedMinutes > 9 ? estimatedMinutes : '0' + estimatedMinutes; estimatedSeconds = estimatedCompletionTime % 60; displaySeconds = estimatedSeconds > 9 ? estimatedSeconds : '0' + estimatedSeconds; } var output = ''; if (displayHours > 0) { output += displayHours + ' hours '; } if (displayMinutes > 0) { output += displayMinutes + ' minutes '; } if (displaySeconds > 0) { output += displaySeconds + ' seconds '; } if (output.length) { progressHelper.callback(output); } } setTimeout(function() { timeCalculator(progress, true); }, step * ONE_SECOND); } // ------------------------- // using FileBufferReader.js var fileSelector = new FileSelector(); // you can force specific files e.g. // image/png, image/*, image/jpeg, video/webm, audio/ogg etc. fileSelector.accept = '*.*'; var fileBufferReader = new FileBufferReader(); fileBufferReader.chunkSize = 100 * 1000; // 100k fileBufferReader.onBegin = FileHelper.onBegin; fileBufferReader.onProgress = FileHelper.onProgress; fileBufferReader.onEnd = FileHelper.onEnd; function onFileSelected(file) { fileSelector.lastSelectedFile = file; btnSelectFile.innerHTML = 'Please wait..';; btnSelectFile.disabled = true; fileBufferReader.readAsArrayBuffer(file, function(metadata) { fileBufferReader.getNextChunk(metadata, getNextChunkCallback); }, { chunkSize: fileBufferReader.chunkSize }); setTimeout(function() { if (fileSelector.lastSelectedFile) return; btnSelectFile.innerHTML = 'Select or Drop a File'; btnSelectFile.disabled = false; }, 5000); } var btnSelectFile = document.getElementById('select-file'); btnSelectFile.onclick = function() { btnSelectFile.disabled = true; fileSelector.selectSingleFile(function(file) { onFileSelected(file); }); }; // drag-drop support function onDragOver() { mainContainer.style.border = '7px solid #98a90f'; mainContainer.style.background = '#ffff13'; mainContainer.style.borderRadisu = '16px'; } function onDragLeave() { mainContainer.style.border = '1px solid rgb(189, 189, 189)'; mainContainer.style.background = 'transparent'; mainContainer.style.borderRadisu = 0; } var mainContainer = document.getElementById('main-container'); document.addEventListener('dragenter', function(e) { e.preventDefault(); e.stopPropagation(); e.dataTransfer.dropEffect = 'copy'; onDragOver(); }, false); document.addEventListener('dragleave', function(e) { e.preventDefault(); e.stopPropagation(); e.dataTransfer.dropEffect = 'copy'; onDragLeave(); }, false); document.addEventListener('dragover', function(e) { e.preventDefault(); e.stopPropagation(); e.dataTransfer.dropEffect = 'copy'; onDragOver(); }, false); document.addEventListener('drop', function(e) { e.preventDefault(); e.stopPropagation(); onDragLeave(); if (!e.dataTransfer.files || !e.dataTransfer.files.length) { return; } var file = e.dataTransfer.files[0]; onFileSelected(file); }, false); // -------------------------------------------------------- function millsecondsToSeconds(millis) { var seconds = ((millis % 60000) / 1000).toFixed(1); return seconds; } function bytesToSize(bytes) { var k = 1000; var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; if (bytes === 0) { return '0 Bytes'; } var i = parseInt(Math.floor(Math.log(bytes) / Math.log(k)), 10); return (bytes / Math.pow(k, i)).toPrecision(3) + ' ' + sizes[i]; } function getToken() { if (window.crypto && window.crypto.getRandomValues && navigator.userAgent.indexOf('Safari') === -1) { var a = window.crypto.getRandomValues(new Uint32Array(3)), token = ''; for (var i = 0, l = a.length; i < l; i++) { token += a[i].toString(36); } return token; } else { return (Math.random() * new Date().getTime()).toString(36).replace(/\./g, ''); } } }, false); ================================================ FILE: FileBufferReader/fbr-client/index.html ================================================  FileBufferReader over Socket.io ================================================ FILE: FileBufferReader/fbr-client/package.json ================================================ { "name": "fbr-client", "preferGlobal": true, "version": "1.0.3", "author": "Muaz Khan (http://www.muazkhan.com/)", "description": "FileBufferReader is a JavaScript library reads file and returns chunkified array-buffers. The resulting buffers can be shared using WebRTC data channels or socket.io. Share files same as Skype do!", "scripts": { "start": "node server.js port=9001" }, "main": "server.js", "repository": { "type": "git", "url": "https://github.com/muaz-khan/FileBufferReader.git" }, "keywords": [ "webrtc", "file", "file-sharing", "datachannel", "data-channels", "file-reading", "file-buffer", "buffer", "arraybuffer", "buffers", "chunkifier", "fragmenter", "library", "javascript", "webrtc-experiment", "javascript-library", "muaz", "muaz-khan" ], "analyze": false, "license": "MIT", "bugs": { "url": "https://github.com/muaz-khan/FileBufferReader/issues", "email": "muazkh@gmail.com" }, "homepage": "https://www.WebRTC-Experiment.com/FileBufferReader/", "_from": "fbr@", "dependencies": { "fbr": "latest", "socket.io": "latest" } } ================================================ FILE: FileBufferReader/fbr-client/server.js ================================================ // Muaz Khan - www.MuazKhan.com // MIT License - www.WebRTC-Experiment.com/licence // Documentation - github.com/muaz-khan/FileBufferReader var port = 9001; port == 9001 && process.argv.forEach(function (val, index, array) { if(val.toString().indexOf('port=') != -1) { val = val.toString().split('=')[1]; val = parseInt(val); port = val; } }); var fs = require('fs'); var path = require('path'); // force auto reboot on failures var autoRebootServerOnFailure = true; var server = require('http'); var url = require('url'); function serverHandler(request, response) { try { var uri = url.parse(request.url).pathname, filename = path.join(process.cwd(), uri); var stats; try { stats = fs.lstatSync(filename); } catch (e) { response.writeHead(404, { 'Content-Type': 'text/plain' }); response.write('404 Not Found: ' + path.join('/', uri) + '\n'); response.end(); return; } var contentType; if (fs.statSync(filename).isDirectory()) { contentType = { 'Content-Type': 'text/html' }; filename += '/index.html'; } fs.readFile(filename, 'binary', function(err, file) { if (err) { response.writeHead(500, { 'Content-Type': 'text/plain' }); response.write('404 Not Found: ' + path.join('/', uri) + '\n'); response.end(); return; } response.writeHead(200, contentType); response.write(file, 'binary'); response.end(); }); } catch (e) { response.writeHead(404, { 'Content-Type': 'text/plain' }); response.write('

Unexpected error:



' + e.stack || e.message || JSON.stringify(e)); response.end(); } } var app = server.createServer(serverHandler); function cmd_exec(cmd, args, cb_stdout, cb_end) { var spawn = require('child_process').spawn, child = spawn(cmd, args), me = this; me.exit = 0; me.stdout = ""; child.stdout.on('data', function(data) { cb_stdout(me, data) }); child.stdout.on('end', function() { cb_end(me) }); } function log_console() { console.log(foo.stdout); try { var pidToBeKilled = foo.stdout.split('\nnode ')[1].split(' ')[0]; console.log('------------------------------'); console.log('Please execute below command:'); console.log('\x1b[31m%s\x1b[0m ', 'kill ' + pidToBeKilled); console.log('Then try to run "server.js" again.'); console.log('------------------------------'); } catch (e) {} } function runServer() { app.on('error', function(e) { if (e.code == 'EADDRINUSE') { if (e.address === '0.0.0.0') { e.address = 'localhost'; } var socketURL = 'http://' + e.address + ':' + e.port + '/'; console.log('------------------------------'); console.log('\x1b[31m%s\x1b[0m ', 'Unable to listen on port: ' + e.port); console.log('\x1b[31m%s\x1b[0m ', socketURL + ' is already in use. Please kill below processes using "kill PID".'); console.log('------------------------------'); foo = new cmd_exec('lsof', ['-n', '-i4TCP:9001'], function(me, data) { me.stdout += data.toString(); }, function(me) { me.exit = 1; } ); setTimeout(log_console, 250); } }); app = app.listen(port, process.env.IP || '0.0.0.0', function(error) { var addr = app.address(); if (addr.address === '0.0.0.0') { addr.address = 'localhost'; } var domainURL = 'http://' + addr.address + ':' + addr.port + '/'; console.log('------------------------------'); console.log('socket.io is listening at:'); console.log('\x1b[31m%s\x1b[0m ', '\t' + domainURL); }); var io = require('socket.io')(app); io.on('connection', function(socket){ socket.on('buffer-stream', function(buffer) { socket.broadcast.emit('buffer-stream', buffer); }); }); } if (autoRebootServerOnFailure) { // auto restart app on failure var cluster = require('cluster'); if (cluster.isMaster) { cluster.fork(); cluster.on('exit', function(worker, code, signal) { cluster.fork(); }); } if (cluster.isWorker) { runServer(); } } else { runServer(); } ================================================ FILE: FileBufferReader/index.html ================================================ WebRTC File Sharing | FileBufferReader
Connecting





================================================ FILE: FileBufferReader/package.json ================================================ { "name": "fbr", "preferGlobal": true, "version": "2.0.8", "author": { "name": "Muaz Khan", "email": "muazkh@gmail.com", "url": "http://www.muazkhan.com/" }, "description": "FileBufferReader is a JavaScript library reads file and returns chunkified array-buffers. The resulting buffers can be shared using WebRTC data channels or socket.io. Share files same as Skype do!", "scripts": { "start": "node FileBufferReader.js" }, "main": "./FileBufferReader.js", "repository": { "type": "git", "url": "https://github.com/muaz-khan/FileBufferReader.git" }, "keywords": [ "webrtc", "file", "file-sharing", "datachannel", "data-channels", "file-reading", "file-buffer", "buffer", "arraybuffer", "buffers", "chunkifier", "fragmenter", "library", "javascript", "webrtc-experiment", "javascript-library", "muaz", "muaz-khan" ], "analyze": false, "license": "MIT", "readmeFilename": "README.md", "bugs": { "url": "https://github.com/muaz-khan/FileBufferReader/issues", "email": "muazkh@gmail.com" }, "homepage": "https://www.WebRTC-Experiment.com/FileBufferReader/", "_id": "fbr@", "_from": "fbr@", "devDependencies": { "grunt": "0.4.5", "grunt-bump": "0.7.0", "grunt-cli": "0.1.13", "grunt-contrib-clean": "0.6.0", "grunt-contrib-concat": "0.5.1", "grunt-contrib-copy": "0.8.2", "grunt-contrib-uglify": "0.11.0", "grunt-jsbeautifier": "0.2.10", "grunt-replace": "0.11.0", "load-grunt-tasks": "3.4.0" } } ================================================ FILE: FileBufferReader/server.js ================================================ // http://127.0.0.1:9001 // http://localhost:9001 var server = require('http'), url = require('url'), path = require('path'), fs = require('fs'); function serverHandler(request, response) { var uri = url.parse(request.url).pathname, filename = path.join(process.cwd(), uri); fs.exists(filename, function(exists) { if (!exists) { response.writeHead(404, { 'Content-Type': 'text/plain' }); response.write('404 Not Found: ' + filename + '\n'); response.end(); return; } if (filename.indexOf('favicon.ico') !== -1) { return; } var isWin = !!process.platform.match(/^win/); if (fs.statSync(filename).isDirectory() && !isWin) { filename += '/index.html'; } else if (fs.statSync(filename).isDirectory() && !!isWin) { filename += '\\index.html'; } fs.readFile(filename, 'binary', function(err, file) { if (err) { response.writeHead(500, { 'Content-Type': 'text/plain' }); response.write(err + '\n'); response.end(); return; } var contentType; if (filename.indexOf('.html') !== -1) { contentType = 'text/html'; } if (filename.indexOf('.js') !== -1) { contentType = 'application/javascript'; } if (contentType) { response.writeHead(200, { 'Content-Type': contentType }); } else response.writeHead(200); response.write(file, 'binary'); response.end(); }); }); } var app; app = server.createServer(serverHandler); app = app.listen(process.env.PORT || 9001, process.env.IP || "0.0.0.0", function() { var addr = app.address(); console.log("Server listening at", addr.address + ":" + addr.port); }); ================================================ FILE: Firefox-Extensions/README.md ================================================ # Firefox doesn't need add-ons in order to do screen sharing, since Firefox 52 already. [More info](https://wiki.mozilla.org/Screensharing) # This repository is discontinued from September 01, 2017. ---- # [Firefox Extensions](https://github.com/muaz-khan/Firefox-Extensions) > Enable screen capturing in Firefox for both localhost/127.0.0.1 and `https://www.webrtc-experiment.com` pages. ## Install from Firefox Addons Store * [https://addons.mozilla.org/en-US/firefox/addon/enable-screen-capturing/](https://addons.mozilla.org/en-US/firefox/addon/enable-screen-capturing/) ## Simplest Demo Try this demo after installing above addon: * [https://www.webrtc-experiment.com/getScreenId/](https://www.webrtc-experiment.com/getScreenId/) ## Wanna Deploy it Yourself? 1. Open [`index.js`](https://github.com/muaz-khan/Firefox-Extensions/blob/master/enable-screen-capturing/index.js) 2. Go to line 7 3. Replace `arrayOfMyOwnDomains` array with your own list of domains ```javascript // replace your own domains with below array var arrayOfMyOwnDomains = ['webrtc-experiment.com', 'www.webrtc-experiment.com', 'localhost', '127.0.0.1']; ``` ## How to Deploy? 1) Signup here: * https://addons.mozilla.org/en-US/firefox/users/register 2) Use unique-addon-name here: * https://github.com/muaz-khan/Firefox-Extensions/blob/master/enable-screen-capturing/package.json#L3 3) Add your own domains here: * https://github.com/muaz-khan/Firefox-Extensions/blob/master/enable-screen-capturing/index.js#L7 4) Make XPI of the directory. ``` [sudo] npm install jpm --global jpm run -b nightly # test in Firefox Nightly without making the XPI jpm xpi # it will create xpi file ``` 5) Submit the XPI here: * https://addons.mozilla.org/en-US/developers/addon/submit/1 Follow all steps. Read them carefully. This is hard/tough step to follow. Select valid browsers. E.g. Firefox 38 to Firefox 45. And submit your addon for "review". It will take 2-3 hours for a Mozilla guy to review your addon. Then it will be available to public. ## License [Firefox-Extensions](https://github.com/muaz-khan/Firefox-Extensions) are released under [MIT licence](https://www.webrtc-experiment.com/licence/) . Copyright (c) [Muaz Khan](http://www.MuazKhan.com/). ================================================ FILE: Firefox-Extensions/enable-screen-capturing/README.md ================================================ # Firefox doesn't need add-ons in order to do screen sharing, since Firefox 52 already. [More info](https://wiki.mozilla.org/Screensharing) # This repository is discontinued from September 01, 2017. ---- # [Firefox Extensions](https://github.com/muaz-khan/Firefox-Extensions) > Enable screen capturing in Firefox for both localhost/127.0.0.1 and `https://www.webrtc-experiment.com` pages. > > You have to deploy this addon on Firefox addons-store, yourselves. ## Install from Firefox Addons Store * [https://addons.mozilla.org/en-US/firefox/addon/enable-screen-capturing/](https://addons.mozilla.org/en-US/firefox/addon/enable-screen-capturing/) ### Check if screen capturing is enabled for your domains: ```javascript // ask addon to check if screen capturing enabled for specific domains window.postMessage({ checkIfScreenCapturingEnabled: true }, "*"); // watch addon's response // addon will return "isScreenCapturingEnabled=true|false" window.addEventListener("message", function(event) { if (event.source !== window) return; var addonMessage = event.data; if(!addonMessage || typeof addonMessage.isScreenCapturingEnabled === 'undefined') return; if(addonMessage.isScreenCapturingEnabled === true) { alert(JSON.stringify(addonMessage.domains) + '\n are enabled for screen capturing.'); } else { alert(JSON.stringify(addonMessage.domains) + '\n are NOT enabled for screen capturing.'); } }, false); ``` ### Insights: Your requests to addon: `checkIfScreenCapturingEnabled`: ask addon to check if screen is already enabled for specific domains. Addon responses: 1. `isScreenCapturingEnabled` - Here `true` means domain is already enabled for specific domains. 2. `domains` - list of same domains that are enabled for screen capturing. ## Simplest Demo Try this demo after installing above addon: * [https://www.webrtc-experiment.com/getScreenId/](https://www.webrtc-experiment.com/getScreenId/) ## Wanna Deploy it Yourself? 1. Open [`index.js`](https://github.com/muaz-khan/Firefox-Extensions/blob/master/enable-screen-capturing/index.js) 2. Go to line 7 3. Replace `arrayOfMyOwnDomains` array with your own list of domains ```javascript // replace your own domains with below array var arrayOfMyOwnDomains = ['webrtc-experiment.com', 'www.webrtc-experiment.com', 'localhost', '127.0.0.1']; ``` ## How to Deploy? 1) Signup here: * https://addons.mozilla.org/en-US/firefox/users/register 2) Use unique-addon-name here: * https://github.com/muaz-khan/Firefox-Extensions/blob/master/enable-screen-capturing/package.json#L3 3) Add your own domains here: * https://github.com/muaz-khan/Firefox-Extensions/blob/master/enable-screen-capturing/index.js#L7 4) Make XPI of the directory. ``` [sudo] npm install jpm --global jpm run -b nightly # test in Firefox Nightly without making the XPI jpm xpi # it will create xpi file ``` 5) Submit the XPI here: * https://addons.mozilla.org/en-US/developers/addon/submit/1 Follow all steps. Read them carefully. This is hard/tough step to follow. Select valid browsers. E.g. Firefox 38 to Firefox 45. And submit your addon for "review". It will take 2-3 hours for a Mozilla AMO reviewer to review your addon. Then it will be available to public. ## License [Firefox-Extensions](https://github.com/muaz-khan/Firefox-Extensions) are released under [MIT licence](https://www.webrtc-experiment.com/licence/) . Copyright (c) [Muaz Khan](http://www.MuazKhan.com/). ================================================ FILE: Firefox-Extensions/enable-screen-capturing/content-script.js ================================================ // Muaz Khan - www.MuazKhan.com // MIT License - www.WebRTC-Experiment.com/licence // Github - github.com/muaz-khan/Firefox-Extensions // window.postMessage({ enableScreenCapturing: true }, "*"); window.addEventListener("message", function(event) { // do NOT allow external domains // via: https://github.com/muaz-khan/Firefox-Extensions/issues/11 if (event.source !== document.defaultView) return; var addonMessage = event.data; // this content-script is used to check whether screen capturing is enabled for your own domain or not. if(addonMessage && addonMessage.checkIfScreenCapturingEnabled) { self.port.on('is-screen-capturing-enabled-response', function(response) { window.postMessage(response, '*'); }); self.port.emit('is-screen-capturing-enabled'); } }, false); ================================================ FILE: Firefox-Extensions/enable-screen-capturing/index.js ================================================ // Muaz Khan - www.MuazKhan.com // MIT License - www.WebRTC-Experiment.com/licence // Github - github.com/muaz-khan/Firefox-Extensions var prefService = require('sdk/preferences/service'); var configToReferListOfAllowedDomains = 'media.getusermedia.screensharing.allowed_domains'; var configToEnableScreenCapturing = 'media.getusermedia.screensharing.enabled'; // replace your own domains with below array var arrayOfMyOwnDomains = ['webrtc-experiment.com', 'www.webrtc-experiment.com', 'rtcmulticonnection.herokuapp.com', 'localhost', '127.0.0.1']; // Patterns to match the websites that may check whether ther add-on is installed. // See https://developer.mozilla.org/en-US/Add-ons/SDK/Low-Level_APIs/util_match-pattern var patternsOfMyDomains = ['*.webrtc-experiment.com', '*.rtcmulticonnection.herokuapp.com', /https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?\/.*/]; // e.g. if 127.0.0.1 or localhost is already allowed by anyone else var listOfSimilarAlreadyAllowedDomains = []; // if(prefService.has(configToEnableScreenCapturing)) {} // this flag is enabled by default since Firefox version 37 or 38. // it maybe get removed in version 47-48. (As far as I can assume) // i.e. screen capturing will always be allowed to list of allowed_domains. prefService.set(configToEnableScreenCapturing, true); function addMyOwnDomains() { var existingDomains = prefService.get(configToReferListOfAllowedDomains).split(','); arrayOfMyOwnDomains.forEach(function(domain) { if (existingDomains.indexOf(domain) === -1) { existingDomains.push(domain); } // else { } else if (existingDomains.indexOf(domain) !== -1) { // Seems domain is already in the list. // Keep it when this addon is uninstalled. listOfSimilarAlreadyAllowedDomains.push(domain); } }); prefService.set(configToReferListOfAllowedDomains, existingDomains.join(',')); } addMyOwnDomains(); // below code handles addon-uninstall function removeMyDomainOnUnInstall() { var externalDomains = []; prefService.get(configToReferListOfAllowedDomains).split(',').forEach(function(domain) { // Skip Others Domains if (arrayOfMyOwnDomains.indexOf(domain) === -1) { // if its NOT mine, keep it. externalDomains.push(domain); } else if (listOfSimilarAlreadyAllowedDomains.indexOf(domain) !== -1) { // seems that localhost/127.0.0.1 are already added by external users externalDomains.push(domain); } }); prefService.set(configToReferListOfAllowedDomains, externalDomains.join(',')); } var { when: unload } = require("sdk/system/unload"); // By AMO policy global preferences must be changed back to their original value unload(function() { // remove only my own domains removeMyDomainOnUnInstall(); }); var tabs = require("sdk/tabs"); var mod = require("sdk/page-mod"); var self = require("sdk/self"); var pageMod = mod.PageMod({ include: patternsOfMyDomains, contentScriptFile: "./../content-script.js", contentScriptWhen: "start", // or "ready" onAttach: function(worker) { // webpages can verify if their domains are REALLY enabled or not. worker.port.on("is-screen-capturing-enabled", function() { var isScreenCapturingEnabled = false; var arrayOfEnabledDomains = []; prefService.get(configToReferListOfAllowedDomains).split(',').forEach(function(domain) { if(arrayOfMyOwnDomains.indexOf(domain) !== -1) { // maybe we need to check whether all of, my own, domains are enabled? isScreenCapturingEnabled = true; // we will pass this to the webpage // so webpage can understand which domain is enabled; and which is NOT. arrayOfEnabledDomains.push(domain); } }); worker.port.emit('is-screen-capturing-enabled-response', { isScreenCapturingEnabled: isScreenCapturingEnabled, // pass only those domains that are enabled for screen capturing // however those domains MUST be our own domains: arrayOfEnabledDomains }); }); } }); ================================================ FILE: Firefox-Extensions/enable-screen-capturing/package.json ================================================ { "title": "Enable Screen Capturing in Firefox", "name": "enable-screen-capturing", "id": "{3ef085b0-fe60-11df-8cff-0800200c9a66}", "version": "1.2.001", "description": "This firefox extension enables screen capturing support in Firefox for https://www.webrtc-experiment.com pages.", "main": "index.js", "author": "Muaz Khan ", "engines": { "firefox": ">=38.0a1" }, "license": "MIT", "permissions": { "unsafe-content-script": true } } ================================================ FILE: Firefox-Extensions/enable-screen-capturing/test/test-index.js ================================================ var main = require("../"); exports["test main"] = function(assert) { assert.pass("Unit test running!"); }; exports["test main async"] = function(assert, done) { assert.pass("async Unit test running!"); done(); }; require("sdk/test").run(exports); ================================================ FILE: Firefox-Extensions/enable-screen-capturing-old/README.md ================================================ # Enable Screen Capturing using [Firefox Extensions API](https://github.com/muaz-khan/Firefox-Extensions) Enable screen capturing using Firefox (for https://www.webrtc-experiment.com demos only): * [enable-screen-capturing.xpi](https://www.webrtc-experiment.com/store/firefox-extension/enable-screen-capturing.xpi) # To Install 1. type `about:config` into the URL bar in Firefox 2. in the Search box type `xpinstall.signatures.required` 3. double-click the preference, or right-click and select **"Toggle"**, to set it to `false`. To use in your own domains: Modify `bootstrap.js` file, line 18: ```javascript ['yourDomain.com', 'www.yourDomain.com'].forEach(function(domain) { if (values.indexOf(domain) === -1) { values.push(domain); addon_domains.push(domain); } }); ``` And modify `install.rdf` for you extension information (name, URL, icon etc.) ## Credits [Muaz Khan](https://github.com/muaz-khan): 1. Personal Webpage: http://www.muazkhan.com 2. Email: muazkh@gmail.com 3. Twitter: https://twitter.com/muazkh and https://twitter.com/WebRTCWeb 4. Google+: https://plus.google.com/+WebRTC-Experiment ## License [Firefox-Extensions](https://github.com/muaz-khan/Firefox-Extensions) are released under [MIT licence](https://www.webrtc-experiment.com/licence/) . Copyright (c) [Muaz Khan](https://plus.google.com/+MuazKhan). ================================================ FILE: Firefox-Extensions/enable-screen-capturing-old/bootstrap.js ================================================ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. * taken from: HenrikJoreteg/getScreenMedia/firefox-extension-sample * original source: https://hg.mozilla.org/users/blassey_mozilla.com/screenshare-whitelist/ */ var addon_domains = []; // list of domains the addon added var allowed_domains_pref = 'media.getusermedia.screensharing.allowed_domains'; var enable_screensharing_pref = 'media.getusermedia.screensharing.enabled'; function startup(data, reason) { if (reason === APP_STARTUP) { return; } var prefs = Components.classes['@mozilla.org/preferences-service;1'].getService(Components.interfaces.nsIPrefBranch); var values = prefs.getCharPref(allowed_domains_pref).split(','); ['webrtc-experiment.com', 'www.webrtc-experiment.com'].forEach(function(domain) { if (values.indexOf(domain) === -1) { values.push(domain); addon_domains.push(domain); } }); if(prefs.getBoolPref(enable_screensharing_pref) == false) { prefs.setBoolPref(enable_screensharing_pref, 1); } prefs.setCharPref(allowed_domains_pref, values.join(',')); } function shutdown(data, reason) { if (reason === APP_SHUTDOWN) { return; } var prefs = Components.classes["@mozilla.org/preferences-service;1"] .getService(Components.interfaces.nsIPrefBranch); var values = prefs.getCharPref(allowed_domains_pref).split(','); values = values.filter(function(value) { return addon_domains.indexOf(value) === -1; }); prefs.setCharPref(allowed_domains_pref, values.join(',')); } function install(data, reason) {} function uninstall(data, reason) {} ================================================ FILE: Firefox-Extensions/enable-screen-capturing-old/install.rdf ================================================ muazkh@gmail.com 2 Screen Capturing in Firefox Firefox extension to enable screen capturing 1.0 true Muaz Khan https://www.webrtc-experiment.com/ icon.png {ec8030f7-c20a-464f-9b0e-13a3a9e97384} 33.0 100000.* ================================================ FILE: Firefox-Extensions/enable-screen-capturing-old2/FirefoxScreenAddon.js ================================================ // Muaz Khan - www.MuazKhan.com // MIT License - www.WebRTC-Experiment.com/licence // Github - github.com/muaz-khan/Firefox-Extensions // FirefoxScreenAddon.checkIfScreenCapturingEnabled(domains, callback); // FirefoxScreenAddon.enableScreenCapturing(domains, callback); var FirefoxScreenAddon = (function() { return { checkIfScreenCapturingEnabled: function(domains, callback) { var isCallbackFired = false; // ask addon to check if screen capturing enabled for specific domains window.postMessage({ checkIfScreenCapturingEnabled: true, domains: domains }, "*"); // watch addon's response // addon will return "isScreenCapturingEnabled=true|false" window.addEventListener("message", function(event) { if(!isCallbackFired) return; var addonMessage = event.data; if(!addonMessage || typeof addonMessage.isScreenCapturingEnabled === 'undefined') return; isCallbackFired = true; callback(addonMessage); }, false); }, enableScreenCapturing: function(domains, callback) { var isCallbackFired = false; // request addon to enable screen capturing for your domains window.postMessage({ enableScreenCapturing: true, domains: domains }, "*"); // watch addon's response // addon will return "enabledScreenCapturing=true" for success // else "enabledScreenCapturing=false" for failure (i.e. user rejection) window.addEventListener("message", function(event) { if(!isCallbackFired) return; var addonMessage = event.data; if(!addonMessage || typeof addonMessage.enabledScreenCapturing === 'undefined') return; isCallbackFired = true; callback(addonMessage); }, false); } }; })(); ================================================ FILE: Firefox-Extensions/enable-screen-capturing-old2/README.md ================================================ # [Firefox Extensions](https://github.com/muaz-khan/Firefox-Extensions) > Enable screen capturing in Firefox for both localhost/127.0.0.1 and `https://www.webrtc-experiment.com` pages. ## Install from Firefox Addons Store * [https://addons.mozilla.org/en-US/firefox/addon/enable-screen-capturing/](https://addons.mozilla.org/en-US/firefox/addon/enable-screen-capturing/) ## How to reuse same addon for your own domains? Means that, you **don't need to publish your own addon**, you can reuse above link in your own domains/applications! You should copy/paste following code in your own webpage/domain (HTML/PHP/Python/etc.): ```javascript // request addon to enable screen capturing for your domains window.postMessage({ enableScreenCapturing: true, domains: ["www.yourdomain.com", "yourdomain.com"] }, "*"); // watch addon's response // addon will return "enabledScreenCapturing=true" for success // else "enabledScreenCapturing=false" for failure (i.e. user rejection) window.addEventListener("message", function(event) { var addonMessage = event.data; if(!addonMessage || typeof addonMessage.enabledScreenCapturing === 'undefined') return; if(addonMessage.enabledScreenCapturing === true) { // addonMessage.domains === [array-of-your-domains] alert(JSON.stringify(addonMessage.domains) + ' are enabled for screen capturing.'); } else { // reason === 'user-rejected' alert(addonMessage.reason); } }, false); ``` ## Simplest Demo Try this demo after installing above addon: * [https://www.webrtc-experiment.com/getScreenId/](https://www.webrtc-experiment.com/getScreenId/) ## Wanna Deploy it Yourself? 1. Open [`index.js`](https://github.com/muaz-khan/Firefox-Extensions/blob/master/enable-screen-capturing/index.js) 2. Go to line 7 3. Replace `arrayOfMyOwnDomains` array with your own list of domains ```javascript // replace your own domains with below array var arrayOfMyOwnDomains = ['webrtc-experiment.com', 'www.webrtc-experiment.com', 'localhost', '127.0.0.1']; ``` ## How to Deploy? 1) Signup here: * https://addons.mozilla.org/en-US/firefox/users/register 2) Use unique-addon-name here: * https://github.com/muaz-khan/Firefox-Extensions/blob/master/enable-screen-capturing/package.json#L3 3) Add your own domains here: * https://github.com/muaz-khan/Firefox-Extensions/blob/master/enable-screen-capturing/index.js#L7 4) Make XPI of the directory. ``` [sudo] npm install jpm --global jpm run -b nightly # test in Firefox Nightly without making the XPI jpm xpi # it will create xpi file ``` 5) Submit the XPI here: * https://addons.mozilla.org/en-US/developers/addon/submit/1 Follow all steps. Read them carefully. This is hard/tough step to follow. Select valid browsers. E.g. Firefox 38 to Firefox 45. And submit your addon for "review". It will take 2-3 hours for a Mozilla guy to review your addon. Then it will be available to public. ## License [Firefox-Extensions](https://github.com/muaz-khan/Firefox-Extensions) are released under [MIT licence](https://www.webrtc-experiment.com/licence/) . Copyright (c) [Muaz Khan](http://www.MuazKhan.com/). ================================================ FILE: Firefox-Extensions/enable-screen-capturing-old2/content-script.js ================================================ // Muaz Khan - www.MuazKhan.com // MIT License - www.WebRTC-Experiment.com/licence // Github - github.com/muaz-khan/Firefox-Extensions // window.postMessage({ enableScreenCapturing: true, domains: ["www.firefox.com"] }, "*"); window.addEventListener("message", function(event) { var addonMessage = event.data; if(addonMessage && addonMessage.enableScreenCapturing && addonMessage.domains && addonMessage.domains.length) { var confirmMessage = 'Current webpage requested to enable WebRTC Screen Capturing for following domains:\n'; confirmMessage += JSON.stringify(addonMessage.domains, null, '\t') + '\n\n'; confirmMessage += 'Please confirm to enable "permanent" screen capturing for above domains.'; if(window.confirm(confirmMessage)) { self.port.emit('installation-confirmed', addonMessage.domains); // tell webpage that user confirmed screen capturing & its enabled for his domains. window.postMessage({ enabledScreenCapturing: true, domains: addonMessage.domains }, '*'); } else { // tell webpage that user denied/rejected screen capturing for his domains. window.postMessage({ enabledScreenCapturing: false, domains: addonMessage.domains, reason: 'user-rejected' }, '*'); } } if(addonMessage && addonMessage.checkIfScreenCapturingEnabled && addonMessage.domains && addonMessage.domains.length) { self.port.on('is-screen-capturing-enabled-response', function(response) { window.postMessage(response, '*'); }); self.port.emit('is-screen-capturing-enabled', addonMessage.domains); } }, false); ================================================ FILE: Firefox-Extensions/enable-screen-capturing-old2/index.js ================================================ // Muaz Khan - www.MuazKhan.com // MIT License - www.WebRTC-Experiment.com/licence // Github - github.com/muaz-khan/Firefox-Extensions var prefService = require('sdk/preferences/service'); var configToReferListOfAllowedDomains = 'media.getusermedia.screensharing.allowed_domains'; var configToEnableScreenCapturing = 'media.getusermedia.screensharing.enabled'; // replace your own domains with below array var arrayOfMyOwnDomains = ['webrtc-experiment.com', 'www.webrtc-experiment.com', 'localhost', '127.0.0.1']; // e.g. if 127.0.0.1 or localhost is already allowed by anyone else var listOfSimilarAlreadyAllowedDomains = []; // if(prefService.has(configToEnableScreenCapturing)) {} // this flag is enabled by default since Firefox version 37 or 38. // it maybe get removed in version 47-48. (As far as I can assume) // i.e. screen capturing will always be allowed to list of allowed_domains. prefService.set(configToEnableScreenCapturing, true); function addMyOwnDomains() { var existingDomains = prefService.get(configToReferListOfAllowedDomains).split(','); arrayOfMyOwnDomains.forEach(function(domain) { if (existingDomains.indexOf(domain) === -1) { existingDomains.push(domain); } // else { } else if (existingDomains.indexOf(domain) !== -1) { // Seems domain is already in the list. // Keep it when this addon is uninstalled. listOfSimilarAlreadyAllowedDomains.push(domain); } }); prefService.set(configToReferListOfAllowedDomains, existingDomains.join(',')); } addMyOwnDomains(); // below code handles addon-uninstall function removeMyDomainOnUnInstall() { var externalDomains = []; prefService.get(configToReferListOfAllowedDomains).split(',').forEach(function(domain) { // Skip Others Domains if (arrayOfMyOwnDomains.indexOf(domain) === -1) { // if its NOT mine, keep it. externalDomains.push(domain); } else if (listOfSimilarAlreadyAllowedDomains.indexOf(domain) !== -1) { // seems that localhost/127.0.0.1 are already added by external users externalDomains.push(domain); } }); prefService.set(configToReferListOfAllowedDomains, externalDomains.join(',')); } var { when: unload } = require("sdk/system/unload"); // By AMO policy global preferences must be changed back to their original value unload(function() { // remove only my own domains removeMyDomainOnUnInstall(); }); /* * connect with webpage using postMessage * a webpage can use following API to enable screen capturing for his domains * window.postMessage({ enableScreenCapturing: true, domains: ["www.firefox.com"] }, "*"); * * current firefox user is always asked to confirm whether he is OK to enable screen capturing for requested domains. */ var tabs = require("sdk/tabs"); var mod = require("sdk/page-mod"); var self = require("sdk/self"); var pageMod = mod.PageMod({ include: ["*"], contentScriptFile: "./../content-script.js", contentScriptWhen: "start", // or "ready" onAttach: function(worker) { worker.port.on("installation-confirmed", function(domains) { // make sure that this addon's self-domains (i.e. "arrayOfMyOwnDomains") // are not included in the "listOfSimilarAlreadyAllowedDomains" array. removeMyDomainOnUnInstall(); arrayOfMyOwnDomains = arrayOfMyOwnDomains.concat(domains); addMyOwnDomains(); }); worker.port.on("is-screen-capturing-enabled", function(domains) { var isScreenCapturingEnabled = false; prefService.get(configToReferListOfAllowedDomains).split(',').forEach(function(domain) { if(domains.indexOf(domain) !== -1) { isScreenCapturingEnabled = true; } }); worker.port.emit('is-screen-capturing-enabled-response', { isScreenCapturingEnabled: isScreenCapturingEnabled, domains: domains }); }); } }); ================================================ FILE: Firefox-Extensions/enable-screen-capturing-old2/package.json ================================================ { "title": "Enable Screen Capturing in Firefox", "name": "enable-screen-capturing", "id": "{3ef085b0-fe60-11df-8cff-0800200c9a66}", "version": "1.1.001", "description": "This firefox extension enables screen capturing support in Firefox for https://www.webrtc-experiment.com pages.", "main": "index.js", "author": "Muaz Khan ", "engines": { "firefox": ">=38.0a1" }, "license": "MIT", "permissions": { "unsafe-content-script": true } } ================================================ FILE: Firefox-Extensions/enable-screen-capturing-old2/test/test-index.js ================================================ var main = require("../"); exports["test main"] = function(assert) { assert.pass("Unit test running!"); }; exports["test main async"] = function(assert, done) { assert.pass("async Unit test running!"); done(); }; require("sdk/test").run(exports); ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2012-2020 [Muaz Khan](https://github.com/muaz-khan) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: MediaStreamRecorder/.gitignore ================================================ # Node node_modules # bower bower_components npm-debug.log ================================================ FILE: MediaStreamRecorder/.npmignore ================================================ node_modules lib-cov npm-debug.log bower_components ================================================ FILE: MediaStreamRecorder/.travis.yml ================================================ language: node_js node_js: - "0.11" install: npm install before_script: - npm install grunt-cli@0.1.13 -g - npm install grunt@0.4.5 - grunt after_failure: npm install && grunt matrix: fast_finish: true ================================================ FILE: MediaStreamRecorder/AudioStreamRecorder/FlashAudioRecorder.js ================================================ // muazkh - github.com/muaz-khan // @neizerth - github.com/neizerth // MIT License - https://webrtc-experiment.appspot.com/licence/ // Documentation - https://github.com/streamproc/MediaStreamRecorder // ========================================================== // FlashAudioRecorder.js // Based on recorder.js - https://github.com/jwagener/recorder.js function FlashAudioRecorder(o) { if (o == null) { o = {}; } var self = this, baseUrl = getBaseUrl(), defaults = { swfObjectPath: baseUrl + 'lib/recorder.js/recorder.swf', jsLibPath: baseUrl + 'lib/recorder.js/recorder.js', encoderPath: baseUrl + 'lib/wavencoder/wavencoder.js', flashContainer: null, dataType: 'url', // url | blob | raw | dataUri | false uploadParams: { url: '', audioParam: "", params: {} }, ondataavailable: null, onstop: function(e) { }, onstart: function(e) { }, onFlashSecurity: function(e) { var flashContainer = Recorder.options.flashContainer; flashContainer.style.left = ((window.innerWidth || document.body.offsetWidth) / 2) - 115 + "px"; flashContainer.style.top = ((window.innerHeight || document.body.offsetHeight) / 2) - 70 + "px"; }, onready: function(e) { }, onerror: function(e) { } }, _initialized = false, _startRequest = false, options = extend(defaults, o); include(options.jsLibPath, init); function init() { if (!_initialized) { Recorder._defaultOnShowFlash = true; Recorder.initialize({ swfSrc: options.swfObjectPath, flashContainer: options.flashContainer, onFlashSecurity: function(e) { self.onFlashSecurity(e); }, initialized: function(e) { self.onready(); _initialized = true; if (_startRequest) { start(); } } }); } } function initEncoder() { WavEncoder.defaults = { numChannels: 1, // mono sampleRateHz: 44100, // 44100 Hz bytesPerSample: 2, // 16 bit clip: true }; } function start(interval) { _startRequest = true; if (_initialized) { if (self.state != 'inactive') { var error = { message: 'The object is in an invalid state', code: DOMException.INVALID_STATE_ERR } self.onerror(error); throw error; } self.state = 'recording'; _startRequest = false; Recorder.record({ start: function(e) { self.onstart(e); } }); } } function stop() { if (self.state == 'inactive') { var error = { message: 'The object is in an invalid state', code: DOMException.INVALID_STATE_ERR } self.onerror(error); throw error; } self.state = 'inactive'; _startRequest = false; Recorder.stop(); self.onstop(); if (typeof self.ondataavailable == 'function') { if (self.dataType == 'url') { upload(); } else if (self.dataType != false) { handleBinaryData(); } } } function handleBinaryData() { Recorder.getAudioData(function(data) { if (self.dataType == 'raw') { return self.ondataavailable({ data: data, dataType: self.dataType }); }; include(options.encoderPath, function() { initEncoder(); var datauri = WavEncoder.encode(data); if (self.dataType == 'datauri') { return self.ondataavailable({ data: datauri, dataType: self.dataType }); }; var audioBlob = new Blob([datauri], { type: self.mimeType }); if (self.dataType == 'blob') { return self.ondataavailable({ data: audioBlob, dataType: self.dataType }); }; }); }); } function upload(params) { if (params == null) params = self.uploadParams; params.success = function(msg) { self.ondataavailable({ data: msg, dataType: 'url' }); }; params.error = function(msg) { self.onerror({ msg: msg }); } Recorder.upload(params); } // get script folder function getBaseUrl() { var scripts = document.head.getElementsByTagName("script"); var loc = scripts[scripts.length - 1].src; return loc.substring(0, loc.lastIndexOf('/')) + '/'; } // extending user options function extend(o1, o2) { var obj = {}; for (var i in o1) { if (o2[i] != null) { if (typeof o2[i] == "object") { obj[i] = extend(o2[i], {}); } else { obj[i] = o2[i]; } } else { if (typeof o2[i] == "object") { obj[i] = extend(o1[i], {}); } else { obj[i] = o1[i]; } } } return obj; } function include(src, callback) { var scripts = document.getElementsByTagName('script'), found = false; for (var i = 0, len = scripts.length; i < len; i++) { if (scripts[i].getAttribute('src') == src) { found = true; } } if (found) { if (typeof callback == 'function') { callback(); } } else { var js = document.createElement("script"); js.type = "text/javascript"; js.src = src; if (typeof callback == 'function') { js.onload = callback; } document.body.appendChild(js); } } this.ondataavailable = options.ondataavailable; this.onstop = options.onstop; this.onstart = options.onstart; this.onFlashSecurity = options.onFlashSecurity; this.onerror = options.onerror; this.onready = options.onready; this.state = 'inactive'; this.mimeType = 'audio/wav'; this.uploadParams = options.uploadParams; this.dataType = options.dataType; this.start = start; this.upload = upload; this.stop = stop; this.baseUrl = baseUrl; } ================================================ FILE: MediaStreamRecorder/AudioStreamRecorder/FlashAudioRecorder.md ================================================ # FlashAudioRecorder.js usage This module based on recorder.js library - https://github.com/jwagener/recorder.js It support uploading file after record or getting them as Blob type ro ## 1. Include this module ``` ``` ## 2. Basic usage - through serverside Initialize ``` var flashRecorder = new FlashAudioRecorder({ uploadParams:{ url: "http://url", audioParam: "qqfile", // same as in recorder.js lib params: {} } }); ``` Start capture audio: ``` flashRecorder.start(); ``` Stop capture audio ``` flashRecorder.stop(); ``` Get uploaded file url (you must write your own serverside script to handle upload.) ``` flashRecorder.ondataavailable = function(e) { console.log(e.msg); // server answer } ``` ## 3. Attributes ### dataType Which type of data should be returned this param used when ondataavailable event appended to instance You can pass value in object instance or directly pass to constructor Example 1: ``` var flashRecorder = new FlashAudioRecorder({ dataType:'dataUri' }); ``` Example 2: ``` var flashRecorder = new FlashAudioRecorder(); flashRecorder.dataType = 'blob'; ``` Values: #### false module do nothing after record ends. You can call upload method to upload record manually #### 'url' - Default when record ends, module upload this data immediately #### 'blob' call ondataavailable event with blob, that contains all recorded data. Note: this case gives a js memory leaks With 1m record you can get about 300Mb of memory per your process. Use this metho only on short recordings (< 45 sec) and short-living applications. If you want upload your data immediately after stop recording, its better to use 'url' dataType Example ```javascript var flashRecorder = new FlashAudioRecorder({ dataType:'blob' }); flashRecorder.start(); flashRecorder.stop(); flashRecorder.ondataavailable = function(e) { var formData = new FormData(); formData.append('qqFile', e.data); xhr.send(formData); } ``` #### 'raw' get recorded array. You can easily edit your record. Same as 'blob' dataType Limitations #### 'dataUri' with that type you can easily listen your pre-recorded data or save it to Computer using Javascript File API Same as 'blob' dataType Limitations Example ```javascript var flashRecorder = new FlashAudioRecorder({ dataType:'dataUri' }); flashRecorder.start(); flashRecorder.stop(); flashRecorder.ondataavailable = function(e) { var audio = new Audio(e.data); audio.play(); } ``` ### baseUrl path to FlashAudioRecorder.js. Readonly ## 4. Constructor methods ### swfObjectPath path to recorder.swf default: baseUrl+'lib/recorder.js/recorder.swf' ### jsLibPath path to recorder.js default: baseUrl+'lib/recorder.js/recorder.js' ### encoderPath path to [WavEncoder.js](https://github.com/fritzo/wavencoderjs) THis lib used in 'blob' and 'dataUri' dataTypes default: encoderPath: baseUrl+'lib/wavencoder/wavencoder.js' ### uploadParams params that will be passed to [Recorder.upload function](https://github.com/jwagener/recorder.js) except success and error events (see onerror and ondataavailiable events) Example ``` new FlashAudioRecorder({ uploadParams:{ method: "POST" // (not implemented) (optional, defaults to POST) HTTP Method can be either POST or PUT url: "http://api.soundcloud.com/tracks", // URL to upload to (needs to have a suitable crossdomain.xml for Adobe Flash) audioParam: "track[asset_data]", // Name for the audio data parameter params: { // Additional parameters (needs to be a flat object) "track[title]": "some track", "oauth_token": "VALID_TOKEN" } } }) ``` ## 5. Methods ### start start capture audiostream ### stop stop capture audiostream ### upload upload pre-recorded data ``` var flashRecorder = new FlashAudioRecorder({ dataType:false // manual upload }); flashRecorder.start(); flashRecorder.stop(); flashRecorder.upload({ method: "POST" // (not implemented) (optional, defaults to POST) HTTP Method can be either POST or PUT url: "http://api.soundcloud.com/tracks", // URL to upload to (needs to have a suitable crossdomain.xml for Adobe Flash) audioParam: "track[asset_data]", // Name for the audio data parameter params: { // Additional parameters (needs to be a flat object) "track[title]": "some track", "oauth_token": "VALID_TOKEN" } }); ``` ## 6. Events This module supports all events that you can see in [W3C API](https://dvcs.w3.org/hg/dap/raw-file/tip/media-stream-capture/MediaRecorder.html) You can pass any event function in object instance or directly pass it to constructor params ``` var flashRecorder = new FlashAudioRecorder({ onstart: function() { } }); flashRecorder.onstart = function(e) { console.log('start'); } flashRecorder.onstop = function(e) { console.log('stop'); } flashRecorder.ondataavailable = function(e) { console.log('Data availiable') } // triggering when upload fails flashRecorder.onerror = function(e) { console.log('error'); } ``` Also you can bind this events: ``` // taken from recorder.js onFlashSecurity event // (optional) callback when the flash swf needs to be visible // this allows you to hide/show the flashContainer element on demand. flashRecorder.onFlashSecurity = function(e) { } // triggers when flash movie loaded into DOM flashRecorder.onready = function(e) { } ``` = ##### License [MediaStreamRecorder.js](https://github.com/streamproc/MediaStreamRecorder) library is released under [MIT licence](https://www.webrtc-experiment.com/licence/) . Copyright (c) 2013 [Muaz Khan](https://github.com/muaz-khan) and [neizerth](https://github.com/neizerth). ================================================ FILE: MediaStreamRecorder/AudioStreamRecorder/MediaRecorderWrapper.js ================================================ // ================== // MediaRecorder.js /** * Implementation of https://dvcs.w3.org/hg/dap/raw-file/default/media-stream-capture/MediaRecorder.html * The MediaRecorder accepts a mediaStream as input source passed from UA. When recorder starts, * a MediaEncoder will be created and accept the mediaStream as input source. * Encoder will get the raw data by track data changes, encode it by selected MIME Type, then store the encoded in EncodedBufferCache object. * The encoded data will be extracted on every timeslice passed from Start function call or by RequestData function. * Thread model: * When the recorder starts, it creates a "Media Encoder" thread to read data from MediaEncoder object and store buffer in EncodedBufferCache object. * Also extract the encoded data and create blobs on every timeslice passed from start function or RequestData function called by UA. */ function MediaRecorderWrapper(mediaStream) { var self = this; /** * This method records MediaStream. * @method * @memberof MediaStreamRecorder * @example * recorder.start(5000); */ this.start = function(timeSlice, __disableLogs) { this.timeSlice = timeSlice || 5000; if (!self.mimeType) { self.mimeType = 'video/webm'; } if (self.mimeType.indexOf('audio') !== -1) { if (mediaStream.getVideoTracks().length && mediaStream.getAudioTracks().length) { var stream; if (!!navigator.mozGetUserMedia) { stream = new MediaStream(); stream.addTrack(mediaStream.getAudioTracks()[0]); } else { // webkitMediaStream stream = new MediaStream(mediaStream.getAudioTracks()); } mediaStream = stream; } } if (self.mimeType.indexOf('audio') !== -1) { self.mimeType = IsChrome ? 'audio/webm' : 'audio/ogg'; } self.dontFireOnDataAvailableEvent = false; var recorderHints = { mimeType: self.mimeType }; if (!self.disableLogs && !__disableLogs) { console.log('Passing following params over MediaRecorder API.', recorderHints); } if (mediaRecorder) { // mandatory to make sure Firefox doesn't fails to record streams 3-4 times without reloading the page. mediaRecorder = null; } if (IsChrome && !isMediaRecorderCompatible()) { // to support video-only recording on stable recorderHints = 'video/vp8'; } // http://dxr.mozilla.org/mozilla-central/source/content/media/MediaRecorder.cpp // https://wiki.mozilla.org/Gecko:MediaRecorder // https://dvcs.w3.org/hg/dap/raw-file/default/media-stream-capture/MediaRecorder.html // starting a recording session; which will initiate "Reading Thread" // "Reading Thread" are used to prevent main-thread blocking scenarios try { mediaRecorder = new MediaRecorder(mediaStream, recorderHints); } catch (e) { // if someone passed NON_supported mimeType // or if Firefox on Android mediaRecorder = new MediaRecorder(mediaStream); } if ('canRecordMimeType' in mediaRecorder && mediaRecorder.canRecordMimeType(self.mimeType) === false) { if (!self.disableLogs) { console.warn('MediaRecorder API seems unable to record mimeType:', self.mimeType); } } // i.e. stop recording when

================================================ FILE: part-of-screen-sharing/iframe/otherpage.html ================================================ 

I am other page!

What's up?

Good Luck Sir! ================================================ FILE: part-of-screen-sharing/realtime-chat/No-WebRTC-Chat.html ================================================ NoWebRTC Realtime Text Chat! Muaz Khan

NoWebRTC Realtime text chat! Source code on Github

/ How this work?/ Realtime Chat using RTCDataChannel!
Preview image

intro:

  1. Sharing part of the screen... not the entire screen!
  2. Everything is synchronized in realtime.
  3. It is a realtime text chat with a realtime preview!
  4. You can see what your fellow is typing...in realtime!
  5. Works fine on all modern web browsers. Firefox nightly/ aurora/ stableor Chrome canaryis preferred/recommended
================================================ FILE: part-of-screen-sharing/realtime-chat/README.md ================================================ #### WebRTC Part of Screen Sharing Demos 1. [Realtime Chat](https://googledrive.com/host/0B6GWd_dUUTT8RzVSRVU2MlIxcm8/realtime-chat/) 2. [No-WebRTC Realtime Chat](https://googledrive.com/host/0B6GWd_dUUTT8RzVSRVU2MlIxcm8/realtime-chat/No-WebRTC-Chat.html) 3. [Part of Screen Sharing](https://googledrive.com/host/0B6GWd_dUUTT8RzVSRVU2MlIxcm8/part-of-screen-sharing/) 4. [Part of Screen Sharing using RTCDataChannel](https://googledrive.com/host/0B6GWd_dUUTT8RzVSRVU2MlIxcm8/part-of-screen-sharing/RTCDataChannel/) = #### Browser Support WebRTC [Part of Screen Sharing using RTCDataChannel](https://googledrive.com/host/0B6GWd_dUUTT8RzVSRVU2MlIxcm8/part-of-screen-sharing/RTCDataChannel/) experiment works fine on following web-browsers: | Browser | Support | | ------------- |:-------------:| | Firefox | [Stable](http://www.mozilla.org/en-US/firefox/new/) | | Firefox | [Aurora](http://www.mozilla.org/en-US/firefox/aurora/) | | Firefox | [Nightly](http://nightly.mozilla.org/) | | Google Chrome | [Canary](https://www.google.com/intl/en/chrome/browser/canary.html) | = #### License These WebRTC **Part of Screen Sharing** experiments are released under [MIT licence](https://www.webrtc-experiment.com/licence/) . Copyright (c) 2013 [Muaz Khan](https://plus.google.com/100325991024054712503). ================================================ FILE: part-of-screen-sharing/realtime-chat/how-this-work.html ================================================ How Realtime Text Chat works? Muaz Khan

How realtime chat works? Source code on Github

  1. Tick "Is Sync in Realtime" checkbox if you want to share text in realtime.
  2. Otherwise, type number of milliseconds after to synchronize your state.
  3. Tick "Is Pause Syncing" if you want to take rest.
  4. Tick "Is Code" button if you want to share "code". So, coding fonts will be used in the output panel.
Sharing part of the screen in realtime!

Browser Support:

Works fine on chrome canary and firefox nightly/aurora/stable. No-WebRTC chatworks fine on any browser supports Canvas2D! ================================================ FILE: part-of-screen-sharing/screenshot-dev.js ================================================ // Last time updated at Sep 10, 2015, 08:32:23 // Muaz Khan - https://github.com/muaz-khan // MIT License - https://www.webrtc-experiment.com/licence/ // Documentation - https://github.com/muaz-khan/WebRTC-Experiment/tree/master/part-of-screen-sharing // Note: All libraries listed in this file are "external libraries" // ---- and has their own copyrights. Taken from "html2canvas" project. /* Core -------------------- */ "use strict"; var _html2canvas = {}, previousElement, computedCSS, html2canvas; function h2clog(a) { if (_html2canvas.logging && window.console && window.console.log) { window.console.log(a); } } _html2canvas.Util = {}; _html2canvas.Util.trimText = (function(isNative){ return function(input){ if(isNative) { return isNative.apply( input ); } else { return ((input || '') + '').replace( /^\s+|\s+$/g , '' ); } }; })( String.prototype.trim ); _html2canvas.Util.parseBackgroundImage = function (value) { var whitespace = ' \r\n\t', method, definition, prefix, prefix_i, block, results = [], c, mode = 0, numParen = 0, quote, args; var appendResult = function(){ if(method) { if(definition.substr( 0, 1 ) === '"') { definition = definition.substr( 1, definition.length - 2 ); } if(definition) { args.push(definition); } if(method.substr( 0, 1 ) === '-' && (prefix_i = method.indexOf( '-', 1 ) + 1) > 0) { prefix = method.substr( 0, prefix_i); method = method.substr( prefix_i ); } results.push({ prefix: prefix, method: method.toLowerCase(), value: block, args: args }); } args = []; //for some odd reason, setting .length = 0 didn't work in safari method = prefix = definition = block = ''; }; appendResult(); for(var i = 0, ii = value.length; i -1){ continue; } switch(c) { case '"': if(!quote) { quote = c; } else if(quote === c) { quote = null; } break; case '(': if(quote) { break; } else if(mode === 0) { mode = 1; block += c; continue; } else { numParen++; } break; case ')': if(quote) { break; } else if(mode === 1) { if(numParen === 0) { mode = 0; block += c; appendResult(); continue; } else { numParen--; } } break; case ',': if(quote) { break; } else if(mode === 0) { appendResult(); continue; } else if (mode === 1) { if(numParen === 0 && !method.match(/^url$/i)) { args.push(definition); definition = ''; block += c; continue; } } break; } block += c; if(mode === 0) { method += c; } else { definition += c; } } appendResult(); return results; }; _html2canvas.Util.Bounds = function getBounds (el) { var clientRect, bounds = {}; if (el.getBoundingClientRect){ clientRect = el.getBoundingClientRect(); // TODO add scroll position to bounds, so no scrolling of window necessary bounds.top = clientRect.top; bounds.bottom = clientRect.bottom || (clientRect.top + clientRect.height); bounds.left = clientRect.left; // older IE doesn't have width/height, but top/bottom instead bounds.width = clientRect.width || (clientRect.right - clientRect.left); bounds.height = clientRect.height || (clientRect.bottom - clientRect.top); return bounds; } }; _html2canvas.Util.getCSS = function (el, attribute, index) { // return $(el).css(attribute); var val, isBackgroundSizePosition = attribute.match( /^background(Size|Position)$/ ); function toPX( attribute, val ) { var rsLeft = el.runtimeStyle && el.runtimeStyle[ attribute ], left, style = el.style; // Check if we are not dealing with pixels, (Opera has issues with this) // Ported from jQuery css.js // From the awesome hack by Dean Edwards // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 // If we're not dealing with a regular pixel number // but a number that has a weird ending, we need to convert it to pixels if ( !/^-?[0-9]+\.?[0-9]*(?:px)?$/i.test( val ) && /^-?\d/.test( val ) ) { // Remember the original values left = style.left; // Put in the new values to get a computed value out if ( rsLeft ) { el.runtimeStyle.left = el.currentStyle.left; } style.left = attribute === "fontSize" ? "1em" : (val || 0); val = style.pixelLeft + "px"; // Revert the changed values style.left = left; if ( rsLeft ) { el.runtimeStyle.left = rsLeft; } } if (!/^(thin|medium|thick)$/i.test( val )) { return Math.round(parseFloat( val )) + "px"; } return val; } if (previousElement !== el) { computedCSS = document.defaultView.getComputedStyle(el, null); } val = computedCSS[attribute]; if (isBackgroundSizePosition) { val = (val || '').split( ',' ); val = val[index || 0] || val[0] || 'auto'; val = _html2canvas.Util.trimText(val).split(' '); if(attribute === 'backgroundSize' && (!val[ 0 ] || val[ 0 ].match( /cover|contain|auto/ ))) { //these values will be handled in the parent function } else { val[ 0 ] = ( val[ 0 ].indexOf( "%" ) === -1 ) ? toPX( attribute + "X", val[ 0 ] ) : val[ 0 ]; if(val[ 1 ] === undefined) { if(attribute === 'backgroundSize') { val[ 1 ] = 'auto'; return val; } else { // IE 9 doesn't return double digit always val[ 1 ] = val[ 0 ]; } } val[ 1 ] = ( val[ 1 ].indexOf( "%" ) === -1 ) ? toPX( attribute + "Y", val[ 1 ] ) : val[ 1 ]; } } else if ( /border(Top|Bottom)(Left|Right)Radius/.test( attribute) ) { var arr = val.split(" "); if ( arr.length <= 1 ) { arr[ 1 ] = arr[ 0 ]; } arr[ 0 ] = parseInt( arr[ 0 ], 10 ); arr[ 1 ] = parseInt( arr[ 1 ], 10 ); val = arr; } return val; }; _html2canvas.Util.resizeBounds = function( current_width, current_height, target_width, target_height, stretch_mode ){ var target_ratio = target_width / target_height, current_ratio = current_width / current_height, output_width, output_height; if(!stretch_mode || stretch_mode === 'auto') { output_width = target_width; output_height = target_height; } else { if(target_ratio < current_ratio ^ stretch_mode === 'contain') { output_height = target_height; output_width = target_height * current_ratio; } else { output_width = target_width; output_height = target_width / current_ratio; } } return { width: output_width, height: output_height }; }; function backgroundBoundsFactory( prop, el, bounds, image, imageIndex, backgroundSize ) { var bgposition = _html2canvas.Util.getCSS( el, prop, imageIndex ) , topPos, left, percentage, val; if (bgposition.length === 1){ val = bgposition[0]; bgposition = []; bgposition[0] = val; bgposition[1] = val; } if (bgposition[0].toString().indexOf("%") !== -1){ percentage = (parseFloat(bgposition[0])/100); left = bounds.width * percentage; if(prop !== 'backgroundSize') { left -= (backgroundSize || image).width*percentage; } } else { if(prop === 'backgroundSize') { if(bgposition[0] === 'auto') { left = image.width; } else { if(bgposition[0].match(/contain|cover/)) { var resized = _html2canvas.Util.resizeBounds( image.width, image.height, bounds.width, bounds.height, bgposition[0] ); left = resized.width; topPos = resized.height; } else { left = parseInt (bgposition[0], 10 ); } } } else { left = parseInt( bgposition[0], 10 ); } } if(bgposition[1] === 'auto') { topPos = left / image.width * image.height; } else if (bgposition[1].toString().indexOf("%") !== -1){ percentage = (parseFloat(bgposition[1])/100); topPos = bounds.height * percentage; if(prop !== 'backgroundSize') { topPos -= (backgroundSize || image).height * percentage; } } else { topPos = parseInt(bgposition[1],10); } return [left, topPos]; } _html2canvas.Util.BackgroundPosition = function( el, bounds, image, imageIndex, backgroundSize ) { var result = backgroundBoundsFactory( 'backgroundPosition', el, bounds, image, imageIndex, backgroundSize ); return { left: result[0], top: result[1] }; }; _html2canvas.Util.BackgroundSize = function( el, bounds, image, imageIndex ) { var result = backgroundBoundsFactory( 'backgroundSize', el, bounds, image, imageIndex ); return { width: result[0], height: result[1] }; }; _html2canvas.Util.Extend = function (options, defaults) { for (var key in options) { if (options.hasOwnProperty(key)) { defaults[key] = options[key]; } } return defaults; }; /* * Derived from jQuery.contents() * Copyright 2010, John Resig * Dual licensed under the MIT or GPL Version 2 licenses. * http://jquery.org/license */ _html2canvas.Util.Children = function( elem ) { var children; try { children = (elem.nodeName && elem.nodeName.toUpperCase() === "IFRAME") ? elem.contentDocument || elem.contentWindow.document : (function( array ){ var ret = []; if ( array !== null ) { (function( first, second ) { var i = first.length, j = 0; if ( typeof second.length === "number" ) { for ( var l = second.length; j < l; j++ ) { first[ i++ ] = second[ j ]; } } else { while ( second[j] !== undefined ) { first[ i++ ] = second[ j++ ]; } } first.length = i; return first; })( ret, array ); } return ret; })( elem.childNodes ); } catch (ex) { h2clog("html2canvas.Util.Children failed with exception: " + ex.message); children = []; } return children; }; /* font ----------------------------- */ _html2canvas.Util.Font = (function () { var fontData = {}; return function(font, fontSize, doc) { if (fontData[font + "-" + fontSize] !== undefined) { return fontData[font + "-" + fontSize]; } var container = doc.createElement('div'), img = doc.createElement('img'), span = doc.createElement('span'), sampleText = 'Hidden Text', baseline, middle, metricsObj; container.style.visibility = "hidden"; container.style.fontFamily = font; container.style.fontSize = fontSize; container.style.margin = 0; container.style.padding = 0; doc.body.appendChild(container); // http://probablyprogramming.com/2009/03/15/the-tiniest-gif-ever (handtinywhite.gif) img.src = "data:image/gif;base64,R0lGODlhAQABAIABAP///wAAACwAAAAAAQABAAACAkQBADs="; img.width = 1; img.height = 1; img.style.margin = 0; img.style.padding = 0; img.style.verticalAlign = "baseline"; span.style.fontFamily = font; span.style.fontSize = fontSize; span.style.margin = 0; span.style.padding = 0; span.appendChild(doc.createTextNode(sampleText)); container.appendChild(span); container.appendChild(img); baseline = (img.offsetTop - span.offsetTop) + 1; container.removeChild(span); container.appendChild(doc.createTextNode(sampleText)); container.style.lineHeight = "normal"; img.style.verticalAlign = "super"; middle = (img.offsetTop-container.offsetTop) + 1; metricsObj = { baseline: baseline, lineWidth: 1, middle: middle }; fontData[font + "-" + fontSize] = metricsObj; doc.body.removeChild(container); return metricsObj; }; })(); /* Generate ------------------------- */ (function(){ _html2canvas.Generate = {}; var reGradients = [ /^(-webkit-linear-gradient)\(([a-z\s]+)([\w\d\.\s,%\(\)]+)\)$/, /^(-o-linear-gradient)\(([a-z\s]+)([\w\d\.\s,%\(\)]+)\)$/, /^(-webkit-gradient)\((linear|radial),\s((?:\d{1,3}%?)\s(?:\d{1,3}%?),\s(?:\d{1,3}%?)\s(?:\d{1,3}%?))([\w\d\.\s,%\(\)\-]+)\)$/, /^(-moz-linear-gradient)\(((?:\d{1,3}%?)\s(?:\d{1,3}%?))([\w\d\.\s,%\(\)]+)\)$/, /^(-webkit-radial-gradient)\(((?:\d{1,3}%?)\s(?:\d{1,3}%?)),\s(\w+)\s([a-z\-]+)([\w\d\.\s,%\(\)]+)\)$/, /^(-moz-radial-gradient)\(((?:\d{1,3}%?)\s(?:\d{1,3}%?)),\s(\w+)\s?([a-z\-]*)([\w\d\.\s,%\(\)]+)\)$/, /^(-o-radial-gradient)\(((?:\d{1,3}%?)\s(?:\d{1,3}%?)),\s(\w+)\s([a-z\-]+)([\w\d\.\s,%\(\)]+)\)$/ ]; /* * TODO: Add IE10 vendor prefix (-ms) support * TODO: Add W3C gradient (linear-gradient) support * TODO: Add old Webkit -webkit-gradient(radial, ...) support * TODO: Maybe some RegExp optimizations are possible ;o) */ _html2canvas.Generate.parseGradient = function(css, bounds) { var gradient, i, len = reGradients.length, m1, stop, m2, m2Len, step, m3, tl,tr,br,bl; for(i = 0; i < len; i+=1){ m1 = css.match(reGradients[i]); if(m1) { break; } } if(m1) { switch(m1[1]) { case '-webkit-linear-gradient': case '-o-linear-gradient': gradient = { type: 'linear', x0: null, y0: null, x1: null, y1: null, colorStops: [] }; // get coordinates m2 = m1[2].match(/\w+/g); if(m2){ m2Len = m2.length; for(i = 0; i < m2Len; i+=1){ switch(m2[i]) { case 'top': gradient.y0 = 0; gradient.y1 = bounds.height; break; case 'right': gradient.x0 = bounds.width; gradient.x1 = 0; break; case 'bottom': gradient.y0 = bounds.height; gradient.y1 = 0; break; case 'left': gradient.x0 = 0; gradient.x1 = bounds.width; break; } } } if(gradient.x0 === null && gradient.x1 === null){ // center gradient.x0 = gradient.x1 = bounds.width / 2; } if(gradient.y0 === null && gradient.y1 === null){ // center gradient.y0 = gradient.y1 = bounds.height / 2; } // get colors and stops m2 = m1[3].match(/((?:rgb|rgba)\(\d{1,3},\s\d{1,3},\s\d{1,3}(?:,\s[0-9\.]+)?\)(?:\s\d{1,3}(?:%|px))?)+/g); if(m2){ m2Len = m2.length; step = 1 / Math.max(m2Len - 1, 1); for(i = 0; i < m2Len; i+=1){ m3 = m2[i].match(/((?:rgb|rgba)\(\d{1,3},\s\d{1,3},\s\d{1,3}(?:,\s[0-9\.]+)?\))\s*(\d{1,3})?(%|px)?/); if(m3[2]){ stop = parseFloat(m3[2]); if(m3[3] === '%'){ stop /= 100; } else { // px - stupid opera stop /= bounds.width; } } else { stop = i * step; } gradient.colorStops.push({ color: m3[1], stop: stop }); } } break; case '-webkit-gradient': gradient = { type: m1[2] === 'radial' ? 'circle' : m1[2], // TODO: Add radial gradient support for older mozilla definitions x0: 0, y0: 0, x1: 0, y1: 0, colorStops: [] }; // get coordinates m2 = m1[3].match(/(\d{1,3})%?\s(\d{1,3})%?,\s(\d{1,3})%?\s(\d{1,3})%?/); if(m2){ gradient.x0 = (m2[1] * bounds.width) / 100; gradient.y0 = (m2[2] * bounds.height) / 100; gradient.x1 = (m2[3] * bounds.width) / 100; gradient.y1 = (m2[4] * bounds.height) / 100; } // get colors and stops m2 = m1[4].match(/((?:from|to|color-stop)\((?:[0-9\.]+,\s)?(?:rgb|rgba)\(\d{1,3},\s\d{1,3},\s\d{1,3}(?:,\s[0-9\.]+)?\)\))+/g); if(m2){ m2Len = m2.length; for(i = 0; i < m2Len; i+=1){ m3 = m2[i].match(/(from|to|color-stop)\(([0-9\.]+)?(?:,\s)?((?:rgb|rgba)\(\d{1,3},\s\d{1,3},\s\d{1,3}(?:,\s[0-9\.]+)?\))\)/); stop = parseFloat(m3[2]); if(m3[1] === 'from') { stop = 0.0; } if(m3[1] === 'to') { stop = 1.0; } gradient.colorStops.push({ color: m3[3], stop: stop }); } } break; case '-moz-linear-gradient': gradient = { type: 'linear', x0: 0, y0: 0, x1: 0, y1: 0, colorStops: [] }; // get coordinates m2 = m1[2].match(/(\d{1,3})%?\s(\d{1,3})%?/); // m2[1] == 0% -> left // m2[1] == 50% -> center // m2[1] == 100% -> right // m2[2] == 0% -> top // m2[2] == 50% -> center // m2[2] == 100% -> bottom if(m2){ gradient.x0 = (m2[1] * bounds.width) / 100; gradient.y0 = (m2[2] * bounds.height) / 100; gradient.x1 = bounds.width - gradient.x0; gradient.y1 = bounds.height - gradient.y0; } // get colors and stops m2 = m1[3].match(/((?:rgb|rgba)\(\d{1,3},\s\d{1,3},\s\d{1,3}(?:,\s[0-9\.]+)?\)(?:\s\d{1,3}%)?)+/g); if(m2){ m2Len = m2.length; step = 1 / Math.max(m2Len - 1, 1); for(i = 0; i < m2Len; i+=1){ m3 = m2[i].match(/((?:rgb|rgba)\(\d{1,3},\s\d{1,3},\s\d{1,3}(?:,\s[0-9\.]+)?\))\s*(\d{1,3})?(%)?/); if(m3[2]){ stop = parseFloat(m3[2]); if(m3[3]){ // percentage stop /= 100; } } else { stop = i * step; } gradient.colorStops.push({ color: m3[1], stop: stop }); } } break; case '-webkit-radial-gradient': case '-moz-radial-gradient': case '-o-radial-gradient': gradient = { type: 'circle', x0: 0, y0: 0, x1: bounds.width, y1: bounds.height, cx: 0, cy: 0, rx: 0, ry: 0, colorStops: [] }; // center m2 = m1[2].match(/(\d{1,3})%?\s(\d{1,3})%?/); if(m2){ gradient.cx = (m2[1] * bounds.width) / 100; gradient.cy = (m2[2] * bounds.height) / 100; } // size m2 = m1[3].match(/\w+/); m3 = m1[4].match(/[a-z\-]*/); if(m2 && m3){ switch(m3[0]){ case 'farthest-corner': case 'cover': // is equivalent to farthest-corner case '': // mozilla removes "cover" from definition :( tl = Math.sqrt(Math.pow(gradient.cx, 2) + Math.pow(gradient.cy, 2)); tr = Math.sqrt(Math.pow(gradient.cx, 2) + Math.pow(gradient.y1 - gradient.cy, 2)); br = Math.sqrt(Math.pow(gradient.x1 - gradient.cx, 2) + Math.pow(gradient.y1 - gradient.cy, 2)); bl = Math.sqrt(Math.pow(gradient.x1 - gradient.cx, 2) + Math.pow(gradient.cy, 2)); gradient.rx = gradient.ry = Math.max(tl, tr, br, bl); break; case 'closest-corner': tl = Math.sqrt(Math.pow(gradient.cx, 2) + Math.pow(gradient.cy, 2)); tr = Math.sqrt(Math.pow(gradient.cx, 2) + Math.pow(gradient.y1 - gradient.cy, 2)); br = Math.sqrt(Math.pow(gradient.x1 - gradient.cx, 2) + Math.pow(gradient.y1 - gradient.cy, 2)); bl = Math.sqrt(Math.pow(gradient.x1 - gradient.cx, 2) + Math.pow(gradient.cy, 2)); gradient.rx = gradient.ry = Math.min(tl, tr, br, bl); break; case 'farthest-side': if(m2[0] === 'circle'){ gradient.rx = gradient.ry = Math.max( gradient.cx, gradient.cy, gradient.x1 - gradient.cx, gradient.y1 - gradient.cy ); } else { // ellipse gradient.type = m2[0]; gradient.rx = Math.max( gradient.cx, gradient.x1 - gradient.cx ); gradient.ry = Math.max( gradient.cy, gradient.y1 - gradient.cy ); } break; case 'closest-side': case 'contain': // is equivalent to closest-side if(m2[0] === 'circle'){ gradient.rx = gradient.ry = Math.min( gradient.cx, gradient.cy, gradient.x1 - gradient.cx, gradient.y1 - gradient.cy ); } else { // ellipse gradient.type = m2[0]; gradient.rx = Math.min( gradient.cx, gradient.x1 - gradient.cx ); gradient.ry = Math.min( gradient.cy, gradient.y1 - gradient.cy ); } break; // TODO: add support for "30px 40px" sizes (webkit only) } } // color stops m2 = m1[5].match(/((?:rgb|rgba)\(\d{1,3},\s\d{1,3},\s\d{1,3}(?:,\s[0-9\.]+)?\)(?:\s\d{1,3}(?:%|px))?)+/g); if(m2){ m2Len = m2.length; step = 1 / Math.max(m2Len - 1, 1); for(i = 0; i < m2Len; i+=1){ m3 = m2[i].match(/((?:rgb|rgba)\(\d{1,3},\s\d{1,3},\s\d{1,3}(?:,\s[0-9\.]+)?\))\s*(\d{1,3})?(%|px)?/); if(m3[2]){ stop = parseFloat(m3[2]); if(m3[3] === '%'){ stop /= 100; } else { // px - stupid opera stop /= bounds.width; } } else { stop = i * step; } gradient.colorStops.push({ color: m3[1], stop: stop }); } } break; } } return gradient; }; _html2canvas.Generate.Gradient = function(src, bounds) { if(bounds.width === 0 || bounds.height === 0) { return; } var canvas = document.createElement('canvas'), ctx = canvas.getContext('2d'), gradient, grad, i, len; canvas.width = bounds.width; canvas.height = bounds.height; // TODO: add support for multi defined background gradients gradient = _html2canvas.Generate.parseGradient(src, bounds); if(gradient) { if(gradient.type === 'linear') { grad = ctx.createLinearGradient(gradient.x0, gradient.y0, gradient.x1, gradient.y1); for (i = 0, len = gradient.colorStops.length; i < len; i+=1) { try { grad.addColorStop(gradient.colorStops[i].stop, gradient.colorStops[i].color); } catch(e) { h2clog(['failed to add color stop: ', e, '; tried to add: ', gradient.colorStops[i], '; stop: ', i, '; in: ', src]); } } ctx.fillStyle = grad; ctx.fillRect(0, 0, bounds.width, bounds.height); } else if(gradient.type === 'circle') { grad = ctx.createRadialGradient(gradient.cx, gradient.cy, 0, gradient.cx, gradient.cy, gradient.rx); for (i = 0, len = gradient.colorStops.length; i < len; i+=1) { try { grad.addColorStop(gradient.colorStops[i].stop, gradient.colorStops[i].color); } catch(e) { h2clog(['failed to add color stop: ', e, '; tried to add: ', gradient.colorStops[i], '; stop: ', i, '; in: ', src]); } } ctx.fillStyle = grad; ctx.fillRect(0, 0, bounds.width, bounds.height); } else if(gradient.type === 'ellipse') { // draw circle var canvasRadial = document.createElement('canvas'), ctxRadial = canvasRadial.getContext('2d'), ri = Math.max(gradient.rx, gradient.ry), di = ri * 2, imgRadial; canvasRadial.width = canvasRadial.height = di; grad = ctxRadial.createRadialGradient(gradient.rx, gradient.ry, 0, gradient.rx, gradient.ry, ri); for (i = 0, len = gradient.colorStops.length; i < len; i+=1) { try { grad.addColorStop(gradient.colorStops[i].stop, gradient.colorStops[i].color); } catch(e) { h2clog(['failed to add color stop: ', e, '; tried to add: ', gradient.colorStops[i], '; stop: ', i, '; in: ', src]); } } ctxRadial.fillStyle = grad; ctxRadial.fillRect(0, 0, di, di); ctx.fillStyle = gradient.colorStops[i - 1].color; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.drawImage(canvasRadial, gradient.cx - gradient.rx, gradient.cy - gradient.ry, 2 * gradient.rx, 2 * gradient.ry); } } return canvas; }; _html2canvas.Generate.ListAlpha = function(number) { var tmp = "", modulus; do { modulus = number % 26; tmp = String.fromCharCode((modulus) + 64) + tmp; number = number / 26; }while((number*26) > 26); return tmp; }; _html2canvas.Generate.ListRoman = function(number) { var romanArray = ["M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"], decimal = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1], roman = "", v, len = romanArray.length; if (number <= 0 || number >= 4000) { return number; } for (v=0; v < len; v+=1) { while (number >= decimal[v]) { number -= decimal[v]; roman += romanArray[v]; } } return roman; }; })(); /* Parse ------------------------- */ _html2canvas.Parse = function (images, options) { // this was requested to be removed. via #204 // https://github.com/muaz-khan/WebRTC-Experiment/issues/204 // window.scroll(0,0); var element = (( options.elements === undefined ) ? document.body : options.elements[0]), // select body by default numDraws = 0, doc = element.ownerDocument, support = _html2canvas.Util.Support(options, doc), ignoreElementsRegExp = new RegExp("(" + options.ignoreElements + ")"), body = doc.body, getCSS = _html2canvas.Util.getCSS, pseudoHide = "___html2canvas___pseudoelement", hidePseudoElements = doc.createElement('style'); hidePseudoElements.innerHTML = '.' + pseudoHide + '-before:before { content: "" !important; display: none !important; }' + '.' + pseudoHide + '-after:after { content: "" !important; display: none !important; }'; body.appendChild(hidePseudoElements); images = images || {}; function documentWidth () { return Math.max( Math.max(doc.body.scrollWidth, doc.documentElement.scrollWidth), Math.max(doc.body.offsetWidth, doc.documentElement.offsetWidth), Math.max(doc.body.clientWidth, doc.documentElement.clientWidth) ); } function documentHeight () { return Math.max( Math.max(doc.body.scrollHeight, doc.documentElement.scrollHeight), Math.max(doc.body.offsetHeight, doc.documentElement.offsetHeight), Math.max(doc.body.clientHeight, doc.documentElement.clientHeight) ); } function getCSSInt(element, attribute) { var val = parseInt(getCSS(element, attribute), 10); return (isNaN(val)) ? 0 : val; // borders in old IE are throwing 'medium' for demo.html } function renderRect (ctx, x, y, w, h, bgcolor) { if (bgcolor !== "transparent"){ ctx.setVariable("fillStyle", bgcolor); ctx.fillRect(x, y, w, h); numDraws+=1; } } function textTransform (text, transform) { switch(transform){ case "lowercase": return text.toLowerCase(); case "capitalize": return text.replace( /(^|\s|:|-|\(|\))([a-z])/g , function (m, p1, p2) { if (m.length > 0) { return p1 + p2.toUpperCase(); } } ); case "uppercase": return text.toUpperCase(); default: return text; } } function noLetterSpacing(letter_spacing) { return (/^(normal|none|0px)$/.test(letter_spacing)); } function drawText(currentText, x, y, ctx){ if (currentText !== null && _html2canvas.Util.trimText(currentText).length > 0) { ctx.fillText(currentText, x, y); numDraws+=1; } } function setTextVariables(ctx, el, text_decoration, color) { var align = false, bold = getCSS(el, "fontWeight"), family = getCSS(el, "fontFamily"), size = getCSS(el, "fontSize"); switch(parseInt(bold, 10)){ case 401: bold = "bold"; break; case 400: bold = "normal"; break; } ctx.setVariable("fillStyle", color); ctx.setVariable("font", [getCSS(el, "fontStyle"), getCSS(el, "fontVariant"), bold, size, family].join(" ")); ctx.setVariable("textAlign", (align) ? "right" : "left"); if (text_decoration !== "none"){ return _html2canvas.Util.Font(family, size, doc); } } function renderTextDecoration(ctx, text_decoration, bounds, metrics, color) { switch(text_decoration) { case "underline": // Draws a line at the baseline of the font // TODO As some browsers display the line as more than 1px if the font-size is big, need to take that into account both in position and size renderRect(ctx, bounds.left, Math.round(bounds.top + metrics.baseline + metrics.lineWidth), bounds.width, 1, color); break; case "overline": renderRect(ctx, bounds.left, Math.round(bounds.top), bounds.width, 1, color); break; case "line-through": // TODO try and find exact position for line-through renderRect(ctx, bounds.left, Math.ceil(bounds.top + metrics.middle + metrics.lineWidth), bounds.width, 1, color); break; } } function getTextBounds(state, text, textDecoration, isLast) { var bounds; if (support.rangeBounds) { if (textDecoration !== "none" || _html2canvas.Util.trimText(text).length !== 0) { bounds = textRangeBounds(text, state.node, state.textOffset); } state.textOffset += text.length; } else if (state.node && typeof state.node.nodeValue === "string" ){ var newTextNode = (isLast) ? state.node.splitText(text.length) : null; bounds = textWrapperBounds(state.node); state.node = newTextNode; } return bounds; } function textRangeBounds(text, textNode, textOffset) { var range = doc.createRange(); range.setStart(textNode, textOffset); range.setEnd(textNode, textOffset + text.length); return range.getBoundingClientRect(); } function textWrapperBounds(oldTextNode) { var parent = oldTextNode.parentNode, wrapElement = doc.createElement('wrapper'), backupText = oldTextNode.cloneNode(true); wrapElement.appendChild(oldTextNode.cloneNode(true)); parent.replaceChild(wrapElement, oldTextNode); var bounds = _html2canvas.Util.Bounds(wrapElement); parent.replaceChild(backupText, wrapElement); return bounds; } function renderText(el, textNode, stack) { var ctx = stack.ctx, color = getCSS(el, "color"), textDecoration = getCSS(el, "textDecoration"), textAlign = getCSS(el, "textAlign"), metrics, textList, state = { node: textNode, textOffset: 0 }; if (_html2canvas.Util.trimText(textNode.nodeValue).length > 0) { textNode.nodeValue = textTransform(textNode.nodeValue, getCSS(el, "textTransform")); textAlign = textAlign.replace(["-webkit-auto"],["auto"]); textList = (!options.letterRendering && /^(left|right|justify|auto)$/.test(textAlign) && noLetterSpacing(getCSS(el, "letterSpacing"))) ? textNode.nodeValue.split(/(\b| )/) : textNode.nodeValue.split(""); metrics = setTextVariables(ctx, el, textDecoration, color); if (options.chinese) { textList.forEach(function(word, index) { if (/.*[\u4E00-\u9FA5].*$/.test(word)) { word = word.split(""); word.unshift(index, 1); textList.splice.apply(textList, word); } }); } textList.forEach(function(text, index) { var bounds = getTextBounds(state, text, textDecoration, (index < textList.length - 1)); if (bounds) { drawText(text, bounds.left, bounds.bottom, ctx); renderTextDecoration(ctx, textDecoration, bounds, metrics, color); } }); } } function listPosition (element, val) { var boundElement = doc.createElement( "boundelement" ), originalType, bounds; boundElement.style.display = "inline"; originalType = element.style.listStyleType; element.style.listStyleType = "none"; boundElement.appendChild(doc.createTextNode(val)); element.insertBefore(boundElement, element.firstChild); bounds = _html2canvas.Util.Bounds(boundElement); element.removeChild(boundElement); element.style.listStyleType = originalType; return bounds; } function elementIndex( el ) { var i = -1, count = 1, childs = el.parentNode.childNodes; if (el.parentNode) { while( childs[ ++i ] !== el ) { if ( childs[ i ].nodeType === 1 ) { count++; } } return count; } else { return -1; } } function listItemText(element, type) { var currentIndex = elementIndex(element), text; switch(type){ case "decimal": text = currentIndex; break; case "decimal-leading-zero": text = (currentIndex.toString().length === 1) ? currentIndex = "0" + currentIndex.toString() : currentIndex.toString(); break; case "upper-roman": text = _html2canvas.Generate.ListRoman( currentIndex ); break; case "lower-roman": text = _html2canvas.Generate.ListRoman( currentIndex ).toLowerCase(); break; case "lower-alpha": text = _html2canvas.Generate.ListAlpha( currentIndex ).toLowerCase(); break; case "upper-alpha": text = _html2canvas.Generate.ListAlpha( currentIndex ); break; } text += ". "; return text; } function renderListItem(element, stack, elBounds) { var x, text, ctx = stack.ctx, type = getCSS(element, "listStyleType"), listBounds; if (/^(decimal|decimal-leading-zero|upper-alpha|upper-latin|upper-roman|lower-alpha|lower-greek|lower-latin|lower-roman)$/i.test(type)) { text = listItemText(element, type); listBounds = listPosition(element, text); setTextVariables(ctx, element, "none", getCSS(element, "color")); if (getCSS(element, "listStylePosition") === "inside") { ctx.setVariable("textAlign", "left"); x = elBounds.left; } else { return; } drawText(text, x, listBounds.bottom, ctx); } } function loadImage (src){ var img = images[src]; if (img && img.succeeded === true) { return img.img; } else { return false; } } function clipBounds(src, dst){ var x = Math.max(src.left, dst.left), y = Math.max(src.top, dst.top), x2 = Math.min((src.left + src.width), (dst.left + dst.width)), y2 = Math.min((src.top + src.height), (dst.top + dst.height)); return { left:x, top:y, width:x2-x, height:y2-y }; } function setZ(zIndex, parentZ){ // TODO fix static elements overlapping relative/absolute elements under same stack, if they are defined after them var newContext; if (!parentZ){ newContext = h2czContext(0); return newContext; } if (zIndex !== "auto"){ newContext = h2czContext(zIndex); parentZ.children.push(newContext); return newContext; } return parentZ; } function renderImage(ctx, element, image, bounds, borders) { var paddingLeft = getCSSInt(element, 'paddingLeft'), paddingTop = getCSSInt(element, 'paddingTop'), paddingRight = getCSSInt(element, 'paddingRight'), paddingBottom = getCSSInt(element, 'paddingBottom'); drawImage( ctx, image, 0, //sx 0, //sy image.width, //sw image.height, //sh bounds.left + paddingLeft + borders[3].width, //dx bounds.top + paddingTop + borders[0].width, // dy bounds.width - (borders[1].width + borders[3].width + paddingLeft + paddingRight), //dw bounds.height - (borders[0].width + borders[2].width + paddingTop + paddingBottom) //dh ); } function getBorderData(element) { return ["Top", "Right", "Bottom", "Left"].map(function(side) { return { width: getCSSInt(element, 'border' + side + 'Width'), color: getCSS(element, 'border' + side + 'Color') }; }); } function getBorderRadiusData(element) { return ["TopLeft", "TopRight", "BottomRight", "BottomLeft"].map(function(side) { return getCSS(element, 'border' + side + 'Radius'); }); } var getCurvePoints = (function(kappa) { return function(x, y, r1, r2) { var ox = (r1) * kappa, // control point offset horizontal oy = (r2) * kappa, // control point offset vertical xm = x + r1, // x-middle ym = y + r2; // y-middle return { topLeft: bezierCurve({ x:x, y:ym }, { x:x, y:ym - oy }, { x:xm - ox, y:y }, { x:xm, y:y }), topRight: bezierCurve({ x:x, y:y }, { x:x + ox, y:y }, { x:xm, y:ym - oy }, { x:xm, y:ym }), bottomRight: bezierCurve({ x:xm, y:y }, { x:xm, y:y + oy }, { x:x + ox, y:ym }, { x:x, y:ym }), bottomLeft: bezierCurve({ x:xm, y:ym }, { x:xm - ox, y:ym }, { x:x, y:y + oy }, { x:x, y:y }) }; }; })(4 * ((Math.sqrt(2) - 1) / 3)); function bezierCurve(start, startControl, endControl, end) { var lerp = function (a, b, t) { return { x:a.x + (b.x - a.x) * t, y:a.y + (b.y - a.y) * t }; }; return { start: start, startControl: startControl, endControl: endControl, end: end, subdivide: function(t) { var ab = lerp(start, startControl, t), bc = lerp(startControl, endControl, t), cd = lerp(endControl, end, t), abbc = lerp(ab, bc, t), bccd = lerp(bc, cd, t), dest = lerp(abbc, bccd, t); return [bezierCurve(start, ab, abbc, dest), bezierCurve(dest, bccd, cd, end)]; }, curveTo: function(borderArgs) { borderArgs.push(["bezierCurve", startControl.x, startControl.y, endControl.x, endControl.y, end.x, end.y]); }, curveToReversed: function(borderArgs) { borderArgs.push(["bezierCurve", endControl.x, endControl.y, startControl.x, startControl.y, start.x, start.y]); } }; } function parseCorner(borderArgs, radius1, radius2, corner1, corner2, x, y) { if (radius1[0] > 0 || radius1[1] > 0) { borderArgs.push(["line", corner1[0].start.x, corner1[0].start.y]); corner1[0].curveTo(borderArgs); corner1[1].curveTo(borderArgs); } else { borderArgs.push(["line", x, y]); } if (radius2[0] > 0 || radius2[1] > 0) { borderArgs.push(["line", corner2[0].start.x, corner2[0].start.y]); } } function drawSide(borderData, radius1, radius2, outer1, inner1, outer2, inner2) { var borderArgs = []; if (radius1[0] > 0 || radius1[1] > 0) { borderArgs.push(["line", outer1[1].start.x, outer1[1].start.y]); outer1[1].curveTo(borderArgs); } else { borderArgs.push([ "line", borderData.c1[0], borderData.c1[1]]); } if (radius2[0] > 0 || radius2[1] > 0) { borderArgs.push(["line", outer2[0].start.x, outer2[0].start.y]); outer2[0].curveTo(borderArgs); borderArgs.push(["line", inner2[0].end.x, inner2[0].end.y]); inner2[0].curveToReversed(borderArgs); } else { borderArgs.push([ "line", borderData.c2[0], borderData.c2[1]]); borderArgs.push([ "line", borderData.c3[0], borderData.c3[1]]); } if (radius1[0] > 0 || radius1[1] > 0) { borderArgs.push(["line", inner1[1].end.x, inner1[1].end.y]); inner1[1].curveToReversed(borderArgs); } else { borderArgs.push([ "line", borderData.c4[0], borderData.c4[1]]); } return borderArgs; } function calculateCurvePoints(bounds, borderRadius, borders) { var x = bounds.left, y = bounds.top, width = bounds.width, height = bounds.height, tlh = borderRadius[0][0], tlv = borderRadius[0][1], trh = borderRadius[1][0], trv = borderRadius[1][1], brv = borderRadius[2][0], brh = borderRadius[2][1], blh = borderRadius[3][0], blv = borderRadius[3][1], topWidth = width - trh, rightHeight = height - brv, bottomWidth = width - brh, leftHeight = height - blv; return { topLeftOuter: getCurvePoints( x, y, tlh, tlv ).topLeft.subdivide(0.5), topLeftInner: getCurvePoints( x + borders[3].width, y + borders[0].width, Math.max(0, tlh - borders[3].width), Math.max(0, tlv - borders[0].width) ).topLeft.subdivide(0.5), topRightOuter: getCurvePoints( x + topWidth, y, trh, trv ).topRight.subdivide(0.5), topRightInner: getCurvePoints( x + Math.min(topWidth, width + borders[3].width), y + borders[0].width, (topWidth > width + borders[3].width) ? 0 :trh - borders[3].width, trv - borders[0].width ).topRight.subdivide(0.5), bottomRightOuter: getCurvePoints( x + bottomWidth, y + rightHeight, brh, brv ).bottomRight.subdivide(0.5), bottomRightInner: getCurvePoints( x + Math.min(bottomWidth, width + borders[3].width), y + Math.min(rightHeight, height + borders[0].width), Math.max(0, brh - borders[1].width), Math.max(0, brv - borders[2].width) ).bottomRight.subdivide(0.5), bottomLeftOuter: getCurvePoints( x, y + leftHeight, blh, blv ).bottomLeft.subdivide(0.5), bottomLeftInner: getCurvePoints( x + borders[3].width, y + leftHeight, Math.max(0, blh - borders[3].width), Math.max(0, blv - borders[2].width) ).bottomLeft.subdivide(0.5) }; } function getBorderClip(element, borderPoints, borders, radius, bounds) { var backgroundClip = getCSS(element, 'backgroundClip'), borderArgs = []; switch(backgroundClip) { case "content-box": case "padding-box": parseCorner(borderArgs, radius[0], radius[1], borderPoints.topLeftInner, borderPoints.topRightInner, bounds.left + borders[3].width, bounds.top + borders[0].width); parseCorner(borderArgs, radius[1], radius[2], borderPoints.topRightInner, borderPoints.bottomRightInner, bounds.left + bounds.width - borders[1].width, bounds.top + borders[0].width); parseCorner(borderArgs, radius[2], radius[3], borderPoints.bottomRightInner, borderPoints.bottomLeftInner, bounds.left + bounds.width - borders[1].width, bounds.top + bounds.height - borders[2].width); parseCorner(borderArgs, radius[3], radius[0], borderPoints.bottomLeftInner, borderPoints.topLeftInner, bounds.left + borders[3].width, bounds.top + bounds.height - borders[2].width); break; default: parseCorner(borderArgs, radius[0], radius[1], borderPoints.topLeftOuter, borderPoints.topRightOuter, bounds.left, bounds.top); parseCorner(borderArgs, radius[1], radius[2], borderPoints.topRightOuter, borderPoints.bottomRightOuter, bounds.left + bounds.width, bounds.top); parseCorner(borderArgs, radius[2], radius[3], borderPoints.bottomRightOuter, borderPoints.bottomLeftOuter, bounds.left + bounds.width, bounds.top + bounds.height); parseCorner(borderArgs, radius[3], radius[0], borderPoints.bottomLeftOuter, borderPoints.topLeftOuter, bounds.left, bounds.top + bounds.height); break; } return borderArgs; } function parseBorders(element, bounds, borders){ var x = bounds.left, y = bounds.top, width = bounds.width, height = bounds.height, borderSide, bx, by, bw, bh, borderArgs, // http://www.w3.org/TR/css3-background/#the-border-radius borderRadius = getBorderRadiusData(element), borderPoints = calculateCurvePoints(bounds, borderRadius, borders), borderData = { clip: getBorderClip(element, borderPoints, borders, borderRadius, bounds), borders: [] }; for (borderSide = 0; borderSide < 4; borderSide++) { if (borders[borderSide].width > 0) { bx = x; by = y; bw = width; bh = height - (borders[2].width); switch(borderSide) { case 0: // top border bh = borders[0].width; borderArgs = drawSide({ c1: [bx, by], c2: [bx + bw, by], c3: [bx + bw - borders[1].width, by + bh], c4: [bx + borders[3].width, by + bh] }, borderRadius[0], borderRadius[1], borderPoints.topLeftOuter, borderPoints.topLeftInner, borderPoints.topRightOuter, borderPoints.topRightInner); break; case 1: // right border bx = x + width - (borders[1].width); bw = borders[1].width; borderArgs = drawSide({ c1: [bx + bw, by], c2: [bx + bw, by + bh + borders[2].width], c3: [bx, by + bh], c4: [bx, by + borders[0].width] }, borderRadius[1], borderRadius[2], borderPoints.topRightOuter, borderPoints.topRightInner, borderPoints.bottomRightOuter, borderPoints.bottomRightInner); break; case 2: // bottom border by = (by + height) - (borders[2].width); bh = borders[2].width; borderArgs = drawSide({ c1: [bx + bw, by + bh], c2: [bx, by + bh], c3: [bx + borders[3].width, by], c4: [bx + bw - borders[2].width, by] }, borderRadius[2], borderRadius[3], borderPoints.bottomRightOuter, borderPoints.bottomRightInner, borderPoints.bottomLeftOuter, borderPoints.bottomLeftInner); break; case 3: // left border bw = borders[3].width; borderArgs = drawSide({ c1: [bx, by + bh + borders[2].width], c2: [bx, by], c3: [bx + bw, by + borders[0].width], c4: [bx + bw, by + bh] }, borderRadius[3], borderRadius[0], borderPoints.bottomLeftOuter, borderPoints.bottomLeftInner, borderPoints.topLeftOuter, borderPoints.topLeftInner); break; } borderData.borders.push({ args: borderArgs, color: borders[borderSide].color }); } } return borderData; } function createShape(ctx, args) { var shape = ctx.drawShape(); args.forEach(function(border, index) { shape[(index === 0) ? "moveTo" : border[0] + "To" ].apply(null, border.slice(1)); }); return shape; } function renderBorders(ctx, borderArgs, color) { if (color !== "transparent") { ctx.setVariable( "fillStyle", color); createShape(ctx, borderArgs); ctx.fill(); numDraws+=1; } } function renderFormValue (el, bounds, stack){ var valueWrap = doc.createElement('valuewrap'), cssPropertyArray = ['lineHeight','textAlign','fontFamily','color','fontSize','paddingLeft','paddingTop','width','height','border','borderLeftWidth','borderTopWidth'], textValue, textNode; cssPropertyArray.forEach(function(property) { try { valueWrap.style[property] = getCSS(el, property); } catch(e) { // Older IE has issues with "border" h2clog("html2canvas: Parse: Exception caught in renderFormValue: " + e.message); } }); valueWrap.style.borderColor = "black"; valueWrap.style.borderStyle = "solid"; valueWrap.style.display = "block"; valueWrap.style.position = "absolute"; if (/^(submit|reset|button|text|password)$/.test(el.type) || el.nodeName === "SELECT"){ valueWrap.style.lineHeight = getCSS(el, "height"); } valueWrap.style.top = bounds.top + "px"; valueWrap.style.left = bounds.left + "px"; textValue = (el.nodeName === "SELECT") ? (el.options[el.selectedIndex] || 0).text : el.value; if(!textValue) { textValue = el.placeholder; } textNode = doc.createTextNode(textValue); valueWrap.appendChild(textNode); body.appendChild(valueWrap); renderText(el, textNode, stack); body.removeChild(valueWrap); } function drawImage (ctx) { ctx.drawImage.apply(ctx, Array.prototype.slice.call(arguments, 1)); numDraws+=1; } function getPseudoElement(el, which) { var elStyle = window.getComputedStyle(el, which); if(!elStyle || !elStyle.content || elStyle.content === "none" || elStyle.content === "-moz-alt-content") { return; } var content = elStyle.content + '', first = content.substr( 0, 1 ); //strips quotes if(first === content.substr( content.length - 1 ) && first.match(/'|"/)) { content = content.substr( 1, content.length - 2 ); } var isImage = content.substr( 0, 3 ) === 'url', elps = document.createElement( isImage ? 'img' : 'span' ); elps.className = pseudoHide + "-before " + pseudoHide + "-after"; Object.keys(elStyle).filter(indexedProperty).forEach(function(prop) { elps.style[prop] = elStyle[prop]; }); if(isImage) { elps.src = _html2canvas.Util.parseBackgroundImage(content)[0].args[0]; } else { elps.innerHTML = content; } return elps; } function indexedProperty(property) { return (isNaN(window.parseInt(property, 10))); } function injectPseudoElements(el, stack) { var before = getPseudoElement(el, ':before'), after = getPseudoElement(el, ':after'); if(!before && !after) { return; } if(before) { el.className += " " + pseudoHide + "-before"; el.parentNode.insertBefore(before, el); parseElement(before, stack, true); el.parentNode.removeChild(before); el.className = el.className.replace(pseudoHide + "-before", "").trim(); } if (after) { el.className += " " + pseudoHide + "-after"; el.appendChild(after); parseElement(after, stack, true); el.removeChild(after); el.className = el.className.replace(pseudoHide + "-after", "").trim(); } } function renderBackgroundRepeat(ctx, image, backgroundPosition, bounds) { var offsetX = Math.round(bounds.left + backgroundPosition.left), offsetY = Math.round(bounds.top + backgroundPosition.top); ctx.createPattern(image); ctx.translate(offsetX, offsetY); ctx.fill(); ctx.translate(-offsetX, -offsetY); } function backgroundRepeatShape(ctx, image, backgroundPosition, bounds, left, top, width, height) { var args = []; args.push(["line", Math.round(left), Math.round(top)]); args.push(["line", Math.round(left + width), Math.round(top)]); args.push(["line", Math.round(left + width), Math.round(height + top)]); args.push(["line", Math.round(left), Math.round(height + top)]); createShape(ctx, args); ctx.save(); ctx.clip(); renderBackgroundRepeat(ctx, image, backgroundPosition, bounds); ctx.restore(); } function renderBackgroundColor(ctx, backgroundBounds, bgcolor) { renderRect( ctx, backgroundBounds.left, backgroundBounds.top, backgroundBounds.width, backgroundBounds.height, bgcolor ); } function renderBackgroundRepeating(el, bounds, ctx, image, imageIndex) { var backgroundSize = _html2canvas.Util.BackgroundSize(el, bounds, image, imageIndex), backgroundPosition = _html2canvas.Util.BackgroundPosition(el, bounds, image, imageIndex, backgroundSize), backgroundRepeat = getCSS(el, "backgroundRepeat").split(",").map(function(value) { return value.trim(); }); image = resizeImage(image, backgroundSize); backgroundRepeat = backgroundRepeat[imageIndex] || backgroundRepeat[0]; switch (backgroundRepeat) { case "repeat-x": backgroundRepeatShape(ctx, image, backgroundPosition, bounds, bounds.left, bounds.top + backgroundPosition.top, 99999, image.height); break; case "repeat-y": backgroundRepeatShape(ctx, image, backgroundPosition, bounds, bounds.left + backgroundPosition.left, bounds.top, image.width, 99999); break; case "no-repeat": backgroundRepeatShape(ctx, image, backgroundPosition, bounds, bounds.left + backgroundPosition.left, bounds.top + backgroundPosition.top, image.width, image.height); break; default: renderBackgroundRepeat(ctx, image, backgroundPosition, { top: bounds.top, left: bounds.left, width: image.width, height: image.height }); break; } } function renderBackgroundImage(element, bounds, ctx) { var backgroundImage = getCSS(element, "backgroundImage"), backgroundImages = _html2canvas.Util.parseBackgroundImage(backgroundImage), image, imageIndex = backgroundImages.length; while(imageIndex--) { backgroundImage = backgroundImages[imageIndex]; if (!backgroundImage.args || backgroundImage.args.length === 0) { continue; } var key = backgroundImage.method === 'url' ? backgroundImage.args[0] : backgroundImage.value; image = loadImage(key); // TODO add support for background-origin if (image) { renderBackgroundRepeating(element, bounds, ctx, image, imageIndex); } else { h2clog("html2canvas: Error loading background:", backgroundImage); } } } function resizeImage(image, bounds) { if(image.width === bounds.width && image.height === bounds.height) { return image; } var ctx, canvas = doc.createElement('canvas'); canvas.width = bounds.width; canvas.height = bounds.height; ctx = canvas.getContext("2d"); drawImage(ctx, image, 0, 0, image.width, image.height, 0, 0, bounds.width, bounds.height ); return canvas; } function setOpacity(ctx, element, parentStack) { var opacity = getCSS(element, "opacity") * ((parentStack) ? parentStack.opacity : 1); ctx.setVariable("globalAlpha", opacity); return opacity; } function createStack(element, parentStack, bounds) { var ctx = h2cRenderContext((!parentStack) ? documentWidth() : bounds.width , (!parentStack) ? documentHeight() : bounds.height), stack = { ctx: ctx, zIndex: setZ(getCSS(element, "zIndex"), (parentStack) ? parentStack.zIndex : null), opacity: setOpacity(ctx, element, parentStack), cssPosition: getCSS(element, "position"), borders: getBorderData(element), clip: (parentStack && parentStack.clip) ? _html2canvas.Util.Extend( {}, parentStack.clip ) : null }; // TODO correct overflow for absolute content residing under a static position if (options.useOverflow === true && /(hidden|scroll|auto)/.test(getCSS(element, "overflow")) === true && /(BODY)/i.test(element.nodeName) === false){ stack.clip = (stack.clip) ? clipBounds(stack.clip, bounds) : bounds; } stack.zIndex.children.push(stack); return stack; } function getBackgroundBounds(borders, bounds, clip) { var backgroundBounds = { left: bounds.left + borders[3].width, top: bounds.top + borders[0].width, width: bounds.width - (borders[1].width + borders[3].width), height: bounds.height - (borders[0].width + borders[2].width) }; if (clip) { backgroundBounds = clipBounds(backgroundBounds, clip); } return backgroundBounds; } function renderElement(element, parentStack, pseudoElement){ var bounds = _html2canvas.Util.Bounds(element), image, bgcolor = (ignoreElementsRegExp.test(element.nodeName)) ? "#efefef" : getCSS(element, "backgroundColor"), stack = createStack(element, parentStack, bounds), borders = stack.borders, ctx = stack.ctx, backgroundBounds = getBackgroundBounds(borders, bounds, stack.clip), borderData = parseBorders(element, bounds, borders); createShape(ctx, borderData.clip); ctx.save(); ctx.clip(); if (backgroundBounds.height > 0 && backgroundBounds.width > 0){ renderBackgroundColor(ctx, bounds, bgcolor); renderBackgroundImage(element, backgroundBounds, ctx); } ctx.restore(); borderData.borders.forEach(function(border) { renderBorders(ctx, border.args, border.color); }); if (!pseudoElement) { injectPseudoElements(element, stack); } switch(element.nodeName){ case "IMG": if ((image = loadImage(element.getAttribute('src')))) { renderImage(ctx, element, image, bounds, borders); } else { h2clog("html2canvas: Error loading :" + element.getAttribute('src')); } break; case "INPUT": // TODO add all relevant type's, i.e. HTML5 new stuff // todo add support for placeholder attribute for browsers which support it if (/^(text|url|email|submit|button|reset)$/.test(element.type) && (element.value || element.placeholder).length > 0){ renderFormValue(element, bounds, stack); } break; case "TEXTAREA": if ((element.value || element.placeholder || "").length > 0){ renderFormValue(element, bounds, stack); } break; case "SELECT": if ((element.options||element.placeholder || "").length > 0){ renderFormValue(element, bounds, stack); } break; case "LI": renderListItem(element, stack, backgroundBounds); break; case "VIDEO": // custom code written by Muaz Khan (www.muazkhan.com) // to support