livefootballtv-tizen 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-es5.js ADDED
@@ -0,0 +1,1976 @@
1
+ // app-es5.js – ES5 Compatible Version for Tizen TV
2
+ // Uses local proxy for merichunidya.com streams (requires proxy running)
3
+ var BASE_URL = "http://api.fksoftapi.com/app2/";
4
+ var API_KEY = "cda11NiPOYcy8KrpZs0w6dD9fbE4JzT32BtIeUQjuARoFxMX1C";
5
+ var AES_KEY = "1g2j4d5rb56s39wc";
6
+ var AES_IV = "g4fst5gpd5f5r7j4";
7
+
8
+ // PROXY CONFIGURATION
9
+ // Set this to your local proxy address (or remote CORS proxy)
10
+ // For testing: run 'node local_proxy.js' and set to 'http://localhost:8888'
11
+ // For production: use a remote CORS proxy or set to '' to disable
12
+ var PROXY_URL = 'http://localhost:8888'; // Change this as needed
13
+
14
+ // Domains that require proxy due to Referer header restrictions
15
+ var PROXY_DOMAINS = ['merichunidya.com', 'vividmosaica.com'];
16
+
17
+ /**
18
+ * Check if a URL needs to go through the proxy
19
+ */
20
+ function needsProxy(url) {
21
+ if (!PROXY_URL || !url) return false;
22
+ for (var i = 0; i < PROXY_DOMAINS.length; i++) {
23
+ if (url.indexOf(PROXY_DOMAINS[i]) > -1) {
24
+ return true;
25
+ }
26
+ }
27
+ return false;
28
+ }
29
+
30
+ /**
31
+ * Wrap a URL with the proxy if needed
32
+ */
33
+ function wrapWithProxy(url) {
34
+ if (!needsProxy(url)) return url;
35
+ console.log('[PROXY] Wrapping URL:', url.substring(0, 60) + '...');
36
+ return PROXY_URL + '/proxy?url=' + encodeURIComponent(url);
37
+ }
38
+
39
+
40
+ // --- Polyfills ---
41
+ if (!window.Promise) {
42
+ window.Promise = function (executor) {
43
+ var self = this;
44
+ self.callbacks = [];
45
+ self.state = 'pending';
46
+ self.value = null;
47
+ function resolve(value) {
48
+ if (self.state !== 'pending') return;
49
+ self.state = 'fulfilled';
50
+ self.value = value;
51
+ self.callbacks.forEach(function (cb) { cb.onResolved(value); });
52
+ }
53
+ function reject(reason) {
54
+ if (self.state !== 'pending') return;
55
+ self.state = 'rejected';
56
+ self.value = reason;
57
+ self.callbacks.forEach(function (cb) { cb.onRejected(reason); });
58
+ }
59
+ try { executor(resolve, reject); } catch (e) { reject(e); }
60
+ };
61
+ window.Promise.prototype.then = function (onResolved, onRejected) {
62
+ var self = this;
63
+ return new Promise(function (resolve, reject) {
64
+ function handle(cb, val) {
65
+ try {
66
+ var res = cb ? cb(val) : val;
67
+ if (res && typeof res.then === 'function') res.then(resolve, reject);
68
+ else resolve(res);
69
+ } catch (e) { reject(e); }
70
+ }
71
+ if (self.state === 'pending') {
72
+ self.callbacks.push({
73
+ onResolved: function (v) { handle(onResolved, v); },
74
+ onRejected: function (r) { handle(onRejected || function (e) { throw e; }, r); }
75
+ });
76
+ } else if (self.state === 'fulfilled') {
77
+ setTimeout(function () { handle(onResolved, self.value); }, 0);
78
+ } else {
79
+ setTimeout(function () { handle(onRejected || function (e) { throw e; }, self.value); }, 0);
80
+ }
81
+ });
82
+ };
83
+ window.Promise.prototype.catch = function (onRejected) {
84
+ return this.then(null, onRejected);
85
+ };
86
+ window.Promise.resolve = function (v) { return new Promise(function (r) { r(v); }); };
87
+ window.Promise.reject = function (v) { return new Promise(function (r, j) { j(v); }); };
88
+ }
89
+
90
+ // --- Platform Detection ---
91
+ function isTizen() {
92
+ return typeof tizen !== 'undefined' || (navigator.userAgent && navigator.userAgent.indexOf('Tizen') > -1);
93
+ }
94
+
95
+ // --- Log Level Setting (0=none, 1=error, 2=full) ---
96
+ var logLevel = parseInt(localStorage.getItem('logLevel') || '0', 10); // Default: none
97
+
98
+ // Create debug overlay (hidden by default)
99
+ var debugEl = document.createElement('div');
100
+ debugEl.style.cssText = "position:fixed;bottom:0;left:0;width:100%;height:100px;background:rgba(0,0,0,0.7);color:#0f0;font-size:12px;overflow-y:scroll;z-index:9999;pointer-events:none;display:none;";
101
+ document.body.appendChild(debugEl);
102
+
103
+ function log(msg) {
104
+ if (logLevel < 2) return; // Only show on "full" level
105
+ console.log(msg);
106
+ var line = document.createElement('div');
107
+ line.textContent = "> " + msg;
108
+ debugEl.appendChild(line);
109
+ debugEl.scrollTop = debugEl.scrollHeight;
110
+ debugEl.style.display = 'block';
111
+ }
112
+
113
+ function error(msg) {
114
+ if (logLevel < 1) return; // Only show on "error" or "full" level
115
+ console.error(msg);
116
+ var line = document.createElement('div');
117
+ line.style.color = 'red';
118
+ line.textContent = "ERR: " + msg;
119
+ debugEl.appendChild(line);
120
+ debugEl.scrollTop = debugEl.scrollHeight;
121
+ debugEl.style.display = 'block';
122
+ }
123
+
124
+ // User-facing error overlay (dismissible)
125
+ var playerErrorOverlay = null;
126
+
127
+ function showPlayerError(msg) {
128
+ if (!playerErrorOverlay) {
129
+ playerErrorOverlay = document.createElement('div');
130
+ playerErrorOverlay.id = 'playerErrorOverlay';
131
+ playerErrorOverlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.9);display:flex;flex-direction:column;justify-content:center;align-items:center;z-index:5000;';
132
+
133
+ var msgEl = document.createElement('div');
134
+ msgEl.id = 'playerErrorMsg';
135
+ msgEl.style.cssText = 'color:#ff6b6b;font-size:28px;margin-bottom:30px;text-align:center;padding:20px;';
136
+ playerErrorOverlay.appendChild(msgEl);
137
+
138
+ var okBtn = document.createElement('button');
139
+ okBtn.id = 'playerErrorOkBtn';
140
+ okBtn.textContent = 'OK';
141
+ okBtn.tabIndex = 0;
142
+ okBtn.style.cssText = 'font-size:24px;padding:15px 60px;background:#3ea6ff;color:#000;border:none;border-radius:8px;cursor:pointer;';
143
+ okBtn.onfocus = function () { this.style.outline = '4px solid #fff'; };
144
+ okBtn.onblur = function () { this.style.outline = 'none'; };
145
+ okBtn.onclick = function () { hidePlayerError(); };
146
+ playerErrorOverlay.appendChild(okBtn);
147
+
148
+ document.body.appendChild(playerErrorOverlay);
149
+ }
150
+
151
+ var msgEl = document.getElementById('playerErrorMsg');
152
+ if (msgEl) msgEl.textContent = msg;
153
+
154
+ playerErrorOverlay.style.display = 'flex';
155
+
156
+ // Focus OK button for D-pad navigation
157
+ setTimeout(function () {
158
+ var okBtn = document.getElementById('playerErrorOkBtn');
159
+ if (okBtn) okBtn.focus();
160
+ }, 100);
161
+ }
162
+
163
+ function hidePlayerError() {
164
+ if (playerErrorOverlay) {
165
+ playerErrorOverlay.style.display = 'none';
166
+ }
167
+ }
168
+
169
+ function setLogLevel(level) {
170
+ logLevel = level;
171
+ localStorage.setItem('logLevel', String(level));
172
+ debugEl.style.display = level > 0 ? 'block' : 'none';
173
+ if (level === 0) debugEl.innerHTML = '';
174
+ }
175
+
176
+ // --- Settings UI ---
177
+ document.addEventListener('DOMContentLoaded', function () {
178
+ var settingsBtn = document.getElementById('settingsBtn');
179
+ var settingsDropdown = document.getElementById('settingsDropdown');
180
+ var logLevelSelect = document.getElementById('logLevelSelect');
181
+
182
+ if (settingsBtn && settingsDropdown) {
183
+ // Set initial value from localStorage
184
+ if (logLevelSelect) {
185
+ logLevelSelect.value = String(logLevel);
186
+ }
187
+
188
+ // Toggle dropdown on button click
189
+ settingsBtn.onclick = function (e) {
190
+ e.stopPropagation();
191
+ var isOpen = settingsDropdown.style.display === 'block';
192
+ settingsDropdown.style.display = isOpen ? 'none' : 'block';
193
+ };
194
+
195
+ // Handle log level change
196
+ if (logLevelSelect) {
197
+ logLevelSelect.onchange = function () {
198
+ setLogLevel(parseInt(logLevelSelect.value, 10));
199
+ };
200
+ }
201
+
202
+ // Close dropdown when clicking outside
203
+ document.addEventListener('click', function (e) {
204
+ if (!settingsDropdown.contains(e.target) && e.target !== settingsBtn) {
205
+ settingsDropdown.style.display = 'none';
206
+ }
207
+ });
208
+ }
209
+ });
210
+
211
+ // --- App Logic ---
212
+
213
+ function showLoader() {
214
+ var loader = document.getElementById('loader');
215
+ if (loader) {
216
+ loader.hidden = false;
217
+ loader.setAttribute('aria-hidden', 'false');
218
+ }
219
+ }
220
+
221
+ function hideLoader() {
222
+ var loader = document.getElementById('loader');
223
+ if (loader) {
224
+ loader.hidden = true;
225
+ loader.setAttribute('aria-hidden', 'true');
226
+ }
227
+ }
228
+
229
+ function decryptAES(hexString) {
230
+ try {
231
+ var ciphertext = CryptoJS.enc.Hex.parse(hexString);
232
+ var key = CryptoJS.enc.Utf8.parse(AES_KEY);
233
+ var iv = CryptoJS.enc.Utf8.parse(AES_IV);
234
+ var decrypted = CryptoJS.AES.decrypt(
235
+ { ciphertext: ciphertext },
236
+ key,
237
+ { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.NoPadding }
238
+ );
239
+ return decrypted.toString(CryptoJS.enc.Utf8).replace(/\0+$/, '');
240
+ } catch (e) {
241
+ error("Decryption error: " + e.message);
242
+ return null;
243
+ }
244
+ }
245
+
246
+ function decryptUrl(enc) {
247
+ if (enc.indexOf('http://') === 0 || enc.indexOf('https://') === 0) return enc;
248
+ return decryptAES(enc) || enc;
249
+ }
250
+
251
+ // Decrypt binary data (ArrayBuffer) -> String
252
+ function decryptAESBinary(arrayBuffer) {
253
+ try {
254
+ var u8 = new Uint8Array(arrayBuffer);
255
+ var words = [];
256
+ for (var i = 0; i < u8.length; i += 4) {
257
+ words.push(
258
+ (u8[i] << 24) | (u8[i + 1] << 16) | (u8[i + 2] << 8) | (u8[i + 3])
259
+ );
260
+ }
261
+ var ciphertext = CryptoJS.lib.WordArray.create(words, u8.length);
262
+ var key = CryptoJS.enc.Utf8.parse(AES_KEY);
263
+ var iv = CryptoJS.enc.Utf8.parse(AES_IV);
264
+ var decrypted = CryptoJS.AES.decrypt(
265
+ { ciphertext: ciphertext },
266
+ key,
267
+ { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }
268
+ );
269
+ return decrypted.toString(CryptoJS.enc.Utf8);
270
+ } catch (e) {
271
+ error("Binary decryption error: " + e.message);
272
+ return null;
273
+ }
274
+ }
275
+
276
+ // Decrypt video segment (ArrayBuffer) -> ArrayBuffer
277
+ // Used for encrypted ZIP/CSS/HTML files in M3U8 manifests
278
+ function decryptSegmentBinary(arrayBuffer, customKey, customIv) {
279
+ try {
280
+ log("Decrypting segment (" + arrayBuffer.byteLength + " bytes)");
281
+
282
+ var u8 = new Uint8Array(arrayBuffer);
283
+ var words = [];
284
+ for (var i = 0; i < u8.length; i += 4) {
285
+ words.push(
286
+ (u8[i] << 24) | (u8[i + 1] << 16) | (u8[i + 2] << 8) | (u8[i + 3])
287
+ );
288
+ }
289
+ var ciphertext = CryptoJS.lib.WordArray.create(words, u8.length);
290
+
291
+ // Use custom key/iv if provided, otherwise use defaults
292
+ var key = customKey ? customKey : CryptoJS.enc.Utf8.parse(AES_KEY);
293
+ var iv = customIv ? customIv : CryptoJS.enc.Utf8.parse(AES_IV);
294
+
295
+ var decrypted = CryptoJS.AES.decrypt(
296
+ { ciphertext: ciphertext },
297
+ key,
298
+ { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }
299
+ );
300
+
301
+ // Convert WordArray back to ArrayBuffer
302
+ var decryptedWords = decrypted.words;
303
+ var decryptedSigBytes = decrypted.sigBytes;
304
+ var decryptedU8 = new Uint8Array(decryptedSigBytes);
305
+
306
+ for (var i = 0; i < decryptedSigBytes; i++) {
307
+ decryptedU8[i] = (decryptedWords[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
308
+ }
309
+
310
+ log("Decrypted successfully (" + decryptedU8.byteLength + " bytes)");
311
+ return decryptedU8.buffer;
312
+ } catch (e) {
313
+ error("Segment decryption error: " + e.message);
314
+ return null;
315
+ }
316
+ }
317
+
318
+ // Encryption key cache for M3U8 segments
319
+ var encryptionKeyCache = {};
320
+ var currentEncryptionKey = null;
321
+ var currentEncryptionIv = null;
322
+
323
+ // Fetch and cache encryption key from URI
324
+ function fetchEncryptionKey(keyUri, baseUrl) {
325
+ var absoluteUri;
326
+ try {
327
+ absoluteUri = new URL(keyUri, baseUrl).href;
328
+ } catch (e) {
329
+ absoluteUri = keyUri;
330
+ }
331
+
332
+ // Check cache first
333
+ if (encryptionKeyCache[absoluteUri]) {
334
+ log("Using cached encryption key");
335
+ return Promise.resolve(encryptionKeyCache[absoluteUri]);
336
+ }
337
+
338
+ log("Fetching encryption key from: " + absoluteUri);
339
+
340
+ return smartFetch(absoluteUri, { responseType: 'arraybuffer' })
341
+ .then(function (result) {
342
+ // Convert ArrayBuffer to WordArray for CryptoJS
343
+ var keyData = result.data;
344
+ var u8 = new Uint8Array(keyData);
345
+ var words = [];
346
+ for (var i = 0; i < u8.length; i += 4) {
347
+ words.push(
348
+ (u8[i] << 24) | (u8[i + 1] << 16) | (u8[i + 2] << 8) | (u8[i + 3])
349
+ );
350
+ }
351
+ var keyWordArray = CryptoJS.lib.WordArray.create(words, u8.length);
352
+
353
+ encryptionKeyCache[absoluteUri] = keyWordArray;
354
+ log("Encryption key cached (" + u8.length + " bytes)");
355
+ return keyWordArray;
356
+ })
357
+ .catch(function (err) {
358
+ error("Failed to fetch encryption key: " + err.message);
359
+ // Fallback to default AES key
360
+ log("Using default AES key as fallback");
361
+ var fallbackKey = CryptoJS.enc.Utf8.parse(AES_KEY);
362
+ encryptionKeyCache[absoluteUri] = fallbackKey;
363
+ return fallbackKey;
364
+ });
365
+ }
366
+
367
+ var categoryListEl = document.getElementById("categoryList");
368
+ var contentAreaEl = document.getElementById("contentArea");
369
+
370
+ var state = {
371
+ categories: [],
372
+ currentCategoryId: null,
373
+ posts: [],
374
+ page: 1
375
+ };
376
+
377
+ // Hybrid Fetch - returns {text/data, finalUrl} to capture redirects
378
+ // Uses XHR when running from file:// origin (Tizen packaged app) for better compatibility
379
+ function smartFetch(url, options) {
380
+ options = options || {};
381
+ return new Promise(function (resolve, reject) {
382
+ var timeout = 15000;
383
+ var timer = setTimeout(function () {
384
+ reject(new Error("Timeout"));
385
+ }, timeout);
386
+
387
+ // For Tizen apps (file:// or packaged), use XHR which has no CORS restrictions
388
+ var isLocalOrigin = !window.location.origin || window.location.origin === 'null' ||
389
+ window.location.protocol === 'file:';
390
+
391
+ if (window.fetch && !isLocalOrigin) {
392
+ // For JSON/PHP URLs that might return 301 redirects with Location header,
393
+ // we follow redirects but handle errors specially to extract the redirect URL
394
+ var mightRedirect = url.indexOf('.json') > -1 || url.indexOf('.php') > -1;
395
+ var fetchOpts = {
396
+ method: 'GET'
397
+ };
398
+
399
+ // For CDN URLs, try to set appropriate referrer
400
+ // Note: browsers may ignore this for cross-origin requests
401
+ if (url.indexOf('merichunidya.com') > -1 || url.indexOf('techtuner') > -1 || url.indexOf('vividmosaica') > -1) {
402
+ fetchOpts.referrer = 'https://vividmosaica.com/';
403
+ fetchOpts.referrerPolicy = 'no-referrer-when-downgrade';
404
+ }
405
+
406
+ var finalUrl = url;
407
+
408
+ log("smartFetch: " + url);
409
+
410
+ window.fetch(url, fetchOpts)
411
+ .then(function (res) {
412
+ clearTimeout(timer);
413
+
414
+ finalUrl = res.url || url; // Capture final URL after redirects
415
+ log("smartFetch response: status=" + res.status + " finalUrl=" + finalUrl);
416
+
417
+ // Special handling: if we followed a redirect to m3u8 but got 403,
418
+ // return the URL anyway - the player might handle it differently
419
+ if (!res.ok && mightRedirect && finalUrl.indexOf('.m3u8') > -1) {
420
+ log("Got " + res.status + " after redirect to M3U8. Returning URL for player to handle.");
421
+ return { text: '', finalUrl: finalUrl, status: res.status };
422
+ }
423
+
424
+ if (!res.ok) throw new Error("Status " + res.status);
425
+
426
+ if (options.responseType === 'arraybuffer') {
427
+ return res.arrayBuffer().then(function (data) {
428
+ return { data: data, finalUrl: finalUrl };
429
+ });
430
+ }
431
+ return res.text().then(function (text) {
432
+ return { text: text, finalUrl: finalUrl };
433
+ });
434
+ })
435
+ .then(resolve)
436
+ .catch(function (e) {
437
+ clearTimeout(timer);
438
+ log("smartFetch error: " + e.message);
439
+ reject(e);
440
+ });
441
+ } else {
442
+ var xhr = new XMLHttpRequest();
443
+ xhr.open('GET', url, true);
444
+ if (options.responseType) xhr.responseType = options.responseType;
445
+
446
+ // Add headers for CDN URLs that require them
447
+ if (url.indexOf('merichunidya.com') > -1 || url.indexOf('vividmosaica') > -1) {
448
+ try {
449
+ xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
450
+ } catch (e) { /* ignore */ }
451
+ }
452
+
453
+ xhr.onreadystatechange = function () {
454
+ if (xhr.readyState === 4) {
455
+ clearTimeout(timer);
456
+ var finalUrl = xhr.responseURL || url;
457
+
458
+ // Handle successful response
459
+ if (xhr.status >= 200 && xhr.status < 300) {
460
+ if (options.responseType === 'arraybuffer') {
461
+ resolve({ data: xhr.response, finalUrl: finalUrl });
462
+ } else {
463
+ resolve({ text: xhr.response || xhr.responseText, finalUrl: finalUrl });
464
+ }
465
+ }
466
+ // Handle 403 on redirect URLs - still return the final URL for the player to handle
467
+ else if (xhr.status === 403 && finalUrl.indexOf('.m3u8') > -1) {
468
+ log('XHR 403 on M3U8 redirect, returning URL for player');
469
+ resolve({ text: '', finalUrl: finalUrl, status: 403 });
470
+ }
471
+ else {
472
+ reject(new Error('XHR Status ' + xhr.status));
473
+ }
474
+ }
475
+ };
476
+ xhr.onerror = function () {
477
+ clearTimeout(timer);
478
+ reject(new Error('XHR Network Error'));
479
+ };
480
+ xhr.send();
481
+ }
482
+ });
483
+ }
484
+
485
+ function apiFetch(endpoint, params) {
486
+ params = params || {};
487
+ showLoader();
488
+ log("Fetching " + endpoint);
489
+
490
+ var targetUrl = BASE_URL + endpoint + '?api_key=' + API_KEY;
491
+ for (var k in params) {
492
+ if (params.hasOwnProperty(k)) {
493
+ targetUrl += '&' + k + '=' + params[k];
494
+ }
495
+ }
496
+
497
+ return smartFetch(targetUrl)
498
+ .then(function (result) {
499
+ hideLoader();
500
+ return JSON.parse(result.text);
501
+ })
502
+ .catch(function (e) {
503
+ hideLoader();
504
+ error("Fetch failed: " + e.message);
505
+ return Promise.reject(e);
506
+ });
507
+ }
508
+
509
+ function loadCategories() {
510
+ apiFetch("api/get_category_index/").then(function (data) {
511
+ if (data.categories2) {
512
+ var decrypted = decryptAES(data.categories2);
513
+ var parsed = JSON.parse(decrypted);
514
+ state.categories = Array.isArray(parsed) ? parsed : (parsed.categories || []);
515
+ } else if (data.categories) {
516
+ state.categories = data.categories;
517
+ } else {
518
+ state.categories = [];
519
+ }
520
+ log("Loaded " + state.categories.length + " categories");
521
+ renderCategories();
522
+ if (state.categories.length) {
523
+ selectCategory(state.categories[0].cid || state.categories[0].id);
524
+ }
525
+ }).catch(function (err) {
526
+ error("Load categories fatal: " + err.message);
527
+ });
528
+ }
529
+
530
+ function renderCategories() {
531
+ categoryListEl.innerHTML = "";
532
+ state.categories.forEach(function (cat) {
533
+ var div = document.createElement("div");
534
+ div.className = "category";
535
+
536
+ if (cat.category_image) {
537
+ var img = document.createElement("img");
538
+ img.src = cat.category_image;
539
+ div.appendChild(img);
540
+ }
541
+
542
+ var nameSpan = document.createElement("span");
543
+ nameSpan.textContent = cat.category_name || cat.title || cat.name || "Category";
544
+ div.appendChild(nameSpan);
545
+
546
+ var id = cat.cid || cat.id;
547
+ div.dataset.id = id;
548
+ div.tabIndex = 0;
549
+
550
+ div.onclick = function () { selectCategory(id); };
551
+ // Note: Enter key is handled by navigation.js which calls click()
552
+
553
+ categoryListEl.appendChild(div);
554
+ });
555
+ }
556
+
557
+ function selectCategory(catId) {
558
+ state.currentCategoryId = catId;
559
+ state.page = 1;
560
+
561
+ var cats = document.querySelectorAll('.category');
562
+ for (var i = 0; i < cats.length; i++) {
563
+ cats[i].classList.toggle('active', cats[i].dataset.id == catId);
564
+ }
565
+
566
+ if (window.innerWidth <= 768) {
567
+ categoryListEl.classList.add('hidden-mobile');
568
+ contentAreaEl.classList.remove('hidden-mobile');
569
+ }
570
+
571
+ loadPosts();
572
+ }
573
+
574
+ function loadPosts() {
575
+ if (!state.currentCategoryId) return;
576
+
577
+ var params = { id: state.currentCategoryId, page: state.page, count: 20 };
578
+
579
+ apiFetch("api/get_category_posts/", params).then(function (data) {
580
+ if (data.posts2) {
581
+ var decrypted = decryptAES(data.posts2);
582
+ var parsed = JSON.parse(decrypted);
583
+ state.posts = Array.isArray(parsed) ? parsed : (parsed.posts || []);
584
+ } else if (data.posts) {
585
+ state.posts = data.posts;
586
+ } else {
587
+ state.posts = [];
588
+ }
589
+
590
+ renderPosts();
591
+ }).catch(function (err) {
592
+ error("Load posts failed: " + err.message);
593
+ });
594
+ }
595
+
596
+ function renderPosts() {
597
+ contentAreaEl.innerHTML = "";
598
+
599
+ if (window.innerWidth <= 768) {
600
+ var backBtn = document.createElement("button");
601
+ backBtn.textContent = "← Back";
602
+ backBtn.style.cssText = "width:100%;padding:10px;margin-bottom:10px;background:#333;color:#fff;border:none;";
603
+ backBtn.onclick = function () {
604
+ categoryListEl.classList.remove('hidden-mobile');
605
+ contentAreaEl.classList.add('hidden-mobile');
606
+ };
607
+ contentAreaEl.appendChild(backBtn);
608
+ }
609
+
610
+ var gridContainer = document.createElement("div");
611
+ gridContainer.className = "posts-grid";
612
+
613
+ state.posts.forEach(function (post, idx) {
614
+ var div = document.createElement("div");
615
+ div.className = "post";
616
+ div.tabIndex = 0;
617
+
618
+ var thumbContainer = document.createElement("div");
619
+ thumbContainer.className = "thumbnail-container";
620
+
621
+ var img = document.createElement("img");
622
+ var imgSrc = post.channel_image || post.thumbnail || post.image || "";
623
+
624
+ // Lazy Load
625
+ img.setAttribute('data-src', imgSrc);
626
+ img.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"; // Transparent 1x1 GIF
627
+ img.className = "lazy";
628
+ img.onerror = function () { this.src = 'https://via.placeholder.com/320x180?text=No+Image'; };
629
+
630
+ thumbContainer.appendChild(img);
631
+ LazyLoader.observe(img);
632
+
633
+ var info = document.createElement("div");
634
+ info.className = "info";
635
+
636
+ var title = document.createElement("h3");
637
+ title.textContent = post.channel_name || post.title || "Untitled";
638
+
639
+ info.appendChild(title);
640
+
641
+ div.appendChild(thumbContainer);
642
+ div.appendChild(info);
643
+
644
+ div.onclick = function () { openPost(post); };
645
+ // Note: Enter key is handled by navigation.js which calls click()
646
+
647
+ gridContainer.appendChild(div);
648
+ });
649
+
650
+ contentAreaEl.appendChild(gridContainer);
651
+ }
652
+
653
+ var LazyLoader = (function () {
654
+ var observer;
655
+ var isTizenTV = (typeof tizen !== 'undefined') ||
656
+ (navigator.userAgent && navigator.userAgent.indexOf('Tizen') > -1);
657
+
658
+ function init() {
659
+ // Skip IntersectionObserver on Tizen - it's unreliable on older versions
660
+ if (!isTizenTV && 'IntersectionObserver' in window) {
661
+ observer = new IntersectionObserver(function (entries, obs) {
662
+ entries.forEach(function (entry) {
663
+ if (entry.isIntersecting) {
664
+ var img = entry.target;
665
+ var src = img.getAttribute('data-src');
666
+ if (src) {
667
+ img.src = src;
668
+ img.removeAttribute('data-src');
669
+ img.classList.remove('lazy');
670
+ }
671
+ obs.unobserve(img);
672
+ }
673
+ });
674
+ }, { rootMargin: "200px" });
675
+ }
676
+ }
677
+
678
+ init();
679
+
680
+ return {
681
+ observe: function (el) {
682
+ // On Tizen, load images immediately
683
+ if (isTizenTV || !observer) {
684
+ var src = el.getAttribute('data-src');
685
+ if (src) {
686
+ el.src = src;
687
+ el.removeAttribute('data-src');
688
+ el.classList.remove('lazy');
689
+ }
690
+ } else {
691
+ observer.observe(el);
692
+ }
693
+ }
694
+ };
695
+ })();
696
+
697
+ function isTizen() {
698
+ var ua = navigator.userAgent;
699
+ return ua.indexOf('Tizen') > -1 || ua.indexOf('SMART-TV') > -1;
700
+ }
701
+
702
+ // ========== TECHLYHUB MPD EXTRACTION ==========
703
+ // Detect if URL is a techlyhub embed page (uses embedded Shaka Player with MPD)
704
+ function isTechlyEmbed(url) {
705
+ return url && (
706
+ url.indexOf('techlyhub.win') > -1 ||
707
+ url.indexOf('techlyclub.win') > -1 ||
708
+ url.indexOf('techlyhub.xyz') > -1 ||
709
+ url.indexOf('techlyhub.com') > -1
710
+ );
711
+ }
712
+
713
+ // Extract MPD URL and DRM info from techlyhub page content
714
+ function extractMpdFromTechly(htmlContent) {
715
+ var decoded = decodeObfuscatedTechly(htmlContent);
716
+ if (decoded) {
717
+ log("Successfully decoded techlyhub script, length: " + decoded.length);
718
+
719
+ // Look for the MPD URL variable (format: let VAR = "https://...mpd")
720
+ var mpdMatch = decoded.match(/let\s+\w+\s*=\s*"(https?:\/\/[^"]+\.mpd)"/);
721
+ if (mpdMatch) {
722
+ var mpd = mpdMatch[1];
723
+ log("Found MPD URL: " + mpd);
724
+
725
+ // Also look for ClearKey credentials (format: "keyId:key")
726
+ var clearkeyMatch = decoded.match(/let\s+\w+\s*=\s*"([0-9a-f]{32}):([0-9a-f]{32})"/);
727
+ if (clearkeyMatch) {
728
+ log("Found ClearKey DRM credentials");
729
+ return {
730
+ mpd: mpd,
731
+ clearkey: {
732
+ keyId: clearkeyMatch[1],
733
+ key: clearkeyMatch[2]
734
+ }
735
+ };
736
+ }
737
+
738
+ return { mpd: mpd };
739
+ }
740
+
741
+ // Fallback: look for any MPD URL in decoded content
742
+ var anyMpd = decoded.match(/https?:\/\/[^\s"'<>\\]+\.mpd/i);
743
+ if (anyMpd) {
744
+ return { mpd: anyMpd[0] };
745
+ }
746
+ }
747
+
748
+ return null;
749
+ }
750
+
751
+ // Hunter obfuscation decoder (ES5 compatible)
752
+ // Decodes eval(function(h,u,n,t,e,r){...}("encoded",params)) format
753
+ function decodeObfuscatedTechly(htmlContent) {
754
+ try {
755
+ // Extract parameters from the eval pattern
756
+ var match = htmlContent.match(/eval\(function\s*\(\s*h\s*,\s*u\s*,\s*n\s*,\s*t\s*,\s*e\s*,\s*r\s*\)\s*\{[\s\S]*?\}\s*\(\s*"([^"]+)"\s*,\s*(\d+)\s*,\s*"([^"]+)"\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)\)/);
757
+ if (!match) {
758
+ log("No Hunter eval pattern found");
759
+ return null;
760
+ }
761
+
762
+ var h = match[1]; // Encoded string
763
+ var n = match[3]; // Key string (e.g., "GkcTNOQgK")
764
+ var t = parseInt(match[4], 10); // Offset (e.g., 37)
765
+ var e = parseInt(match[5], 10); // Base (e.g., 6)
766
+
767
+ log("Decoding Hunter obfuscation: encoded=" + h.length + " chars, base=" + e);
768
+
769
+ // Character set for base conversion
770
+ var chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ+/';
771
+
772
+ // Base conversion helper
773
+ function baseConvert(d, fromBase, toBase) {
774
+ var fromChars = chars.slice(0, fromBase);
775
+ var toChars = chars.slice(0, toBase);
776
+
777
+ var dArr = d.split('');
778
+ var j = 0;
779
+ for (var i = dArr.length - 1; i >= 0; i--) {
780
+ var pos = fromChars.indexOf(dArr[i]);
781
+ if (pos !== -1) {
782
+ j += pos * Math.pow(fromBase, dArr.length - 1 - i);
783
+ }
784
+ }
785
+
786
+ var k = '';
787
+ while (j > 0) {
788
+ k = toChars.charAt(j % toBase) + k;
789
+ j = Math.floor(j / toBase);
790
+ }
791
+ return k || '0';
792
+ }
793
+
794
+ // Decode the encoded string
795
+ var decoded = '';
796
+ var i = 0;
797
+ while (i < h.length) {
798
+ var s = '';
799
+ while (i < h.length && h.charAt(i) !== n.charAt(e)) {
800
+ s += h.charAt(i);
801
+ i++;
802
+ }
803
+ i++; // Skip the delimiter
804
+
805
+ // Replace key characters with their indices
806
+ for (var j = 0; j < n.length; j++) {
807
+ s = s.split(n.charAt(j)).join(String(j));
808
+ }
809
+
810
+ // Convert from base e to base 10, subtract offset, get character
811
+ var charCode = parseInt(baseConvert(s, e, 10), 10) - t;
812
+ if (charCode > 0 && charCode < 65536) {
813
+ decoded += String.fromCharCode(charCode);
814
+ }
815
+ }
816
+
817
+ return decoded;
818
+ } catch (err) {
819
+ log("Hunter decode error: " + err.message);
820
+ return null;
821
+ }
822
+ }
823
+ // ========== END TECHLYHUB MPD EXTRACTION ==========
824
+
825
+ function resolveStreamUrl(url) {
826
+ // Handle techlyhub embed pages - extract MPD and play directly instead of iframe
827
+ if (isTechlyEmbed(url)) {
828
+ log("Detected techlyhub embed page, fetching to extract MPD...");
829
+ return smartFetch(url)
830
+ .then(function (result) {
831
+ var extracted = extractMpdFromTechly(result.text);
832
+ if (extracted && extracted.mpd) {
833
+ log("Extracted MPD URL from techlyhub: " + extracted.mpd);
834
+
835
+ // Return MPD with optional DRM info
836
+ var response = { url: extracted.mpd };
837
+ if (extracted.clearkey) {
838
+ response.clearkey = extracted.clearkey;
839
+ log("Extracted ClearKey DRM credentials");
840
+ }
841
+ return response;
842
+ }
843
+ // If we can't extract the MPD, mark it so we know in openPost
844
+ log("Could not extract MPD from techlyhub page, falling back to iframe");
845
+ return { url: url, useFallbackIframe: true };
846
+ })
847
+ .catch(function (err) {
848
+ error("Failed to fetch techlyhub page: " + err.message);
849
+ return { url: url, useFallbackIframe: true };
850
+ });
851
+ }
852
+
853
+ if (url.indexOf('.json') > -1 || url.indexOf('.php') > -1 || url.indexOf('.js') > -1) {
854
+ log("Resolving: " + url);
855
+
856
+ return smartFetch(url)
857
+ .then(function (result) {
858
+ var text = result.text;
859
+ var finalUrl = result.finalUrl;
860
+
861
+ // IMPORTANT: If server redirected to M3U8 URL directly, use that!
862
+ // This handles viki.json which returns 301 redirect to the actual stream
863
+ if (finalUrl && finalUrl !== url && finalUrl.indexOf('.m3u8') > -1) {
864
+ log("Server redirected to M3U8: " + finalUrl);
865
+ // Wrap with proxy if needed for domains that require Referer header
866
+ var proxiedUrl = wrapWithProxy(finalUrl);
867
+ return { url: proxiedUrl };
868
+ }
869
+
870
+
871
+ try {
872
+ // Handle JS files that might contain storage.googleapis.com URLs
873
+ if (url.indexOf('.js') > -1) {
874
+ // Match https:// or https:\/\/ and allow backslashes in the path for now
875
+ var match = text.match(/https?:\\?\/\\?\/[a-zA-Z0-9\-\.]*storage\.googleapis\.com[^"'\s]+/);
876
+ if (match) {
877
+ var streamUrl = match[0];
878
+ // Unescape escaped slashes if present
879
+ streamUrl = streamUrl.replace(/\\\//g, '/');
880
+ log("Found Google Storage URL in JS: " + streamUrl);
881
+ return { url: streamUrl };
882
+ }
883
+ }
884
+
885
+ var data = JSON.parse(text);
886
+ var stream = data.url || data.stream || data.file || data.source || data.src || data.link || data.hls;
887
+ if (stream) {
888
+ log("Found stream in JSON: " + stream);
889
+
890
+ if (stream.indexOf('.apk') > -1 || stream.indexOf('.zip') > -1) {
891
+ log("Detected encrypted stream (.apk/.zip). Downloading...");
892
+ var apkUrl = stream;
893
+
894
+ return smartFetch(apkUrl, { responseType: 'arraybuffer' })
895
+ .then(function (bufferResult) {
896
+ var buffer = bufferResult.data;
897
+ log("Downloaded (" + buffer.byteLength + " bytes). Decrypting...");
898
+ var decryptedText = decryptAESBinary(buffer);
899
+ if (!decryptedText) throw new Error("Decryption returned null");
900
+
901
+ log("Decrypted successfully. Rewriting...");
902
+ var rewritten = rewriteM3u8(decryptedText, stream, "");
903
+ return { url: "virtual:apk", content: rewritten };
904
+ });
905
+ }
906
+
907
+ // Wrap with proxy if needed
908
+ return { url: wrapWithProxy(stream) };
909
+ }
910
+ } catch (e) { }
911
+
912
+ if (text.indexOf('#EXTM3U') > -1) {
913
+ log("Found M3U8 text, rewriting...");
914
+ var rewritten = rewriteM3u8(text, url, "");
915
+ return { url: "virtual:m3u8", content: rewritten, originalUrl: url };
916
+ }
917
+ return { url: url };
918
+ })
919
+ .catch(function (err) {
920
+ error("Resolution failed: " + err.message);
921
+ return { url: url };
922
+ });
923
+ }
924
+ return Promise.resolve({ url: url });
925
+ }
926
+
927
+ function rewriteM3u8(text, baseUrl, proxyBase) {
928
+ var lines = text.split('\n');
929
+ var newLines = [];
930
+ var encryptionKeyUri = null;
931
+ var encryptionIv = null;
932
+ var hasExtM3u = false;
933
+
934
+ for (var i = 0; i < lines.length; i++) {
935
+ var line = lines[i].trim();
936
+
937
+ // Check if this is the #EXTM3U line
938
+ if (line.indexOf('#EXTM3U') === 0) {
939
+ hasExtM3u = true;
940
+ newLines.push(line);
941
+ // Add codec hints right after #EXTM3U to help HLS.js know what to expect
942
+ newLines.push('#EXT-X-INDEPENDENT-SEGMENTS');
943
+ continue;
944
+ }
945
+
946
+ if (line && line.indexOf('#') !== 0) {
947
+ try {
948
+ var absoluteUrl = new URL(line, baseUrl).href;
949
+ if (proxyBase) {
950
+ absoluteUrl = proxyBase + encodeURIComponent(absoluteUrl);
951
+ }
952
+ line = absoluteUrl;
953
+ } catch (e) {
954
+ console.warn("URL resolve error", e);
955
+ }
956
+ } else if (line.indexOf('#EXT-X-KEY') === 0) {
957
+ // Extract encryption key info but DON'T add to output manifest
958
+ // This prevents HLS.js from trying its own decryption - we handle it in CustomLoader
959
+ var uriMatch = line.match(/URI="([^"]+)"/);
960
+ if (uriMatch) {
961
+ var keyUri = uriMatch[1];
962
+
963
+ var ivMatch = line.match(/IV=0[xX]([0-9a-fA-F]+)/);
964
+ if (ivMatch && ivMatch[1] !== 'null') {
965
+ encryptionIv = CryptoJS.enc.Hex.parse(ivMatch[1]);
966
+ log("Found encryption IV: " + ivMatch[1]);
967
+ }
968
+
969
+ try {
970
+ var absoluteKey = new URL(keyUri, baseUrl).href;
971
+ if (proxyBase) {
972
+ absoluteKey = proxyBase + encodeURIComponent(absoluteKey);
973
+ }
974
+ encryptionKeyUri = absoluteKey;
975
+
976
+ log("Detected encryption key in manifest: " + absoluteKey);
977
+ fetchEncryptionKey(absoluteKey, baseUrl).then(function (key) {
978
+ currentEncryptionKey = key;
979
+ currentEncryptionIv = encryptionIv;
980
+ log("Encryption key loaded and ready for decryption");
981
+ });
982
+ } catch (e) {
983
+ console.warn("Key resolve error", e);
984
+ }
985
+ }
986
+ // Skip adding this line - don't include #EXT-X-KEY in output
987
+ continue;
988
+ }
989
+ newLines.push(line);
990
+ }
991
+
992
+ return newLines.join('\n');
993
+
994
+ }
995
+
996
+ function CustomLoader(config) {
997
+ // Store the HLS config in the instance
998
+ var hlsConfig = config;
999
+
1000
+ this.load = function (context, loadConfig, callbacks) {
1001
+ var url = context.url;
1002
+
1003
+ // Debug ALL requests
1004
+ log("CustomLoader.load called: " + url + " type=" + context.type);
1005
+
1006
+ // Debug log for virtual URLs
1007
+ if (url.indexOf('virtual:') === 0) {
1008
+ console.log("CustomLoader: Loading virtual URL", url, "Has content:", !!hlsConfig.customContent);
1009
+ }
1010
+
1011
+ // Check for virtual content - use hlsConfig from closure
1012
+ if ((url === "virtual:apk" || url === "virtual:m3u8") && hlsConfig.customContent) {
1013
+ var now = performance.now();
1014
+ var stats = {
1015
+ loading: { start: now, first: now, end: now },
1016
+ parsing: { start: now, end: now },
1017
+ buffering: { start: now, first: now, end: now },
1018
+ trequest: now,
1019
+ tfirst: now,
1020
+ tload: now,
1021
+ loaded: hlsConfig.customContent.length,
1022
+ total: hlsConfig.customContent.length
1023
+ };
1024
+ callbacks.onSuccess({ url: url, data: hlsConfig.customContent }, stats, context);
1025
+ return;
1026
+ }
1027
+
1028
+ var responseType = context.responseType;
1029
+
1030
+ // Check if this is an encrypted segment (ZIP/CSS/HTML/JS file from M3U8)
1031
+ // These are disguised video segments that need decryption
1032
+ var looksEncrypted = url.match(/\.(zip|css|html|js)(\?.*)?$/i);
1033
+ var isNotManifest = context.type !== 'manifest' && context.type !== 'level' && context.type !== 'playlist';
1034
+
1035
+ if (looksEncrypted && isNotManifest) {
1036
+ log("Checking segment: " + url);
1037
+
1038
+ // Force binary response for potential encrypted segments
1039
+ smartFetch(url, { responseType: 'arraybuffer' })
1040
+ .then(function (result) {
1041
+ var arrayBuffer = result.data;
1042
+ var data = new Uint8Array(arrayBuffer);
1043
+
1044
+ // Check if segment is already valid MPEG-TS (starts with sync byte 0x47)
1045
+ if (data.length > 0 && data[0] === 0x47) {
1046
+ log("HLS: Segment already valid MPEG-TS, skipping decryption");
1047
+ var now = performance.now();
1048
+ var stats = {
1049
+ loading: { start: now, first: now, end: now },
1050
+ parsing: { start: now, end: now },
1051
+ buffering: { start: now, first: now, end: now },
1052
+ trequest: now,
1053
+ tfirst: now,
1054
+ tload: now,
1055
+ loaded: arrayBuffer.byteLength,
1056
+ total: arrayBuffer.byteLength
1057
+ };
1058
+ callbacks.onSuccess({ url: url, data: arrayBuffer }, stats, context);
1059
+ return;
1060
+ }
1061
+
1062
+ // Only decrypt if we have an encryption key
1063
+ if (!currentEncryptionKey) {
1064
+ log("HLS: No encryption key found, passing through as-is");
1065
+ var now = performance.now();
1066
+ var stats = {
1067
+ loading: { start: now, first: now, end: now },
1068
+ parsing: { start: now, end: now },
1069
+ buffering: { start: now, first: now, end: now },
1070
+ trequest: now,
1071
+ tfirst: now,
1072
+ tload: now,
1073
+ loaded: arrayBuffer.byteLength,
1074
+ total: arrayBuffer.byteLength
1075
+ };
1076
+ callbacks.onSuccess({ url: url, data: arrayBuffer }, stats, context);
1077
+ return;
1078
+ }
1079
+
1080
+ // Decrypt the segment
1081
+ log("Loading encrypted segment: " + url);
1082
+ var decryptedBuffer = decryptSegmentBinary(arrayBuffer, currentEncryptionKey, currentEncryptionIv);
1083
+
1084
+ if (!decryptedBuffer) {
1085
+ throw new Error("Decryption failed");
1086
+ }
1087
+
1088
+ var now = performance.now();
1089
+ var stats = {
1090
+ loading: { start: now, first: now, end: now },
1091
+ parsing: { start: now, end: now },
1092
+ buffering: { start: now, first: now, end: now },
1093
+ trequest: now,
1094
+ tfirst: now,
1095
+ tload: now,
1096
+ loaded: decryptedBuffer.byteLength,
1097
+ total: decryptedBuffer.byteLength
1098
+ };
1099
+
1100
+ callbacks.onSuccess({ url: url, data: decryptedBuffer }, stats, context);
1101
+ })
1102
+ .catch(function (e) {
1103
+ error("Segment load failed: " + e.message);
1104
+ callbacks.onError({ code: 500, text: e.message }, context, e);
1105
+ });
1106
+ return;
1107
+ }
1108
+
1109
+ smartFetch(url, { responseType: responseType })
1110
+ .then(function (result) {
1111
+ // Extract data based on response type
1112
+ var data = responseType === 'arraybuffer' ? result.data : result.text;
1113
+ var now = performance.now();
1114
+ var stats = {
1115
+ loading: { start: now, first: now, end: now },
1116
+ parsing: { start: now, end: now },
1117
+ buffering: { start: now, first: now, end: now },
1118
+ trequest: now,
1119
+ tfirst: now,
1120
+ tload: now,
1121
+ loaded: (data && (data.byteLength || data.length)) || 0,
1122
+ total: (data && (data.byteLength || data.length)) || 0
1123
+ };
1124
+
1125
+ if (context.type === 'manifest' || context.type === 'level' || context.type === 'playlist') {
1126
+ var text = data;
1127
+ if (typeof text === 'string' && text.indexOf('#EXTM3U') > -1) {
1128
+ text = rewriteM3u8(text, url, "");
1129
+ }
1130
+ callbacks.onSuccess({ url: url, data: text }, stats, context);
1131
+ } else {
1132
+ callbacks.onSuccess({ url: url, data: data }, stats, context);
1133
+ }
1134
+ })
1135
+ .catch(function (e) {
1136
+ callbacks.onError({ code: 500, text: e.message }, context, e);
1137
+ });
1138
+ };
1139
+
1140
+ this.abort = function () { };
1141
+ this.destroy = function () { };
1142
+ }
1143
+
1144
+ function processM3u8(text, baseUrl) {
1145
+ var lines = text.split('\n');
1146
+ var newLines = [];
1147
+
1148
+ for (var i = 0; i < lines.length; i++) {
1149
+ var line = lines[i].trim();
1150
+ if (line && line.indexOf('#') !== 0) {
1151
+ try {
1152
+ var absoluteUrl = new URL(line, baseUrl).href;
1153
+ line = absoluteUrl;
1154
+ } catch (e) {
1155
+ console.warn("URL resolve error", e);
1156
+ }
1157
+ } else if (line.indexOf('#EXT-X-KEY') === 0) {
1158
+ var uriMatch = line.match(/URI="([^"]+)"/);
1159
+ if (uriMatch) {
1160
+ var keyUri = uriMatch[1];
1161
+ try {
1162
+ var absoluteKey = new URL(keyUri, baseUrl).href;
1163
+ line = line.replace(uriMatch[1], absoluteKey);
1164
+ } catch (e) {
1165
+ console.warn("Key resolve error", e);
1166
+ }
1167
+ }
1168
+ }
1169
+ newLines.push(line);
1170
+ }
1171
+ var newText = newLines.join('\n');
1172
+
1173
+ // UTF-8 safe encoding for Data URI
1174
+ return "data:application/vnd.apple.mpegurl;base64," + btoa(unescape(encodeURIComponent(newText)));
1175
+ }
1176
+
1177
+ function closePlayer() {
1178
+ log("closePlayer() called");
1179
+
1180
+ var modal = document.getElementById('videoPlayerModal');
1181
+ var videoEl = document.getElementById('videoPlayer');
1182
+ var iframeEl = document.getElementById('videoIframe');
1183
+
1184
+ // Hide modal first
1185
+ if (modal) {
1186
+ modal.hidden = true;
1187
+ modal.setAttribute('hidden', '');
1188
+ modal.style.display = 'none';
1189
+ modal.style.visibility = 'hidden';
1190
+ log("Modal hidden");
1191
+ }
1192
+
1193
+ // Cleanup Shaka player FIRST (before clearing video src)
1194
+ if (window.shakaPlayer) {
1195
+ try {
1196
+ window.shakaPlayer.destroy();
1197
+ log("Shaka player destroyed");
1198
+ } catch (e) {
1199
+ log("Shaka destroy error: " + e.message);
1200
+ }
1201
+ window.shakaPlayer = null;
1202
+ }
1203
+
1204
+ // Cleanup Shaka interval
1205
+ if (window.shakaManifestInterval) {
1206
+ clearInterval(window.shakaManifestInterval);
1207
+ window.shakaManifestInterval = null;
1208
+ log("Shaka interval cleared");
1209
+ }
1210
+
1211
+ // Reset debug flag for next player session
1212
+ window.shakaNavDebugLogged = false;
1213
+
1214
+ // Unregister virtual scheme
1215
+ if (typeof shaka !== 'undefined' && shaka.net && shaka.net.NetworkingEngine) {
1216
+ try {
1217
+ shaka.net.NetworkingEngine.unregisterScheme('virtual');
1218
+ log("Virtual scheme unregistered");
1219
+ } catch (e) {
1220
+ // Scheme might not be registered, ignore
1221
+ }
1222
+ }
1223
+
1224
+ // Cleanup AVPlay (Tizen native player)
1225
+ if (window.currentAVPlay && typeof webapis !== 'undefined' && webapis.avplay) {
1226
+ try {
1227
+ webapis.avplay.stop();
1228
+ webapis.avplay.close();
1229
+ log("AVPlay stopped and closed");
1230
+ } catch (e) {
1231
+ log("AVPlay cleanup error: " + e.message);
1232
+ }
1233
+ window.currentAVPlay = false;
1234
+ }
1235
+
1236
+ // Cleanup HLS
1237
+ if (window.hls) {
1238
+ try {
1239
+ window.hls.destroy();
1240
+ log("HLS destroyed");
1241
+ } catch (e) {
1242
+ log("HLS destroy error: " + e.message);
1243
+ }
1244
+ window.hls = null;
1245
+ }
1246
+
1247
+ // Stop and cleanup video element
1248
+ if (videoEl) {
1249
+ try {
1250
+ videoEl.pause();
1251
+ videoEl.src = '';
1252
+ videoEl.removeAttribute('src');
1253
+ videoEl.load(); // Reset the video element
1254
+ log("Video element cleaned");
1255
+ } catch (e) {
1256
+ log("Video cleanup error: " + e.message);
1257
+ }
1258
+ }
1259
+
1260
+ // Stop iframe
1261
+ if (iframeEl) {
1262
+ iframeEl.src = '';
1263
+ log("Iframe cleared");
1264
+ }
1265
+
1266
+ // Exit player mode in navigation
1267
+ if (typeof DPadNavigator !== 'undefined') {
1268
+ DPadNavigator.exitPlayerMode();
1269
+ log("Exited player navigation mode");
1270
+ }
1271
+
1272
+ log("Player closed successfully");
1273
+ }
1274
+
1275
+ // Expose closePlayer to global window scope for cross-script access
1276
+ window.closePlayer = closePlayer;
1277
+
1278
+ // Attach close button handler on page load
1279
+ document.addEventListener('DOMContentLoaded', function () {
1280
+ var closeBtn = document.getElementById('closePlayer');
1281
+ if (closeBtn) {
1282
+ closeBtn.onclick = function () {
1283
+ closePlayer();
1284
+ };
1285
+ }
1286
+
1287
+ // Global keyboard handler - always close player on Escape/Back when visible
1288
+ document.addEventListener('keydown', function (e) {
1289
+ var modal = document.getElementById('videoPlayerModal');
1290
+
1291
+ // Check if modal is visible using multiple methods
1292
+ var isModalVisible = modal && (
1293
+ modal.style.display === 'flex' ||
1294
+ modal.style.display === 'block' ||
1295
+ (!modal.hidden && modal.style.display !== 'none')
1296
+ );
1297
+
1298
+ if (isModalVisible) {
1299
+ var key = e.key || e.keyCode;
1300
+ var keyName = typeof key === 'string' ? key : '';
1301
+ var keyCode = typeof key === 'number' ? key : e.keyCode;
1302
+
1303
+ log("Key pressed in player: " + keyName + " (code: " + keyCode + ")");
1304
+
1305
+ // Tizen remote back button is code 10009
1306
+ if (keyName === 'Escape' || keyName === 'Back' || keyName === 'Backspace' || keyCode === 10009 || keyCode === 27 || keyCode === 8) {
1307
+ log("Close key detected - closing player");
1308
+ e.preventDefault();
1309
+ e.stopPropagation();
1310
+ e.stopImmediatePropagation();
1311
+ closePlayer();
1312
+ return false;
1313
+ }
1314
+ }
1315
+ }, true); // Use capture phase to get event first
1316
+ });
1317
+
1318
+ function openPost(post) {
1319
+ var rawUrl = post.channel_url || post.url || post.link;
1320
+ if (!rawUrl) return;
1321
+
1322
+ var initialUrl = decryptUrl(rawUrl);
1323
+ log("Opening: " + initialUrl);
1324
+
1325
+ // Clear any previous error overlay
1326
+ hidePlayerError();
1327
+
1328
+ var modal = document.getElementById('videoPlayerModal');
1329
+ var videoEl = document.getElementById('videoPlayer');
1330
+ var iframeEl = document.getElementById('videoIframe');
1331
+ var playBtn = document.getElementById('manualPlayBtn');
1332
+
1333
+ if (!playBtn) {
1334
+ playBtn = document.createElement('button');
1335
+ playBtn.id = 'manualPlayBtn';
1336
+ playBtn.textContent = "▶ Click to Play";
1337
+ playBtn.style.cssText = "position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-size:24px;padding:15px 30px;background:red;color:white;border:none;border-radius:8px;z-index:100;display:none;cursor:pointer;";
1338
+ modal.querySelector('.video-container').appendChild(playBtn);
1339
+ playBtn.onclick = function () {
1340
+ playBtn.style.display = 'none';
1341
+ videoEl.play().catch(function (e) {
1342
+ error("Manual play failed: " + e.message);
1343
+ });
1344
+ };
1345
+ } else {
1346
+ playBtn.style.display = 'none';
1347
+ }
1348
+
1349
+ if (window.hls) {
1350
+ window.hls.destroy();
1351
+ window.hls = null;
1352
+ }
1353
+ if (window.shakaManifestInterval) {
1354
+ clearInterval(window.shakaManifestInterval);
1355
+ window.shakaManifestInterval = null;
1356
+ }
1357
+ // Unregister virtual scheme so it doesn't affect other streams
1358
+ if (typeof shaka !== 'undefined' && shaka.net && shaka.net.NetworkingEngine) {
1359
+ try {
1360
+ shaka.net.NetworkingEngine.unregisterScheme('virtual');
1361
+ } catch (e) {
1362
+ // Scheme might not be registered, ignore
1363
+ }
1364
+ }
1365
+ videoEl.pause();
1366
+ videoEl.src = "";
1367
+ videoEl.removeAttribute('src');
1368
+ videoEl.style.display = 'none';
1369
+ iframeEl.src = "";
1370
+ iframeEl.style.display = 'none';
1371
+
1372
+ var errs = modal.querySelectorAll('.error-message');
1373
+ for (var i = 0; i < errs.length; i++) errs[i].remove();
1374
+
1375
+ modal.hidden = false;
1376
+ modal.removeAttribute('hidden'); // Ensure hidden attribute is removed
1377
+ modal.style.display = 'flex'; // Force display
1378
+ modal.style.visibility = 'visible'; // Force visibility
1379
+ modal.style.opacity = '1'; // Force opacity
1380
+ modal.style.zIndex = '9999'; // Force to top
1381
+ log("Modal should now be visible. Hidden: " + modal.hidden + ", hasAttribute: " + modal.hasAttribute('hidden') + ", display: " + modal.style.display);
1382
+ showLoader();
1383
+
1384
+ // Enter player navigation mode for D-pad
1385
+ if (typeof DPadNavigator !== 'undefined') {
1386
+ DPadNavigator.enterPlayerMode();
1387
+ }
1388
+
1389
+ resolveStreamUrl(initialUrl).then(function (result) {
1390
+ hideLoader();
1391
+ var url = result.url;
1392
+ var customContent = result.content;
1393
+ var clearkey = result.clearkey; // ClearKey DRM credentials from techlyhub
1394
+ var useFallbackIframe = result.useFallbackIframe;
1395
+
1396
+ log("Playing: " + url);
1397
+
1398
+ var isVirtual = url.indexOf('virtual:') === 0;
1399
+ var isMpd = url.indexOf('.mpd') > -1;
1400
+ var isStream = isVirtual || isMpd || url.indexOf('.m3u8') > -1 || url.indexOf('.ts') > -1 || url.indexOf('.mp4') > -1 || url.indexOf('blob:') === 0 || url.indexOf('data:') === 0;
1401
+ var isPage = ((url.indexOf('.php') > -1 || url.indexOf('.html') > -1) && !isStream) || useFallbackIframe;
1402
+
1403
+ if (isPage) {
1404
+ useIframe(url);
1405
+ return;
1406
+ }
1407
+
1408
+
1409
+ videoEl.style.display = 'block';
1410
+ videoEl.setAttribute('playsinline', '');
1411
+ videoEl.setAttribute('webkit-playsinline', '');
1412
+
1413
+ if (isTizen()) {
1414
+ if (isVirtual) {
1415
+ // Use Shaka for virtual streams (encrypted M3U8s)
1416
+ if (typeof shaka !== 'undefined' && shaka.Player.isBrowserSupported()) {
1417
+ playShaka(result);
1418
+ } else if (typeof Hls !== 'undefined' && Hls.isSupported()) {
1419
+ playHls(result);
1420
+ } else {
1421
+ error("No player supported for virtual stream");
1422
+ }
1423
+ return;
1424
+ }
1425
+
1426
+ if (url.indexOf('blob:') === 0 || url.indexOf('data:') === 0 || url.indexOf('.m3u8') > -1 || isMpd) {
1427
+ if (typeof shaka !== 'undefined' && shaka.Player.isBrowserSupported()) {
1428
+ playShaka(result);
1429
+ } else if (typeof Hls !== 'undefined' && Hls.isSupported() && !isMpd) {
1430
+ // HLS.js doesn't support MPD, only use for HLS streams
1431
+ playHls(result);
1432
+ } else if (!isMpd) {
1433
+ videoEl.src = url;
1434
+ videoEl.play();
1435
+ } else {
1436
+ error("No player available for MPD stream");
1437
+ }
1438
+ } else {
1439
+
1440
+ videoEl.removeAttribute('crossorigin');
1441
+ videoEl.src = url;
1442
+ videoEl.play().catch(function (e) {
1443
+ log("Native play failed, trying Shaka");
1444
+ if (typeof shaka !== 'undefined' && shaka.Player.isBrowserSupported()) {
1445
+ playShaka(result);
1446
+ } else if (typeof Hls !== 'undefined' && Hls.isSupported()) {
1447
+ playHls(result);
1448
+ }
1449
+ });
1450
+ }
1451
+ } else {
1452
+ // Desktop/non-Tizen - prefer Shaka for better encryption handling
1453
+ if (typeof shaka !== 'undefined' && shaka.Player.isBrowserSupported()) {
1454
+ playShaka(result);
1455
+ } else if (typeof Hls !== 'undefined' && Hls.isSupported()) {
1456
+ playHls(result);
1457
+ } else if (videoEl.canPlayType('application/vnd.apple.mpegurl')) {
1458
+ videoEl.src = url;
1459
+ videoEl.play();
1460
+ } else {
1461
+ openIframe(url);
1462
+ }
1463
+ }
1464
+
1465
+ function playHls(srcObj) {
1466
+ // srcObj may contain {url, content}
1467
+ var src = srcObj.url || srcObj;
1468
+ var customContent = srcObj.content;
1469
+
1470
+ // Configure Hls with custom loader - ALWAYS use it for encrypted segment support
1471
+ var config = {
1472
+ debug: true,
1473
+ pLoader: CustomLoader, // Playlist loader
1474
+ loader: CustomLoader, // Manifest loader
1475
+ fLoader: CustomLoader // Fragment (segment) loader - handles encrypted .js/.zip/.html files
1476
+ };
1477
+
1478
+ if (customContent) {
1479
+ // Store the custom content for virtual manifests
1480
+ config.customContent = customContent;
1481
+ log("Using custom loader for inline manifest");
1482
+ }
1483
+
1484
+ var hls = new Hls(config);
1485
+ window.hls = hls;
1486
+
1487
+ hls.loadSource(src);
1488
+ hls.attachMedia(videoEl);
1489
+
1490
+ hls.on(Hls.Events.MANIFEST_PARSED, function () {
1491
+ videoEl.play().catch(function (e) {
1492
+ log("Autoplay blocked: " + e.message);
1493
+ playBtn.style.display = 'block';
1494
+ playBtn.focus();
1495
+ });
1496
+ });
1497
+
1498
+ hls.on(Hls.Events.ERROR, function (event, data) {
1499
+ if (data.fatal) {
1500
+ log("Hls Error: " + data.type + " " + (data.details || ""));
1501
+ hls.destroy();
1502
+ window.hls = null;
1503
+ // Try AVPlay as last resort on Tizen
1504
+ if (isTizen() && typeof webapis !== 'undefined' && webapis.avplay) {
1505
+ log("Trying AVPlay fallback after HLS.js failure...");
1506
+ playAVPlay(src);
1507
+ } else {
1508
+ showPlayerError("Playback failed. Stream unavailable.");
1509
+ }
1510
+ }
1511
+ });
1512
+ }
1513
+
1514
+ // AVPlay - Tizen native player that can set headers
1515
+ function playAVPlay(url) {
1516
+ if (typeof webapis === 'undefined' || !webapis.avplay) {
1517
+ showPlayerError("AVPlay not available. Cannot play this stream.");
1518
+ return;
1519
+ }
1520
+
1521
+ log("AVPlay: Playing " + url);
1522
+
1523
+ // Hide HTML5 video element
1524
+ videoEl.style.display = 'none';
1525
+
1526
+ try {
1527
+ webapis.avplay.open(url);
1528
+
1529
+ // Set display area to full screen
1530
+ var container = document.getElementById('videoPlayerModal');
1531
+ if (container) {
1532
+ var rect = container.getBoundingClientRect();
1533
+ webapis.avplay.setDisplayRect(0, 0, rect.width || 1920, rect.height || 1080);
1534
+ } else {
1535
+ webapis.avplay.setDisplayRect(0, 0, 1920, 1080);
1536
+ }
1537
+
1538
+ // Note: AVPlay doesn't support custom HTTP headers like Referer
1539
+ // However, Tizen's native networking often bypasses CORS restrictions
1540
+ // so the stream may still work without custom headers
1541
+ log("AVPlay: Attempting playback (custom headers not supported)");
1542
+
1543
+ webapis.avplay.setListener({
1544
+ onbufferingstart: function () { log("AVPlay: Buffering..."); },
1545
+ onbufferingprogress: function (percent) { log("AVPlay: Buffer " + percent + "%"); },
1546
+ onbufferingcomplete: function () { log("AVPlay: Buffering complete"); },
1547
+ oncurrentplaytime: function (time) { /* playback time update */ },
1548
+ onevent: function (eventType, eventData) { log("AVPlay event: " + eventType); },
1549
+ onerror: function (errorType) {
1550
+ showPlayerError("AVPlay error: " + errorType);
1551
+ },
1552
+ onsubtitlechange: function () { },
1553
+ ondrmevent: function () { },
1554
+ onstreamcompleted: function () {
1555
+ log("AVPlay: Stream completed");
1556
+ closePlayer();
1557
+ }
1558
+ });
1559
+
1560
+ webapis.avplay.prepareAsync(function () {
1561
+ log("AVPlay: Prepared, starting playback");
1562
+ webapis.avplay.play();
1563
+ window.currentAVPlay = true;
1564
+ }, function (e) {
1565
+ showPlayerError("AVPlay failed: " + (e.message || e));
1566
+ });
1567
+
1568
+ } catch (e) {
1569
+ showPlayerError("AVPlay error: " + e.message);
1570
+ }
1571
+ }
1572
+
1573
+ function playShaka(srcObj) {
1574
+ var src = srcObj.url || srcObj;
1575
+ var customContent = srcObj.content;
1576
+ var clearkeyInfo = srcObj.clearkey; // ClearKey DRM credentials from techlyhub
1577
+
1578
+ // Install Shaka polyfills
1579
+ shaka.polyfill.installAll();
1580
+
1581
+ if (!shaka.Player.isBrowserSupported()) {
1582
+ error("Shaka Player not supported in this browser");
1583
+ playHls(srcObj); // Fallback to HLS.js
1584
+ return;
1585
+ }
1586
+
1587
+ // Get the container for Shaka UI
1588
+ var container = videoEl.parentElement;
1589
+
1590
+ // Destroy existing UI if any
1591
+ if (window.shakaUi) {
1592
+ try {
1593
+ window.shakaUi.destroy();
1594
+ } catch (e) { }
1595
+ window.shakaUi = null;
1596
+ }
1597
+ if (window.shakaPlayer) {
1598
+ try {
1599
+ window.shakaPlayer.destroy();
1600
+ } catch (e) { }
1601
+ window.shakaPlayer = null;
1602
+ }
1603
+
1604
+ // Create player
1605
+ var player = new shaka.Player(videoEl);
1606
+ window.shakaPlayer = player;
1607
+
1608
+ // Create Shaka UI overlay for controls (bottom bar, three-dot menu)
1609
+ var ui = new shaka.ui.Overlay(player, container, videoEl);
1610
+ window.shakaUi = ui;
1611
+
1612
+ // Configure UI to show quality selection in overflow menu
1613
+ ui.configure({
1614
+ addBigPlayButton: true,
1615
+ addSeekBar: true,
1616
+ controlPanelElements: [
1617
+ 'play_pause',
1618
+ 'time_and_duration',
1619
+ 'spacer',
1620
+ 'mute',
1621
+ 'volume',
1622
+ 'fullscreen',
1623
+ 'overflow_menu'
1624
+ ],
1625
+ overflowMenuButtons: [
1626
+ 'quality',
1627
+ 'playback_rate',
1628
+ 'captions'
1629
+ ]
1630
+ });
1631
+
1632
+ log("Shaka: UI Overlay created with quality menu");
1633
+
1634
+ // Log what UI controls were created (for debugging)
1635
+ setTimeout(function () {
1636
+ var controlPanel = container.querySelector('.shaka-controls-button-panel');
1637
+ var overflowBtn = container.querySelector('.shaka-overflow-menu-button');
1638
+ log("Shaka UI Debug: Control panel found: " + !!controlPanel);
1639
+ log("Shaka UI Debug: Overflow button found: " + !!overflowBtn);
1640
+ if (controlPanel) {
1641
+ var buttons = controlPanel.querySelectorAll('button');
1642
+ log("Shaka UI Debug: Found " + buttons.length + " control buttons");
1643
+ }
1644
+ }, 500);
1645
+
1646
+ // Configure ClearKey DRM if credentials were extracted from techlyhub
1647
+ if (clearkeyInfo && clearkeyInfo.keyId && clearkeyInfo.key) {
1648
+ log("Shaka: Configuring ClearKey DRM for techlyhub stream");
1649
+ var clearKeys = {};
1650
+ clearKeys[clearkeyInfo.keyId] = clearkeyInfo.key;
1651
+ player.configure({
1652
+ drm: {
1653
+ clearKeys: clearKeys
1654
+ }
1655
+ });
1656
+ log("Shaka: ClearKey DRM configured");
1657
+ }
1658
+
1659
+ // Add request filter to inject required headers for M3U8 server
1660
+ // The server checks Referer/Origin and returns 403 if they don't match
1661
+ player.getNetworkingEngine().registerRequestFilter(function (type, request) {
1662
+ // For requests to the M3U8 CDN, add the expected headers
1663
+ if (request.uris && request.uris[0] && request.uris[0].indexOf('merichunidya.com') > -1) {
1664
+ request.headers['Referer'] = 'https://vividmosaica.com/';
1665
+ request.headers['Origin'] = 'https://vividmosaica.com';
1666
+ log("Shaka: Added vividmosaica.com headers for CDN request");
1667
+ }
1668
+ });
1669
+
1670
+
1671
+ // Store manifest info for refreshing live streams
1672
+
1673
+ var storedManifestContent = customContent;
1674
+ var originalSourceUrl = srcObj.originalUrl || null; // Store original URL for refreshes
1675
+ var isFirstLoad = true; // Track first load vs refresh
1676
+ var isRefreshing = false; // Track if background refresh is in progress
1677
+
1678
+ // Background manifest pre-fetcher for smooth live streaming
1679
+ var manifestRefreshInterval = null;
1680
+ if (customContent && originalSourceUrl) {
1681
+ // Pre-fetch manifest every 10 seconds in background
1682
+ manifestRefreshInterval = setInterval(function () {
1683
+ if (isRefreshing) return; // Skip if already refreshing
1684
+ isRefreshing = true;
1685
+
1686
+ resolveStreamUrl(originalSourceUrl).then(function (result) {
1687
+ if (result && result.content) {
1688
+ storedManifestContent = result.content;
1689
+ // log("Shaka: Background manifest refresh complete"); // Quiet background refresh
1690
+ }
1691
+ isRefreshing = false;
1692
+ }).catch(function (e) {
1693
+ isRefreshing = false;
1694
+ });
1695
+ }, 10000); // Every 10 seconds
1696
+
1697
+ // Store interval for cleanup
1698
+ window.shakaManifestInterval = manifestRefreshInterval;
1699
+ }
1700
+
1701
+ // Register custom scheme handler for virtual manifests
1702
+ // This intercepts requests BEFORE they go to the network
1703
+ if (customContent) {
1704
+ shaka.net.NetworkingEngine.registerScheme('virtual', function (uri, request, requestType, progressUpdated, headersReceived) {
1705
+ // Return a promise that resolves with the manifest content IMMEDIATELY
1706
+ return new Promise(function (resolve, reject) {
1707
+ // Always serve cached content immediately (background fetcher keeps it fresh)
1708
+ try {
1709
+ var encoder = new TextEncoder();
1710
+ var data = encoder.encode(storedManifestContent);
1711
+
1712
+ resolve({
1713
+ uri: uri,
1714
+ originalUri: uri,
1715
+ data: data.buffer,
1716
+ status: 200,
1717
+ headers: {},
1718
+ timeMs: 0,
1719
+ fromCache: !isFirstLoad
1720
+ });
1721
+
1722
+ if (isFirstLoad) {
1723
+ log("Shaka: Served virtual manifest (first load)");
1724
+ isFirstLoad = false;
1725
+ }
1726
+ } catch (e) {
1727
+ reject(e);
1728
+ }
1729
+ });
1730
+ });
1731
+ }
1732
+
1733
+ // Add response filter for encrypted segments
1734
+ player.getNetworkingEngine().registerResponseFilter(function (type, response) {
1735
+ var url = response.uri;
1736
+
1737
+ // Check if this looks like an encrypted segment (by extension)
1738
+ var looksEncrypted = url.match(/\.(zip|css|html|js|apk|txt|xml)(\?.*)?$/i);
1739
+
1740
+ if (looksEncrypted && type === shaka.net.NetworkingEngine.RequestType.SEGMENT) {
1741
+ // Check if segment is already valid MPEG-TS (starts with sync byte 0x47)
1742
+ var data = new Uint8Array(response.data);
1743
+ if (data.length > 0 && data[0] === 0x47) {
1744
+ log("Shaka: Segment already valid MPEG-TS, skipping decryption");
1745
+ return; // Already valid video, don't decrypt
1746
+ }
1747
+
1748
+ // Only decrypt if we have an encryption key from the manifest
1749
+ if (!currentEncryptionKey) {
1750
+ log("Shaka: No encryption key found in manifest, skipping decryption");
1751
+ return;
1752
+ }
1753
+
1754
+ log("Shaka: Decrypting segment: " + url.substring(0, 60) + "...");
1755
+
1756
+ try {
1757
+ var decrypted = decryptSegmentBinary(response.data, currentEncryptionKey, currentEncryptionIv);
1758
+ if (decrypted) {
1759
+ response.data = decrypted;
1760
+ log("Shaka: Decrypted successfully");
1761
+ }
1762
+ } catch (e) {
1763
+ error("Shaka: Decryption failed: " + e.message);
1764
+ }
1765
+ }
1766
+ });
1767
+
1768
+ // Error handler - ignore manifest refresh errors for live streams
1769
+ player.addEventListener('error', function (event) {
1770
+ var err = event.detail;
1771
+ // Error 1002 is network error - often happens on manifest refresh for blob URLs
1772
+ if (err.code === 1002) {
1773
+ // Check if we need to refresh manifest ourselves
1774
+ log("Shaka: Manifest refresh issue, will try to recover");
1775
+ return; // Don't show error to user
1776
+ }
1777
+ // Error 1003 is BAD_HTTP_STATUS (like 429 Too Many Requests)
1778
+ if (err.code === 1003) {
1779
+ log("Shaka: HTTP error (possibly rate limited), will retry");
1780
+ return; // Let Shaka handle retries
1781
+ }
1782
+ // Error 3014 is MEDIA_SOURCE_OPERATION_THREW - often just means stream ended
1783
+ if (err.code === 3014) {
1784
+ log("Shaka: Media source issue (stream may have ended)");
1785
+ return; // Don't show as error
1786
+ }
1787
+ error("Shaka error: " + err.code + " - " + (err.message || ""));
1788
+ });
1789
+
1790
+ // For virtual manifests, we need to configure Shaka differently
1791
+ var loadUrl = src;
1792
+
1793
+ if (customContent) {
1794
+ // Use virtual:// scheme that will be served by custom scheme handler
1795
+ loadUrl = 'virtual://manifest/playlist.m3u8';
1796
+ log("Shaka: Using virtual scheme for manifest: " + loadUrl);
1797
+
1798
+ // Configure for live stream - reduce refresh frequency to avoid rate limits
1799
+ player.configure({
1800
+ streaming: {
1801
+ retryParameters: {
1802
+ maxAttempts: 2,
1803
+ baseDelay: 2000, // Wait 2s before retry
1804
+ backoffFactor: 2,
1805
+ fuzzFactor: 0.5
1806
+ },
1807
+ stallEnabled: false,
1808
+ gapDetectionThreshold: 0.5,
1809
+ updateIntervalSeconds: 30 // Update manifest every 30 seconds
1810
+ },
1811
+ manifest: {
1812
+ retryParameters: {
1813
+ maxAttempts: 1,
1814
+ timeout: 10000,
1815
+ baseDelay: 3000 // Wait 3s before retrying manifest
1816
+ },
1817
+ availabilityWindowOverride: 300, // 5 min window
1818
+ defaultPresentationDelay: 10, // Start 10s from live edge
1819
+ disableVideo: false,
1820
+ disableAudio: false
1821
+ }
1822
+ });
1823
+ } else {
1824
+ // For regular (non-virtual) streams, also configure to avoid rate limits
1825
+ player.configure({
1826
+ streaming: {
1827
+ updateIntervalSeconds: 15, // Update every 15 seconds
1828
+ retryParameters: {
1829
+ maxAttempts: 3,
1830
+ baseDelay: 2000,
1831
+ backoffFactor: 2
1832
+ }
1833
+ },
1834
+ manifest: {
1835
+ retryParameters: {
1836
+ maxAttempts: 2,
1837
+ timeout: 10000,
1838
+ baseDelay: 3000
1839
+ }
1840
+ }
1841
+ });
1842
+ }
1843
+
1844
+ // Load and play
1845
+ player.load(loadUrl).then(function () {
1846
+ log("Shaka: Manifest loaded successfully");
1847
+
1848
+
1849
+ videoEl.play().catch(function (e) {
1850
+ log("Autoplay blocked: " + e.message);
1851
+ playBtn.style.display = 'block';
1852
+ playBtn.focus();
1853
+ });
1854
+ }).catch(function (e) {
1855
+ error("Shaka load error: " + e.message);
1856
+ // Try HLS.js as fallback
1857
+ log("Trying HLS.js fallback...");
1858
+ playHls(srcObj);
1859
+ });
1860
+ }
1861
+
1862
+
1863
+
1864
+ function tryNativeFallback(src) {
1865
+ log("Native Fallback");
1866
+ videoEl.removeAttribute('crossorigin');
1867
+ videoEl.src = src;
1868
+ var p = videoEl.play();
1869
+ if (p) {
1870
+ p.catch(function (e) {
1871
+ log("Native failed: " + e.message);
1872
+ playBtn.style.display = 'block';
1873
+ playBtn.focus();
1874
+ });
1875
+ }
1876
+ }
1877
+
1878
+ function fallbackToIframe() {
1879
+ if (isStream) {
1880
+ error("Video playback failed.");
1881
+ } else {
1882
+ videoEl.style.display = 'none';
1883
+ videoEl.pause();
1884
+ videoEl.src = "";
1885
+ useIframe(initialUrl);
1886
+ }
1887
+ }
1888
+
1889
+ function useIframe(src) {
1890
+ log("Iframe Mode");
1891
+ videoEl.style.display = 'none';
1892
+ iframeEl.style.display = 'block';
1893
+ iframeEl.src = src;
1894
+ }
1895
+ });
1896
+
1897
+ document.getElementById('closePlayer').onclick = function () {
1898
+ closePlayer();
1899
+ };
1900
+ }
1901
+
1902
+ // Start
1903
+ loadCategories();
1904
+
1905
+ // Initialize D-pad navigation for TV
1906
+ if (typeof DPadNavigator !== 'undefined') {
1907
+ DPadNavigator.init();
1908
+ }
1909
+
1910
+ // ========== PROXY STATUS CHECKING ==========
1911
+ var proxyCheckInterval = null;
1912
+
1913
+ function checkProxyStatus() {
1914
+ if (typeof window.StreamProxy === 'undefined' || typeof window.StreamProxy.checkProxyHealth !== 'function') {
1915
+ log('[PROXY-STATUS] StreamProxy not available');
1916
+ updateProxyIndicator(false);
1917
+ return;
1918
+ }
1919
+
1920
+ log('[PROXY-STATUS] Checking proxy health...');
1921
+
1922
+ window.StreamProxy.checkProxyHealth(function (isAvailable) {
1923
+ updateProxyIndicator(isAvailable);
1924
+ log('[PROXY-STATUS] Proxy is ' + (isAvailable ? 'RUNNING ✓' : 'NOT RUNNING ✗'));
1925
+ });
1926
+ }
1927
+
1928
+ function updateProxyIndicator(isAvailable) {
1929
+ var indicator = document.getElementById('proxyStatusIndicator');
1930
+ var dot = document.getElementById('proxyDot');
1931
+
1932
+ if (!indicator || !dot) return;
1933
+
1934
+ if (isAvailable) {
1935
+ dot.style.background = '#4caf50'; // Green
1936
+ indicator.title = 'Proxy Status: Running ✓';
1937
+ } else {
1938
+ dot.style.background = '#f44336'; // Red
1939
+ indicator.title = 'Proxy Status: Not Running ✗';
1940
+ }
1941
+ }
1942
+
1943
+ // Initialize proxy status check on page load
1944
+ document.addEventListener('DOMContentLoaded', function () {
1945
+ log('[PROXY-STATUS] Initializing proxy status check...');
1946
+
1947
+ // Initial check
1948
+ checkProxyStatus();
1949
+
1950
+ // Set up periodic check every 30 seconds
1951
+ if (typeof window.StreamProxy !== 'undefined') {
1952
+ proxyCheckInterval = setInterval(function () {
1953
+ checkProxyStatus();
1954
+ }, 30000);
1955
+ }
1956
+
1957
+ // Add click handler for manual check
1958
+ var proxyIndicator = document.getElementById('proxyStatusIndicator');
1959
+ if (proxyIndicator) {
1960
+ proxyIndicator.onclick = function () {
1961
+ log('[PROXY-STATUS] Manual check triggered');
1962
+ proxyIndicator.title = 'Proxy Status: Checking...';
1963
+ checkProxyStatus();
1964
+ };
1965
+
1966
+ // Add Enter key handler for D-pad navigation
1967
+ proxyIndicator.addEventListener('keydown', function (e) {
1968
+ if (e.key === 'Enter' || e.keyCode === 13) {
1969
+ e.preventDefault();
1970
+ log('[PROXY-STATUS] Manual check triggered (D-pad)');
1971
+ proxyIndicator.title = 'Proxy Status: Checking...';
1972
+ checkProxyStatus();
1973
+ }
1974
+ });
1975
+ }
1976
+ });