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 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
+ }