| 'use strict'; |
| |
| const receiveButton = document.getElementById('receiveButton'); |
| receiveButton.addEventListener('click', onReceive); |
| const keyboardCaptureButton = document.getElementById('keyboardCaptureBtn'); |
| keyboardCaptureButton.addEventListener('click', onKeyboardCaptureClick); |
| |
| const deviceScreen = document.getElementById('deviceScreen'); |
| |
| deviceScreen.addEventListener("click", onInitialClick); |
| |
| function onInitialClick(e) { |
| // This stupid thing makes sure that we disable controls after the first click... |
| // Why not just disable controls altogether you ask? Because then audio won't play |
| // because these days user-interaction is required to enable audio playback... |
| console.log("onInitialClick"); |
| |
| deviceScreen.controls = false; |
| deviceScreen.removeEventListener("click", onInitialClick); |
| } |
| |
| let pc1; |
| let pc2; |
| |
| let dataChannel; |
| |
| let ws; |
| |
| let offerResolve; |
| let iceCandidateResolve; |
| |
| let videoStream; |
| |
| let mouseIsDown = false; |
| |
| const is_chrome = navigator.userAgent.indexOf("Chrome") !== -1; |
| |
| function handleDataChannelStatusChange(event) { |
| console.log('handleDataChannelStatusChange state=' + dataChannel.readyState); |
| |
| if (dataChannel.readyState == "open") { |
| dataChannel.send("Hello, world!"); |
| } |
| } |
| |
| function handleDataChannelMessage(event) { |
| console.log('handleDataChannelMessage data="' + event.data + '"'); |
| } |
| |
| function onKeyboardCaptureClick(e) { |
| const selectedClass = 'selected'; |
| if (keyboardCaptureButton.classList.contains(selectedClass)) { |
| stopKeyboardTracking(); |
| keyboardCaptureButton.classList.remove(selectedClass); |
| } else { |
| startKeyboardTracking(); |
| keyboardCaptureButton.classList.add(selectedClass); |
| } |
| } |
| |
| async function onReceive() { |
| console.log('onReceive'); |
| receiveButton.disabled = true; |
| |
| init_logcat(); |
| |
| const wsProtocol = (location.protocol == "http:") ? "ws:" : "wss:"; |
| |
| ws = new WebSocket(wsProtocol + "//" + location.host + "/control"); |
| // temporarily disable audio to free ports in the server since it's only |
| // producing silence anyways. |
| var search = location.search + "&disable_audio=1"; |
| search = '?' + search.substr(1); |
| |
| ws.onopen = function() { |
| console.log("onopen"); |
| ws.send('{\r\n' |
| + '"type": "greeting",\r\n' |
| + '"message": "Hello, world!",\r\n' |
| + '"path": "' + location.pathname + search + '"\r\n' |
| + '}'); |
| }; |
| ws.onmessage = function(e) { |
| console.log("onmessage " + e.data); |
| |
| let data = JSON.parse(e.data); |
| if (data.type == "hello") { |
| kickoff(); |
| } else if (data.type == "offer" && offerResolve) { |
| offerResolve(data.sdp); |
| offerResolve = undefined; |
| } else if (data.type == "ice-candidate" && iceCandidateResolve) { |
| iceCandidateResolve(data); |
| |
| iceCandidateResolve = undefined; |
| } |
| }; |
| |
| pc2 = new RTCPeerConnection(); |
| console.log('got pc2=' + pc2); |
| |
| pc2.addEventListener( |
| 'icecandidate', e => onIceCandidate(pc2, e)); |
| |
| pc2.addEventListener( |
| 'iceconnectionstatechange', e => onIceStateChange(pc2, e)); |
| |
| pc2.addEventListener( |
| 'connectionstatechange', e => { |
| console.log("connection state = " + pc2.connectionState); |
| }); |
| |
| pc2.addEventListener('track', onGotRemoteStream); |
| |
| dataChannel = pc2.createDataChannel("data-channel"); |
| dataChannel.onopen = handleDataChannelStatusChange; |
| dataChannel.onclose = handleDataChannelStatusChange; |
| dataChannel.onmessage = handleDataChannelMessage; |
| } |
| |
| async function kickoff() { |
| console.log('createOffer start'); |
| |
| try { |
| var offer = await getWsOffer(); |
| await onCreateOfferSuccess(offer); |
| } catch (e) { |
| console.log('createOffer FAILED '); |
| } |
| } |
| |
| async function onCreateOfferSuccess(desc) { |
| console.log(`Offer ${desc.sdp}`); |
| |
| try { |
| pc2.setRemoteDescription(desc); |
| } catch (e) { |
| console.log('setRemoteDescription pc2 FAILED'); |
| return; |
| } |
| |
| console.log('setRemoteDescription pc2 successful.'); |
| |
| try { |
| setWsLocalDescription(desc); |
| } catch (e) { |
| console.log('setLocalDescription pc1 FAILED'); |
| return; |
| } |
| |
| console.log('setLocalDescription pc1 successful.'); |
| |
| try { |
| const answer = await pc2.createAnswer(); |
| |
| await onCreateAnswerSuccess(answer); |
| } catch (e) { |
| console.log('createAnswer FAILED'); |
| } |
| } |
| |
| function setWsRemoteDescription(desc) { |
| ws.send('{\r\n' |
| + '"type": "set-remote-desc",\r\n' |
| + '"sdp": "' + desc.sdp + '"\r\n' |
| + '}'); |
| } |
| |
| function setWsLocalDescription(desc) { |
| ws.send('{\r\n' |
| + '"type": "set-local-desc",\r\n' |
| + '"sdp": "' + desc.sdp + '"\r\n' |
| + '}'); |
| } |
| |
| async function getWsOffer() { |
| const offerPromise = new Promise(function(resolve, reject) { |
| offerResolve = resolve; |
| }); |
| |
| ws.send('{\r\n' |
| + '"type": "request-offer",\r\n' |
| + (is_chrome ? '"is_chrome": 1\r\n' |
| : '"is_chrome": 0\r\n') |
| + '}'); |
| |
| const sdp = await offerPromise; |
| |
| return { type: "offer", sdp: sdp }; |
| } |
| |
| async function getWsIceCandidate(mid) { |
| console.log("getWsIceCandidate (mid=" + mid + ")"); |
| |
| const answerPromise = new Promise(function(resolve, reject) { |
| iceCandidateResolve = resolve; |
| }); |
| |
| ws.send('{\r\n' |
| + '"type": "get-ice-candidate",\r\n' |
| + '"mid": ' + mid + ',\r\n' |
| + '}'); |
| |
| const replyInfo = await answerPromise; |
| |
| console.log("got replyInfo '" + replyInfo + "'"); |
| |
| if (replyInfo == undefined || replyInfo.candidate == undefined) { |
| return null; |
| } |
| |
| const replyCandidate = replyInfo.candidate; |
| const mlineIndex = replyInfo.mlineIndex; |
| |
| let result; |
| try { |
| result = new RTCIceCandidate( |
| { |
| sdpMid: mid, |
| sdpMLineIndex: mlineIndex, |
| candidate: replyCandidate |
| }); |
| } |
| catch (e) { |
| console.log("new RTCIceCandidate FAILED. " + e); |
| return undefined; |
| } |
| |
| console.log("got result " + result); |
| |
| return result; |
| } |
| |
| async function addRemoteIceCandidate(mid) { |
| const candidate = await getWsIceCandidate(mid); |
| |
| if (!candidate) { |
| return false; |
| } |
| |
| try { |
| await pc2.addIceCandidate(candidate); |
| } catch (e) { |
| console.log("addIceCandidate pc2 FAILED w/ " + e); |
| return false; |
| } |
| |
| console.log("addIceCandidate pc2 successful. (mid=" |
| + mid + ", mlineIndex=" + candidate.sdpMLineIndex + ")"); |
| |
| return true; |
| } |
| |
| async function onCreateAnswerSuccess(desc) { |
| console.log(`Answer ${desc.sdp}`); |
| |
| try { |
| await pc2.setLocalDescription(desc); |
| } catch (e) { |
| console.log('setLocalDescription pc2 FAILED ' + e); |
| return; |
| } |
| |
| console.log('setLocalDescription pc2 successful.'); |
| |
| try { |
| setWsRemoteDescription(desc); |
| } catch (e) { |
| console.log('setRemoteDescription pc1 FAILED'); |
| return; |
| } |
| |
| console.log('setRemoteDescription pc1 successful.'); |
| |
| if (!await addRemoteIceCandidate(0)) { |
| return; |
| } |
| await addRemoteIceCandidate(1); |
| await addRemoteIceCandidate(2); |
| } |
| |
| function getPcName(pc) { |
| return ((pc == pc2) ? "pc2" : "pc1"); |
| } |
| |
| async function onIceCandidate(pc, e) { |
| console.log( |
| getPcName(pc) |
| + ' onIceCandidate ' |
| + (e.candidate ? ('"' + e.candidate.candidate + '"') : '(null)') |
| + " " |
| + (e.candidate ? ('sdmMid: ' + e.candidate.sdpMid) : '(null)') |
| + " " |
| + (e.candidate ? ('sdpMLineIndex: ' + e.candidate.sdpMLineIndex) : '(null)')); |
| |
| if (!e.candidate) { |
| return; |
| } |
| |
| let other_pc = (pc == pc2) ? pc1 : pc2; |
| |
| if (other_pc) { |
| try { |
| await other_pc.addIceCandidate(e.candidate); |
| } catch (e) { |
| console.log('addIceCandidate FAILED ' + e); |
| return; |
| } |
| |
| console.log('addIceCandidate successful.'); |
| } |
| } |
| |
| async function onIceStateChange(pc, e) { |
| console.log( |
| 'onIceStateChange ' + getPcName(pc) + " '" + pc.iceConnectionState + "'"); |
| |
| if (pc.iceConnectionState == "connected") { |
| deviceScreen.srcObject = videoStream; |
| |
| startMouseTracking() |
| } else if (pc.iceConnectionState == "disconnected") { |
| stopMouseTracking() |
| } |
| } |
| |
| async function onGotRemoteStream(e) { |
| console.log('onGotRemoteStream ' + e); |
| |
| const track = e.track; |
| |
| console.log('track = ' + track); |
| console.log('track.kind = ' + track.kind); |
| console.log('track.readyState = ' + track.readyState); |
| console.log('track.enabled = ' + track.enabled); |
| |
| if (track.kind == "video") { |
| videoStream = e.streams[0]; |
| } |
| } |
| |
| function startMouseTracking() { |
| if (window.PointerEvent) { |
| deviceScreen.addEventListener("pointerdown", onStartDrag); |
| deviceScreen.addEventListener("pointermove", onContinueDrag); |
| deviceScreen.addEventListener("pointerup", onEndDrag); |
| } else if (window.TouchEvent) { |
| deviceScreen.addEventListener("touchstart", onStartDrag); |
| deviceScreen.addEventListener("touchmove", onContinueDrag); |
| deviceScreen.addEventListener("touchend", onEndDrag); |
| } else if (window.MouseEvent) { |
| deviceScreen.addEventListener("mousedown", onStartDrag); |
| deviceScreen.addEventListener("mousemove", onContinueDrag); |
| deviceScreen.addEventListener("mouseup", onEndDrag); |
| } |
| } |
| |
| function stopMouseTracking() { |
| if (window.PointerEvent) { |
| deviceScreen.removeEventListener("pointerdown", onStartDrag); |
| deviceScreen.removeEventListener("pointermove", onContinueDrag); |
| deviceScreen.removeEventListener("pointerup", onEndDrag); |
| } else if (window.TouchEvent) { |
| deviceScreen.removeEventListener("touchstart", onStartDrag); |
| deviceScreen.removeEventListener("touchmove", onContinueDrag); |
| deviceScreen.removeEventListener("touchend", onEndDrag); |
| } else if (window.MouseEvent) { |
| deviceScreen.removeEventListener("mousedown", onStartDrag); |
| deviceScreen.removeEventListener("mousemove", onContinueDrag); |
| deviceScreen.removeEventListener("mouseup", onEndDrag); |
| } |
| } |
| |
| function startKeyboardTracking() { |
| document.addEventListener('keydown', onKeyEvent); |
| document.addEventListener('keyup', onKeyEvent); |
| } |
| |
| function stopKeyboardTracking() { |
| document.removeEventListener('keydown', onKeyEvent); |
| document.removeEventListener('keyup', onKeyEvent); |
| } |
| |
| function onStartDrag(e) { |
| e.preventDefault(); |
| |
| // console.log("mousedown at " + e.pageX + " / " + e.pageY); |
| mouseIsDown = true; |
| |
| sendMouseUpdate(true, e); |
| } |
| |
| function onEndDrag(e) { |
| e.preventDefault(); |
| |
| // console.log("mouseup at " + e.pageX + " / " + e.pageY); |
| mouseIsDown = false; |
| |
| sendMouseUpdate(false, e); |
| } |
| |
| function onContinueDrag(e) { |
| e.preventDefault(); |
| |
| // console.log("mousemove at " + e.pageX + " / " + e.pageY + ", down=" + mouseIsDown); |
| if (mouseIsDown) { |
| sendMouseUpdate(true, e); |
| } |
| } |
| |
| function sendMouseUpdate(down, e) { |
| var x = e.offsetX; |
| var y = e.offsetY; |
| |
| const videoWidth = deviceScreen.videoWidth; |
| const videoHeight = deviceScreen.videoHeight; |
| const elementWidth = deviceScreen.width; |
| const elementHeight = deviceScreen.height; |
| |
| // vh*ew > eh*vw? then scale h instead of w |
| const scaleHeight = videoHeight * elementWidth > videoWidth * elementHeight; |
| var elementScaling = 0, videoScaling = 0; |
| if (scaleHeight) { |
| elementScaling = elementHeight; |
| videoScaling = videoHeight; |
| } else { |
| elementScaling = elementWidth; |
| videoScaling = videoWidth; |
| } |
| |
| // Substract the offset produced by the difference in aspect ratio if any. |
| if (scaleHeight) { |
| x -= (elementWidth - elementScaling * videoWidth / videoScaling) / 2; |
| } else { |
| y -= (elementHeight - elementScaling * videoHeight / videoScaling) / 2; |
| } |
| |
| // Convert to coordinates relative to the video |
| x = videoScaling * x / elementScaling; |
| y = videoScaling * y / elementScaling; |
| |
| ws.send('{\r\n' |
| + '"type": "set-mouse-position",\r\n' |
| + '"down": ' + (down ? "1" : "0") + ',\r\n' |
| + '"x": ' + Math.trunc(x) + ',\r\n' |
| + '"y": ' + Math.trunc(y) + '\r\n' |
| + '}'); |
| } |
| |
| function onKeyEvent(e) { |
| e.preventDefault(); |
| ws.send('{"type": "key-event","keycode": "'+e.code+'", "event_type": "'+e.type+'"}'); |
| } |