ironhide-tv-debug 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/app.js +637 -0
- package/app/index.html +43 -0
- package/app/style.css +200 -0
- package/package.json +31 -0
package/app/app.js
ADDED
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
// IronHide TizenBrew App
|
|
2
|
+
// Native AVPlayer video playback controlled via WebSocket from Mac
|
|
3
|
+
|
|
4
|
+
(function () {
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
// --- Config ---
|
|
8
|
+
var SERVER_IP = '192.168.1.242';
|
|
9
|
+
var SERVER_PORT = 8787;
|
|
10
|
+
var WS_URL = 'ws://' + SERVER_IP + ':' + SERVER_PORT;
|
|
11
|
+
var RECONNECT_DELAY = 3000;
|
|
12
|
+
var OSD_TIMEOUT = 5000;
|
|
13
|
+
var SEEK_STEP = 10000; // 10s in ms
|
|
14
|
+
|
|
15
|
+
// --- DOM ---
|
|
16
|
+
var osd = document.getElementById('osd');
|
|
17
|
+
var osdTitle = document.getElementById('osd-title');
|
|
18
|
+
var statusIcon = document.getElementById('status-icon');
|
|
19
|
+
var seekIndicator = document.getElementById('seek-indicator');
|
|
20
|
+
var timeCurrent = document.getElementById('time-current');
|
|
21
|
+
var timeDuration = document.getElementById('time-duration');
|
|
22
|
+
var progressFill = document.getElementById('progress-fill');
|
|
23
|
+
var connectScreen = document.getElementById('connect-screen');
|
|
24
|
+
var connectStatus = document.getElementById('connect-status');
|
|
25
|
+
var connectSpinner = document.getElementById('connect-spinner');
|
|
26
|
+
var debugLog = document.getElementById('debug-log');
|
|
27
|
+
var debugLines = [];
|
|
28
|
+
|
|
29
|
+
function dbg(msg) {
|
|
30
|
+
var ts = new Date().toLocaleTimeString();
|
|
31
|
+
debugLines.push(ts + ' ' + msg);
|
|
32
|
+
if (debugLines.length > 15) debugLines.shift();
|
|
33
|
+
if (debugLog) debugLog.innerHTML = debugLines.join('<br>');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// --- State ---
|
|
37
|
+
var ws = null;
|
|
38
|
+
var currentTitle = '';
|
|
39
|
+
var duration = 0; // ms
|
|
40
|
+
var position = 0; // ms
|
|
41
|
+
var playerState = 'NONE'; // NONE, IDLE, READY, PLAYING, PAUSED
|
|
42
|
+
var osdTimer = null;
|
|
43
|
+
var positionInterval = null;
|
|
44
|
+
var seekAccumulator = 0;
|
|
45
|
+
var seekTimeout = null;
|
|
46
|
+
var isBuffering = false;
|
|
47
|
+
|
|
48
|
+
// --- Register remote keys ---
|
|
49
|
+
function registerKeys() {
|
|
50
|
+
var keys = [
|
|
51
|
+
'MediaPlay', 'MediaPause', 'MediaPlayPause',
|
|
52
|
+
'MediaStop', 'MediaFastForward', 'MediaRewind',
|
|
53
|
+
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'
|
|
54
|
+
];
|
|
55
|
+
for (var i = 0; i < keys.length; i++) {
|
|
56
|
+
try { tizen.tvinputdevice.registerKey(keys[i]); } catch (e) {}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// --- Time formatting ---
|
|
61
|
+
function msToHMS(ms) {
|
|
62
|
+
var totalSec = Math.floor(ms / 1000);
|
|
63
|
+
var h = Math.floor(totalSec / 3600);
|
|
64
|
+
var m = Math.floor((totalSec % 3600) / 60);
|
|
65
|
+
var s = totalSec % 60;
|
|
66
|
+
return h + ':' + (m < 10 ? '0' : '') + m + ':' + (s < 10 ? '0' : '') + s;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// --- OSD ---
|
|
70
|
+
function showOSD(timeout) {
|
|
71
|
+
osd.classList.add('visible');
|
|
72
|
+
clearTimeout(osdTimer);
|
|
73
|
+
if (timeout !== false) {
|
|
74
|
+
osdTimer = setTimeout(function () {
|
|
75
|
+
if (playerState === 'PLAYING' && !isBuffering) {
|
|
76
|
+
osd.classList.remove('visible');
|
|
77
|
+
}
|
|
78
|
+
}, timeout || OSD_TIMEOUT);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function hideOSD() {
|
|
83
|
+
osd.classList.remove('visible');
|
|
84
|
+
clearTimeout(osdTimer);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function updateProgress() {
|
|
88
|
+
timeCurrent.textContent = msToHMS(position);
|
|
89
|
+
timeDuration.textContent = msToHMS(duration);
|
|
90
|
+
var pct = duration > 0 ? (position / duration) * 100 : 0;
|
|
91
|
+
progressFill.style.width = pct + '%';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function showStatus(icon, persistent) {
|
|
95
|
+
statusIcon.textContent = icon;
|
|
96
|
+
if (!persistent) {
|
|
97
|
+
setTimeout(function () { statusIcon.textContent = ''; }, 1500);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// --- AVPlayer ---
|
|
102
|
+
function playStream(url, title) {
|
|
103
|
+
dbg('playStream: ' + (title || 'no title'));
|
|
104
|
+
dbg('URL: ' + (url || 'NONE').slice(0, 100));
|
|
105
|
+
currentTitle = title || '';
|
|
106
|
+
osdTitle.textContent = currentTitle;
|
|
107
|
+
position = 0;
|
|
108
|
+
duration = 0;
|
|
109
|
+
seekAccumulator = 0;
|
|
110
|
+
|
|
111
|
+
// Close any existing playback
|
|
112
|
+
try { webapis.avplay.stop(); } catch (e) {}
|
|
113
|
+
try { webapis.avplay.close(); } catch (e) {}
|
|
114
|
+
|
|
115
|
+
connectScreen.style.display = 'none';
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
dbg('avplay.open...');
|
|
119
|
+
webapis.avplay.open(url);
|
|
120
|
+
dbg('avplay.open OK');
|
|
121
|
+
webapis.avplay.setDisplayRect(0, 0, 1920, 1080);
|
|
122
|
+
|
|
123
|
+
webapis.avplay.setListener({
|
|
124
|
+
onbufferingstart: function () {
|
|
125
|
+
dbg('buffering start');
|
|
126
|
+
isBuffering = true;
|
|
127
|
+
showStatus('', true);
|
|
128
|
+
statusIcon.innerHTML = '<div class="buffering-spinner"></div>';
|
|
129
|
+
showOSD(false);
|
|
130
|
+
sendEvent('buffering_start');
|
|
131
|
+
},
|
|
132
|
+
onbufferingprogress: function (pct) {
|
|
133
|
+
dbg('buffering ' + pct + '%');
|
|
134
|
+
sendEvent('buffering_progress', { percent: pct });
|
|
135
|
+
},
|
|
136
|
+
onbufferingcomplete: function () {
|
|
137
|
+
dbg('buffering complete');
|
|
138
|
+
isBuffering = false;
|
|
139
|
+
statusIcon.innerHTML = '';
|
|
140
|
+
showOSD(OSD_TIMEOUT);
|
|
141
|
+
sendEvent('buffering_complete');
|
|
142
|
+
},
|
|
143
|
+
oncurrentplaytime: function (ms) {
|
|
144
|
+
position = ms;
|
|
145
|
+
updateProgress();
|
|
146
|
+
},
|
|
147
|
+
onstreamcompleted: function () {
|
|
148
|
+
dbg('stream completed');
|
|
149
|
+
playerState = 'IDLE';
|
|
150
|
+
stopPositionPolling();
|
|
151
|
+
showOSD(false);
|
|
152
|
+
showStatus('', true);
|
|
153
|
+
sendEvent('completed');
|
|
154
|
+
},
|
|
155
|
+
onevent: function (type, data) {
|
|
156
|
+
dbg('avplay event: ' + type);
|
|
157
|
+
sendEvent('avplay_event', { type: type, data: data });
|
|
158
|
+
},
|
|
159
|
+
onerror: function (type) {
|
|
160
|
+
dbg('avplay ERROR: ' + type);
|
|
161
|
+
playerState = 'IDLE';
|
|
162
|
+
stopPositionPolling();
|
|
163
|
+
sendEvent('error', { type: type });
|
|
164
|
+
showStatus('Error', true);
|
|
165
|
+
},
|
|
166
|
+
onsubtitlechange: function () {},
|
|
167
|
+
ondrmevent: function () {}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Try to enable 4K if available
|
|
171
|
+
try { webapis.avplay.setStreamingProperty('SET_MODE_4K', 'true'); } catch (e) {}
|
|
172
|
+
|
|
173
|
+
isBuffering = true;
|
|
174
|
+
statusIcon.innerHTML = '<div class="buffering-spinner"></div>';
|
|
175
|
+
showOSD(false);
|
|
176
|
+
|
|
177
|
+
dbg('prepareAsync...');
|
|
178
|
+
webapis.avplay.prepareAsync(
|
|
179
|
+
function () {
|
|
180
|
+
duration = webapis.avplay.getDuration();
|
|
181
|
+
dbg('prepared OK, dur=' + duration + 'ms');
|
|
182
|
+
updateProgress();
|
|
183
|
+
webapis.avplay.play();
|
|
184
|
+
playerState = 'PLAYING';
|
|
185
|
+
dbg('play() called, state=PLAYING');
|
|
186
|
+
isBuffering = false;
|
|
187
|
+
statusIcon.innerHTML = '';
|
|
188
|
+
showOSD(OSD_TIMEOUT);
|
|
189
|
+
startPositionPolling();
|
|
190
|
+
sendEvent('playing', { duration: duration });
|
|
191
|
+
},
|
|
192
|
+
function (err) {
|
|
193
|
+
dbg('prepareAsync FAILED: ' + String(err));
|
|
194
|
+
sendEvent('error', { type: 'prepare_failed', detail: String(err) });
|
|
195
|
+
showStatus('Error', true);
|
|
196
|
+
}
|
|
197
|
+
);
|
|
198
|
+
} catch (err) {
|
|
199
|
+
dbg('open FAILED: ' + String(err));
|
|
200
|
+
sendEvent('error', { type: 'open_failed', detail: String(err) });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function resumeAt(url, title, positionMs) {
|
|
205
|
+
// Play then seek to position
|
|
206
|
+
currentTitle = title || '';
|
|
207
|
+
osdTitle.textContent = currentTitle;
|
|
208
|
+
|
|
209
|
+
var originalOnPlay = null;
|
|
210
|
+
|
|
211
|
+
try { webapis.avplay.stop(); } catch (e) {}
|
|
212
|
+
try { webapis.avplay.close(); } catch (e) {}
|
|
213
|
+
|
|
214
|
+
connectScreen.style.display = 'none';
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
webapis.avplay.open(url);
|
|
218
|
+
webapis.avplay.setDisplayRect(0, 0, 1920, 1080);
|
|
219
|
+
|
|
220
|
+
webapis.avplay.setListener({
|
|
221
|
+
onbufferingstart: function () {
|
|
222
|
+
isBuffering = true;
|
|
223
|
+
statusIcon.innerHTML = '<div class="buffering-spinner"></div>';
|
|
224
|
+
showOSD(false);
|
|
225
|
+
},
|
|
226
|
+
onbufferingprogress: function () {},
|
|
227
|
+
onbufferingcomplete: function () {
|
|
228
|
+
isBuffering = false;
|
|
229
|
+
statusIcon.innerHTML = '';
|
|
230
|
+
showOSD(OSD_TIMEOUT);
|
|
231
|
+
},
|
|
232
|
+
oncurrentplaytime: function (ms) {
|
|
233
|
+
position = ms;
|
|
234
|
+
updateProgress();
|
|
235
|
+
},
|
|
236
|
+
onstreamcompleted: function () {
|
|
237
|
+
playerState = 'IDLE';
|
|
238
|
+
stopPositionPolling();
|
|
239
|
+
sendEvent('completed');
|
|
240
|
+
},
|
|
241
|
+
onevent: function () {},
|
|
242
|
+
onerror: function (type) {
|
|
243
|
+
playerState = 'IDLE';
|
|
244
|
+
stopPositionPolling();
|
|
245
|
+
sendEvent('error', { type: type });
|
|
246
|
+
},
|
|
247
|
+
onsubtitlechange: function () {},
|
|
248
|
+
ondrmevent: function () {}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
try { webapis.avplay.setStreamingProperty('SET_MODE_4K', 'true'); } catch (e) {}
|
|
252
|
+
|
|
253
|
+
isBuffering = true;
|
|
254
|
+
statusIcon.innerHTML = '<div class="buffering-spinner"></div>';
|
|
255
|
+
showOSD(false);
|
|
256
|
+
|
|
257
|
+
webapis.avplay.prepareAsync(
|
|
258
|
+
function () {
|
|
259
|
+
duration = webapis.avplay.getDuration();
|
|
260
|
+
updateProgress();
|
|
261
|
+
webapis.avplay.play();
|
|
262
|
+
playerState = 'PLAYING';
|
|
263
|
+
|
|
264
|
+
// Seek to saved position
|
|
265
|
+
if (positionMs > 0) {
|
|
266
|
+
webapis.avplay.seekTo(positionMs,
|
|
267
|
+
function () {
|
|
268
|
+
position = positionMs;
|
|
269
|
+
updateProgress();
|
|
270
|
+
isBuffering = false;
|
|
271
|
+
statusIcon.innerHTML = '';
|
|
272
|
+
showOSD(OSD_TIMEOUT);
|
|
273
|
+
startPositionPolling();
|
|
274
|
+
sendEvent('playing', { duration: duration, resumed_at: positionMs });
|
|
275
|
+
},
|
|
276
|
+
function () {
|
|
277
|
+
// Seek failed, just play from start
|
|
278
|
+
isBuffering = false;
|
|
279
|
+
statusIcon.innerHTML = '';
|
|
280
|
+
showOSD(OSD_TIMEOUT);
|
|
281
|
+
startPositionPolling();
|
|
282
|
+
sendEvent('playing', { duration: duration });
|
|
283
|
+
}
|
|
284
|
+
);
|
|
285
|
+
} else {
|
|
286
|
+
isBuffering = false;
|
|
287
|
+
statusIcon.innerHTML = '';
|
|
288
|
+
showOSD(OSD_TIMEOUT);
|
|
289
|
+
startPositionPolling();
|
|
290
|
+
sendEvent('playing', { duration: duration });
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
function (err) {
|
|
294
|
+
sendEvent('error', { type: 'prepare_failed', detail: String(err) });
|
|
295
|
+
}
|
|
296
|
+
);
|
|
297
|
+
} catch (err) {
|
|
298
|
+
sendEvent('error', { type: 'open_failed', detail: String(err) });
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function togglePlayPause() {
|
|
303
|
+
try {
|
|
304
|
+
var state = webapis.avplay.getState();
|
|
305
|
+
if (state === 'PLAYING') {
|
|
306
|
+
webapis.avplay.pause();
|
|
307
|
+
playerState = 'PAUSED';
|
|
308
|
+
showStatus('\u23F8', false);
|
|
309
|
+
showOSD(false); // Keep OSD while paused
|
|
310
|
+
sendEvent('paused', { position: position });
|
|
311
|
+
} else if (state === 'PAUSED') {
|
|
312
|
+
webapis.avplay.play();
|
|
313
|
+
playerState = 'PLAYING';
|
|
314
|
+
showStatus('\u25B6', false);
|
|
315
|
+
showOSD(OSD_TIMEOUT);
|
|
316
|
+
sendEvent('playing', { position: position });
|
|
317
|
+
}
|
|
318
|
+
} catch (e) {}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function seekRelative(deltaMs) {
|
|
322
|
+
// Accumulate rapid seeks
|
|
323
|
+
seekAccumulator += deltaMs;
|
|
324
|
+
clearTimeout(seekTimeout);
|
|
325
|
+
|
|
326
|
+
// Show seek indicator
|
|
327
|
+
var label = seekAccumulator > 0
|
|
328
|
+
? '+' + Math.round(seekAccumulator / 1000) + 's'
|
|
329
|
+
: Math.round(seekAccumulator / 1000) + 's';
|
|
330
|
+
seekIndicator.textContent = label;
|
|
331
|
+
seekIndicator.classList.add('visible');
|
|
332
|
+
showOSD(false);
|
|
333
|
+
|
|
334
|
+
seekTimeout = setTimeout(function () {
|
|
335
|
+
var target = Math.max(0, Math.min(duration, position + seekAccumulator));
|
|
336
|
+
seekAccumulator = 0;
|
|
337
|
+
seekIndicator.classList.remove('visible');
|
|
338
|
+
|
|
339
|
+
try {
|
|
340
|
+
webapis.avplay.seekTo(target,
|
|
341
|
+
function () {
|
|
342
|
+
position = target;
|
|
343
|
+
updateProgress();
|
|
344
|
+
showOSD(OSD_TIMEOUT);
|
|
345
|
+
sendEvent('seeked', { position: target });
|
|
346
|
+
},
|
|
347
|
+
function () {
|
|
348
|
+
showOSD(OSD_TIMEOUT);
|
|
349
|
+
}
|
|
350
|
+
);
|
|
351
|
+
} catch (e) {}
|
|
352
|
+
}, 500);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function seekAbsolute(ms) {
|
|
356
|
+
var target = Math.max(0, Math.min(duration, ms));
|
|
357
|
+
try {
|
|
358
|
+
webapis.avplay.seekTo(target,
|
|
359
|
+
function () {
|
|
360
|
+
position = target;
|
|
361
|
+
updateProgress();
|
|
362
|
+
showOSD(OSD_TIMEOUT);
|
|
363
|
+
sendEvent('seeked', { position: target });
|
|
364
|
+
},
|
|
365
|
+
function () {}
|
|
366
|
+
);
|
|
367
|
+
} catch (e) {}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function stopPlayback() {
|
|
371
|
+
try {
|
|
372
|
+
webapis.avplay.stop();
|
|
373
|
+
webapis.avplay.close();
|
|
374
|
+
} catch (e) {}
|
|
375
|
+
playerState = 'IDLE';
|
|
376
|
+
stopPositionPolling();
|
|
377
|
+
sendEvent('stopped', { position: position, duration: duration });
|
|
378
|
+
connectScreen.style.display = 'flex';
|
|
379
|
+
connectStatus.textContent = 'Waiting for next stream...';
|
|
380
|
+
connectStatus.className = 'connect-status connected';
|
|
381
|
+
connectSpinner.style.display = 'none';
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function startPositionPolling() {
|
|
385
|
+
stopPositionPolling();
|
|
386
|
+
positionInterval = setInterval(function () {
|
|
387
|
+
try {
|
|
388
|
+
position = webapis.avplay.getCurrentTime();
|
|
389
|
+
// Send position to server every 5s for history tracking
|
|
390
|
+
} catch (e) {}
|
|
391
|
+
}, 1000);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function stopPositionPolling() {
|
|
395
|
+
clearInterval(positionInterval);
|
|
396
|
+
positionInterval = null;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// --- WebSocket ---
|
|
400
|
+
function connect() {
|
|
401
|
+
connectStatus.textContent = 'Connecting to ' + SERVER_IP + '...';
|
|
402
|
+
connectStatus.className = 'connect-status';
|
|
403
|
+
connectSpinner.style.display = 'block';
|
|
404
|
+
|
|
405
|
+
try {
|
|
406
|
+
ws = new WebSocket(WS_URL);
|
|
407
|
+
} catch (e) {
|
|
408
|
+
connectStatus.textContent = 'WebSocket not available';
|
|
409
|
+
connectStatus.className = 'connect-status error';
|
|
410
|
+
setTimeout(connect, RECONNECT_DELAY);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
ws.onopen = function () {
|
|
415
|
+
dbg('WS connected to ' + SERVER_IP);
|
|
416
|
+
connectStatus.textContent = 'Connected to IronHide';
|
|
417
|
+
connectStatus.className = 'connect-status connected';
|
|
418
|
+
connectSpinner.style.display = 'none';
|
|
419
|
+
sendEvent('ready');
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
ws.onmessage = function (e) {
|
|
423
|
+
var cmd;
|
|
424
|
+
try { cmd = JSON.parse(e.data); } catch (err) { dbg('bad JSON: ' + e.data.slice(0, 50)); return; }
|
|
425
|
+
handleCommand(cmd);
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
ws.onclose = function () {
|
|
429
|
+
ws = null;
|
|
430
|
+
if (playerState === 'NONE' || playerState === 'IDLE') {
|
|
431
|
+
connectScreen.style.display = 'flex';
|
|
432
|
+
connectStatus.textContent = 'Disconnected. Reconnecting...';
|
|
433
|
+
connectStatus.className = 'connect-status error';
|
|
434
|
+
connectSpinner.style.display = 'block';
|
|
435
|
+
}
|
|
436
|
+
setTimeout(connect, RECONNECT_DELAY);
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
ws.onerror = function () {
|
|
440
|
+
// onclose will fire after this
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function handleCommand(cmd) {
|
|
445
|
+
dbg('CMD: ' + cmd.action + (cmd.url ? ' url=' + cmd.url.slice(0, 80) + '...' : ''));
|
|
446
|
+
switch (cmd.action) {
|
|
447
|
+
case 'play':
|
|
448
|
+
if (cmd.resumeFrom && cmd.resumeFrom > 0) {
|
|
449
|
+
resumeAt(cmd.url, cmd.title, cmd.resumeFrom);
|
|
450
|
+
} else {
|
|
451
|
+
playStream(cmd.url, cmd.title);
|
|
452
|
+
}
|
|
453
|
+
break;
|
|
454
|
+
|
|
455
|
+
case 'pause':
|
|
456
|
+
try {
|
|
457
|
+
webapis.avplay.pause();
|
|
458
|
+
playerState = 'PAUSED';
|
|
459
|
+
showStatus('\u23F8', false);
|
|
460
|
+
showOSD(false);
|
|
461
|
+
sendEvent('paused', { position: position });
|
|
462
|
+
} catch (e) {}
|
|
463
|
+
break;
|
|
464
|
+
|
|
465
|
+
case 'resume':
|
|
466
|
+
try {
|
|
467
|
+
webapis.avplay.play();
|
|
468
|
+
playerState = 'PLAYING';
|
|
469
|
+
showStatus('\u25B6', false);
|
|
470
|
+
showOSD(OSD_TIMEOUT);
|
|
471
|
+
sendEvent('playing', { position: position });
|
|
472
|
+
} catch (e) {}
|
|
473
|
+
break;
|
|
474
|
+
|
|
475
|
+
case 'stop':
|
|
476
|
+
stopPlayback();
|
|
477
|
+
break;
|
|
478
|
+
|
|
479
|
+
case 'seek':
|
|
480
|
+
if (cmd.relative) {
|
|
481
|
+
seekRelative(cmd.ms);
|
|
482
|
+
} else {
|
|
483
|
+
seekAbsolute(cmd.ms);
|
|
484
|
+
}
|
|
485
|
+
break;
|
|
486
|
+
|
|
487
|
+
case 'status':
|
|
488
|
+
sendStatus();
|
|
489
|
+
break;
|
|
490
|
+
|
|
491
|
+
case 'get_position':
|
|
492
|
+
sendMsg({
|
|
493
|
+
type: 'position',
|
|
494
|
+
position: position,
|
|
495
|
+
duration: duration,
|
|
496
|
+
state: playerState
|
|
497
|
+
});
|
|
498
|
+
break;
|
|
499
|
+
|
|
500
|
+
default:
|
|
501
|
+
sendEvent('unknown_command', { action: cmd.action });
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function sendMsg(obj) {
|
|
506
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
507
|
+
ws.send(JSON.stringify(obj));
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function sendEvent(event, detail) {
|
|
512
|
+
sendMsg({ type: 'event', event: event, detail: detail || {} });
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function sendStatus() {
|
|
516
|
+
var state = 'IDLE';
|
|
517
|
+
try { state = webapis.avplay.getState(); } catch (e) {}
|
|
518
|
+
sendMsg({
|
|
519
|
+
type: 'status',
|
|
520
|
+
state: state,
|
|
521
|
+
position: position,
|
|
522
|
+
duration: duration,
|
|
523
|
+
title: currentTitle,
|
|
524
|
+
isBuffering: isBuffering
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// --- Position reporting (for watch history) ---
|
|
529
|
+
setInterval(function () {
|
|
530
|
+
if (playerState === 'PLAYING' || playerState === 'PAUSED') {
|
|
531
|
+
sendMsg({
|
|
532
|
+
type: 'position',
|
|
533
|
+
position: position,
|
|
534
|
+
duration: duration,
|
|
535
|
+
state: playerState
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
}, 5000);
|
|
539
|
+
|
|
540
|
+
// --- Remote control ---
|
|
541
|
+
document.addEventListener('keydown', function (e) {
|
|
542
|
+
var code = e.keyCode;
|
|
543
|
+
|
|
544
|
+
// Always show OSD on any key press during playback
|
|
545
|
+
if (playerState === 'PLAYING' || playerState === 'PAUSED') {
|
|
546
|
+
showOSD(OSD_TIMEOUT);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
switch (code) {
|
|
550
|
+
// Play
|
|
551
|
+
case 415:
|
|
552
|
+
if (playerState === 'PAUSED') {
|
|
553
|
+
try { webapis.avplay.play(); } catch (ex) {}
|
|
554
|
+
playerState = 'PLAYING';
|
|
555
|
+
showStatus('\u25B6', false);
|
|
556
|
+
showOSD(OSD_TIMEOUT);
|
|
557
|
+
sendEvent('playing', { position: position });
|
|
558
|
+
}
|
|
559
|
+
break;
|
|
560
|
+
|
|
561
|
+
// Pause
|
|
562
|
+
case 19:
|
|
563
|
+
if (playerState === 'PLAYING') {
|
|
564
|
+
try { webapis.avplay.pause(); } catch (ex) {}
|
|
565
|
+
playerState = 'PAUSED';
|
|
566
|
+
showStatus('\u23F8', false);
|
|
567
|
+
showOSD(false);
|
|
568
|
+
sendEvent('paused', { position: position });
|
|
569
|
+
}
|
|
570
|
+
break;
|
|
571
|
+
|
|
572
|
+
// Play/Pause toggle (center button on some remotes)
|
|
573
|
+
case 10252:
|
|
574
|
+
togglePlayPause();
|
|
575
|
+
break;
|
|
576
|
+
|
|
577
|
+
// Stop
|
|
578
|
+
case 413:
|
|
579
|
+
stopPlayback();
|
|
580
|
+
break;
|
|
581
|
+
|
|
582
|
+
// Fast Forward (right on d-pad also seeks)
|
|
583
|
+
case 417:
|
|
584
|
+
case 39: // Right arrow
|
|
585
|
+
seekRelative(SEEK_STEP);
|
|
586
|
+
break;
|
|
587
|
+
|
|
588
|
+
// Rewind (left on d-pad also seeks)
|
|
589
|
+
case 412:
|
|
590
|
+
case 37: // Left arrow
|
|
591
|
+
seekRelative(-SEEK_STEP);
|
|
592
|
+
break;
|
|
593
|
+
|
|
594
|
+
// Enter/OK — toggle play/pause or show OSD
|
|
595
|
+
case 13:
|
|
596
|
+
if (playerState === 'PLAYING' || playerState === 'PAUSED') {
|
|
597
|
+
togglePlayPause();
|
|
598
|
+
}
|
|
599
|
+
break;
|
|
600
|
+
|
|
601
|
+
// Back/Return — stop playback
|
|
602
|
+
case 10009:
|
|
603
|
+
if (playerState === 'PLAYING' || playerState === 'PAUSED') {
|
|
604
|
+
stopPlayback();
|
|
605
|
+
}
|
|
606
|
+
break;
|
|
607
|
+
|
|
608
|
+
// Number keys 0-9 — jump to percentage of video
|
|
609
|
+
default:
|
|
610
|
+
if (code >= 48 && code <= 57) {
|
|
611
|
+
var pct = (code - 48) * 10; // 0=0%, 1=10%, ..., 9=90%
|
|
612
|
+
if (duration > 0) {
|
|
613
|
+
seekAbsolute(duration * pct / 100);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
break;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
e.preventDefault();
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
// --- Init ---
|
|
623
|
+
function init() {
|
|
624
|
+
dbg('IronHide TV v1.0.1');
|
|
625
|
+
dbg('webapis: ' + (typeof webapis !== 'undefined' ? 'yes' : 'NO'));
|
|
626
|
+
dbg('avplay: ' + (typeof webapis !== 'undefined' && webapis.avplay ? 'yes' : 'NO'));
|
|
627
|
+
try { registerKeys(); } catch (e) { dbg('registerKeys err: ' + e); }
|
|
628
|
+
connect();
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Wait for Tizen APIs to be ready
|
|
632
|
+
if (typeof tizen !== 'undefined') {
|
|
633
|
+
init();
|
|
634
|
+
} else {
|
|
635
|
+
window.addEventListener('load', init);
|
|
636
|
+
}
|
|
637
|
+
})();
|
package/app/index.html
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=1920, initial-scale=1">
|
|
6
|
+
<title>IronHide</title>
|
|
7
|
+
<link rel="stylesheet" href="style.css">
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<!-- AVPlayer object -->
|
|
11
|
+
<object id="av-player" type="application/avplayer"></object>
|
|
12
|
+
|
|
13
|
+
<!-- OSD overlay -->
|
|
14
|
+
<div id="osd">
|
|
15
|
+
<div class="osd-top">
|
|
16
|
+
<div class="osd-title" id="osd-title"></div>
|
|
17
|
+
</div>
|
|
18
|
+
<div class="osd-center">
|
|
19
|
+
<div class="status-icon" id="status-icon"></div>
|
|
20
|
+
<div class="seek-indicator" id="seek-indicator"></div>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="osd-bottom">
|
|
23
|
+
<div class="osd-time">
|
|
24
|
+
<span id="time-current">0:00:00</span>
|
|
25
|
+
<span id="time-duration">0:00:00</span>
|
|
26
|
+
</div>
|
|
27
|
+
<div class="progress-bar">
|
|
28
|
+
<div class="progress-fill" id="progress-fill"></div>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<!-- Connection screen -->
|
|
34
|
+
<div id="connect-screen">
|
|
35
|
+
<div class="logo">IRONHIDE</div>
|
|
36
|
+
<div class="connect-status" id="connect-status">Connecting to server...</div>
|
|
37
|
+
<div class="spinner" id="connect-spinner"></div>
|
|
38
|
+
<div class="debug-log" id="debug-log"></div>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<script src="app.js"></script>
|
|
42
|
+
</body>
|
|
43
|
+
</html>
|
package/app/style.css
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
* {
|
|
2
|
+
margin: 0;
|
|
3
|
+
padding: 0;
|
|
4
|
+
box-sizing: border-box;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
body {
|
|
8
|
+
background: #000;
|
|
9
|
+
color: #fff;
|
|
10
|
+
font-family: -apple-system, 'SamsungOne', sans-serif;
|
|
11
|
+
overflow: hidden;
|
|
12
|
+
width: 1920px;
|
|
13
|
+
height: 1080px;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/* AVPlayer object fills the screen */
|
|
17
|
+
#av-player {
|
|
18
|
+
position: absolute;
|
|
19
|
+
left: 0;
|
|
20
|
+
top: 0;
|
|
21
|
+
width: 1920px;
|
|
22
|
+
height: 1080px;
|
|
23
|
+
z-index: 1;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* OSD overlay - fades in/out */
|
|
27
|
+
#osd {
|
|
28
|
+
position: absolute;
|
|
29
|
+
left: 0;
|
|
30
|
+
top: 0;
|
|
31
|
+
width: 1920px;
|
|
32
|
+
height: 1080px;
|
|
33
|
+
z-index: 100;
|
|
34
|
+
pointer-events: none;
|
|
35
|
+
opacity: 0;
|
|
36
|
+
transition: opacity 0.3s ease;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
#osd.visible {
|
|
40
|
+
opacity: 1;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* Top bar: title */
|
|
44
|
+
.osd-top {
|
|
45
|
+
position: absolute;
|
|
46
|
+
top: 0;
|
|
47
|
+
left: 0;
|
|
48
|
+
right: 0;
|
|
49
|
+
padding: 40px 60px;
|
|
50
|
+
background: linear-gradient(to bottom, rgba(0,0,0,0.8) 0%, transparent 100%);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.osd-title {
|
|
54
|
+
font-size: 42px;
|
|
55
|
+
font-weight: 600;
|
|
56
|
+
text-shadow: 0 2px 8px rgba(0,0,0,0.8);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* Bottom bar: progress + time */
|
|
60
|
+
.osd-bottom {
|
|
61
|
+
position: absolute;
|
|
62
|
+
bottom: 0;
|
|
63
|
+
left: 0;
|
|
64
|
+
right: 0;
|
|
65
|
+
padding: 40px 60px 50px;
|
|
66
|
+
background: linear-gradient(to top, rgba(0,0,0,0.85) 0%, transparent 100%);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.osd-time {
|
|
70
|
+
display: flex;
|
|
71
|
+
justify-content: space-between;
|
|
72
|
+
font-size: 28px;
|
|
73
|
+
margin-bottom: 16px;
|
|
74
|
+
font-variant-numeric: tabular-nums;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.progress-bar {
|
|
78
|
+
width: 100%;
|
|
79
|
+
height: 8px;
|
|
80
|
+
background: rgba(255,255,255,0.2);
|
|
81
|
+
border-radius: 4px;
|
|
82
|
+
overflow: hidden;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.progress-bar:hover,
|
|
86
|
+
.progress-bar.seeking {
|
|
87
|
+
height: 12px;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.progress-fill {
|
|
91
|
+
height: 100%;
|
|
92
|
+
background: #e50914;
|
|
93
|
+
border-radius: 4px;
|
|
94
|
+
transition: width 0.3s linear;
|
|
95
|
+
min-width: 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* Center status (buffering, paused, etc) */
|
|
99
|
+
.osd-center {
|
|
100
|
+
position: absolute;
|
|
101
|
+
top: 50%;
|
|
102
|
+
left: 50%;
|
|
103
|
+
transform: translate(-50%, -50%);
|
|
104
|
+
text-align: center;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.status-icon {
|
|
108
|
+
font-size: 80px;
|
|
109
|
+
text-shadow: 0 4px 16px rgba(0,0,0,0.6);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* Seek indicator */
|
|
113
|
+
.seek-indicator {
|
|
114
|
+
font-size: 48px;
|
|
115
|
+
font-weight: 600;
|
|
116
|
+
text-shadow: 0 2px 8px rgba(0,0,0,0.8);
|
|
117
|
+
opacity: 0;
|
|
118
|
+
transition: opacity 0.2s;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.seek-indicator.visible {
|
|
122
|
+
opacity: 1;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* Connection screen */
|
|
126
|
+
#connect-screen {
|
|
127
|
+
position: absolute;
|
|
128
|
+
left: 0;
|
|
129
|
+
top: 0;
|
|
130
|
+
width: 1920px;
|
|
131
|
+
height: 1080px;
|
|
132
|
+
z-index: 200;
|
|
133
|
+
display: flex;
|
|
134
|
+
flex-direction: column;
|
|
135
|
+
align-items: center;
|
|
136
|
+
justify-content: center;
|
|
137
|
+
background: #0a0a0a;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.logo {
|
|
141
|
+
font-size: 72px;
|
|
142
|
+
font-weight: 700;
|
|
143
|
+
letter-spacing: -2px;
|
|
144
|
+
margin-bottom: 20px;
|
|
145
|
+
color: #e50914;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.connect-status {
|
|
149
|
+
font-size: 28px;
|
|
150
|
+
color: #888;
|
|
151
|
+
margin-top: 16px;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.connect-status.connected {
|
|
155
|
+
color: #4caf50;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.connect-status.error {
|
|
159
|
+
color: #f44336;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.debug-log {
|
|
163
|
+
margin-top: 30px;
|
|
164
|
+
width: 1400px;
|
|
165
|
+
max-height: 400px;
|
|
166
|
+
overflow-y: auto;
|
|
167
|
+
text-align: left;
|
|
168
|
+
font-family: monospace;
|
|
169
|
+
font-size: 20px;
|
|
170
|
+
color: #aaa;
|
|
171
|
+
line-height: 1.4;
|
|
172
|
+
padding: 16px;
|
|
173
|
+
background: rgba(255,255,255,0.05);
|
|
174
|
+
border-radius: 8px;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/* Spinner */
|
|
178
|
+
@keyframes spin {
|
|
179
|
+
to { transform: rotate(360deg); }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.spinner {
|
|
183
|
+
width: 48px;
|
|
184
|
+
height: 48px;
|
|
185
|
+
border: 4px solid rgba(255,255,255,0.1);
|
|
186
|
+
border-top-color: #e50914;
|
|
187
|
+
border-radius: 50%;
|
|
188
|
+
animation: spin 1s linear infinite;
|
|
189
|
+
margin-top: 30px;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/* Buffering spinner overlay */
|
|
193
|
+
.buffering-spinner {
|
|
194
|
+
width: 64px;
|
|
195
|
+
height: 64px;
|
|
196
|
+
border: 4px solid rgba(255,255,255,0.15);
|
|
197
|
+
border-top-color: #fff;
|
|
198
|
+
border-radius: 50%;
|
|
199
|
+
animation: spin 0.8s linear infinite;
|
|
200
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ironhide-tv-debug",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "IronHide native TV player for Samsung Tizen via TizenBrew",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"packageType": "app",
|
|
7
|
+
"appName": "IronHide",
|
|
8
|
+
"appPath": "app/index.html",
|
|
9
|
+
"keys": [
|
|
10
|
+
"MediaPlay",
|
|
11
|
+
"MediaPause",
|
|
12
|
+
"MediaPlayPause",
|
|
13
|
+
"MediaStop",
|
|
14
|
+
"MediaFastForward",
|
|
15
|
+
"MediaRewind",
|
|
16
|
+
"0",
|
|
17
|
+
"1",
|
|
18
|
+
"2",
|
|
19
|
+
"3",
|
|
20
|
+
"4",
|
|
21
|
+
"5",
|
|
22
|
+
"6",
|
|
23
|
+
"7",
|
|
24
|
+
"8",
|
|
25
|
+
"9"
|
|
26
|
+
],
|
|
27
|
+
"files": [
|
|
28
|
+
"app",
|
|
29
|
+
"package.json"
|
|
30
|
+
]
|
|
31
|
+
}
|