myetv-player 1.7.0 ā 1.7.2
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/README.md +461 -154
- package/css/myetv-player.css +36 -0
- package/css/myetv-player.min.css +1 -1
- package/dist/myetv-player.js +415 -89
- package/dist/myetv-player.min.js +338 -60
- package/package.json +2 -1
- package/plugins/autosub/myetv-autosub-plugin.js +545 -188
- package/plugins/autosub/readme.md +156 -66
- package/plugins/radio/api.php +154 -0
- package/plugins/radio/icons/bolt-lightning-solid.png +0 -0
- package/plugins/radio/icons/compact-disc-solid.png +0 -0
- package/plugins/radio/icons/guitar-solid.png +0 -0
- package/plugins/radio/icons/headphones-solid.png +0 -0
- package/plugins/radio/icons/medal-solid.png +0 -0
- package/plugins/radio/icons/music-solid.png +0 -0
- package/plugins/radio/icons/newspaper-solid.png +0 -0
- package/plugins/radio/icons/radio-solid.png +0 -0
- package/plugins/radio/icons/readme.md +1 -0
- package/plugins/radio/myetv-radio-plugin.js +1655 -0
- package/plugins/radio/php/data/readme.md +1 -0
- package/plugins/radio/php/radio_stats.php +203 -0
- package/plugins/radio/proxy.php +26 -0
- package/plugins/radio/readme.md +176 -0
- package/plugins/radio/station_example.json +47 -0
- package/plugins/video-loading/myetv-video-loading-plugin.js +157 -0
- package/plugins/video-loading/readme.md +87 -0
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MYETV Player - Auto Subtitles Plugin
|
|
3
|
-
* Automatically generates subtitles via Transformers.js + Whisper
|
|
3
|
+
* Automatically generates subtitles via Transformers.js + Whisper
|
|
4
|
+
* OR Translates existing player subtitles on-the-fly.
|
|
4
5
|
*
|
|
5
6
|
* Compatible with myetv-player plugin system (registerMYETVPlugin)
|
|
6
7
|
* Author: Oscar Cosimo - MYETV
|
|
7
8
|
*
|
|
8
9
|
* Options:
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
10
|
+
* language {string} Transcription language ('italian','english','it','en',...). Default: null (auto-detect)
|
|
11
|
+
* modelSize {string} 'tiny' (~39MB), 'base' (~74MB), 'small' (~244MB). Default: 'base'
|
|
12
|
+
* autoGenerate {boolean} Auto-generate subtitles on video load. Default: false
|
|
13
|
+
* autoTranslation {string} Auto-translate subtitles into this language on load (ISO 2-letter code, e.g. 'en','it'). Default: null
|
|
14
|
+
* showButton {boolean} Show button in the control bar. Default: true
|
|
15
|
+
* position {string} 'right', 'left', or 'topbar'. Default: 'right'
|
|
16
|
+
* subtitleStyle {object} Custom CSS style object for the subtitle text div
|
|
17
|
+
* cacheEnabled {boolean} Cache transcription and translations in localStorage. Default: true
|
|
18
|
+
* idCache {string} Stable unique ID for localStorage cache key
|
|
18
19
|
*/
|
|
19
20
|
|
|
20
21
|
(function () {
|
|
@@ -212,7 +213,9 @@
|
|
|
212
213
|
@media (max-width: 600px) {
|
|
213
214
|
.myetv-autosub-overlay { bottom: 60px; width: 96%; }
|
|
214
215
|
.myetv-autosub-text { font-size: clamp(0.6em, 3.5vw, 0.85em); padding: 3px 8px 4px; }
|
|
215
|
-
.myetv-autosub-drag-handle { display: none; }
|
|
216
|
+
.myetv-autosub-drag-handle { display: none; }
|
|
217
|
+
.myetv-autosub-overlay { pointer-events: all; cursor: grab; }
|
|
218
|
+
.myetv-autosub-overlay.dragging { cursor: grabbing; }
|
|
216
219
|
}
|
|
217
220
|
@media (max-width: 400px) {
|
|
218
221
|
.myetv-autosub-text { font-size: clamp(0.55em, 4vw, 0.75em); }
|
|
@@ -220,7 +223,14 @@
|
|
|
220
223
|
`;
|
|
221
224
|
|
|
222
225
|
const ICON_CC = `<svg viewBox="0 0 24 24"><path d="M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm-9 8H9.5v-.5h-2v3h2V14H11v1c0 .55-.45 1-1 1H7c-.55 0-1-.45-1-1v-4c0-.55.45-1 1-1h3c.55 0 1 .45 1 1v1zm7 0h-1.5v-.5h-2v3h2V14H18v1c0 .55-.45 1-1 1h-3c-.55 0-1-.45-1-1v-4c0-.55.45-1 1-1h3c.55 0 1 .45 1 1v1z"/></svg>`;
|
|
223
|
-
const ICON_LOADING = `<svg viewBox="0 0 24 24"
|
|
226
|
+
const ICON_LOADING = `<svg viewBox="0 0 24 24">
|
|
227
|
+
<path d="M12 2A10 10 0 0 1 22 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none">
|
|
228
|
+
<animateTransform attributeName="transform" type="rotate" from="0 12 12" to="360 12 12" dur="1s" repeatCount="indefinite"/>
|
|
229
|
+
</path>
|
|
230
|
+
<g transform="translate(4, 5) scale(0.65)">
|
|
231
|
+
<path d="M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm-9 8H9.5v-.5h-2v3h2V14H11v1c0 .55-.45 1-1 1H7c-.55 0-1-.45-1-1v-4c0-.55.45-1 1-1h3c.55 0 1 .45 1 1v1zm7 0h-1.5v-.5h-2v3h2V14H18v1c0 .55-.45 1-1 1h-3c-.55 0-1-.45-1-1v-4c0-.55.45-1 1-1h3c.55 0 1 .45 1 1v1z"/>
|
|
232
|
+
</g>
|
|
233
|
+
</svg>`;
|
|
224
234
|
|
|
225
235
|
const WHISPER_MODELS = {
|
|
226
236
|
tiny: 'Xenova/whisper-tiny',
|
|
@@ -284,19 +294,25 @@
|
|
|
284
294
|
modelSize: 'base',
|
|
285
295
|
autoGenerate: false,
|
|
286
296
|
showButton: true,
|
|
287
|
-
position: 'right',
|
|
297
|
+
position: 'right',
|
|
288
298
|
subtitleStyle: {},
|
|
289
299
|
cacheEnabled: true,
|
|
290
300
|
idCache: null,
|
|
291
|
-
autoTranslation: null,
|
|
301
|
+
autoTranslation: null,
|
|
302
|
+
translationEngine: null,
|
|
292
303
|
}, options);
|
|
293
304
|
|
|
294
305
|
this.isGenerating = false;
|
|
295
|
-
this.subtitles = [];
|
|
296
|
-
this.subtitlesTrans = [];
|
|
297
|
-
this.translateLang = 'off';
|
|
298
|
-
this._transCache = {};
|
|
306
|
+
this.subtitles = []; // Original segments (generated OR imported)
|
|
307
|
+
this.subtitlesTrans = []; // Translated segments
|
|
308
|
+
this.translateLang = 'off';
|
|
309
|
+
this._transCache = {}; // Persistent cache for translations { langCode: [...] }
|
|
299
310
|
this.subVisible = false;
|
|
311
|
+
|
|
312
|
+
// Flags for Native Subtitle Integration
|
|
313
|
+
this.hasNativeTracks = false;
|
|
314
|
+
this.isImportedSubs = false;
|
|
315
|
+
|
|
300
316
|
this.updateInterval = null;
|
|
301
317
|
this.btnWrapEl = null;
|
|
302
318
|
this.btnEl = null;
|
|
@@ -307,7 +323,7 @@
|
|
|
307
323
|
this._worker = null;
|
|
308
324
|
// Drag state
|
|
309
325
|
this._drag = { active: false, startX: 0, startY: 0, origLeft: 0, origTop: 0 };
|
|
310
|
-
// Overlay position
|
|
326
|
+
// Overlay position
|
|
311
327
|
this._overlayPos = null;
|
|
312
328
|
}
|
|
313
329
|
|
|
@@ -316,94 +332,263 @@
|
|
|
316
332
|
this._createOverlay();
|
|
317
333
|
this._createPanel();
|
|
318
334
|
if (this.opts.showButton) this._createButton();
|
|
335
|
+
|
|
336
|
+
// Integrate with player initialization lifecycle
|
|
337
|
+
this.player.addEventListener('playerready', () => {
|
|
338
|
+
this._checkForNativeTracks();
|
|
339
|
+
|
|
340
|
+
// If autoTranslation is enabled and we have native tracks, import immediately!
|
|
341
|
+
if (this.opts.autoTranslation && this.hasNativeTracks) {
|
|
342
|
+
// Give fetch a moment to load custom tracks via the core player
|
|
343
|
+
const attemptImport = setInterval(() => {
|
|
344
|
+
if (this._importPlayerSubtitles()) {
|
|
345
|
+
clearInterval(attemptImport);
|
|
346
|
+
if (this.player.options.debug) console.log('[AutoSub] Auto-imported native tracks for translation');
|
|
347
|
+
this.subVisible = true;
|
|
348
|
+
this._startDisplay();
|
|
349
|
+
this._setBtnActive(true);
|
|
350
|
+
|
|
351
|
+
// Trigger translation
|
|
352
|
+
this._setTranslationLang(this.opts.autoTranslation.toLowerCase().trim().slice(0, 2));
|
|
353
|
+
}
|
|
354
|
+
}, 500);
|
|
355
|
+
// Stop trying after 5 seconds to avoid infinite loops
|
|
356
|
+
setTimeout(() => clearInterval(attemptImport), 5000);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
|
|
319
360
|
if (this.opts.autoTranslation) {
|
|
320
361
|
this._scheduleAutoTranslation();
|
|
321
362
|
}
|
|
363
|
+
|
|
322
364
|
if (this.opts.autoGenerate) {
|
|
323
|
-
this.video.addEventListener('loadedmetadata', () =>
|
|
365
|
+
this.video.addEventListener('loadedmetadata', () => {
|
|
366
|
+
// Do NOT generate if the player already loaded native tracks
|
|
367
|
+
if (!this._checkForNativeTracks()) {
|
|
368
|
+
this.generate();
|
|
369
|
+
}
|
|
370
|
+
}, { once: true });
|
|
324
371
|
}
|
|
372
|
+
|
|
325
373
|
if (this.player.options.debug) console.log('[AutoSub] Plugin setup complete');
|
|
326
374
|
}
|
|
327
375
|
|
|
376
|
+
// Checks if the player has <track> elements or VTT/SRT data
|
|
377
|
+
_checkForNativeTracks() {
|
|
378
|
+
if (this.player.textTracks && this.player.textTracks.length > 0) {
|
|
379
|
+
this.hasNativeTracks = true;
|
|
380
|
+
if (this.btnEl) {
|
|
381
|
+
this.btnEl.setAttribute('title', 'Translate Subtitles');
|
|
382
|
+
}
|
|
383
|
+
return true;
|
|
384
|
+
}
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Steal subtitles from the player's custom parsed arrays
|
|
389
|
+
_importPlayerSubtitles() {
|
|
390
|
+
if (this.player.customSubtitles && this.player.customSubtitles.length > 0) {
|
|
391
|
+
let track = null;
|
|
392
|
+
|
|
393
|
+
// Try current active track first
|
|
394
|
+
if (this.player.currentCustomTrackIndex >= 0) {
|
|
395
|
+
track = this.player.customSubtitles[this.player.currentCustomTrackIndex];
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Fallback to first available track with data
|
|
399
|
+
if (!track || !track.subtitles || track.subtitles.length === 0) {
|
|
400
|
+
track = this.player.customSubtitles.find(t => t.subtitles && t.subtitles.length > 0);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (track && track.subtitles && track.subtitles.length > 0) {
|
|
404
|
+
// Deep copy segments
|
|
405
|
+
this.subtitles = track.subtitles.map(s => ({ start: s.start, end: s.end, text: s.text }));
|
|
406
|
+
this.isImportedSubs = true;
|
|
407
|
+
|
|
408
|
+
if (track.language) {
|
|
409
|
+
this.opts.language = track.language;
|
|
410
|
+
this._lastDetectedLang = track.language;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Disable native player subtitles to avoid double rendering (we take control)
|
|
414
|
+
if (typeof this.player.disableSubtitles === 'function') {
|
|
415
|
+
this.player.disableSubtitles();
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Load any previously cached translations for this imported subtitle
|
|
419
|
+
const cached = this._loadFromCache();
|
|
420
|
+
if (cached && cached.transCache) {
|
|
421
|
+
this._transCache = cached.transCache;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Save imported state immediately
|
|
425
|
+
this._saveToCache(this.subtitles, null, null, this._lastDetectedLang);
|
|
426
|
+
|
|
427
|
+
return true;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
|
|
328
433
|
_scheduleAutoTranslation() {
|
|
329
|
-
// Light polling: waits until this.subtitles is populated (from cache or transcription),
|
|
330
|
-
// then triggers translation automatically
|
|
331
434
|
const lang = this.opts.autoTranslation.toLowerCase().trim().slice(0, 2);
|
|
332
|
-
|
|
435
|
+
this.translateLang = lang;
|
|
333
436
|
|
|
437
|
+
let tries = 0;
|
|
334
438
|
const check = setInterval(() => {
|
|
335
439
|
tries++;
|
|
336
|
-
|
|
440
|
+
// Wait until transcription/import is fully complete
|
|
441
|
+
if (this.subtitles.length > 0 && !this.isGenerating) {
|
|
337
442
|
clearInterval(check);
|
|
338
|
-
if (this.player.options.debug)
|
|
339
|
-
|
|
443
|
+
if (this.player.options.debug) console.log('[AutoSub] autoTranslation final pass, lang:', lang);
|
|
444
|
+
|
|
445
|
+
this.subtitlesTrans = [];
|
|
446
|
+
// Note: We DO NOT clear this._transCache here, to preserve cached translations!
|
|
340
447
|
this._setTranslationLang(lang);
|
|
341
448
|
this._updateMenu();
|
|
342
449
|
return;
|
|
343
450
|
}
|
|
344
|
-
// Stop waiting after 5 minutes (enough for any transcription, even large videos with slow models)
|
|
345
451
|
if (tries > 300) clearInterval(check);
|
|
346
452
|
}, 1000);
|
|
347
453
|
}
|
|
348
454
|
|
|
349
455
|
async handleButtonClick() {
|
|
350
|
-
// If
|
|
456
|
+
// 1. If native tracks exist but haven't been imported yet, try to import them
|
|
457
|
+
if (this._checkForNativeTracks() && this.subtitles.length === 0) {
|
|
458
|
+
const imported = this._importPlayerSubtitles();
|
|
459
|
+
if (imported) {
|
|
460
|
+
this.subVisible = true;
|
|
461
|
+
this._startDisplay();
|
|
462
|
+
this._setBtnActive(true);
|
|
463
|
+
this._updateMenu();
|
|
464
|
+
if (this.player.options.debug) console.log('[AutoSub] Imported subtitles from player on click');
|
|
465
|
+
this._toggleMenu();
|
|
466
|
+
return;
|
|
467
|
+
} else {
|
|
468
|
+
// Fetch is probably still pending. Check cache just in case we already saved imported subs.
|
|
469
|
+
const cached = this._loadFromCache();
|
|
470
|
+
if (cached && cached.isImportedSubs) {
|
|
471
|
+
this.subtitles = cached.subtitles;
|
|
472
|
+
this._transCache = cached.transCache || {};
|
|
473
|
+
this.isImportedSubs = true;
|
|
474
|
+
this.subVisible = true;
|
|
475
|
+
this._startDisplay();
|
|
476
|
+
this._setBtnActive(true);
|
|
477
|
+
this._updateMenu();
|
|
478
|
+
this._toggleMenu();
|
|
479
|
+
return;
|
|
480
|
+
} else {
|
|
481
|
+
// Let the user know we are processing the native tracks
|
|
482
|
+
this._updatePanel('Reading native subtitles...', 100);
|
|
483
|
+
this._openPanel();
|
|
484
|
+
setTimeout(() => this._closePanel(), 1500);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// 2. If NO native tracks, check cache for Whisper generated ones
|
|
351
491
|
if (this.subtitles.length === 0 && !this.isGenerating) {
|
|
352
492
|
const cached = this._loadFromCache();
|
|
353
493
|
if (cached) {
|
|
354
|
-
this.subtitles = cached;
|
|
494
|
+
this.subtitles = cached.subtitles;
|
|
495
|
+
this._transCache = cached.transCache || {};
|
|
496
|
+
this.isImportedSubs = cached.isImportedSubs || false;
|
|
497
|
+
|
|
498
|
+
if (this.opts.autoTranslation) {
|
|
499
|
+
const tLang = this.opts.autoTranslation.toLowerCase().trim().slice(0, 2);
|
|
500
|
+
this.translateLang = tLang;
|
|
501
|
+
if (this._transCache[tLang]) {
|
|
502
|
+
this.subtitlesTrans = [...this._transCache[tLang]];
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
355
506
|
this.subVisible = true;
|
|
356
507
|
this._startDisplay();
|
|
357
508
|
this._setBtnActive(true);
|
|
358
509
|
this._updateMenu();
|
|
510
|
+
|
|
511
|
+
if (this.translateLang && this.translateLang !== 'off') {
|
|
512
|
+
this._setTranslationLang(this.translateLang);
|
|
513
|
+
}
|
|
514
|
+
|
|
359
515
|
if (this.player.options.debug)
|
|
360
|
-
console.log('[AutoSub] Loaded from cache:', cached.length, 'segments');
|
|
361
|
-
|
|
516
|
+
console.log('[AutoSub] Loaded from cache:', cached.subtitles.length, 'segments');
|
|
517
|
+
|
|
362
518
|
this._toggleMenu();
|
|
363
519
|
return;
|
|
364
520
|
}
|
|
365
521
|
}
|
|
366
|
-
|
|
522
|
+
|
|
523
|
+
// Default: Toggle the menu
|
|
367
524
|
this._toggleMenu();
|
|
368
525
|
}
|
|
369
526
|
|
|
370
527
|
async generate() {
|
|
371
|
-
if
|
|
528
|
+
// Block generation if we are already using native imported subtitles
|
|
529
|
+
if (this.isGenerating || this.isImportedSubs) return;
|
|
530
|
+
|
|
531
|
+
let partialCache = null;
|
|
372
532
|
|
|
373
|
-
// Check cache before doing anything
|
|
374
533
|
if (this.subtitles.length === 0) {
|
|
375
534
|
const cached = this._loadFromCache();
|
|
376
535
|
if (cached) {
|
|
377
|
-
this.subtitles = cached;
|
|
536
|
+
this.subtitles = cached.subtitles;
|
|
537
|
+
this._transCache = cached.transCache || {};
|
|
538
|
+
this.isImportedSubs = cached.isImportedSubs || false;
|
|
539
|
+
|
|
540
|
+
if (this.opts.autoTranslation) {
|
|
541
|
+
const tLang = this.opts.autoTranslation.toLowerCase().trim().slice(0, 2);
|
|
542
|
+
this.translateLang = tLang;
|
|
543
|
+
if (this._transCache[tLang]) {
|
|
544
|
+
this.subtitlesTrans = [...this._transCache[tLang]];
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
378
548
|
this.subVisible = true;
|
|
379
549
|
this._startDisplay();
|
|
380
550
|
this._setBtnActive(true);
|
|
381
551
|
this._updateMenu();
|
|
552
|
+
|
|
553
|
+
if (cached.chunksCompleted === null || cached.chunksCompleted >= cached.totalChunks) {
|
|
554
|
+
if (this.player.options.debug) console.log('[AutoSub] Full cache hit, skipping transcription');
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
partialCache = cached;
|
|
382
559
|
if (this.player.options.debug)
|
|
383
|
-
console.log('[AutoSub]
|
|
384
|
-
return;
|
|
560
|
+
console.log('[AutoSub] Partial cache found ā resuming from chunk', cached.chunksCompleted, 'of', cached.totalChunks);
|
|
385
561
|
}
|
|
386
562
|
}
|
|
387
563
|
|
|
388
564
|
this.isGenerating = true;
|
|
389
565
|
this._setBtnGenerating(true);
|
|
390
566
|
this._updatePanel('Extracting audio...', 5);
|
|
391
|
-
|
|
392
567
|
try {
|
|
393
568
|
const audioData = await this._extractAudio();
|
|
394
569
|
this._updatePanel('Downloading Transformers.js bundle...', 30);
|
|
395
570
|
await this._ensureBundle();
|
|
396
571
|
this._updatePanel('Preparing Whisper worker...', 38);
|
|
397
|
-
await this._transcribeChunksStreaming(audioData);
|
|
398
|
-
|
|
572
|
+
await this._transcribeChunksStreaming(audioData, partialCache);
|
|
573
|
+
|
|
574
|
+
this.subtitles.sort((a, b) => a.start - b.start);
|
|
575
|
+
|
|
576
|
+
this._saveToCache(this.subtitles, null, null, this._lastDetectedLang || null);
|
|
399
577
|
this._updatePanel('Subtitles ready!', 100);
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
578
|
+
this._setBtnActive(true);
|
|
579
|
+
this._updateMenu();
|
|
580
|
+
|
|
581
|
+
// Re-trigger translation if one was active
|
|
582
|
+
if (this.translateLang && this.translateLang !== 'off') {
|
|
583
|
+
const lang = this.translateLang;
|
|
584
|
+
this.translateLang = 'off';
|
|
585
|
+
this.subtitlesTrans = [];
|
|
586
|
+
await this._setTranslationLang(lang);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
setTimeout(() => { this._closePanel(); }, 800);
|
|
590
|
+
|
|
591
|
+
if (this.player.options.debug) console.log('[AutoSub] Generation complete:', this.subtitles.length, 'segments');
|
|
407
592
|
} catch (err) {
|
|
408
593
|
if (this.player.options.debug) console.error('[AutoSub] Generation error:', err);
|
|
409
594
|
this._updatePanel('Error: ' + (err.message || String(err)), 0);
|
|
@@ -423,6 +608,10 @@
|
|
|
423
608
|
dispose() {
|
|
424
609
|
this._stopDisplay();
|
|
425
610
|
this._destroyDrag();
|
|
611
|
+
if (this._resizeObserver) {
|
|
612
|
+
this._resizeObserver.disconnect();
|
|
613
|
+
this._resizeObserver = null;
|
|
614
|
+
}
|
|
426
615
|
if (this._captureTimer) { clearTimeout(this._captureTimer); this._captureTimer = null; }
|
|
427
616
|
if (this._captureProgressInterval) { clearInterval(this._captureProgressInterval); this._captureProgressInterval = null; }
|
|
428
617
|
if (this._worker) { this._worker.terminate(); this._worker = null; }
|
|
@@ -461,7 +650,10 @@
|
|
|
461
650
|
|
|
462
651
|
const text = document.createElement('div');
|
|
463
652
|
text.className = 'myetv-autosub-text';
|
|
464
|
-
|
|
653
|
+
|
|
654
|
+
const styleWithoutFont = Object.assign({}, this.opts.subtitleStyle);
|
|
655
|
+
delete styleWithoutFont.fontSize;
|
|
656
|
+
Object.assign(text.style, styleWithoutFont);
|
|
465
657
|
|
|
466
658
|
overlay.appendChild(handle);
|
|
467
659
|
overlay.appendChild(text);
|
|
@@ -471,11 +663,50 @@
|
|
|
471
663
|
this.overlayText = text;
|
|
472
664
|
|
|
473
665
|
this._initDrag(handle, overlay);
|
|
666
|
+
|
|
667
|
+
this._resizeObserver = new ResizeObserver(() => {
|
|
668
|
+
this._applyResponsiveFontSize();
|
|
669
|
+
});
|
|
670
|
+
this._resizeObserver.observe(this.container);
|
|
671
|
+
|
|
672
|
+
this._applyResponsiveFontSize();
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
_applyResponsiveFontSize() {
|
|
676
|
+
if (!this.overlayText) return;
|
|
677
|
+
|
|
678
|
+
const rect = this.container.getBoundingClientRect();
|
|
679
|
+
const w = rect.width;
|
|
680
|
+
const h = rect.height;
|
|
681
|
+
|
|
682
|
+
const REFERENCE_WIDTH = 800;
|
|
683
|
+
|
|
684
|
+
let basePx = 18;
|
|
685
|
+
const userFontSize = this.opts.subtitleStyle?.fontSize;
|
|
686
|
+
if (userFontSize) {
|
|
687
|
+
const match = String(userFontSize).match(/([\d.]+)(px|em|rem)?/);
|
|
688
|
+
if (match) {
|
|
689
|
+
const val = parseFloat(match[1]);
|
|
690
|
+
const unit = match[2] || 'px';
|
|
691
|
+
if (unit === 'px') basePx = val;
|
|
692
|
+
else if (unit === 'em' || unit === 'rem') basePx = val * 16;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
let size = basePx * (w / REFERENCE_WIDTH);
|
|
697
|
+
|
|
698
|
+
if (w < 260 || h < 150) {
|
|
699
|
+
size = Math.min(size, 8);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
size = Math.max(size, 8);
|
|
703
|
+
size = Math.min(size, basePx);
|
|
704
|
+
|
|
705
|
+
this.overlayText.style.fontSize = size + 'px';
|
|
474
706
|
}
|
|
475
707
|
|
|
476
708
|
_initDrag(handle, overlay) {
|
|
477
709
|
const onStart = (e) => {
|
|
478
|
-
// Prevent conflicts with player controls
|
|
479
710
|
e.stopPropagation();
|
|
480
711
|
const isTouch = e.type === 'touchstart';
|
|
481
712
|
const clientX = isTouch ? e.touches[0].clientX : e.clientX;
|
|
@@ -484,7 +715,6 @@
|
|
|
484
715
|
const rect = overlay.getBoundingClientRect();
|
|
485
716
|
const contRect = this.container.getBoundingClientRect();
|
|
486
717
|
|
|
487
|
-
// Switch to absolute positioning on first drag
|
|
488
718
|
if (!this._overlayPos) {
|
|
489
719
|
const currentLeft = rect.left - contRect.left + rect.width / 2;
|
|
490
720
|
const currentTop = rect.top - contRect.top + rect.height / 2;
|
|
@@ -537,7 +767,9 @@
|
|
|
537
767
|
};
|
|
538
768
|
|
|
539
769
|
handle.addEventListener('mousedown', onStart);
|
|
770
|
+
overlay.addEventListener('touchstart', onStart, { passive: false });
|
|
540
771
|
handle.addEventListener('touchstart', onStart, { passive: false });
|
|
772
|
+
|
|
541
773
|
this._drag._onEnd = onEnd;
|
|
542
774
|
}
|
|
543
775
|
|
|
@@ -600,7 +832,6 @@
|
|
|
600
832
|
_createButton() {
|
|
601
833
|
const position = this.opts.position;
|
|
602
834
|
|
|
603
|
-
// Button wrapper only (menu is appended directly to body)
|
|
604
835
|
const wrap = document.createElement('div');
|
|
605
836
|
wrap.style.cssText = 'position:relative;display:inline-flex;align-items:center;';
|
|
606
837
|
|
|
@@ -616,13 +847,11 @@
|
|
|
616
847
|
|
|
617
848
|
wrap.appendChild(btn);
|
|
618
849
|
|
|
619
|
-
// Menu is appended directly to body ā escapes any stacking context from the player
|
|
620
850
|
const menu = document.createElement('div');
|
|
621
851
|
menu.className = 'myetv-autosub-menu';
|
|
622
852
|
menu.addEventListener('click', (e) => e.stopPropagation());
|
|
623
853
|
document.body.appendChild(menu);
|
|
624
854
|
|
|
625
|
-
// Insert button into the player DOM
|
|
626
855
|
if (position === 'topbar') {
|
|
627
856
|
const settingsControl = this.container.querySelector('.settings-control');
|
|
628
857
|
if (settingsControl) settingsControl.insertBefore(wrap, settingsControl.firstChild);
|
|
@@ -646,7 +875,6 @@
|
|
|
646
875
|
this.btnEl = btn;
|
|
647
876
|
this.menuEl = menu;
|
|
648
877
|
|
|
649
|
-
// Close menu when clicking outside
|
|
650
878
|
this._menuOutsideHandler = (e) => {
|
|
651
879
|
if (!menu.contains(e.target) && e.target !== btn) {
|
|
652
880
|
this._closeMenuNow();
|
|
@@ -654,7 +882,6 @@
|
|
|
654
882
|
};
|
|
655
883
|
document.addEventListener('click', this._menuOutsideHandler);
|
|
656
884
|
|
|
657
|
-
// Close menu on Escape key
|
|
658
885
|
this._menuEscHandler = (e) => {
|
|
659
886
|
if (e.key === 'Escape') this._closeMenuNow();
|
|
660
887
|
};
|
|
@@ -672,10 +899,8 @@
|
|
|
672
899
|
return;
|
|
673
900
|
}
|
|
674
901
|
|
|
675
|
-
// Rebuild menu content
|
|
676
902
|
this._buildMenu();
|
|
677
903
|
|
|
678
|
-
// Position before showing (measure offset first)
|
|
679
904
|
this.menuEl.style.visibility = 'hidden';
|
|
680
905
|
this.menuEl.style.display = 'block';
|
|
681
906
|
|
|
@@ -685,14 +910,12 @@
|
|
|
685
910
|
const vpH = window.innerHeight;
|
|
686
911
|
const vpW = window.innerWidth;
|
|
687
912
|
|
|
688
|
-
// Open above or below depending on available space
|
|
689
913
|
const spaceAbove = btnRect.top - 8;
|
|
690
914
|
const spaceBelow = vpH - btnRect.bottom - 8;
|
|
691
915
|
let top = (spaceAbove >= menuH || spaceAbove >= spaceBelow)
|
|
692
916
|
? btnRect.top - menuH - 8
|
|
693
917
|
: btnRect.bottom + 8;
|
|
694
918
|
|
|
695
|
-
// Align to the right edge of the button, clamped to viewport
|
|
696
919
|
let left = btnRect.right - menuW;
|
|
697
920
|
left = Math.max(4, Math.min(vpW - menuW - 4, left));
|
|
698
921
|
top = Math.max(4, Math.min(vpH - menuH - 4, top));
|
|
@@ -737,26 +960,29 @@
|
|
|
737
960
|
sec1.appendChild(itemOff);
|
|
738
961
|
this.menuEl.appendChild(sec1);
|
|
739
962
|
|
|
740
|
-
// āā SECTION 2: GENERATE / PANEL
|
|
741
|
-
|
|
963
|
+
// āā SECTION 2: GENERATE / PANEL (Only if NOT using native imported subs)
|
|
964
|
+
if (!this.isImportedSubs) {
|
|
965
|
+
const sec2 = this._menuSection('Management');
|
|
742
966
|
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
967
|
+
const genLabel = isGenerating ? 'ā³ Generating...' : hasSubs ? 'š Regenerate transcription' : 'šļø Generate subtitles';
|
|
968
|
+
const itemGen = this._menuItem(genLabel, false, isGenerating, () => {
|
|
969
|
+
this._deleteCache();
|
|
970
|
+
this.subtitles = [];
|
|
971
|
+
this.subtitlesTrans = [];
|
|
972
|
+
this._transCache = {};
|
|
973
|
+
this._closeMenuNow();
|
|
974
|
+
this.generate();
|
|
975
|
+
});
|
|
751
976
|
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
977
|
+
const itemPanel = this._menuItem('š Show progress', false, !isGenerating, () => {
|
|
978
|
+
this._closeMenuNow();
|
|
979
|
+
this._openPanel();
|
|
980
|
+
});
|
|
756
981
|
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
982
|
+
sec2.appendChild(itemGen);
|
|
983
|
+
if (isGenerating) sec2.appendChild(itemPanel);
|
|
984
|
+
this.menuEl.appendChild(sec2);
|
|
985
|
+
}
|
|
760
986
|
|
|
761
987
|
// āā SECTION 3: TRANSLATION āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
762
988
|
if (hasSubs) {
|
|
@@ -831,14 +1057,20 @@
|
|
|
831
1057
|
|
|
832
1058
|
async _setTranslationLang(langCode) {
|
|
833
1059
|
this.translateLang = langCode;
|
|
834
|
-
|
|
835
1060
|
if (langCode === 'off') {
|
|
836
1061
|
this.subtitlesTrans = [];
|
|
837
1062
|
if (this.player.options.debug) console.log('[AutoSub] Translation disabled');
|
|
838
1063
|
return;
|
|
839
1064
|
}
|
|
840
1065
|
|
|
841
|
-
|
|
1066
|
+
const sourceLang = this._resolveLanguage(this.opts.language);
|
|
1067
|
+
if (sourceLang && sourceLang === langCode.slice(0, 2)) {
|
|
1068
|
+
if (this.player.options.debug) console.log('[AutoSub] Source = target lang, skipping translation');
|
|
1069
|
+
this.subtitlesTrans = [];
|
|
1070
|
+
this.translateLang = 'off';
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
842
1074
|
if (this._transCache[langCode]) {
|
|
843
1075
|
this.subtitlesTrans = this._transCache[langCode];
|
|
844
1076
|
if (this.player.options.debug) console.log('[AutoSub] Translation cache hit:', langCode);
|
|
@@ -849,10 +1081,13 @@
|
|
|
849
1081
|
this._setBtnGenerating(true);
|
|
850
1082
|
|
|
851
1083
|
try {
|
|
852
|
-
// Batch segments to minimize API requests (max ~400 chars per request)
|
|
853
1084
|
const translated = await this._translateBatch(this.subtitles, langCode);
|
|
854
1085
|
this.subtitlesTrans = translated;
|
|
855
1086
|
this._transCache[langCode] = translated;
|
|
1087
|
+
|
|
1088
|
+
// Save translations persistently to cache
|
|
1089
|
+
this._saveToCache(this.subtitles, null, null, this._lastDetectedLang);
|
|
1090
|
+
|
|
856
1091
|
if (this.player.options.debug) console.log('[AutoSub] Translation done:', translated.length, 'segments');
|
|
857
1092
|
} catch (err) {
|
|
858
1093
|
if (this.player.options.debug) console.warn('[AutoSub] Translation error:', err.message);
|
|
@@ -863,61 +1098,133 @@
|
|
|
863
1098
|
}
|
|
864
1099
|
}
|
|
865
1100
|
|
|
1101
|
+
async _translateText(text, sourceLang, targetLang) {
|
|
1102
|
+
const engine = this.opts.translationEngine;
|
|
1103
|
+
|
|
1104
|
+
if (!engine || engine.type === 'mymemory') {
|
|
1105
|
+
const url = `https://api.mymemory.translated.net/get?q=${encodeURIComponent(text)}&langpair=${sourceLang}|${targetLang}`;
|
|
1106
|
+
const resp = await fetch(url);
|
|
1107
|
+
const json = await resp.json();
|
|
1108
|
+
if (json.responseStatus && json.responseStatus !== 200) {
|
|
1109
|
+
throw new Error('MyMemory error: ' + json.responseDetails + ' (status ' + json.responseStatus + ')');
|
|
1110
|
+
}
|
|
1111
|
+
return json.responseData?.translatedText ?? text;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
if (engine.type === 'libretranslate') {
|
|
1115
|
+
const body = { q: text, source: sourceLang, target: targetLang, format: 'text' };
|
|
1116
|
+
if (engine.apiKey) body.api_key = engine.apiKey;
|
|
1117
|
+
const resp = await fetch(engine.url.replace(/\/$/, '') + '/translate', {
|
|
1118
|
+
method: 'POST',
|
|
1119
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1120
|
+
body: JSON.stringify(body)
|
|
1121
|
+
});
|
|
1122
|
+
if (!resp.ok) throw new Error('LibreTranslate HTTP ' + resp.status);
|
|
1123
|
+
const json = await resp.json();
|
|
1124
|
+
if (json.error) throw new Error('LibreTranslate: ' + json.error);
|
|
1125
|
+
return json.translatedText ?? text;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
if (engine.type === 'marianmt') {
|
|
1129
|
+
const resp = await fetch(engine.url.replace(/\/$/, '') + '/translate', {
|
|
1130
|
+
method: 'POST',
|
|
1131
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1132
|
+
body: JSON.stringify({ q: text, source: sourceLang, target: targetLang })
|
|
1133
|
+
});
|
|
1134
|
+
if (!resp.ok) throw new Error('MarianMT HTTP ' + resp.status);
|
|
1135
|
+
const json = await resp.json();
|
|
1136
|
+
return json.translatedText ?? json.translation ?? text;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
throw new Error('Unknown translation engine: ' + engine.type);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
866
1142
|
async _translateBatch(segments, targetLang) {
|
|
1143
|
+
const SEP = ' ||| ';
|
|
867
1144
|
const BATCH_CHARS = 400;
|
|
868
|
-
const SEP = '\n||||\n';
|
|
869
1145
|
|
|
870
|
-
// āā Detect source language āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
871
|
-
// 1. Use the language set in plugin options (if provided)
|
|
872
|
-
// 2. Otherwise try to detect it from the first segment via MyMemory
|
|
873
|
-
// 3. Absolute fallback: 'en'
|
|
874
1146
|
let sourceLang = null;
|
|
875
|
-
|
|
876
1147
|
if (this.opts.language) {
|
|
877
|
-
// Normalize: 'italian' ā 'it', 'it' ā 'it'
|
|
878
1148
|
const lower = this.opts.language.toLowerCase().trim();
|
|
879
|
-
// Reverse lookup in LANGUAGE_MAP (value ā key)
|
|
880
1149
|
const found = Object.entries(LANGUAGE_MAP).find(([k, v]) => k === lower || v === lower);
|
|
881
|
-
sourceLang = found ? found[0] : lower.slice(0, 2);
|
|
1150
|
+
sourceLang = found ? found[0] : lower.slice(0, 2);
|
|
882
1151
|
}
|
|
883
|
-
|
|
884
1152
|
if (!sourceLang && segments.length > 0) {
|
|
885
|
-
// Try to detect language from the first segment via MyMemory
|
|
886
1153
|
try {
|
|
887
1154
|
const sample = segments[0].text.slice(0, 100);
|
|
888
1155
|
const detectUrl = `https://api.mymemory.translated.net/get?q=${encodeURIComponent(sample)}&langpair=en|en`;
|
|
889
1156
|
const resp = await fetch(detectUrl);
|
|
890
1157
|
const json = await resp.json();
|
|
891
|
-
// MyMemory returns the detected language in responseData
|
|
892
1158
|
const detected = json.responseData?.detectedLanguage;
|
|
893
|
-
if (
|
|
894
|
-
|
|
895
|
-
}
|
|
896
|
-
} catch (_) { }
|
|
1159
|
+
if (/[a-z]{2}-[A-Z]{2}/.test(detected)) sourceLang = detected.slice(0, 2);
|
|
1160
|
+
} catch { }
|
|
897
1161
|
}
|
|
1162
|
+
if (!sourceLang) sourceLang = 'en';
|
|
898
1163
|
|
|
899
|
-
|
|
1164
|
+
const normTarget = LANGUAGE_MAP[targetLang]
|
|
1165
|
+
? targetLang
|
|
1166
|
+
: (Object.entries(LANGUAGE_MAP).find(([k, v]) => v === targetLang)?.[0] ?? targetLang.slice(0, 2));
|
|
900
1167
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
if (this.player.options.debug)
|
|
904
|
-
console.log('[AutoSub] Source === target lang, skipping translation');
|
|
1168
|
+
if (sourceLang === normTarget) {
|
|
1169
|
+
if (this.player.options.debug) console.log('[AutoSub] Source = target lang, skipping translation');
|
|
905
1170
|
return segments.map(s => ({ ...s }));
|
|
906
1171
|
}
|
|
907
1172
|
|
|
908
|
-
if (this.player.options.debug)
|
|
909
|
-
console.log('[AutoSub] Translating', sourceLang, 'ā', targetLang);
|
|
1173
|
+
if (this.player.options.debug) console.log('[AutoSub] Translating', sourceLang, 'ā', normTarget);
|
|
910
1174
|
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
1175
|
+
const engine = this.opts.translationEngine;
|
|
1176
|
+
|
|
1177
|
+
if (engine?.type === 'libretranslate') {
|
|
1178
|
+
const BATCH_SIZE = 50;
|
|
1179
|
+
const result = [];
|
|
1180
|
+
|
|
1181
|
+
let url = engine.url.replace(/\/$/, '');
|
|
1182
|
+
if (!url.endsWith('/translate') && !url.includes('.php')) url += '/translate';
|
|
1183
|
+
|
|
1184
|
+
for (let i = 0; i < segments.length; i += BATCH_SIZE) {
|
|
1185
|
+
const batch = segments.slice(i, i + BATCH_SIZE);
|
|
1186
|
+
try {
|
|
1187
|
+
const body = {
|
|
1188
|
+
q: batch.map(s => s.text),
|
|
1189
|
+
source: sourceLang,
|
|
1190
|
+
target: normTarget,
|
|
1191
|
+
format: 'text'
|
|
1192
|
+
};
|
|
1193
|
+
if (engine.apiKey) body.api_key = engine.apiKey;
|
|
1194
|
+
|
|
1195
|
+
const resp = await fetch(url, {
|
|
1196
|
+
method: 'POST',
|
|
1197
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1198
|
+
body: JSON.stringify(body)
|
|
1199
|
+
});
|
|
1200
|
+
if (!resp.ok) throw new Error(`LibreTranslate HTTP ${resp.status}`);
|
|
1201
|
+
const json = await resp.json();
|
|
1202
|
+
if (json.error) throw new Error(`LibreTranslate: ${json.error}`);
|
|
1203
|
+
|
|
1204
|
+
const translations = Array.isArray(json.translatedText)
|
|
1205
|
+
? json.translatedText
|
|
1206
|
+
: [json.translatedText];
|
|
1207
|
+
|
|
1208
|
+
batch.forEach((seg, j) => result.push({
|
|
1209
|
+
start: seg.start,
|
|
1210
|
+
end: seg.end,
|
|
1211
|
+
text: translations[j] ?? seg.text
|
|
1212
|
+
}));
|
|
1213
|
+
} catch (err) {
|
|
1214
|
+
if (this.player.options.debug) console.warn('[AutoSub] LibreTranslate batch error', err.message);
|
|
1215
|
+
batch.forEach(seg => result.push({ ...seg }));
|
|
1216
|
+
}
|
|
1217
|
+
if (i + BATCH_SIZE < segments.length)
|
|
1218
|
+
await new Promise(r => setTimeout(r, 100));
|
|
1219
|
+
}
|
|
1220
|
+
return result;
|
|
1221
|
+
}
|
|
915
1222
|
|
|
1223
|
+
const batches = [];
|
|
1224
|
+
let current = [], charCount = 0;
|
|
916
1225
|
for (const seg of segments) {
|
|
917
1226
|
if (charCount + seg.text.length > BATCH_CHARS && current.length > 0) {
|
|
918
|
-
batches.push(current);
|
|
919
|
-
current = [];
|
|
920
|
-
charCount = 0;
|
|
1227
|
+
batches.push(current); current = []; charCount = 0;
|
|
921
1228
|
}
|
|
922
1229
|
current.push(seg);
|
|
923
1230
|
charCount += seg.text.length;
|
|
@@ -927,27 +1234,17 @@
|
|
|
927
1234
|
const result = [];
|
|
928
1235
|
for (const batch of batches) {
|
|
929
1236
|
const sourceText = batch.map(s => s.text).join(SEP);
|
|
930
|
-
const url = `https://api.mymemory.translated.net/get?q=${encodeURIComponent(sourceText)}&langpair=${sourceLang}|${targetLang}`;
|
|
931
1237
|
try {
|
|
932
|
-
const
|
|
933
|
-
const
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
const translated = json.responseData?.translatedText || sourceText;
|
|
943
|
-
const parts = translated.split('||||').map(s => s.trim().replace(/^\n+|\n+$/g, ''));
|
|
944
|
-
batch.forEach((seg, i) => {
|
|
945
|
-
result.push({ start: seg.start, end: seg.end, text: parts[i] || seg.text });
|
|
946
|
-
});
|
|
947
|
-
await new Promise(r => setTimeout(r, 120)); // rate-limit delay between batches
|
|
948
|
-
} catch (_) {
|
|
949
|
-
batch.forEach(seg => result.push({ ...seg })); // fallback: keep original text
|
|
1238
|
+
const translated = await this._translateText(sourceText, sourceLang, normTarget);
|
|
1239
|
+
const parts = translated.split(SEP).map(s => s.trim().replace(/&/g, '&'));
|
|
1240
|
+
batch.forEach((seg, i) => result.push({
|
|
1241
|
+
start: seg.start, end: seg.end, text: parts[i] ?? seg.text
|
|
1242
|
+
}));
|
|
1243
|
+
} catch (err) {
|
|
1244
|
+
if (this.player.options.debug) console.warn('[AutoSub] Translation batch error', err.message);
|
|
1245
|
+
batch.forEach(seg => result.push({ ...seg }));
|
|
950
1246
|
}
|
|
1247
|
+
await new Promise(r => setTimeout(r, 120));
|
|
951
1248
|
}
|
|
952
1249
|
return result;
|
|
953
1250
|
}
|
|
@@ -958,6 +1255,7 @@
|
|
|
958
1255
|
|
|
959
1256
|
_startDisplay() {
|
|
960
1257
|
this._stopDisplay();
|
|
1258
|
+
this._applyResponsiveFontSize();
|
|
961
1259
|
this.updateInterval = setInterval(() => this._tick(), 80);
|
|
962
1260
|
}
|
|
963
1261
|
|
|
@@ -973,7 +1271,6 @@
|
|
|
973
1271
|
_tick() {
|
|
974
1272
|
if (!this.subVisible) return;
|
|
975
1273
|
|
|
976
|
-
// Use translated segments if available, otherwise fall back to originals
|
|
977
1274
|
const pool = (this.translateLang !== 'off' && this.subtitlesTrans.length > 0)
|
|
978
1275
|
? this.subtitlesTrans
|
|
979
1276
|
: this.subtitles;
|
|
@@ -1010,7 +1307,6 @@
|
|
|
1010
1307
|
|
|
1011
1308
|
_resolveMpdUrl() {
|
|
1012
1309
|
try {
|
|
1013
|
-
// Salvato sul container dal player al momento di initialize()
|
|
1014
1310
|
const url = this.container.dataset.mpdUrl || this.video.dataset.mpdUrl || null;
|
|
1015
1311
|
if (url && !url.startsWith('blob:')) return url;
|
|
1016
1312
|
} catch (_) { }
|
|
@@ -1035,7 +1331,6 @@
|
|
|
1035
1331
|
if (this.player.options.debug)
|
|
1036
1332
|
console.log('[AutoSub] _extractAudio ā currentSrc:', this.video.currentSrc, '| adaptiveStreamingType:', this.player.adaptiveStreamingType);
|
|
1037
1333
|
|
|
1038
|
-
// āā DASH: resolve real MPD URL via dash.js instance āāāāāāāāāāāāāāāāāāā
|
|
1039
1334
|
const mpdUrl = this._resolveMpdUrl();
|
|
1040
1335
|
if (mpdUrl) {
|
|
1041
1336
|
if (this.player.options.debug) console.log('[AutoSub] DASH mode ā mpdUrl:', mpdUrl);
|
|
@@ -1046,13 +1341,11 @@
|
|
|
1046
1341
|
}
|
|
1047
1342
|
}
|
|
1048
1343
|
|
|
1049
|
-
// āā HLS āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
1050
1344
|
const src = this.video.currentSrc || this.video.src || '';
|
|
1051
1345
|
if (src.includes('.m3u8') || this.player.adaptiveStreamingType === 'hls') {
|
|
1052
1346
|
return await this._extractAudioFromCaptureStream();
|
|
1053
1347
|
}
|
|
1054
1348
|
|
|
1055
|
-
// āā Normal MP4 / WebM ā direct fetch āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
1056
1349
|
try { return await this._extractAudioFromFetch(); }
|
|
1057
1350
|
catch (err) {
|
|
1058
1351
|
if (this.player.options.debug) console.warn('[AutoSub] Fetch strategy failed:', err.message, 'ā fallback to captureStream');
|
|
@@ -1070,7 +1363,6 @@
|
|
|
1070
1363
|
const parser = new DOMParser();
|
|
1071
1364
|
const mpdDoc = parser.parseFromString(mpdText, 'application/xml');
|
|
1072
1365
|
|
|
1073
|
-
// Check for XML parse errors
|
|
1074
1366
|
const parseErr = mpdDoc.querySelector('parsererror');
|
|
1075
1367
|
if (parseErr) throw new Error('MPD XML parse error: ' + parseErr.textContent.slice(0, 100));
|
|
1076
1368
|
|
|
@@ -1090,7 +1382,6 @@
|
|
|
1090
1382
|
|
|
1091
1383
|
this._updatePanel('Downloading audio segments (0/' + segmentUrls.length + ')...', 12);
|
|
1092
1384
|
|
|
1093
|
-
// Download segments in parallel batches of 5
|
|
1094
1385
|
const BATCH = 5;
|
|
1095
1386
|
const segmentBuffers = new Array(segmentUrls.length);
|
|
1096
1387
|
|
|
@@ -1110,7 +1401,6 @@
|
|
|
1110
1401
|
);
|
|
1111
1402
|
}
|
|
1112
1403
|
|
|
1113
|
-
// Concatenate all segment buffers into one
|
|
1114
1404
|
this._updatePanel('Decoding audio...', 30);
|
|
1115
1405
|
const totalBytes = segmentBuffers.reduce((s, b) => s + b.byteLength, 0);
|
|
1116
1406
|
const combined = new Uint8Array(totalBytes);
|
|
@@ -1146,13 +1436,11 @@
|
|
|
1146
1436
|
|
|
1147
1437
|
_findAudioAdaptationSet(mpdDoc) {
|
|
1148
1438
|
const sets = Array.from(mpdDoc.querySelectorAll('AdaptationSet'));
|
|
1149
|
-
// Prefer explicit audio contentType or mimeType
|
|
1150
1439
|
return sets.find(s =>
|
|
1151
1440
|
s.getAttribute('contentType') === 'audio' ||
|
|
1152
1441
|
(s.getAttribute('mimeType') || '').startsWith('audio') ||
|
|
1153
1442
|
s.querySelector('Representation[mimeType^="audio"]') !== null
|
|
1154
1443
|
) || sets.find(s =>
|
|
1155
|
-
// Fallback: AdaptationSet that is not video
|
|
1156
1444
|
!(s.getAttribute('mimeType') || '').startsWith('video') &&
|
|
1157
1445
|
s.getAttribute('contentType') !== 'video'
|
|
1158
1446
|
);
|
|
@@ -1171,7 +1459,6 @@
|
|
|
1171
1459
|
_buildSegmentUrlList(mpdDoc, adaptationSet, representation, baseUrl) {
|
|
1172
1460
|
const urls = [];
|
|
1173
1461
|
|
|
1174
|
-
// āā SegmentList āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
1175
1462
|
const segList = representation.querySelector('SegmentList')
|
|
1176
1463
|
|| adaptationSet.querySelector('SegmentList');
|
|
1177
1464
|
if (segList) {
|
|
@@ -1187,7 +1474,6 @@
|
|
|
1187
1474
|
return urls;
|
|
1188
1475
|
}
|
|
1189
1476
|
|
|
1190
|
-
// āā SegmentTemplate āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
1191
1477
|
const segTpl = representation.querySelector('SegmentTemplate')
|
|
1192
1478
|
|| adaptationSet.querySelector('SegmentTemplate');
|
|
1193
1479
|
if (segTpl) {
|
|
@@ -1222,7 +1508,6 @@
|
|
|
1222
1508
|
return urls;
|
|
1223
1509
|
}
|
|
1224
1510
|
|
|
1225
|
-
// āā BaseURL (single audio file) āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
1226
1511
|
const baseURLEl = representation.querySelector('BaseURL')
|
|
1227
1512
|
|| adaptationSet.querySelector('BaseURL');
|
|
1228
1513
|
if (baseURLEl) {
|
|
@@ -1240,7 +1525,6 @@
|
|
|
1240
1525
|
}
|
|
1241
1526
|
|
|
1242
1527
|
_parseDuration(iso) {
|
|
1243
|
-
// Parse ISO 8601 duration: PT1H2M3.5S
|
|
1244
1528
|
const m = iso.match(/PT(?:(\d+)H)?(?:(\d+)M)?(?:([\d.]+)S)?/);
|
|
1245
1529
|
if (!m) return 0;
|
|
1246
1530
|
return (parseFloat(m[1] || 0) * 3600) +
|
|
@@ -1314,7 +1598,6 @@
|
|
|
1314
1598
|
if (e.data.size === 0) return;
|
|
1315
1599
|
chunkQueue.push(e.data);
|
|
1316
1600
|
|
|
1317
|
-
// Estimate accumulated seconds using rough bitrate of 16 kbps (opus)
|
|
1318
1601
|
const totalSize = chunkQueue.reduce((s, b) => s + b.size, 0);
|
|
1319
1602
|
const estimatedSec = totalSize / (16000 / 8);
|
|
1320
1603
|
|
|
@@ -1328,7 +1611,6 @@
|
|
|
1328
1611
|
};
|
|
1329
1612
|
|
|
1330
1613
|
recorder.onstop = async () => {
|
|
1331
|
-
// Flush any remaining audio as the last chunk
|
|
1332
1614
|
if (chunkQueue.length > 0 && this._onAudioChunkReady) {
|
|
1333
1615
|
const blob = new Blob([...chunkQueue], { type: recorder.mimeType || 'audio/webm' });
|
|
1334
1616
|
chunkQueue.length = 0;
|
|
@@ -1340,7 +1622,6 @@
|
|
|
1340
1622
|
|
|
1341
1623
|
recorder.onerror = e => reject(e.error || new Error('MediaRecorder error'));
|
|
1342
1624
|
|
|
1343
|
-
// Update capture progress based on video currentTime
|
|
1344
1625
|
const duration = isFinite(this.video.duration) && this.video.duration > 0
|
|
1345
1626
|
? this.video.duration : null;
|
|
1346
1627
|
|
|
@@ -1364,10 +1645,8 @@
|
|
|
1364
1645
|
|
|
1365
1646
|
this.video.addEventListener('ended', stop, { once: true });
|
|
1366
1647
|
|
|
1367
|
-
// Slice every 2s so ondataavailable fires frequently
|
|
1368
1648
|
recorder.start(2000);
|
|
1369
1649
|
|
|
1370
|
-
// Resolve immediately with sentinel ā actual transcription flows via callbacks
|
|
1371
1650
|
resolve({ _captureMode: true, stop });
|
|
1372
1651
|
});
|
|
1373
1652
|
}
|
|
@@ -1417,17 +1696,14 @@ self.onmessage = async function(e) {
|
|
|
1417
1696
|
}
|
|
1418
1697
|
if (type === 'transcribe') {
|
|
1419
1698
|
try {
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
result, timeOffset: payload.timeOffset,
|
|
1426
|
-
chunkIndex: payload.chunkIndex, totalChunks: payload.totalChunks,
|
|
1427
|
-
isLast: payload.isLast
|
|
1428
|
-
}});
|
|
1699
|
+
const audio = new Float32Array(payload.audio);
|
|
1700
|
+
const opts = { return_timestamps: true, task: 'transcribe', chunk_length_s: payload.chunkSec };
|
|
1701
|
+
if (payload.lang) opts.language = payload.lang;
|
|
1702
|
+
const result = await transcriber(audio, opts);
|
|
1703
|
+
const detectedLang = result.language || payload.lang || null;
|
|
1704
|
+
self.postMessage({ type: 'chunk_done', payload: { result, timeOffset: payload.timeOffset, chunkIndex: payload.chunkIndex, totalChunks: payload.totalChunks, isLast: payload.isLast, detectedLang } });
|
|
1429
1705
|
} catch(err) {
|
|
1430
|
-
|
|
1706
|
+
self.postMessage({ type: 'error', payload: err.message || String(err) });
|
|
1431
1707
|
}
|
|
1432
1708
|
}
|
|
1433
1709
|
};
|
|
@@ -1455,9 +1731,7 @@ self.onmessage = async function(e) {
|
|
|
1455
1731
|
);
|
|
1456
1732
|
}
|
|
1457
1733
|
|
|
1458
|
-
async _transcribeChunksStreaming(audioData) {
|
|
1459
|
-
|
|
1460
|
-
// āā CAPTURE STREAM MODE (HLS or DASH fallback) āāāāāāāāāāāāāāāāāāāāāāā
|
|
1734
|
+
async _transcribeChunksStreaming(audioData, partialCache = null) {
|
|
1461
1735
|
if (audioData?._captureMode) {
|
|
1462
1736
|
const SAMPLE_RATE = 16000;
|
|
1463
1737
|
const lang = this._resolveLanguage(this.opts.language);
|
|
@@ -1465,7 +1739,6 @@ self.onmessage = async function(e) {
|
|
|
1465
1739
|
|
|
1466
1740
|
if (!this.subVisible) { this.subVisible = true; this._startDisplay(); }
|
|
1467
1741
|
|
|
1468
|
-
// Worker starts in parallel while audio is still being captured
|
|
1469
1742
|
const worker = this._createTranscriberWorker();
|
|
1470
1743
|
this._worker = worker;
|
|
1471
1744
|
|
|
@@ -1547,13 +1820,21 @@ self.onmessage = async function(e) {
|
|
|
1547
1820
|
|
|
1548
1821
|
if (type === 'chunk_done') {
|
|
1549
1822
|
const { result, timeOffset, isLast } = payload;
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1823
|
+
|
|
1824
|
+
let rawChunks = (result.chunks || [])
|
|
1825
|
+
.filter(c => c.text?.trim())
|
|
1826
|
+
.map(c => {
|
|
1827
|
+
const ts0 = (c.timestamp?.[0] != null) ? c.timestamp[0] : 0;
|
|
1828
|
+
const ts1 = (c.timestamp?.[1] != null) ? c.timestamp[1] : ts0 + 5;
|
|
1829
|
+
return { text: c.text.trim(), start: ts0 + timeOffset, end: ts1 + timeOffset };
|
|
1830
|
+
});
|
|
1831
|
+
|
|
1832
|
+
if (rawChunks.length === 0 && result.text?.trim()) {
|
|
1833
|
+
rawChunks = [{ text: result.text.trim(), start: timeOffset, end: timeOffset + 30 }];
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1555
1836
|
this.subtitles.push(...this._normalizeChunks(rawChunks));
|
|
1556
|
-
if (this.opts.cacheEnabled) this._saveToCache(this.subtitles);
|
|
1837
|
+
if (this.opts.cacheEnabled) this._saveToCache(this.subtitles, null, null);
|
|
1557
1838
|
|
|
1558
1839
|
activeJob = false;
|
|
1559
1840
|
|
|
@@ -1586,15 +1867,22 @@ self.onmessage = async function(e) {
|
|
|
1586
1867
|
return promise;
|
|
1587
1868
|
}
|
|
1588
1869
|
|
|
1589
|
-
// āā NORMAL MODE (Float32Array from fetch or DASH segment download) āāāā
|
|
1590
1870
|
const float32Audio = audioData;
|
|
1591
1871
|
const SAMPLE_RATE = 16000;
|
|
1592
1872
|
const CHUNK_SEC = 30;
|
|
1593
1873
|
const chunkSize = SAMPLE_RATE * CHUNK_SEC;
|
|
1594
1874
|
const totalChunks = Math.ceil(float32Audio.length / chunkSize);
|
|
1595
1875
|
const lang = this._resolveLanguage(this.opts.language);
|
|
1876
|
+
let detectedLang = lang || partialCache?.detectedLang || null;
|
|
1596
1877
|
const modelId = WHISPER_MODELS[this.opts.modelSize] || WHISPER_MODELS.base;
|
|
1597
1878
|
|
|
1879
|
+
const startChunk = (partialCache?.chunksCompleted != null && partialCache.chunksCompleted < totalChunks)
|
|
1880
|
+
? partialCache.chunksCompleted
|
|
1881
|
+
: 0;
|
|
1882
|
+
|
|
1883
|
+
if (this.player.options.debug && startChunk > 0)
|
|
1884
|
+
console.log('[AutoSub] Resuming transcription from chunk', startChunk + 1, 'of', totalChunks);
|
|
1885
|
+
|
|
1598
1886
|
if (!this.subVisible) { this.subVisible = true; this._startDisplay(); }
|
|
1599
1887
|
|
|
1600
1888
|
const worker = this._createTranscriberWorker();
|
|
@@ -1619,27 +1907,68 @@ self.onmessage = async function(e) {
|
|
|
1619
1907
|
if (type === 'ready') {
|
|
1620
1908
|
URL.revokeObjectURL(worker._bundleURL);
|
|
1621
1909
|
URL.revokeObjectURL(worker._workerURL);
|
|
1622
|
-
this.
|
|
1623
|
-
this.
|
|
1910
|
+
if (this.player.options.debug) console.log('[AutoSub] Worker ready ā sending chunk', startChunk + 1, 'of', totalChunks);
|
|
1911
|
+
this._updatePanel('Transcribing chunk ' + (startChunk + 1) + ' of ' + totalChunks + '...', 64);
|
|
1912
|
+
this._sendChunk(worker, float32Audio, startChunk, chunkSize, totalChunks, detectedLang, CHUNK_SEC);
|
|
1624
1913
|
}
|
|
1625
1914
|
|
|
1626
1915
|
if (type === 'chunk_done') {
|
|
1627
1916
|
const { result, timeOffset, chunkIndex } = payload;
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
}
|
|
1633
|
-
this.
|
|
1634
|
-
|
|
1917
|
+
|
|
1918
|
+
if (chunkIndex === startChunk && payload.detectedLang && !detectedLang) {
|
|
1919
|
+
detectedLang = payload.detectedLang;
|
|
1920
|
+
if (this.player.options.debug) console.log('[AutoSub] Language auto-detected:', detectedLang);
|
|
1921
|
+
}
|
|
1922
|
+
this._saveToCache(this.subtitles, chunkIndex + 1, totalChunks, detectedLang);
|
|
1923
|
+
|
|
1924
|
+
let rawChunks = (result.chunks || [])
|
|
1925
|
+
.filter(c => c.text?.trim())
|
|
1926
|
+
.map(c => {
|
|
1927
|
+
const ts0 = (c.timestamp?.[0] != null) ? c.timestamp[0] : 0;
|
|
1928
|
+
const ts1 = (c.timestamp?.[1] != null) ? c.timestamp[1] : ts0 + 5;
|
|
1929
|
+
return { text: c.text.trim(), start: ts0 + timeOffset, end: ts1 + timeOffset };
|
|
1930
|
+
});
|
|
1931
|
+
|
|
1932
|
+
if (rawChunks.length === 0 && result.text?.trim()) {
|
|
1933
|
+
rawChunks = [{ text: result.text.trim(), start: timeOffset, end: timeOffset + CHUNK_SEC }];
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
const normalized = this._normalizeChunks(rawChunks);
|
|
1937
|
+
this.subtitles.push(...normalized);
|
|
1938
|
+
|
|
1939
|
+
this._saveToCache(this.subtitles, chunkIndex + 1, totalChunks, detectedLang);
|
|
1940
|
+
|
|
1941
|
+
if (this.translateLang && this.translateLang !== 'off' && normalized.length > 0) {
|
|
1942
|
+
const currentLang = this.translateLang;
|
|
1943
|
+
this._translateBatch(normalized, currentLang).then(translated => {
|
|
1944
|
+
if (this.translateLang === currentLang) {
|
|
1945
|
+
this.subtitlesTrans.push(...translated);
|
|
1946
|
+
this.subtitlesTrans.sort((a, b) => a.start - b.start);
|
|
1947
|
+
|
|
1948
|
+
if (!this._transCache[currentLang]) this._transCache[currentLang] = [];
|
|
1949
|
+
this._transCache[currentLang].push(...translated);
|
|
1950
|
+
this._transCache[currentLang].sort((a, b) => a.start - b.start);
|
|
1951
|
+
this._saveToCache(this.subtitles, chunkIndex + 1, totalChunks, detectedLang);
|
|
1952
|
+
}
|
|
1953
|
+
}).catch(() => {
|
|
1954
|
+
if (this.translateLang === currentLang) {
|
|
1955
|
+
this.subtitlesTrans.push(...normalized);
|
|
1956
|
+
this.subtitlesTrans.sort((a, b) => a.start - b.start);
|
|
1957
|
+
}
|
|
1958
|
+
});
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1635
1961
|
|
|
1636
1962
|
const next = chunkIndex + 1;
|
|
1637
1963
|
if (next < totalChunks) {
|
|
1638
1964
|
const pct = 64 + Math.round((next / totalChunks) * 33);
|
|
1639
1965
|
this._updatePanel('Transcribing chunk ' + (next + 1) + ' of ' + totalChunks + '...', pct);
|
|
1640
|
-
this._sendChunk(worker, float32Audio, next, chunkSize, totalChunks,
|
|
1966
|
+
this._sendChunk(worker, float32Audio, next, chunkSize, totalChunks, detectedLang, CHUNK_SEC);
|
|
1641
1967
|
} else {
|
|
1642
|
-
|
|
1968
|
+
if (this.player.options.debug) console.log('[AutoSub] All chunks done, subtitles:', this.subtitles.length);
|
|
1969
|
+
worker.terminate(); this._worker = null;
|
|
1970
|
+
resolve();
|
|
1971
|
+
this._lastDetectedLang = detectedLang;
|
|
1643
1972
|
}
|
|
1644
1973
|
}
|
|
1645
1974
|
|
|
@@ -1652,6 +1981,7 @@ self.onmessage = async function(e) {
|
|
|
1652
1981
|
};
|
|
1653
1982
|
|
|
1654
1983
|
worker.postMessage({ type: 'init', payload: { modelId } });
|
|
1984
|
+
if (this.player.options.debug) console.log('[AutoSub] Init sent (normal), modelId:', modelId, 'totalChunks:', totalChunks, 'startChunk:', startChunk);
|
|
1655
1985
|
});
|
|
1656
1986
|
}
|
|
1657
1987
|
|
|
@@ -1696,7 +2026,6 @@ self.onmessage = async function(e) {
|
|
|
1696
2026
|
const safe = String(this.opts.idCache).replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
1697
2027
|
return CACHE_PREFIX + safe;
|
|
1698
2028
|
}
|
|
1699
|
-
// Use the MPD URL or player.options.src as stable key ā currentSrc may be a blob URL
|
|
1700
2029
|
const src = this._resolveMpdUrl()
|
|
1701
2030
|
|| this.player.options?.src
|
|
1702
2031
|
|| this.video.currentSrc
|
|
@@ -1720,18 +2049,46 @@ self.onmessage = async function(e) {
|
|
|
1720
2049
|
if (!raw) return null;
|
|
1721
2050
|
const parsed = JSON.parse(raw);
|
|
1722
2051
|
if (!Array.isArray(parsed.subtitles) || !parsed.subtitles.length) return null;
|
|
1723
|
-
return
|
|
1724
|
-
|
|
2052
|
+
return {
|
|
2053
|
+
subtitles: parsed.subtitles,
|
|
2054
|
+
chunksCompleted: parsed.chunksCompleted ?? null,
|
|
2055
|
+
totalChunks: parsed.totalChunks ?? null,
|
|
2056
|
+
detectedLang: parsed.detectedLang ?? null,
|
|
2057
|
+
transCache: parsed.transCache || {},
|
|
2058
|
+
isImportedSubs: parsed.isImportedSubs || false
|
|
2059
|
+
};
|
|
2060
|
+
} catch {
|
|
2061
|
+
return null;
|
|
2062
|
+
}
|
|
1725
2063
|
}
|
|
1726
2064
|
|
|
1727
|
-
_saveToCache(subtitles) {
|
|
2065
|
+
_saveToCache(subtitles, chunksCompleted = null, totalChunks = null, detectedLang = null) {
|
|
1728
2066
|
if (!this.opts.cacheEnabled || !subtitles.length) return;
|
|
1729
2067
|
try {
|
|
1730
2068
|
const key = this._getCacheKey();
|
|
1731
|
-
const data = JSON.stringify({
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
2069
|
+
const data = JSON.stringify({
|
|
2070
|
+
subtitles,
|
|
2071
|
+
chunksCompleted,
|
|
2072
|
+
totalChunks,
|
|
2073
|
+
detectedLang,
|
|
2074
|
+
transCache: this._transCache || {},
|
|
2075
|
+
isImportedSubs: this.isImportedSubs || false,
|
|
2076
|
+
savedAt: Date.now()
|
|
2077
|
+
});
|
|
2078
|
+
try {
|
|
2079
|
+
localStorage.setItem(key, data);
|
|
2080
|
+
} catch {
|
|
2081
|
+
this._evictOldestCache();
|
|
2082
|
+
try { localStorage.setItem(key, data); } catch { /* storage full */ }
|
|
2083
|
+
}
|
|
2084
|
+
} catch { /* ignore */ }
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
_deleteCache() {
|
|
2088
|
+
if (!this.opts.cacheEnabled) return;
|
|
2089
|
+
try {
|
|
2090
|
+
localStorage.removeItem(this._getCacheKey());
|
|
2091
|
+
} catch { }
|
|
1735
2092
|
}
|
|
1736
2093
|
|
|
1737
2094
|
_evictOldestCache() {
|
|
@@ -1771,4 +2128,4 @@ self.onmessage = async function(e) {
|
|
|
1771
2128
|
};
|
|
1772
2129
|
|
|
1773
2130
|
register();
|
|
1774
|
-
})();
|
|
2131
|
+
})();
|