1'use strict'; 2 3const receiveButton = document.getElementById('receiveButton'); 4receiveButton.addEventListener('click', onReceive); 5const keyboardCaptureButton = document.getElementById('keyboardCaptureBtn'); 6keyboardCaptureButton.addEventListener('click', onKeyboardCaptureClick); 7 8const deviceScreen = document.getElementById('deviceScreen'); 9 10deviceScreen.addEventListener("click", onInitialClick); 11 12function onInitialClick(e) { 13 // This stupid thing makes sure that we disable controls after the first click... 14 // Why not just disable controls altogether you ask? Because then audio won't play 15 // because these days user-interaction is required to enable audio playback... 16 console.log("onInitialClick"); 17 18 deviceScreen.controls = false; 19 deviceScreen.removeEventListener("click", onInitialClick); 20} 21 22let pc1; 23let pc2; 24 25let dataChannel; 26 27let ws; 28 29let offerResolve; 30let iceCandidateResolve; 31 32let videoStream; 33 34let mouseIsDown = false; 35 36const is_chrome = navigator.userAgent.indexOf("Chrome") !== -1; 37 38function handleDataChannelStatusChange(event) { 39 console.log('handleDataChannelStatusChange state=' + dataChannel.readyState); 40 41 if (dataChannel.readyState == "open") { 42 dataChannel.send("Hello, world!"); 43 } 44} 45 46function handleDataChannelMessage(event) { 47 console.log('handleDataChannelMessage data="' + event.data + '"'); 48} 49 50function onKeyboardCaptureClick(e) { 51 const selectedClass = 'selected'; 52 if (keyboardCaptureButton.classList.contains(selectedClass)) { 53 stopKeyboardTracking(); 54 keyboardCaptureButton.classList.remove(selectedClass); 55 } else { 56 startKeyboardTracking(); 57 keyboardCaptureButton.classList.add(selectedClass); 58 } 59} 60 61async function onReceive() { 62 console.log('onReceive'); 63 receiveButton.disabled = true; 64 65 init_logcat(); 66 67 const wsProtocol = (location.protocol == "http:") ? "ws:" : "wss:"; 68 69 ws = new WebSocket(wsProtocol + "//" + location.host + "/control"); 70 // temporarily disable audio to free ports in the server since it's only 71 // producing silence anyways. 72 var search = location.search + "&disable_audio=1"; 73 search = '?' + search.substr(1); 74 75 ws.onopen = function() { 76 console.log("onopen"); 77 ws.send('{\r\n' 78 + '"type": "greeting",\r\n' 79 + '"message": "Hello, world!",\r\n' 80 + '"path": "' + location.pathname + search + '"\r\n' 81 + '}'); 82 }; 83 ws.onmessage = function(e) { 84 console.log("onmessage " + e.data); 85 86 let data = JSON.parse(e.data); 87 if (data.type == "hello") { 88 kickoff(); 89 } else if (data.type == "offer" && offerResolve) { 90 offerResolve(data.sdp); 91 offerResolve = undefined; 92 } else if (data.type == "ice-candidate" && iceCandidateResolve) { 93 iceCandidateResolve(data); 94 95 iceCandidateResolve = undefined; 96 } 97 }; 98 99 pc2 = new RTCPeerConnection(); 100 console.log('got pc2=' + pc2); 101 102 pc2.addEventListener( 103 'icecandidate', e => onIceCandidate(pc2, e)); 104 105 pc2.addEventListener( 106 'iceconnectionstatechange', e => onIceStateChange(pc2, e)); 107 108 pc2.addEventListener( 109 'connectionstatechange', e => { 110 console.log("connection state = " + pc2.connectionState); 111 }); 112 113 pc2.addEventListener('track', onGotRemoteStream); 114 115 dataChannel = pc2.createDataChannel("data-channel"); 116 dataChannel.onopen = handleDataChannelStatusChange; 117 dataChannel.onclose = handleDataChannelStatusChange; 118 dataChannel.onmessage = handleDataChannelMessage; 119} 120 121async function kickoff() { 122 console.log('createOffer start'); 123 124 try { 125 var offer = await getWsOffer(); 126 await onCreateOfferSuccess(offer); 127 } catch (e) { 128 console.log('createOffer FAILED '); 129 } 130} 131 132async function onCreateOfferSuccess(desc) { 133 console.log(`Offer ${desc.sdp}`); 134 135 try { 136 pc2.setRemoteDescription(desc); 137 } catch (e) { 138 console.log('setRemoteDescription pc2 FAILED'); 139 return; 140 } 141 142 console.log('setRemoteDescription pc2 successful.'); 143 144 try { 145 setWsLocalDescription(desc); 146 } catch (e) { 147 console.log('setLocalDescription pc1 FAILED'); 148 return; 149 } 150 151 console.log('setLocalDescription pc1 successful.'); 152 153 try { 154 const answer = await pc2.createAnswer(); 155 156 await onCreateAnswerSuccess(answer); 157 } catch (e) { 158 console.log('createAnswer FAILED'); 159 } 160} 161 162function setWsRemoteDescription(desc) { 163 ws.send('{\r\n' 164 + '"type": "set-remote-desc",\r\n' 165 + '"sdp": "' + desc.sdp + '"\r\n' 166 + '}'); 167} 168 169function setWsLocalDescription(desc) { 170 ws.send('{\r\n' 171 + '"type": "set-local-desc",\r\n' 172 + '"sdp": "' + desc.sdp + '"\r\n' 173 + '}'); 174} 175 176async function getWsOffer() { 177 const offerPromise = new Promise(function(resolve, reject) { 178 offerResolve = resolve; 179 }); 180 181 ws.send('{\r\n' 182 + '"type": "request-offer",\r\n' 183 + (is_chrome ? '"is_chrome": 1\r\n' 184 : '"is_chrome": 0\r\n') 185 + '}'); 186 187 const sdp = await offerPromise; 188 189 return { type: "offer", sdp: sdp }; 190} 191 192async function getWsIceCandidate(mid) { 193 console.log("getWsIceCandidate (mid=" + mid + ")"); 194 195 const answerPromise = new Promise(function(resolve, reject) { 196 iceCandidateResolve = resolve; 197 }); 198 199 ws.send('{\r\n' 200 + '"type": "get-ice-candidate",\r\n' 201 + '"mid": ' + mid + ',\r\n' 202 + '}'); 203 204 const replyInfo = await answerPromise; 205 206 console.log("got replyInfo '" + replyInfo + "'"); 207 208 if (replyInfo == undefined || replyInfo.candidate == undefined) { 209 return null; 210 } 211 212 const replyCandidate = replyInfo.candidate; 213 const mlineIndex = replyInfo.mlineIndex; 214 215 let result; 216 try { 217 result = new RTCIceCandidate( 218 { 219 sdpMid: mid, 220 sdpMLineIndex: mlineIndex, 221 candidate: replyCandidate 222 }); 223 } 224 catch (e) { 225 console.log("new RTCIceCandidate FAILED. " + e); 226 return undefined; 227 } 228 229 console.log("got result " + result); 230 231 return result; 232} 233 234async function addRemoteIceCandidate(mid) { 235 const candidate = await getWsIceCandidate(mid); 236 237 if (!candidate) { 238 return false; 239 } 240 241 try { 242 await pc2.addIceCandidate(candidate); 243 } catch (e) { 244 console.log("addIceCandidate pc2 FAILED w/ " + e); 245 return false; 246 } 247 248 console.log("addIceCandidate pc2 successful. (mid=" 249 + mid + ", mlineIndex=" + candidate.sdpMLineIndex + ")"); 250 251 return true; 252} 253 254async function onCreateAnswerSuccess(desc) { 255 console.log(`Answer ${desc.sdp}`); 256 257 try { 258 await pc2.setLocalDescription(desc); 259 } catch (e) { 260 console.log('setLocalDescription pc2 FAILED ' + e); 261 return; 262 } 263 264 console.log('setLocalDescription pc2 successful.'); 265 266 try { 267 setWsRemoteDescription(desc); 268 } catch (e) { 269 console.log('setRemoteDescription pc1 FAILED'); 270 return; 271 } 272 273 console.log('setRemoteDescription pc1 successful.'); 274 275 if (!await addRemoteIceCandidate(0)) { 276 return; 277 } 278 await addRemoteIceCandidate(1); 279 await addRemoteIceCandidate(2); 280} 281 282function getPcName(pc) { 283 return ((pc == pc2) ? "pc2" : "pc1"); 284} 285 286async function onIceCandidate(pc, e) { 287 console.log( 288 getPcName(pc) 289 + ' onIceCandidate ' 290 + (e.candidate ? ('"' + e.candidate.candidate + '"') : '(null)') 291 + " " 292 + (e.candidate ? ('sdmMid: ' + e.candidate.sdpMid) : '(null)') 293 + " " 294 + (e.candidate ? ('sdpMLineIndex: ' + e.candidate.sdpMLineIndex) : '(null)')); 295 296 if (!e.candidate) { 297 return; 298 } 299 300 let other_pc = (pc == pc2) ? pc1 : pc2; 301 302 if (other_pc) { 303 try { 304 await other_pc.addIceCandidate(e.candidate); 305 } catch (e) { 306 console.log('addIceCandidate FAILED ' + e); 307 return; 308 } 309 310 console.log('addIceCandidate successful.'); 311 } 312} 313 314async function onIceStateChange(pc, e) { 315 console.log( 316 'onIceStateChange ' + getPcName(pc) + " '" + pc.iceConnectionState + "'"); 317 318 if (pc.iceConnectionState == "connected") { 319 deviceScreen.srcObject = videoStream; 320 321 startMouseTracking() 322 } else if (pc.iceConnectionState == "disconnected") { 323 stopMouseTracking() 324 } 325} 326 327async function onGotRemoteStream(e) { 328 console.log('onGotRemoteStream ' + e); 329 330 const track = e.track; 331 332 console.log('track = ' + track); 333 console.log('track.kind = ' + track.kind); 334 console.log('track.readyState = ' + track.readyState); 335 console.log('track.enabled = ' + track.enabled); 336 337 if (track.kind == "video") { 338 videoStream = e.streams[0]; 339 } 340} 341 342function startMouseTracking() { 343 if (window.PointerEvent) { 344 deviceScreen.addEventListener("pointerdown", onStartDrag); 345 deviceScreen.addEventListener("pointermove", onContinueDrag); 346 deviceScreen.addEventListener("pointerup", onEndDrag); 347 } else if (window.TouchEvent) { 348 deviceScreen.addEventListener("touchstart", onStartDrag); 349 deviceScreen.addEventListener("touchmove", onContinueDrag); 350 deviceScreen.addEventListener("touchend", onEndDrag); 351 } else if (window.MouseEvent) { 352 deviceScreen.addEventListener("mousedown", onStartDrag); 353 deviceScreen.addEventListener("mousemove", onContinueDrag); 354 deviceScreen.addEventListener("mouseup", onEndDrag); 355 } 356} 357 358function stopMouseTracking() { 359 if (window.PointerEvent) { 360 deviceScreen.removeEventListener("pointerdown", onStartDrag); 361 deviceScreen.removeEventListener("pointermove", onContinueDrag); 362 deviceScreen.removeEventListener("pointerup", onEndDrag); 363 } else if (window.TouchEvent) { 364 deviceScreen.removeEventListener("touchstart", onStartDrag); 365 deviceScreen.removeEventListener("touchmove", onContinueDrag); 366 deviceScreen.removeEventListener("touchend", onEndDrag); 367 } else if (window.MouseEvent) { 368 deviceScreen.removeEventListener("mousedown", onStartDrag); 369 deviceScreen.removeEventListener("mousemove", onContinueDrag); 370 deviceScreen.removeEventListener("mouseup", onEndDrag); 371 } 372} 373 374function startKeyboardTracking() { 375 document.addEventListener('keydown', onKeyEvent); 376 document.addEventListener('keyup', onKeyEvent); 377} 378 379function stopKeyboardTracking() { 380 document.removeEventListener('keydown', onKeyEvent); 381 document.removeEventListener('keyup', onKeyEvent); 382} 383 384function onStartDrag(e) { 385 e.preventDefault(); 386 387 // console.log("mousedown at " + e.pageX + " / " + e.pageY); 388 mouseIsDown = true; 389 390 sendMouseUpdate(true, e); 391} 392 393function onEndDrag(e) { 394 e.preventDefault(); 395 396 // console.log("mouseup at " + e.pageX + " / " + e.pageY); 397 mouseIsDown = false; 398 399 sendMouseUpdate(false, e); 400} 401 402function onContinueDrag(e) { 403 e.preventDefault(); 404 405 // console.log("mousemove at " + e.pageX + " / " + e.pageY + ", down=" + mouseIsDown); 406 if (mouseIsDown) { 407 sendMouseUpdate(true, e); 408 } 409} 410 411function sendMouseUpdate(down, e) { 412 var x = e.offsetX; 413 var y = e.offsetY; 414 415 const videoWidth = deviceScreen.videoWidth; 416 const videoHeight = deviceScreen.videoHeight; 417 const elementWidth = deviceScreen.width; 418 const elementHeight = deviceScreen.height; 419 420 // vh*ew > eh*vw? then scale h instead of w 421 const scaleHeight = videoHeight * elementWidth > videoWidth * elementHeight; 422 var elementScaling = 0, videoScaling = 0; 423 if (scaleHeight) { 424 elementScaling = elementHeight; 425 videoScaling = videoHeight; 426 } else { 427 elementScaling = elementWidth; 428 videoScaling = videoWidth; 429 } 430 431 // Substract the offset produced by the difference in aspect ratio if any. 432 if (scaleHeight) { 433 x -= (elementWidth - elementScaling * videoWidth / videoScaling) / 2; 434 } else { 435 y -= (elementHeight - elementScaling * videoHeight / videoScaling) / 2; 436 } 437 438 // Convert to coordinates relative to the video 439 x = videoScaling * x / elementScaling; 440 y = videoScaling * y / elementScaling; 441 442 ws.send('{\r\n' 443 + '"type": "set-mouse-position",\r\n' 444 + '"down": ' + (down ? "1" : "0") + ',\r\n' 445 + '"x": ' + Math.trunc(x) + ',\r\n' 446 + '"y": ' + Math.trunc(y) + '\r\n' 447 + '}'); 448} 449 450function onKeyEvent(e) { 451 e.preventDefault(); 452 ws.send('{"type": "key-event","keycode": "'+e.code+'", "event_type": "'+e.type+'"}'); 453}