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 +1976 -0
- package/app/config.xml +15 -0
- package/app/css/controls.css +54 -0
- package/app/icon.png +0 -0
- package/app/index.html +318 -0
- package/app/js/crypto-js.min.js +1 -0
- package/app/js/hls.min.js +2 -0
- package/app/js/shaka-player.compiled.min.js +1 -0
- package/app/js/shaka-player.ui.debug.js +3068 -0
- package/app/native_service/README.md +94 -0
- package/app/native_service/inc/proxy_service.h +22 -0
- package/app/native_service/src/proxy_service.cpp +453 -0
- package/app/native_service/tizen-manifest.xml +23 -0
- package/app/navigation.js +663 -0
- package/app/styles.css +539 -0
- package/package.json +11 -0
- package/service.js +121 -0
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
|
+
});
|