├── README.md ├── assets ├── promo440x280.png └── screenshot640x400.png ├── doc └── readme.html ├── extension ├── audio │ ├── are-you-for-real.m4a │ ├── bewdy.m4a │ ├── bonzer.m4a │ ├── fair-dinkum.m4a │ ├── fair-game.m4a │ ├── gday.m4a │ ├── get-real.m4a │ ├── no-dramas.m4a │ ├── no-worries.m4a │ ├── oh-mate.m4a │ ├── oh-you-piker (1).m4a │ ├── oh-you-piker.m4a │ ├── rack-off.m4a │ ├── ripper.m4a │ ├── ta-ta.m4a │ ├── toodle-oo.m4a │ ├── totally-stoked.m4a │ └── whinger.m4a ├── css │ ├── global.css │ ├── injected.css │ └── popup.css ├── images │ ├── pause22.png │ ├── tabCapture128.png │ ├── tabCapture16.png │ ├── tabCapture22.png │ ├── tabCapture32.png │ └── tabCapture48.png ├── js │ ├── background.js │ ├── contentscript.js │ ├── lib │ │ ├── adapter.js │ │ ├── socket.io-client.js │ │ └── socket.io.js │ └── popup.js └── manifest.json ├── originals ├── apprtcJavaScript.js ├── media-record.svg ├── paused128.png ├── paused16.png ├── paused22.png ├── paused32.png ├── paused48.png ├── recording22.png └── svg.html └── psd ├── buttons.psd ├── icon.psd ├── kangaroo.psd ├── promo440x280.psd └── screenshot640x400.psd /README.md: -------------------------------------------------------------------------------- 1 | rtcshare 2 | ======== 3 | 4 | I built this WebRTC screensharing demo a couple of years ago. 5 | 6 | It's now a bit out of date, but hopefully some of the code might still be useful. 7 | -------------------------------------------------------------------------------- /assets/promo440x280.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samdutton/rtcshare/6998287784e52583b2d3ff82d42605bd68d2420f/assets/promo440x280.png -------------------------------------------------------------------------------- /assets/screenshot640x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samdutton/rtcshare/6998287784e52583b2d3ff82d42605bd68d2420f/assets/screenshot640x400.png -------------------------------------------------------------------------------- /doc/readme.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | The Framegrabber Chrome extension 10 | 11 | 82 | 83 | 84 | 85 | 90 | 91 | 94 | 95 | 96 | 97 | 98 | 99 |
100 | 101 |

Framegrabber

102 | 103 |

Framegrabber is a simple extension for bookmarking video timecodes and taking framegrabs.

104 | 105 |

Framegrabs are still images of individual film frames.

106 | 107 |

The Framegrabber extension makes it possible to take framegrabs from HTML5 video. Framegrabs can be stored in a local database or opened in a tab so they can be saved as JPEG files.

108 | 109 |

Framegrabber is also useful for bookmarking video timecodes.

110 | 111 |

Framegrabber works for any page that uses the HTML video element.

112 | 113 |

One major caveat: to take framegrabs, video must be from the same host as the page it's on, so Framegrabber can't take framegrabs on sites like YouTube and Vimeo. If Framegrabber cannot take a framegrab, it stores only the current timecode.

114 | 115 |

Videos from which framegrabs can taken can be found on many sites, including Dive Into HTML5, Mozilla and my own website samdutton.com.

116 | 117 |

How to use Framegrabber

118 | 119 |

To save framegrabs, use the icons that the extension displays at the top left of video(s) using the HTML video element:

120 | 124 | 125 |

Note that on some pages, you may need to start playing the video before an HTML video element is actually added to the page.

126 | 127 |

Click the extension icon (to the right of the address bar) to display stored framegrabs in a popup. Click a framegrab image in the popup to navigate to the video and timecode from which the framegrab was taken.

128 | 129 | 130 |

How does it work?

131 | 132 |

Framegrabber uses several relatively new web technologies, including Canvas, HTMLMediaElement and Web SQL Database.

133 | 134 |

Below are some technical details of how the extension works.

135 | 136 |

