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}