This repository has been archived by the owner on Dec 20, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathtvremote.js
454 lines (377 loc) · 13.1 KB
/
tvremote.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
'use strict';
// This global var must be injected by the background page when creating this
// window. It will be read by the functions here, but will not be modified.
// See background.js for details.
//
// var TV_OPTS = {};
// Global vars with the current state.
var SOCKET_ID = null;
var SELF_IP = null;
var STATUS = {
'connection_successful': null, // null, true, false.
'access_granted': null, // null, true, false.
'error': null // null, String
};
var RECV_CALLBACK = null;
var IS_ALREADY_HANDLING_CLICK = false;
// error is the error message string.
function STATUS_reset(error) {
STATUS.connection_successful = null;
STATUS.access_granted = null;
STATUS.error = null;
if (error) {
STATUS.error = error;
}
if (RECV_CALLBACK) {
RECV_CALLBACK();
}
}
//////////////////////////////////////////////////////////////////////
// Network-related functions, thin wrappers around Chrome API.
// Creates a new socket and connects to it.
// Chrome requires two API calls, which are encapsulated in here.
function easy_connect(ip, port, success_callback, failure_callback) {
chrome.sockets.tcp.create({
// bufferSize should be a power of two.
// Given the extremely simple protocol implemented here, we can use a
// very small buffer size.
'bufferSize': 256
}, function(createInfo) {
chrome.sockets.tcp.connect(createInfo.socketId,
ip, port, function(result) {
if (result < 0 || chrome.runtime.lastError) {
if (failure_callback) failure_callback(createInfo, result, chrome.runtime.lastError.message);
} else {
if (success_callback) success_callback(createInfo, result);
}
});
});
}
// Chrome API requires an ArrayBuffer. This wrapper function is more lenient
// and accepts either an ArrayBuffer or a TypedArray (such as Uint8Array).
function easy_send(socketId, data, success_callback, failure_callback) {
if (data.buffer) { // If data is a TypedArray or DataView.
data = data.buffer; // Get the ArrayBuffer behind it.
}
chrome.sockets.tcp.send(socketId, data, function(sendInfo) {
if (sendInfo.resultCode < 0 || chrome.runtime.lastError) {
if (failure_callback) failure_callback(sendInfo.resultCode, chrome.runtime.lastError.message);
} else if (sendInfo.resultCode == 0 && sendInfo.bytesSent != data.byteLength) {
if (failure_callback) failure_callback(sendInfo.resultCode, 'Sent only ' + sendInfo.bytesSent + ' of ' + data.length + ' bytes.');
} else {
if (success_callback) success_callback();
}
});
}
//////////////////////////////////////////////////////////////////////
// Convenience functions, just to make the code easier to write without
// worrying about details.
//
// For convenience, only a single socket is kept open.
// Disconnects the current socket.
// Does nothing it there is no active socket.
// Disconnecting DOES NOT change STATUS. It is by design, to let the user see
// the last known status.
function disconnect(callback) {
if (SOCKET_ID !== null) {
// console.log('Disconnecting SOCKET_ID = ' + SOCKET_ID);
chrome.sockets.tcp.close(SOCKET_ID, function() {
// console.log('Disconnected.');
SOCKET_ID = null;
if (callback) callback();
});
} else {
if (callback) callback();
}
}
// Connects the socket.
// Automatically disconnects any previous socket.
function connect(callback, failure_callback) {
disconnect(function() {
// console.log('Starting up connection to ', TV_OPTS.tv_ip, TV_OPTS.tv_port);
easy_connect(TV_OPTS.tv_ip, TV_OPTS.tv_port, function(socketInfo, result) {
SOCKET_ID = socketInfo.socketId;
SELF_IP = socketInfo.localAddress;
STATUS.error = null;
// console.log('Connected. SOCKET_ID = ' + SOCKET_ID + ', SELF_IP = ' + SELF_IP);
if (callback) callback();
}, function(socketInfo, result, message) {
console.error('Connection error: ', result, message);
STATUS_reset('Connection error: ' + message);
if (failure_callback) failure_callback();
});
});
}
// Sends a packet.
function send(data, callback, failure_callback) {
if (SOCKET_ID === null) {
//throw 'Programming error! Socket is not connected!';
// console.log('Not sending because there is no socket.');
if (failure_callback) failure_callback();
return;
}
// console.log('Sending a packet.');
easy_send(SOCKET_ID, data, function() {
// console.log('Sent a packet.');
STATUS.error = null;
if (callback) callback();
}, function(result, message) {
console.error('Send error: ', result, message);
STATUS_reset('Send error: ' + message);
if (failure_callback) failure_callback();
});
}
// Receives a packet.
function on_receive_handler(info) {
if (info.socketId != SOCKET_ID) {
return;
}
var response = unpack_auth_response(info.data);
var auth_response = understand_auth_response(response);
// console.log('Received: ', info.socketId, response.header, response.magic_string, response.payload, auth_response);
if (response.header >= 0 && response.header <= 2) {
STATUS.connection_successful = true;
} else {
STATUS.connection_successful = false;
}
if (auth_response == AuthResponse.GRANTED) {
STATUS.access_granted = true;
} else if (auth_response == AuthResponse.DENIED) {
STATUS.access_granted = false;
}
if (RECV_CALLBACK) {
RECV_CALLBACK();
}
if (auth_response == AuthResponse.DENIED) {
//disconnect();
}
}
function on_receive_error_handler(info) {
if (info.socketId != SOCKET_ID) {
return;
}
if (info.resultCode == -100) {
// NET_CONNECTION_CLOSED, which is not an error.
disconnect();
return;
}
console.error('Error received: ', info.resultCode);
STATUS_reset('Error received: ' + info.resultCode);
disconnect();
}
//////////////////////////////////////////////////////////////////////
// High-level send-key functions.
function send_key(key_code, callback, failure_callback) {
connect(function() {
var auth_data = build_auth_packet(SELF_IP, TV_OPTS.unique_id, TV_OPTS.display_name);
send(auth_data, function() {
var key_packet = build_key_packet(key_code);
send(key_packet, callback, failure_callback);
}, failure_callback);
}, failure_callback);
}
function send_multiple_keys_single_connection(keys, callback) {
// Making a copy of the array, so that this function can make changes to
// the copied array without changing the original one.
var keys = keys.slice();
var failure_callback = callback;
function _send_multiple_keys_recursive() {
if (keys.length < 1) {
if (callback) callback();
return;
}
var key = keys.shift();
var key_packet = build_key_packet(key);
send(key_packet, _send_multiple_keys_recursive, failure_callback);
}
connect(function() {
var auth_data = build_auth_packet(SELF_IP, TV_OPTS.unique_id, TV_OPTS.display_name);
send(auth_data, _send_multiple_keys_recursive, failure_callback);
}, failure_callback);
}
function send_multiple_keys_multiple_connections(keys, callback) {
// Making a copy of the array, so that this function can make changes to
// the copied array without changing the original one.
var keys = keys.slice();
var failure_callback = callback;
function _send_multiple_keys_recursive() {
if (keys.length < 1) {
if (callback) callback();
return;
}
var key = keys.shift();
send_key(key, function() {
setTimeout(_send_multiple_keys_recursive, 50);
}, failure_callback);
}
_send_multiple_keys_recursive();
}
//////////////////////////////////////////////////////////////////////
// Layout stuff.
function create_button_grid_from_layout(layout) {
var section = document.createElement('section');
section.classList.add('tvremote', 'grid');
if (layout.fontSize) {
section.style.fontSize = layout.fontSize;
}
var i, j, cell;
var divrow, span, button;
for (i = 0; i < layout.rows.length; i++) {
divrow = document.createElement('div');
divrow.classList.add('row');
section.appendChild(divrow);
for (j = 0; j < layout.rows[i].cells.length; j++) {
cell = layout.rows[i].cells[j];
if (cell.label === null) {
span = document.createElement('span');
span.classList.add('cell', 'empty');
divrow.appendChild(span);
continue;
}
button = document.createElement('button');
button.appendChild(document.createTextNode(cell.label));
button.dataset.key = cell.key;
button.classList.add('cell', 'tvremotebutton');
divrow.appendChild(button);
if (cell.css) {
button.className += ' ' + cell.css;
}
}
}
return section;
}
function create_svg_file_layout(layout) {
var section = document.createElement('section');
section.classList.add('tvremote', 'svgfile');
// It is necessary to insert the SVG code inline, otherwise it won't be
// possible to handle click events (at least not in a straight-forward
// way).
var request = new XMLHttpRequest();
request.onload = function() {
var raw = this.responseText;
raw = raw.replace(/^\s*<!DOCTYPE[^>]*>/i, '');
section.innerHTML = raw;
};
request.open('GET', layout.file, true);
request.overrideMimeType('text/plain; charset=utf8');
request.send();
return section;
}
function build_layout(layout) {
var layout_container = document.getElementById('layout_container');
// Constructing the buttons.
layout_container.innerHTML = '';
if (layout.layout == 'grid') {
layout_container.appendChild(create_button_grid_from_layout(layout));
} else if (layout.layout == 'svgfile') {
layout_container.appendChild(create_svg_file_layout(layout));
} else {
alert('Invalid value: "layout": "' + layout.layout + '"');
}
}
//////////////////////////////////////////////////////////////////////
function tvremote_key_click_handler(ev) {
if (IS_ALREADY_HANDLING_CLICK) {
// Ideally, the clicks would be queued. But the easiest solution is to
// just ignore them.
return;
}
var key = ev.target.dataset.key;
if (key) {
ev.preventDefault();
ev.stopPropagation();
IS_ALREADY_HANDLING_CLICK = true;
// Splitting the comma-separated list. Also trimming the whitespace.
var keys = key.split(',');
var i;
for (i = 0; i < keys.length; i++) {
keys[i] = keys[i].trim();
}
if (TV_OPTS.macro_behavior === 'multiple_connections') {
send_multiple_keys_multiple_connections(keys, function() {
IS_ALREADY_HANDLING_CLICK = false;
});
} else if (TV_OPTS.macro_behavior === 'single_connection') {
send_multiple_keys_single_connection(keys, function() {
IS_ALREADY_HANDLING_CLICK = false;
});
} else {
STATUS.error = '(Internal error) Unknown macro_behavior value: ' + TV_OPTS.macro_behavior;
console.error(STATUS.error);
update_status_ui();
}
}
}
function close_window() {
window.close();
}
function open_options() {
document.getElementById('options_button').blur();
chrome.runtime.getBackgroundPage(function(background) {
background.open_options_window();
});
}
// Inserts a ZWSP character in sensible places, to make the text look better
// when broken.
// https://en.wikipedia.org/wiki/Zero-width_space
function insert_ZWSP(s) {
return s.replace(/(:+)/g, '$1\u200B').replace(/(_+|\.+)/g, '\u200B$1');
}
function update_status_ui() {
var status_container = document.getElementById('status_container');
var status_label = document.getElementById('status_label');
// var status_icon = document.getElementById('status_icon');
status_container.classList.remove('gray', 'yellow', 'green', 'red');
if (STATUS.error) {
status_container.classList.add('red');
status_label.value = insert_ZWSP(STATUS.error);
} else if (STATUS.connection_successful === true) {
if (STATUS.access_granted === true) {
status_container.classList.add('green');
status_label.value = 'Working correctly.';
} else if (STATUS.access_granted === false) {
status_container.classList.add('red');
status_label.value = 'TV remote control access was DENIED.';
} else {
status_container.classList.add('yellow');
status_label.value = 'Connection to the TV is working.';
}
} else if (STATUS.connection_successful === false) {
status_container.classList.add('red');
status_label.value = 'Connection failed.';
} else {
status_container.classList.add('gray');
status_label.value = 'Not connected.';
}
}
//////////////////////////////////////////////////////////////////////
// Initialization.
function set_class_dont_highlight_focused() {
if (TV_OPTS.highlight_focused) {
document.body.classList.remove('dont_highlight_focused');
} else {
document.body.classList.add('dont_highlight_focused');
}
}
function init(tab_id, bgpage) {
// Single click handler for all TV remote buttons.
var layout_container = document.getElementById('layout_container');
layout_container.addEventListener('click', tvremote_key_click_handler);
set_class_dont_highlight_focused();
build_layout(LAYOUT);
// Status indicator.
RECV_CALLBACK = update_status_ui;
update_status_ui();
// Close button.
var close_button = document.getElementById('close_button');
close_button.addEventListener('click', close_window);
// Options button.
var options_button = document.getElementById('options_button');
options_button.addEventListener('click', open_options);
// Handling TCP responses using the convoluted Chrome API.
chrome.sockets.tcp.onReceive.addListener(on_receive_handler);
chrome.sockets.tcp.onReceiveError.addListener(on_receive_error_handler);
}
// This script is being included with the "defer" attribute, which means it
// will only be executed after the document has been parsed.
init();