Framegrabber creates a canvas element (but doesn't add it to the DOM) then uses the drawImage() canvas context method to draw a video frame on it. The canvas toDataURL() method is then used to create a data URL string representing the image. The image data URL can then either be opened in a new tab, or stored locally along with the URL of the page containing the video and the timecode of the framegrab. Except in order to view pages containing framegrabs, no server or internet access is required.

137 | 138 |

Local storage is accomplished using the Chrome Web SQL Data 139 | base implementation, which is fast and reliable enough to store strings such as image data URLs, which can be 200KB or more in size (i.e. 200,000+ characters in length).

140 | 141 |

When the extension icon is clicked, data URLs are retrieved from the local database and set as the src value for framegrabs displayed in the popup.

142 | 143 | 144 | 145 |

Feedback

146 | 147 |

Please send bug reports, comments or feature requests to samdutton@gmail.com.

148 | 149 |

For more information, please visit my website samdutton.com or my blog at samdutton.wordpress.com.

150 | 151 | 152 | 153 |
154 | 155 | 156 | 157 | -------------------------------------------------------------------------------- /extension/audio/are-you-for-real.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samdutton/rtcshare/6998287784e52583b2d3ff82d42605bd68d2420f/extension/audio/are-you-for-real.m4a -------------------------------------------------------------------------------- /extension/audio/bewdy.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samdutton/rtcshare/6998287784e52583b2d3ff82d42605bd68d2420f/extension/audio/bewdy.m4a -------------------------------------------------------------------------------- /extension/audio/bonzer.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samdutton/rtcshare/6998287784e52583b2d3ff82d42605bd68d2420f/extension/audio/bonzer.m4a -------------------------------------------------------------------------------- /extension/audio/fair-dinkum.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samdutton/rtcshare/6998287784e52583b2d3ff82d42605bd68d2420f/extension/audio/fair-dinkum.m4a -------------------------------------------------------------------------------- /extension/audio/fair-game.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samdutton/rtcshare/6998287784e52583b2d3ff82d42605bd68d2420f/extension/audio/fair-game.m4a -------------------------------------------------------------------------------- /extension/audio/gday.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samdutton/rtcshare/6998287784e52583b2d3ff82d42605bd68d2420f/extension/audio/gday.m4a -------------------------------------------------------------------------------- /extension/audio/get-real.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samdutton/rtcshare/6998287784e52583b2d3ff82d42605bd68d2420f/extension/audio/get-real.m4a -------------------------------------------------------------------------------- /extension/audio/no-dramas.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samdutton/rtcshare/6998287784e52583b2d3ff82d42605bd68d2420f/extension/audio/no-dramas.m4a -------------------------------------------------------------------------------- /extension/audio/no-worries.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samdutton/rtcshare/6998287784e52583b2d3ff82d42605bd68d2420f/extension/audio/no-worries.m4a -------------------------------------------------------------------------------- /extension/audio/oh-mate.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samdutton/rtcshare/6998287784e52583b2d3ff82d42605bd68d2420f/extension/audio/oh-mate.m4a -------------------------------------------------------------------------------- /extension/audio/oh-you-piker (1).m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samdutton/rtcshare/6998287784e52583b2d3ff82d42605bd68d2420f/extension/audio/oh-you-piker (1).m4a -------------------------------------------------------------------------------- /extension/audio/oh-you-piker.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samdutton/rtcshare/6998287784e52583b2d3ff82d42605bd68d2420f/extension/audio/oh-you-piker.m4a -------------------------------------------------------------------------------- /extension/audio/rack-off.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samdutton/rtcshare/6998287784e52583b2d3ff82d42605bd68d2420f/extension/audio/rack-off.m4a -------------------------------------------------------------------------------- /extension/audio/ripper.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samdutton/rtcshare/6998287784e52583b2d3ff82d42605bd68d2420f/extension/audio/ripper.m4a -------------------------------------------------------------------------------- /extension/audio/ta-ta.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samdutton/rtcshare/6998287784e52583b2d3ff82d42605bd68d2420f/extension/audio/ta-ta.m4a -------------------------------------------------------------------------------- /extension/audio/toodle-oo.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samdutton/rtcshare/6998287784e52583b2d3ff82d42605bd68d2420f/extension/audio/toodle-oo.m4a -------------------------------------------------------------------------------- /extension/audio/totally-stoked.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samdutton/rtcshare/6998287784e52583b2d3ff82d42605bd68d2420f/extension/audio/totally-stoked.m4a -------------------------------------------------------------------------------- /extension/audio/whinger.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samdutton/rtcshare/6998287784e52583b2d3ff82d42605bd68d2420f/extension/audio/whinger.m4a -------------------------------------------------------------------------------- /extension/css/global.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #666; 3 | font-family: Arial, sans-serif; 4 | padding: 50px; 5 | } 6 | 7 | div#container { 8 | background: #000; 9 | margin: 0 auto 0 auto; 10 | padding: 20px 20px 20px 20px; 11 | width: 530px; 12 | } 13 | 14 | 15 | #videoCanvasImageContainer { 16 | margin: 0 0 20px 0; 17 | } 18 | 19 | #video { 20 | height: 96px; 21 | margin: 0 20px 0 0; 22 | width: 160px; 23 | } 24 | 25 | #canvas { 26 | display: none; 27 | height: 96px; 28 | margin: 0 20px 0 0; 29 | width: 160px; 30 | } 31 | 32 | #img { 33 | border: none; 34 | height: 96px; 35 | width: 160px; 36 | } 37 | 38 | 39 | #framegrabs { 40 | background: #666; 41 | height: 115px; 42 | margin: 0 0 20px 0; 43 | overflow-y: scroll; 44 | padding: 20px 0 0 20px; 45 | width: 200px; 46 | } 47 | 48 | #framegrabs > div { 49 | height: 96px; 50 | margin: 0 20px 20px 0; 51 | position: relative; 52 | width: 160px; 53 | } 54 | 55 | #framegrabs div.framegrabTimecode { 56 | background: #aaa; 57 | color: black; 58 | font-size: 70%; 59 | height: 15px; 60 | opacity: 0.6; 61 | padding: 2px 5px 1px 5px; 62 | right: 0px; 63 | position: absolute; 64 | text-align: right; 65 | top: 10px; 66 | z-index: 1; 67 | } 68 | 69 | #framegrabs img { 70 | cursor: pointer; 71 | float: left; 72 | position: absolute; 73 | } 74 | 75 | 76 | div#scrubBarContainer { 77 | border: 1px solid green; 78 | margin: 0 0 20px 0; 79 | } 80 | 81 | div#buttons input { 82 | margin: 0 11px 0 0; 83 | width: 90px; 84 | } -------------------------------------------------------------------------------- /extension/css/injected.css: -------------------------------------------------------------------------------- 1 | div.framegrabberControls { 2 | opacity: 0.8; 3 | position: absolute; 4 | left: 0px; 5 | padding: 10px 10px 10px 10px; 6 | z-index: 2000; // youtube video-blocker is 1010 7 | } 8 | 9 | div.framegrabberControls:hover { 10 | opacity: 1; 11 | } 12 | 13 | div.framegrabberControls img { 14 | display: block; 15 | cursor: pointer !important;; 16 | float: left; 17 | height: 15px; 18 | margin: 0 10px 0 0; 19 | opacity: 0.8; 20 | width: 15px; 21 | } 22 | 23 | div.framegrabberControls img:hover { 24 | opacity: 1; 25 | } 26 | 27 | div.framegrabberControls img.grab { 28 | } 29 | 30 | div.framegrabberControls img.openFramegrabInNewTab { 31 | } -------------------------------------------------------------------------------- /extension/css/popup.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Lucida Grande, Open Sans, Arial, sans-serif; 3 | padding: 10px 10px 10px 10px; 4 | width: 270px; 5 | } 6 | 7 | label{ 8 | font-size: 14px; 9 | margin: 0 6px 0 0; 10 | } 11 | 12 | input[type=checkbox]{ 13 | } 14 | 15 | fieldset{ 16 | border: 1px solid #ddd; 17 | color: #999; 18 | border-radius: 3px; 19 | padding: 15px 15px 15px 15px; 20 | } 21 | 22 | fieldset div{ 23 | color: #666; 24 | } 25 | 26 | legend{ 27 | border: 1px solid #ddd; 28 | border-radius: 3px; 29 | font-family: Lucida Grande, Open Sans, Arial, sans-serif; 30 | font-size: 13px; 31 | padding: 2px 4px 2px 4px; 32 | } 33 | 34 | 35 | div#languageSelectContainer { 36 | margin: 0 0 30px 0; 37 | } 38 | -------------------------------------------------------------------------------- /extension/images/pause22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samdutton/rtcshare/6998287784e52583b2d3ff82d42605bd68d2420f/extension/images/pause22.png -------------------------------------------------------------------------------- /extension/images/tabCapture128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samdutton/rtcshare/6998287784e52583b2d3ff82d42605bd68d2420f/extension/images/tabCapture128.png -------------------------------------------------------------------------------- /extension/images/tabCapture16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samdutton/rtcshare/6998287784e52583b2d3ff82d42605bd68d2420f/extension/images/tabCapture16.png -------------------------------------------------------------------------------- /extension/images/tabCapture22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samdutton/rtcshare/6998287784e52583b2d3ff82d42605bd68d2420f/extension/images/tabCapture22.png -------------------------------------------------------------------------------- /extension/images/tabCapture32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samdutton/rtcshare/6998287784e52583b2d3ff82d42605bd68d2420f/extension/images/tabCapture32.png -------------------------------------------------------------------------------- /extension/images/tabCapture48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samdutton/rtcshare/6998287784e52583b2d3ff82d42605bd68d2420f/extension/images/tabCapture48.png -------------------------------------------------------------------------------- /extension/js/background.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // to includet socket.io.js from node server 4 | // would love to know if there's a better way... 5 | (function() { 6 | var ga = document.createElement('script'); 7 | ga.type = 'text/javascript'; 8 | ga.src = 'https://samdutton-nodertc.jit.su/socket.io/socket.io.js'; 9 | var s = document.getElementsByTagName('script')[0]; 10 | s.parentNode.insertBefore(ga, s); 11 | })(); 12 | 13 | var audioElement; 14 | var isChannelReady; 15 | var isInitiator; 16 | var isStarted; 17 | var localStream; 18 | var pc; 19 | var remoteStream; 20 | var socket; 21 | var turnReady; 22 | 23 | var localVideo = document.querySelector('#localVideo'); 24 | var remoteVideo = document.querySelector('#remoteVideo'); 25 | 26 | // should change to extension install event 27 | window.onload = init; 28 | function init() { 29 | // if (typeof localStorage["capturing"] === "undefined") { 30 | localStorage["capturing"] = "off"; 31 | // } 32 | } 33 | 34 | function handleCapture(stream){ 35 | console.log("backround.js stream: ", stream); 36 | localStream = stream; // set global used by apprtc code and when stopping stream 37 | initialize(); // start of connection process using apprtc code below 38 | } 39 | 40 | function startCapture(){ 41 | chrome.tabs.getSelected(null, function(tab) { 42 | var selectedTabId = tab.id; 43 | chrome.tabCapture.capture({audio:true, video:true}, handleCapture); 44 | }); 45 | } 46 | 47 | 48 | // extension methods 49 | 50 | var iconPath = "images/"; 51 | var iconCapture = "tabCapture22.png"; 52 | var iconPause = "pause22.png"; 53 | 54 | 55 | // when the record/pause button is clicked toggle the button icon and title 56 | chrome.browserAction.onClicked.addListener(function(tab) { 57 | var currentMode = localStorage["capturing"]; 58 | var newMode = currentMode === "on" ? "off" : "on"; 59 | // start capture 60 | if (newMode === "on"){ 61 | appendIframe(); // capture starts once iframe created 62 | // stop capture 63 | } else { 64 | chrome.tabs.getSelected(null, function(tab){ 65 | console.log("stop capture! newMode :", newMode); 66 | var selectedTabId = tab.id; 67 | localStream.stop(); 68 | onRemoteHangup(); 69 | // chrome.tabCapture.capture(selectedTabId, {audio:false, video:false}); 70 | }); 71 | } 72 | localStorage["capturing"] = newMode; 73 | // if capturing is now on, display pause icon -- and vice versa 74 | var iconFileName = newMode === "on" ? iconPause : iconCapture; 75 | chrome.browserAction.setIcon({path: iconPath + iconFileName}); 76 | var title = newMode === "on" ? "Click to stop capture" : "Click to start capture"; 77 | chrome.browserAction.setTitle({"title": title}); 78 | }); 79 | 80 | 81 | 82 | 83 | 84 | 85 | ///////////////////////////////// 86 | 87 | var pc_config = webrtcDetectedBrowser === 'firefox' ? 88 | {'iceServers':[{'url':'stun:23.21.150.121'}]} : // number IP 89 | {'iceServers': [{'url': 'stun:stun.l.google.com:19302'}]}; 90 | 91 | var pc_constraints = {'optional': [{'DtlsSrtpKeyAgreement': true}]}; 92 | 93 | // Set up audio and video regardless of what devices are present. 94 | var sdpConstraints = {'mandatory': { 95 | 'OfferToReceiveAudio':true, 96 | 'OfferToReceiveVideo':true }}; 97 | 98 | ///////////////////////////////////////////// 99 | 100 | function setupSocket(){ 101 | // var room = location.pathname.substring(1); 102 | // if (room === '') { 103 | // // room = prompt('Enter room name:'); 104 | // room = 'rtcshare'; 105 | // } else { 106 | // // 107 | // } 108 | 109 | socket = io.connect('samdutton-nodertc.jit.su') 110 | 111 | socket.on('created', function (room){ 112 | console.log('Created room ' + room); 113 | isInitiator = true; 114 | // initiator does screencapture and Web Audio sharing 115 | console.log('starting screencapture'); 116 | startCapture(); 117 | }); 118 | 119 | socket.on('full', function (room){ 120 | console.log('Room ' + room + ' is full'); 121 | }); 122 | 123 | socket.on('join', function (room){ 124 | console.log('Another peer made a request to join room ' + room); 125 | console.log('This peer is the initiator of room ' + room + '!'); 126 | isChannelReady = true; 127 | }); 128 | 129 | socket.on('joined', function (room){ 130 | isInitiator = false; 131 | console.log('This peer has joined room ' + room); 132 | isChannelReady = true; 133 | // initiator does screencapture and Web Audio sharing 134 | var constraints = {video: true}; 135 | getUserMedia(constraints, handleUserMedia, handleUserMediaError); 136 | console.log('Getting user media with constraints', constraints); 137 | }); 138 | 139 | socket.on('log', function (array){ 140 | console.log.apply(console, array); 141 | }); 142 | 143 | //////////////////////////////////////////////// 144 | 145 | function sendMessage(message){ 146 | console.log('Sending message: ', message); 147 | socket.emit('message', message); 148 | } 149 | 150 | socket.on('message', function (message){ 151 | console.log('Received message:', message); 152 | if (message === 'got user media') { 153 | maybeStart(); 154 | } else if (message.type === 'offer') { 155 | if (!isInitiator && !isStarted) { 156 | maybeStart(); 157 | } 158 | pc.setRemoteDescription(new RTCSessionDescription(message)); 159 | doAnswer(); 160 | } else if (message.type === 'answer' && isStarted) { 161 | pc.setRemoteDescription(new RTCSessionDescription(message)); 162 | } else if (message.type === 'candidate' && isStarted) { 163 | var candidate = new RTCIceCandidate({sdpMLineIndex:message.label, 164 | candidate:message.candidate}); 165 | pc.addIceCandidate(candidate); 166 | } else if (message === 'bye' && isStarted) { 167 | handleRemoteHangup(); 168 | } 169 | }); 170 | 171 | var room = 'rtcshare'; 172 | 173 | if (room !== '') { 174 | console.log('Create or join room', room); 175 | socket.emit('create or join', room); 176 | } 177 | 178 | //////////////////////////////////////////////////// 179 | } 180 | 181 | // must wait for socket.io.js to load :( 182 | // there must be a better way... 183 | setTimeout(setupSocket, 1000); 184 | 185 | function handleUserMedia(stream) { 186 | localStream = stream; 187 | // attachMediaStream(localVideo, stream); 188 | console.log('Adding local stream.'); 189 | sendMessage('got user media'); 190 | if (isInitiator) { 191 | maybeStart(); 192 | } 193 | } 194 | 195 | function handleUserMediaError(error){ 196 | console.log('navigator.getUserMedia error: ', error); 197 | } 198 | 199 | 200 | // requestTurn('https://computeengineondemand.appspot.com/turn?username=41784574&key=4080218913'); 201 | 202 | function maybeStart() { 203 | if (!isStarted && localStream && isChannelReady) { 204 | createPeerConnection(); 205 | pc.addStream(localStream); 206 | isStarted = true; 207 | if (isInitiator) { 208 | doCall(); 209 | } 210 | } 211 | } 212 | 213 | window.onbeforeunload = function(e){ 214 | sendMessage('bye'); 215 | } 216 | 217 | ///////////////////////////////////////////////////////// 218 | 219 | function createPeerConnection() { 220 | try { 221 | pc = new RTCPeerConnection(pc_config, pc_constraints); 222 | pc.onicecandidate = handleIceCandidate; 223 | console.log('Created RTCPeerConnnection with:\n' + 224 | ' config: \'' + JSON.stringify(pc_config) + '\';\n' + 225 | ' constraints: \'' + JSON.stringify(pc_constraints) + '\'.'); 226 | } catch (e) { 227 | console.log('Failed to create PeerConnection, exception: ' + e.message); 228 | alert('Cannot create RTCPeerConnection object.'); 229 | return; 230 | } 231 | pc.onaddstream = handleRemoteStreamAdded; 232 | pc.onremovestream = handleRemoteStreamRemoved; 233 | } 234 | 235 | function handleIceCandidate(event) { 236 | console.log('handleIceCandidate event: ', event); 237 | if (event.candidate) { 238 | sendMessage({ 239 | type: 'candidate', 240 | label: event.candidate.sdpMLineIndex, 241 | id: event.candidate.sdpMid, 242 | candidate: event.candidate.candidate}); 243 | } else { 244 | console.log('End of candidates.'); 245 | } 246 | } 247 | 248 | function handleRemoteStreamAdded(event) { 249 | console.log('Remote stream added.'); 250 | // reattachMediaStream(miniVideo, localVideo); 251 | if (!isInitiator){ 252 | attachMediaStream(remoteVideo, event.stream); 253 | } 254 | remoteStream = event.stream; 255 | // waitForRemoteVideo(); 256 | } 257 | 258 | function doCall() { 259 | var constraints = {'optional': [], 'mandatory': {'MozDontOfferDataChannel': true}}; 260 | // temporary measure to remove Moz* constraints in Chrome 261 | if (webrtcDetectedBrowser === 'chrome') { 262 | for (var prop in constraints.mandatory) { 263 | if (prop.indexOf('Moz') !== -1) { 264 | delete constraints.mandatory[prop]; 265 | } 266 | } 267 | } 268 | constraints = mergeConstraints(constraints, sdpConstraints); 269 | console.log('Sending offer to peer, with constraints: \n' + 270 | ' \'' + JSON.stringify(constraints) + '\'.'); 271 | if (isInitiator) { 272 | addWebAudio(); 273 | } 274 | pc.createOffer(setLocalAndSendMessage, null, constraints); 275 | } 276 | 277 | function doAnswer() { 278 | console.log('Sending answer to peer.'); 279 | pc.createAnswer(setLocalAndSendMessage, null, sdpConstraints); 280 | } 281 | 282 | function mergeConstraints(cons1, cons2) { 283 | var merged = cons1; 284 | for (var name in cons2.mandatory) { 285 | merged.mandatory[name] = cons2.mandatory[name]; 286 | } 287 | merged.optional.concat(cons2.optional); 288 | return merged; 289 | } 290 | 291 | function setLocalAndSendMessage(sessionDescription) { 292 | // Set Opus as the preferred codec in SDP if Opus is present. 293 | sessionDescription.sdp = preferOpus(sessionDescription.sdp); 294 | pc.setLocalDescription(sessionDescription); 295 | sendMessage(sessionDescription); 296 | } 297 | 298 | function requestTurn(turn_url) { 299 | var turnExists = false; 300 | for (var i in pc_config.iceServers) { 301 | if (pc_config.iceServers[i].url.substr(0, 5) === 'turn:') { 302 | turnExists = true; 303 | turnReady = true; 304 | break; 305 | } 306 | } 307 | if (!turnExists) { 308 | console.log('Getting TURN server from ', turn_url); 309 | // No TURN server. Get one from computeengineondemand.appspot.com: 310 | var xhr = new XMLHttpRequest(); 311 | xhr.onreadystatechange = function(){ 312 | if (xhr.readyState === 4 && xhr.status === 200) { 313 | var turnServer = JSON.parse(xhr.responseText); 314 | console.log('Got TURN server: ', turnServer); 315 | pc_config.iceServers.push({ 316 | 'url': 'turn:' + turnServer.username + '@' + turnServer.turn, 317 | 'credential': turnServer.password 318 | }); 319 | turnReady = true; 320 | } 321 | }; 322 | xhr.open('GET', turn_url, true); 323 | xhr.send(); 324 | } 325 | } 326 | 327 | function handleRemoteStreamAdded(event) { 328 | console.log('Remote stream added.'); 329 | // reattachMediaStream(miniVideo, localVideo); 330 | attachMediaStream(remoteVideo, event.stream); 331 | remoteStream = event.stream; 332 | // waitForRemoteVideo(); 333 | } 334 | function handleRemoteStreamRemoved(event) { 335 | console.log('Remote stream removed. Event: ', event); 336 | } 337 | 338 | function hangup() { 339 | console.log('Hanging up.'); 340 | stop(); 341 | sendMessage('bye'); 342 | } 343 | 344 | function handleRemoteHangup() { 345 | console.log('Session terminated.'); 346 | stop(); 347 | isInitiator = false; 348 | } 349 | 350 | function stop() { 351 | isStarted = false; 352 | // isAudioMuted = false; 353 | // isVideoMuted = false; 354 | pc.close(); 355 | pc = null; 356 | } 357 | 358 | /////////////////////////////////////////// 359 | 360 | // Set Opus as the default audio codec if it's present. 361 | function preferOpus(sdp) { 362 | var sdpLines = sdp.split('\r\n'); 363 | var mLineIndex; 364 | // Search for m line. 365 | for (var i = 0; i < sdpLines.length; i++) { 366 | if (sdpLines[i].search('m=audio') !== -1) { 367 | mLineIndex = i; 368 | break; 369 | } 370 | } 371 | if (mLineIndex === null) { 372 | return sdp; 373 | } 374 | 375 | // If Opus is available, set it as the default in m line. 376 | for (i = 0; i < sdpLines.length; i++) { 377 | if (sdpLines[i].search('opus/48000') !== -1) { 378 | var opusPayload = extractSdp(sdpLines[i], /:(\d+) opus\/48000/i); 379 | if (opusPayload) { 380 | sdpLines[mLineIndex] = setDefaultCodec(sdpLines[mLineIndex], opusPayload); 381 | } 382 | break; 383 | } 384 | } 385 | 386 | // Remove CN in m line and sdp. 387 | sdpLines = removeCN(sdpLines, mLineIndex); 388 | 389 | sdp = sdpLines.join('\r\n'); 390 | return sdp; 391 | } 392 | 393 | function extractSdp(sdpLine, pattern) { 394 | var result = sdpLine.match(pattern); 395 | return result && result.length === 2 ? result[1] : null; 396 | } 397 | 398 | // Set the selected codec to the first in m line. 399 | function setDefaultCodec(mLine, payload) { 400 | var elements = mLine.split(' '); 401 | var newLine = []; 402 | var index = 0; 403 | for (var i = 0; i < elements.length; i++) { 404 | if (index === 3) { // Format of media starts from the fourth. 405 | newLine[index++] = payload; // Put target payload to the first. 406 | } 407 | if (elements[i] !== payload) { 408 | newLine[index++] = elements[i]; 409 | } 410 | } 411 | return newLine.join(' '); 412 | } 413 | 414 | // Strip CN from sdp before CN constraints is ready. 415 | function removeCN(sdpLines, mLineIndex) { 416 | var mLineElements = sdpLines[mLineIndex].split(' '); 417 | // Scan from end for the convenience of removing an item. 418 | for (var i = sdpLines.length-1; i >= 0; i--) { 419 | var payload = extractSdp(sdpLines[i], /a=rtpmap:(\d+) CN\/\d+/i); 420 | if (payload) { 421 | var cnPos = mLineElements.indexOf(payload); 422 | if (cnPos !== -1) { 423 | // Remove CN payload from m line. 424 | mLineElements.splice(cnPos, 1); 425 | } 426 | // Remove CN line in sdp 427 | sdpLines.splice(i, 1); 428 | } 429 | } 430 | 431 | sdpLines[mLineIndex] = mLineElements.join(' '); 432 | return sdpLines; 433 | } 434 | 435 | /////////////////////////////////// 436 | 437 | // add stream from Web Audio 438 | function addWebAudio(){ 439 | // cope with browser differences 440 | var context; 441 | if (typeof webkitAudioContext === "function") { 442 | context = new webkitAudioContext(); 443 | } else if (typeof AudioContext === "function") { 444 | context = new AudioContext(); 445 | } else { 446 | alert("Sorry! Web Audio is not supported by this browser"); 447 | } 448 | 449 | // use the audio element to create the source node 450 | audioElement = document.createElement("Audio"); // global scope, for console 451 | audioElement.src = 'audio/human-voice.wav'; 452 | audioElement.loop = true; 453 | var sourceNode = context.createMediaElementSource(audioElement); 454 | // sourceNode.loop = true; 455 | //audioElement.addEventListener('loadedmetadata', function(){console.log('loadedmetadata')}); 456 | 457 | // connect the source node to a filter node 458 | var filterNode = context.createBiquadFilter(); 459 | // see https://dvcs.w3.org/hg/audio/raw-file/tip/webaudio/specification.html#BiquadFilterNode-section 460 | filterNode.type = 1; // HIGHPASS 461 | // cutoff frequency: for HIGHPASS, audio is attenuated below this frequency 462 | sourceNode.connect(filterNode); 463 | 464 | // connect the filter node to a gain node (to change audio volume) 465 | var gainNode = context.createGainNode(); 466 | // default is 1 (no change); less than 1 means audio is attenuated, and vice versa 467 | gainNode.gain.value = 0.5; 468 | filterNode.connect(gainNode); 469 | 470 | var mediaStreamDestination = context.createMediaStreamDestination(); 471 | gainNode.connect(mediaStreamDestination); 472 | pc.addStream(mediaStreamDestination.stream); 473 | audioElement.play(); 474 | console.log('audioElement play() called, mediaStreamDestination.stream: ', mediaStreamDestination.stream); 475 | } 476 | -------------------------------------------------------------------------------- /extension/js/contentscript.js: -------------------------------------------------------------------------------- 1 | // document.body.onkeydown = function(event){ 2 | // chrome.extension.sendMessage({type: "playSound", keyCode: event.keyCode}, function(response) { 3 | // // console.log("response", response); 4 | // }); 5 | // }; 6 | 7 | // chrome.extension.sendMessage({type: "hello", }, function(response) { 8 | // console.log("response", response); 9 | // }); 10 | -------------------------------------------------------------------------------- /extension/js/lib/adapter.js: -------------------------------------------------------------------------------- 1 | var RTCPeerConnection = null; 2 | var getUserMedia = null; 3 | var attachMediaStream = null; 4 | var reattachMediaStream = null; 5 | var webrtcDetectedBrowser = null; 6 | var webrtcDetectedVersion = null; 7 | 8 | function trace(text) { 9 | // This function is used for logging. 10 | if (text[text.length - 1] == '\n') { 11 | text = text.substring(0, text.length - 1); 12 | } 13 | console.log((performance.now() / 1000).toFixed(3) + ": " + text); 14 | } 15 | 16 | if (navigator.mozGetUserMedia) { 17 | console.log("This appears to be Firefox"); 18 | 19 | webrtcDetectedBrowser = "firefox"; 20 | 21 | // The RTCPeerConnection object. 22 | RTCPeerConnection = mozRTCPeerConnection; 23 | 24 | // The RTCSessionDescription object. 25 | RTCSessionDescription = mozRTCSessionDescription; 26 | 27 | // The RTCIceCandidate object. 28 | RTCIceCandidate = mozRTCIceCandidate; 29 | 30 | // Get UserMedia (only difference is the prefix). 31 | // Code from Adam Barth. 32 | getUserMedia = navigator.mozGetUserMedia.bind(navigator); 33 | 34 | // Creates Turn Uri with new turn format. 35 | createIceServer = function(turn_url, username, password) { 36 | var iceServer = { 'url': turn_url, 37 | 'credential': password, 38 | 'username': username }; 39 | return iceServer; 40 | }; 41 | 42 | // Attach a media stream to an element. 43 | attachMediaStream = function(element, stream) { 44 | console.log("Attaching media stream"); 45 | element.mozSrcObject = stream; 46 | element.play(); 47 | }; 48 | 49 | reattachMediaStream = function(to, from) { 50 | console.log("Reattaching media stream"); 51 | to.mozSrcObject = from.mozSrcObject; 52 | to.play(); 53 | }; 54 | 55 | // Fake get{Video,Audio}Tracks 56 | MediaStream.prototype.getVideoTracks = function() { 57 | return []; 58 | }; 59 | 60 | MediaStream.prototype.getAudioTracks = function() { 61 | return []; 62 | }; 63 | } else if (navigator.webkitGetUserMedia) { 64 | console.log("This appears to be Chrome"); 65 | 66 | webrtcDetectedBrowser = "chrome"; 67 | webrtcDetectedVersion = 68 | parseInt(navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)[2]); 69 | 70 | // For pre-M28 chrome versions use old turn format, else use the new format. 71 | if (webrtcDetectedVersion < 28) { 72 | createIceServer = function(turn_url, username, password) { 73 | var iceServer = { 'url': 'turn:' + username + '@' + turn_url, 74 | 'credential': password }; 75 | return iceServer; 76 | }; 77 | } else { 78 | createIceServer = function(turn_url, username, password) { 79 | var iceServer = { 'url': turn_url, 80 | 'credential': password, 81 | 'username': username }; 82 | return iceServer; 83 | }; 84 | } 85 | 86 | // The RTCPeerConnection object. 87 | RTCPeerConnection = webkitRTCPeerConnection; 88 | 89 | // Get UserMedia (only difference is the prefix). 90 | // Code from Adam Barth. 91 | getUserMedia = navigator.webkitGetUserMedia.bind(navigator); 92 | 93 | // Attach a media stream to an element. 94 | attachMediaStream = function(element, stream) { 95 | if (typeof element.srcObject !== 'undefined') { 96 | element.srcObject = stream; 97 | } else if (typeof element.mozSrcObject !== 'undefined') { 98 | element.mozSrcObject = stream; 99 | } else if (typeof element.src !== 'undefined') { 100 | element.src = URL.createObjectURL(stream); 101 | } else { 102 | console.log('Error attaching stream to element.'); 103 | } 104 | }; 105 | 106 | reattachMediaStream = function(to, from) { 107 | to.src = from.src; 108 | }; 109 | 110 | // The representation of tracks in a stream is changed in M26. 111 | // Unify them for earlier Chrome versions in the coexisting period. 112 | if (!webkitMediaStream.prototype.getVideoTracks) { 113 | webkitMediaStream.prototype.getVideoTracks = function() { 114 | return this.videoTracks; 115 | }; 116 | webkitMediaStream.prototype.getAudioTracks = function() { 117 | return this.audioTracks; 118 | }; 119 | } 120 | 121 | // New syntax of getXXXStreams method in M26. 122 | if (!webkitRTCPeerConnection.prototype.getLocalStreams) { 123 | webkitRTCPeerConnection.prototype.getLocalStreams = function() { 124 | return this.localStreams; 125 | }; 126 | webkitRTCPeerConnection.prototype.getRemoteStreams = function() { 127 | return this.remoteStreams; 128 | }; 129 | } 130 | } else { 131 | console.log("Browser does not appear to be WebRTC-capable"); 132 | } 133 | -------------------------------------------------------------------------------- /extension/js/lib/socket.io-client.js: -------------------------------------------------------------------------------- 1 | (function(){function require(p,parent,orig){var path=require.resolve(p),mod=require.modules[path];console.log('path, p: ', path, p);if(null==path){orig=orig||p;parent=parent||"root";throw new Error('failed to require "'+orig+'" from "'+parent+'"')}if(!mod.exports){mod.exports={};mod.client=mod.component=true;mod.call(this,mod,mod.exports,require.relative(path))}return mod.exports}require.modules={};require.aliases={};require.resolve=function(path){var orig=path,reg=path+".js",regJSON=path+".json",index=path+"/index.js",indexJSON=path+"/index.json";return require.modules[reg]&®||require.modules[regJSON]&®JSON||require.modules[index]&&index||require.modules[indexJSON]&&indexJSON||require.modules[orig]&&orig||require.aliases[index]};require.normalize=function(curr,path){var segs=[];if("."!=path.charAt(0))return path;curr=curr.split("/");path=path.split("/");for(var i=0;i1){return{type:packetslist[type],data:data.substring(1)}}else{return{type:packetslist[type]}}};exports.encodePayload=function(packets){if(!packets.length){return"0:"}var encoded="",message;for(var i=0,l=packets.length;i')}catch(e){iframe=document.createElement("iframe");iframe.name=self.iframeId}iframe.id=self.iframeId;self.form.appendChild(iframe);self.iframe=iframe}initIframe();this.area.value=data.replace(rNewline,"\\n");try{this.form.submit()}catch(e){}if(this.iframe.attachEvent){this.iframe.onreadystatechange=function(){if(self.iframe.readyState=="complete"){complete()}}}else{this.iframe.onload=complete}}});require.register("learnboost-engine.io-client/lib/transports/websocket.js",function(module,exports,require){var Transport=require("../transport"),parser=require("../parser"),util=require("../util"),debug=require("debug")("engine.io-client:websocket");module.exports=WS;var global="undefined"!=typeof window?window:global;function WS(opts){Transport.call(this,opts)}util.inherits(WS,Transport);WS.prototype.name="websocket";WS.prototype.doOpen=function(){if(!this.check()){return}var self=this;this.socket=new(ws())(this.uri());this.socket.onopen=function(){self.onOpen()};this.socket.onclose=function(){self.onClose()};this.socket.onmessage=function(ev){self.onData(ev.data)};this.socket.onerror=function(e){self.onError("websocket error",e)}};WS.prototype.write=function(packets){for(var i=0,l=packets.length;i=hour)return(ms/hour).toFixed(1)+"h";if(ms>=min)return(ms/min).toFixed(1)+"m";if(ms>=sec)return(ms/sec|0)+"s";return ms+"ms"};debug.enabled=function(name){for(var i=0,len=debug.skips.length;ithis._reconnectionAttempts){this.emit("reconnect_failed");this.reconnecting=false}else{var delay=this.attempts*this._reconnectionDelay;delay=Math.min(delay,this._reconnectionDelayMax);debug("will wait %d before reconnect attempt",delay);this.reconnecting=true;var timer=setTimeout(function(){debug("attemptign reconnect");self.open(function(err){if(err){debug("reconnect attempt error");self.reconnect();return self.emit("reconnect_error",err.data)}else{debug("reconnect success");self.onreconnect()}})},delay);this.subs.push({destroy:function(){clearTimeout(timer)}})}};Manager.prototype.onreconnect=function(){var attempt=this.attempts;this.attempts=0;this.reconnecting=false;this.emit("reconnect",attempt)};try{bind=require("bind");object=require("object")}catch(e){bind=require("bind-component");object=require("object-component")}});require.register("socket.io/lib/engine.js",function(module,exports,require){var engine;try{engine=require("engine.io-client")}catch(e){engine=require("engine.io")}module.exports=engine});require.register("socket.io/lib/socket.js",function(module,exports,require){var parser=require("socket.io-parser"),Emitter=require("./emitter"),toArray=require("to-array"),debug=require("debug")("socket.io-client:socket"),on=require("./on"),bind;module.exports=exports=Socket;var events=exports.events=["connect","disconnect","error"];var emit=Emitter.prototype.emit;function Socket(io,nsp){this.io=io;this.nsp=nsp;this.json=this;this.ids=0;this.acks={};this.open();this.buffer=[];this.connected=false}Emitter(Socket.prototype);Socket.prototype.open=Socket.prototype.connect=function(){var io=this.io;io.open();if("open"==this.io.readyState)this.onopen();this.subs=[on(io,"open",bind(this,"onopen")),on(io,"error",bind(this,"onerror"))]};Socket.prototype.send=function(){var args=toArray(arguments);args.shift("message");this.emit.apply(this,args);return this};Socket.prototype.emit=function(ev){if(~events.indexOf(ev)){emit.apply(this,arguments)}else{var args=toArray(arguments);var packet={type:parser.EVENT,args:args};if("function"==typeof args[args.length-1]){debug("emitting packet with ack id %d",this.ids);this.acks[this.ids]=args.pop();packet.id=this.ids++}this.packet(packet)}return this};Socket.prototype.packet=function(packet){packet.nsp=this.nsp;this.io.write(parser.encode(packet))};Socket.prototype.onerror=function(data){this.emit("error",data)};Socket.prototype.onopen=function(){var io=this.io;this.subs.push(on(io,"packet",bind(this,"onpacket")),on(io,"close",bind(this,"onclose")))};Socket.prototype.onclose=function(reason){debug("close (%s)",reason);this.emit("disconnect",reason)};Socket.prototype.onpacket=function(packet){if(packet.nsp!=this.nsp)return;switch(packet.type){case parser.CONNECT:this.onconnect();break;case parser.EVENT:this.onevent(packet);break;case parser.ACK:this.onack(packet);break;case parser.DISCONNECT:this.ondisconnect();break;case parser.ERROR:this.emit("error",packet.data);break}};Socket.prototype.onevent=function(packet){var args=packet.data||[];debug("emitting event %j",args);if(packet.id){debug("attaching ack callback to event");args.push(this.ack(packet.id))}if(this.connected){emit.apply(this,args)}else{this.buffer.push(args)}};Socket.prototype.ack=function(){var self=this;var sent=false;return function(){if(sent)return;var args=toArray(arguments);debug("sending ack %j",args);self.packet({type:parser.ACK,data:args})}};Socket.prototype.onack=function(packet){debug("calling ack %s with %j",packet.id,packet.data);this.acks[packet.id].apply(this,packet.data);delete this.acks[packet.id]};Socket.prototype.onconnect=function(){this.emit("connect");this.connected=true;this.emitBuffered()};Socket.prototype.emitBuffered=function(){for(var i=0;i 5 | * MIT Licensed 6 | */ 7 | 8 | /** 9 | * Module dependencies. 10 | */ 11 | 12 | var client = require('socket.io-client'); 13 | 14 | /** 15 | * Version. 16 | */ 17 | 18 | exports.version = '0.9.11'; 19 | 20 | /** 21 | * Supported protocol version. 22 | */ 23 | 24 | exports.protocol = 1; 25 | 26 | /** 27 | * Client that we serve. 28 | */ 29 | 30 | exports.clientVersion = client.version; 31 | 32 | /** 33 | * Attaches a manager 34 | * 35 | * @param {HTTPServer/Number} a HTTP/S server or a port number to listen on. 36 | * @param {Object} opts to be passed to Manager and/or http server 37 | * @param {Function} callback if a port is supplied 38 | * @api public 39 | */ 40 | 41 | exports.listen = function (server, options, fn) { 42 | if ('function' == typeof server) { 43 | console.warn('Socket.IO\'s `listen()` method expects an `http.Server` instance\n' 44 | + 'as its first parameter. Are you migrating from Express 2.x to 3.x?\n' 45 | + 'If so, check out the "Socket.IO compatibility" section at:\n' 46 | + 'https://github.com/visionmedia/express/wiki/Migrating-from-2.x-to-3.x'); 47 | } 48 | 49 | if ('function' == typeof options) { 50 | fn = options; 51 | options = {}; 52 | } 53 | 54 | if ('undefined' == typeof server) { 55 | // create a server that listens on port 80 56 | server = 80; 57 | } 58 | 59 | if ('number' == typeof server) { 60 | // if a port number is passed 61 | var port = server; 62 | 63 | if (options && options.key) 64 | server = require('https').createServer(options); 65 | else 66 | server = require('http').createServer(); 67 | 68 | // default response 69 | server.on('request', function (req, res) { 70 | res.writeHead(200); 71 | res.end('Welcome to socket.io.'); 72 | }); 73 | 74 | server.listen(port, fn); 75 | } 76 | 77 | // otherwise assume a http/s server 78 | return new exports.Manager(server, options); 79 | }; 80 | 81 | /** 82 | * Manager constructor. 83 | * 84 | * @api public 85 | */ 86 | 87 | exports.Manager = require('./manager'); 88 | 89 | /** 90 | * Transport constructor. 91 | * 92 | * @api public 93 | */ 94 | 95 | exports.Transport = require('./transport'); 96 | 97 | /** 98 | * Socket constructor. 99 | * 100 | * @api public 101 | */ 102 | 103 | exports.Socket = require('./socket'); 104 | 105 | /** 106 | * Static constructor. 107 | * 108 | * @api public 109 | */ 110 | 111 | exports.Static = require('./static'); 112 | 113 | /** 114 | * Store constructor. 115 | * 116 | * @api public 117 | */ 118 | 119 | exports.Store = require('./store'); 120 | 121 | /** 122 | * Memory Store constructor. 123 | * 124 | * @api public 125 | */ 126 | 127 | exports.MemoryStore = require('./stores/memory'); 128 | 129 | /** 130 | * Redis Store constructor. 131 | * 132 | * @api public 133 | */ 134 | 135 | exports.RedisStore = require('./stores/redis'); 136 | 137 | /** 138 | * Parser. 139 | * 140 | * @api public 141 | */ 142 | 143 | exports.parser = require('./parser'); 144 | -------------------------------------------------------------------------------- /extension/js/popup.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samdutton/rtcshare/6998287784e52583b2d3ff82d42605bd68d2420f/extension/js/popup.js -------------------------------------------------------------------------------- /extension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "chrome.tabCapture demo", 3 | "version" : "1.0", 4 | "manifest_version" : 2, 5 | "description" : "Use chrome.tabCapture for screensharing.", 6 | "content_security_policy": "script-src 'self' https://samdutton-nodertc.jit.su; object-src 'self'", 7 | "background": { 8 | "scripts": ["js/lib/adapter.js", "js/background.js"]/*, 9 | "persistent": false*/ 10 | }, 11 | "browser_action" : { 12 | "default_icon" : "images/tabCapture22.png", 13 | "default_title" : "Click to start capture" 14 | }, 15 | // "content_scripts" : [ 16 | // { 17 | // "matches" : [ 18 | // "http://*/*", 19 | // "https://*/*" 20 | // ], 21 | // "js" : ["js/contentscript.js"], 22 | // "run_at" : "document_end", 23 | // "all_frames" : true 24 | // } 25 | // ], 26 | "icons" : { 27 | "16" : "images/tabCapture16.png", 28 | "22" : "images/tabCapture22.png", 29 | "32" : "images/tabCapture32.png", 30 | "48" : "images/tabCapture48.png", 31 | "128": "images/tabCapture128.png" 32 | }, 33 | "permissions": [ 34 | "activeTab", 35 | "tabCapture" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /originals/apprtcJavaScript.js: -------------------------------------------------------------------------------- 1 | // var localVideo; 2 | // var miniVideo; 3 | // var remoteVideo; 4 | var localStream; 5 | var remoteStream; 6 | var channel; 7 | var channelReady = false; 8 | var channelRefreshTimer; 9 | var pc; 10 | var socket; 11 | // var initiator = initiator; 12 | var started = false; 13 | // Set up audio and video regardless of what devices are present. 14 | var mediaConstraints = {'mandatory': { 15 | 'OfferToReceiveAudio':true, 16 | 'OfferToReceiveVideo':true }}; 17 | var isVideoMuted = false; 18 | var isAudioMuted = false; 19 | 20 | function initialize() { 21 | console.log("Initializing; room = " + room_key + "."); 22 | // card = document.getElementById("card"); 23 | // localVideo = document.getElementById("localVideo"); 24 | // miniVideo = document.getElementById("miniVideo"); 25 | // remoteVideo = document.getElementById("remoteVideo"); 26 | resetStatus(); 27 | openChannel(token); 28 | startTokenRefresh(); 29 | // doGetUserMedia(); 30 | // we have the stream from chrome.tabCapture at this point, so: 31 | // Caller creates PeerConnection. 32 | if (initiator) maybeStart(); 33 | } 34 | 35 | function openChannel(channelToken) { 36 | console.log("Opening channel."); 37 | var channel = new goog.appengine.Channel(channelToken); 38 | var handler = { 39 | 'onopen': onChannelOpened, 40 | 'onmessage': onChannelMessage, 41 | 'onerror': onChannelError, 42 | 'onclose': onChannelClosed 43 | }; 44 | socket = channel.open(handler); 45 | } 46 | 47 | function resetStatus() { 48 | if (!initiator) { 49 | setStatus("Waiting for someone to join: room_link"); 50 | } else { 51 | setStatus("Initializing..."); 52 | } 53 | } 54 | 55 | function doGetUserMedia() { 56 | // Call into getUserMedia via the polyfill (adapter.js). 57 | var constraints = media_constraints; 58 | try { 59 | // getUserMedia({'audio':true, 'video':constraints}, onUserMediaSuccess, 60 | // onUserMediaError); 61 | console.log("Requested access to local media with mediaConstraints:\n" + 62 | " \"" + JSON.stringify(constraints) + "\""); 63 | } catch (e) { 64 | alert("getUserMedia() failed. Is this a WebRTC capable browser?"); 65 | console.log("getUserMedia failed with exception: " + e.message); 66 | } 67 | } 68 | 69 | function createPeerConnection() { 70 | // var pc_config = pc_config; 71 | try { 72 | // Create an RTCPeerConnection via the polyfill (adapter.js). 73 | pc = new RTCPeerConnection(pc_config); 74 | pc.onicecandidate = onIceCandidate; 75 | console.log("Created RTCPeerConnnection with config:\n" + " \"" + 76 | JSON.stringify(pc_config) + "\"."); 77 | } catch (e) { 78 | console.log("Failed to create PeerConnection, exception: " + e.message); 79 | alert("Cannot create RTCPeerConnection object; WebRTC is not supported by this browser."); 80 | return; 81 | } 82 | 83 | pc.onconnecting = onSessionConnecting; 84 | pc.onopen = onSessionOpened; 85 | pc.onaddstream = onRemoteStreamAdded; 86 | pc.onremovestream = onRemoteStreamRemoved; 87 | } 88 | 89 | function maybeStart() { 90 | if (!started && localStream && channelReady) { 91 | setStatus("Connecting..."); 92 | console.log("Creating PeerConnection."); 93 | createPeerConnection(); 94 | console.log("Adding local stream."); 95 | pc.addStream(localStream); 96 | started = true; 97 | // Caller initiates offer to peer. 98 | if (initiator) 99 | doCall(); 100 | } 101 | } 102 | 103 | function setStatus(state) { 104 | console.log(state); 105 | // footer.innerHTML = state; 106 | } 107 | 108 | function doCall() { 109 | console.log("Sending offer to peer."); 110 | pc.createOffer(setLocalAndSendMessage, null, mediaConstraints); 111 | } 112 | 113 | function doAnswer() { 114 | console.log("Sending answer to peer."); 115 | pc.createAnswer(setLocalAndSendMessage, null, mediaConstraints); 116 | } 117 | 118 | function setLocalAndSendMessage(sessionDescription) { 119 | // Set Opus as the preferred codec in SDP if Opus is present. 120 | sessionDescription.sdp = preferOpus(sessionDescription.sdp); 121 | pc.setLocalDescription(sessionDescription); 122 | sendMessage(sessionDescription); 123 | } 124 | 125 | function sendMessage(message) { 126 | var msgString = JSON.stringify(message); 127 | console.log('C->S: ' + msgString); 128 | path = "/message?r=" + room_key + "&u=" + me; 129 | var xhr = new XMLHttpRequest(); 130 | xhr.open('POST', path, true); 131 | xhr.send(msgString); 132 | } 133 | 134 | function processSignalingMessage(message) { 135 | var msg = JSON.parse(message); 136 | 137 | if (msg.type === 'offer') { 138 | // Callee creates PeerConnection 139 | if (!initiator && !started) 140 | maybeStart(); 141 | 142 | pc.setRemoteDescription(new RTCSessionDescription(msg)); 143 | doAnswer(); 144 | } else if (msg.type === 'answer' && started) { 145 | pc.setRemoteDescription(new RTCSessionDescription(msg)); 146 | } else if (msg.type === 'candidate' && started) { 147 | var candidate = new RTCIceCandidate({sdpMLineIndex:msg.label, 148 | candidate:msg.candidate}); 149 | pc.addIceCandidate(candidate); 150 | } else if (msg.type === 'bye' && started) { 151 | onRemoteHangup(); 152 | } else if (msg.type === 'tokenResponse') { 153 | reopenChannel(msg.token); 154 | } 155 | } 156 | 157 | function onChannelOpened() { 158 | console.log('Channel opened.'); 159 | channelReady = true; 160 | if (initiator) maybeStart(); 161 | } 162 | function onChannelMessage(message) { 163 | console.log('S->C: ' + message.data); 164 | processSignalingMessage(message.data); 165 | } 166 | function onChannelError() { 167 | console.log('Channel error.'); 168 | } 169 | function onChannelClosed() { 170 | console.log('Channel closed.'); 171 | } 172 | 173 | function onUserMediaSuccess(stream) { 174 | console.log("User has granted access to local media."); 175 | // Call the polyfill wrapper to attach the media stream to this element. 176 | // attachMediaStream(localVideo, stream); 177 | // localVideo.style.opacity = 1; 178 | // localStream = stream; 179 | // Caller creates PeerConnection. 180 | if (initiator) maybeStart(); 181 | } 182 | 183 | function onUserMediaError(error) { 184 | console.log("Failed to get access to local media. Error code was " + error.code); 185 | alert("Failed to get access to local media. Error code was " + error.code + "."); 186 | } 187 | 188 | function onIceCandidate(event) { 189 | if (event.candidate) { 190 | sendMessage({type: 'candidate', 191 | label: event.candidate.sdpMLineIndex, 192 | id: event.candidate.sdpMid, 193 | candidate: event.candidate.candidate}); 194 | } else { 195 | console.log("End of candidates."); 196 | } 197 | } 198 | 199 | function onSessionConnecting(message) { 200 | console.log("Session connecting."); 201 | } 202 | function onSessionOpened(message) { 203 | console.log("Session opened."); 204 | } 205 | 206 | function onRemoteStreamAdded(event) { 207 | console.log("Remote stream added."); 208 | // TODO(ekr@rtfm.com): Copy the minivideo on Firefox 209 | // miniVideo.src = localVideo.src; 210 | // attachMediaStream(remoteVideo, event.stream); 211 | remoteStream = event.stream; 212 | waitForRemoteVideo(); 213 | } 214 | function onRemoteStreamRemoved(event) { 215 | console.log("Remote stream removed."); 216 | } 217 | 218 | function onHangup() { 219 | console.log("Hanging up."); 220 | transitionToDone(); 221 | stop(); 222 | // will trigger BYE from server 223 | socket.close(); 224 | stopTokenRefresh(); 225 | } 226 | 227 | function onRemoteHangup() { 228 | console.log('Session terminated.'); 229 | transitionToWaiting(); 230 | stop(); 231 | initiator = 0; 232 | } 233 | 234 | function stop() { 235 | started = false; 236 | isAudioMuted = false; 237 | isVideoMuted = false; 238 | pc.close(); 239 | pc = null; 240 | } 241 | 242 | function waitForRemoteVideo() { 243 | if (remoteStream.videoTracks.length === 0 || remoteVideo.currentTime > 0) { 244 | transitionToActive(); 245 | } else { 246 | setTimeout(waitForRemoteVideo, 100); 247 | } 248 | } 249 | function transitionToActive() { 250 | // remoteVideo.style.opacity = 1; 251 | // card.style.webkitTransform = "rotateY(180deg)"; 252 | // setTimeout(function() { localVideo.src = ""; }, 500); 253 | // setTimeout(function() { miniVideo.style.opacity = 1; }, 1000); 254 | setStatus(""); 255 | } 256 | function transitionToWaiting() { 257 | // card.style.webkitTransform = "rotateY(0deg)"; 258 | setTimeout(function() { 259 | // localVideo.src = miniVideo.src; 260 | // miniVideo.src = ""; 261 | // remoteVideo.src = ""; 262 | }, 500); 263 | // miniVideo.style.opacity = 0; 264 | // remoteVideo.style.opacity = 0; 265 | resetStatus(); 266 | } 267 | function transitionToDone() { 268 | // localVideo.style.opacity = 0; 269 | // remoteVideo.style.opacity = 0; 270 | // miniVideo.style.opacity = 0; 271 | setStatus("You have left the call. Click here to rejoin."); 272 | } 273 | function enterFullScreen() { 274 | container.webkitRequestFullScreen(); 275 | } 276 | 277 | function toggleVideoMute() { 278 | if (localStream.videoTracks.length === 0) { 279 | console.log("No local video available."); 280 | return; 281 | } 282 | 283 | if (isVideoMuted) { 284 | for (i = 0; i < localStream.videoTracks.length; i++) { 285 | localStream.videoTracks[i].enabled = true; 286 | } 287 | console.log("Video unmuted."); 288 | } else { 289 | for (i = 0; i < localStream.videoTracks.length; i++) { 290 | localStream.videoTracks[i].enabled = false; 291 | } 292 | console.log("Video muted."); 293 | } 294 | 295 | isVideoMuted = !isVideoMuted; 296 | } 297 | 298 | function toggleAudioMute() { 299 | if (localStream.audioTracks.length === 0) { 300 | console.log("No local audio available."); 301 | return; 302 | } 303 | 304 | if (isAudioMuted) { 305 | for (i = 0; i < localStream.audioTracks.length; i++) { 306 | localStream.audioTracks[i].enabled = true; 307 | } 308 | console.log("Audio unmuted."); 309 | } else { 310 | for (i = 0; i < localStream.audioTracks.length; i++){ 311 | localStream.audioTracks[i].enabled = false; 312 | } 313 | console.log("Audio muted."); 314 | } 315 | 316 | isAudioMuted = !isAudioMuted; 317 | } 318 | 319 | setTimeout(initialize, 1); 320 | 321 | // Send BYE on refreshing(or leaving) a demo page 322 | // to ensure the room is cleaned for next session. 323 | window.onbeforeunload = function() { 324 | sendMessage({type: 'bye'}); 325 | //Delay 100ms to ensure 'bye' arrives first. 326 | setTimeout(function(){}, 100); 327 | } 328 | 329 | // Ctrl-D: toggle audio mute; Ctrl-E: toggle video mute. 330 | // On Mac, Command key is instead of Ctrl. 331 | // Return false to screen out original Chrome shortcuts. 332 | document.onkeydown = function() { 333 | if (navigator.appVersion.indexOf("Mac") != -1) { 334 | if (event.metaKey && event.keyCode === 68) { 335 | toggleAudioMute(); 336 | return false; 337 | } 338 | if (event.metaKey && event.keyCode === 69) { 339 | toggleVideoMute(); 340 | return false; 341 | } 342 | } else { 343 | if (event.ctrlKey && event.keyCode === 68) { 344 | toggleAudioMute(); 345 | return false; 346 | } 347 | if (event.ctrlKey && event.keyCode === 69) { 348 | toggleVideoMute(); 349 | return false; 350 | } 351 | } 352 | } 353 | 354 | // Refresh the channel token periodically before it expires. 355 | function reopenChannel(channelToken) { 356 | socket.close(); 357 | openChannel(channelToken); 358 | } 359 | 360 | function startTokenRefresh() { 361 | var interval = token_timeout; 362 | // Make sure the refresh timer is 10 sec less than token timeout. 363 | interval = (interval -10 ) > 0 ? interval - 10: interval; 364 | channelRefreshTimer = setInterval(function() { 365 | sendMessage({type: 'tokenRequest'})}, interval * 1000); 366 | } 367 | 368 | function stopTokenRefresh() { 369 | if (channelRefreshTimer) 370 | clearInterval(channelRefreshTimer); 371 | } 372 | 373 | // Set Opus as the default audio codec if it's present. 374 | function preferOpus(sdp) { 375 | var sdpLines = sdp.split('\r\n'); 376 | 377 | // Search for m line. 378 | for (var i = 0; i < sdpLines.length; i++) { 379 | if (sdpLines[i].search('m=audio') !== -1) { 380 | var mLineIndex = i; 381 | break; 382 | } 383 | } 384 | if (mLineIndex === null) 385 | return sdp; 386 | 387 | // If Opus is available, set it as the default in m line. 388 | for (var i = 0; i < sdpLines.length; i++) { 389 | if (sdpLines[i].search('opus/48000') !== -1) { 390 | var opusPayload = extractSdp(sdpLines[i], /:(\d+) opus\/48000/i); 391 | if (opusPayload) 392 | sdpLines[mLineIndex] = setDefaultCodec(sdpLines[mLineIndex], opusPayload); 393 | break; 394 | } 395 | } 396 | 397 | // Remove CN in m line and sdp. 398 | sdpLines = removeCN(sdpLines, mLineIndex); 399 | 400 | sdp = sdpLines.join('\r\n'); 401 | return sdp; 402 | } 403 | 404 | function extractSdp(sdpLine, pattern) { 405 | var result = sdpLine.match(pattern); 406 | return (result && result.length == 2)? result[1]: null; 407 | } 408 | 409 | // Set the selected codec to the first in m line. 410 | function setDefaultCodec(mLine, payload) { 411 | var elements = mLine.split(' '); 412 | var newLine = new Array(); 413 | var index = 0; 414 | for (var i = 0; i < elements.length; i++) { 415 | if (index === 3) // Format of media starts from the fourth. 416 | newLine[index++] = payload; // Put target payload to the first. 417 | if (elements[i] !== payload) 418 | newLine[index++] = elements[i]; 419 | } 420 | return newLine.join(' '); 421 | } 422 | 423 | // Strip CN from sdp before CN constraints is ready. 424 | function removeCN(sdpLines, mLineIndex) { 425 | var mLineElements = sdpLines[mLineIndex].split(' '); 426 | // Scan from end for the convenience of removing an item. 427 | for (var i = sdpLines.length-1; i >= 0; i--) { 428 | var payload = extractSdp(sdpLines[i], /a=rtpmap:(\d+) CN\/\d+/i); 429 | if (payload) { 430 | var cnPos = mLineElements.indexOf(payload); 431 | if (cnPos !== -1) { 432 | // Remove CN payload from m line. 433 | mLineElements.splice(cnPos, 1); 434 | } 435 | // Remove CN line in sdp 436 | sdpLines.splice(i, 1); 437 | } 438 | } 439 | 440 | sdpLines[mLineIndex] = mLineElements.join(' '); 441 | return sdpLines; 442 | } 443 | 444 | window.addEventListener("message", function receiveMessage(event) { 445 | alert("child got a message:\n\n" + event.data + "\n\n...and will now send a message back again."); 446 | event.source.postMessage("Hi from child.html!", "*"); 447 | }); 448 | -------------------------------------------------------------------------------- /originals/media-record.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 25 | 27 | 34 | 36 | 40 | 44 | 45 | 47 | 51 | 55 | 56 | 59 | 63 | 67 | 68 | 71 | 75 | 79 | 80 | 91 | 102 | 113 | 122 | 133 | 134 | 160 | 164 | 168 | 172 | 176 | 180 | 184 | 188 | 192 | 196 | 200 | 204 | 208 | 212 | 216 | 220 | 224 | 228 | 232 | 244 | 245 | 247 | 248 | 250 | image/svg+xml 251 | 253 | Media Record 254 | 255 | 256 | Lapo Calamandrei 257 | 258 | 259 | 261 | 262 | 263 | media 264 | player 265 | record 266 | music 267 | sound 268 | video 269 | 270 | 271 | 272 | 274 | 276 | 278 | 280 | 281 | 282 | 283 | 289 | 301 | 314 | 324 | 334 | 339 | 340 | 341 | -------------------------------------------------------------------------------- /originals/paused128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samdutton/rtcshare/6998287784e52583b2d3ff82d42605bd68d2420f/originals/paused128.png -------------------------------------------------------------------------------- /originals/paused16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samdutton/rtcshare/6998287784e52583b2d3ff82d42605bd68d2420f/originals/paused16.png -------------------------------------------------------------------------------- /originals/paused22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samdutton/rtcshare/6998287784e52583b2d3ff82d42605bd68d2420f/originals/paused22.png -------------------------------------------------------------------------------- /originals/paused32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samdutton/rtcshare/6998287784e52583b2d3ff82d42605bd68d2420f/originals/paused32.png -------------------------------------------------------------------------------- /originals/paused48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samdutton/rtcshare/6998287784e52583b2d3ff82d42605bd68d2420f/originals/paused48.png -------------------------------------------------------------------------------- /originals/recording22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samdutton/rtcshare/6998287784e52583b2d3ff82d42605bd68d2420f/originals/recording22.png -------------------------------------------------------------------------------- /originals/svg.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /psd/buttons.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samdutton/rtcshare/6998287784e52583b2d3ff82d42605bd68d2420f/psd/buttons.psd -------------------------------------------------------------------------------- /psd/icon.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samdutton/rtcshare/6998287784e52583b2d3ff82d42605bd68d2420f/psd/icon.psd -------------------------------------------------------------------------------- /psd/kangaroo.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samdutton/rtcshare/6998287784e52583b2d3ff82d42605bd68d2420f/psd/kangaroo.psd -------------------------------------------------------------------------------- /psd/promo440x280.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samdutton/rtcshare/6998287784e52583b2d3ff82d42605bd68d2420f/psd/promo440x280.psd -------------------------------------------------------------------------------- /psd/screenshot640x400.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samdutton/rtcshare/6998287784e52583b2d3ff82d42605bd68d2420f/psd/screenshot640x400.psd --------------------------------------------------------------------------------