libbitsub 1.12.1 → 1.13.1
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 +7 -5
- package/dist/ts/frame-export.d.ts.map +1 -1
- package/dist/ts/frame-export.js +10 -0
- package/dist/ts/frame-export.js.map +1 -1
- package/dist/ts/parsers.d.ts.map +1 -1
- package/dist/ts/parsers.js +8 -4
- package/dist/ts/parsers.js.map +1 -1
- package/dist/ts/range-loader.d.ts +2 -0
- package/dist/ts/range-loader.d.ts.map +1 -1
- package/dist/ts/range-loader.js +99 -21
- package/dist/ts/range-loader.js.map +1 -1
- package/dist/ts/renderers.d.ts +26 -4
- package/dist/ts/renderers.d.ts.map +1 -1
- package/dist/ts/renderers.js +227 -126
- package/dist/ts/renderers.js.map +1 -1
- package/dist/ts/types.d.ts +8 -0
- package/dist/ts/types.d.ts.map +1 -1
- package/dist/ts/utils.d.ts +14 -0
- package/dist/ts/utils.d.ts.map +1 -1
- package/dist/ts/utils.js +68 -15
- package/dist/ts/utils.js.map +1 -1
- package/dist/ts/video-frame-scheduler.d.ts +18 -1
- package/dist/ts/video-frame-scheduler.d.ts.map +1 -1
- package/dist/ts/video-frame-scheduler.js +83 -4
- package/dist/ts/video-frame-scheduler.js.map +1 -1
- package/dist/ts/worker.d.ts.map +1 -1
- package/dist/ts/worker.js +15 -6
- package/dist/ts/worker.js.map +1 -1
- package/package.json +2 -1
- package/pkg/README.md +7 -5
- package/pkg/libbitsub.d.ts +15 -0
- package/pkg/libbitsub.js +27 -0
- package/pkg/libbitsub_bg.wasm +0 -0
- package/pkg/libbitsub_bg.wasm.d.ts +3 -0
- package/pkg/package.json +1 -1
package/dist/ts/renderers.js
CHANGED
|
@@ -7,7 +7,7 @@ import { fetchSubtitleAsset, fetchSubtitleText } from './range-loader';
|
|
|
7
7
|
import { initWasm } from './wasm';
|
|
8
8
|
import { getOrCreateWorker, sendToWorker } from './worker';
|
|
9
9
|
import { canUseWorkerOffscreenRender } from './capabilities';
|
|
10
|
-
import { binarySearchTimestamp, convertFrameData, createWorkerSessionId, createWorkerState, detectSubtitleFormat, getSubtitleBounds, isMksSource, setCacheLimit as applyCacheLimit, setCachedFrame } from './utils';
|
|
10
|
+
import { binarySearchTimestamp, convertFrameData, createWorkerSessionId, createWorkerState, detectSubtitleFormat, getSubtitleBounds, isMksSource, setCacheLimit as applyCacheLimit, setCachedFrame, spliceTimestamps } from './utils';
|
|
11
11
|
import { PgsParser, DvbParser, VobSubParserLowLevel } from './parsers';
|
|
12
12
|
import { WebGPURenderer, isWebGPUSupported } from './webgpu-renderer';
|
|
13
13
|
import { WebGL2Renderer, isWebGL2Supported } from './webgl2-renderer';
|
|
@@ -58,6 +58,8 @@ class BaseVideoSubtitleRenderer {
|
|
|
58
58
|
tempCanvas = null;
|
|
59
59
|
tempCtx = null;
|
|
60
60
|
lastRenderedData = null;
|
|
61
|
+
/** Clear the canvas instead of holding the previous cue while the next bitmap decodes. */
|
|
62
|
+
clearStaleWhilePending = false;
|
|
61
63
|
lastCueIndex = null;
|
|
62
64
|
currentCueMetadata = null;
|
|
63
65
|
parserMetadata = null;
|
|
@@ -69,6 +71,9 @@ class BaseVideoSubtitleRenderer {
|
|
|
69
71
|
prefetchAfter = 0;
|
|
70
72
|
streamingLoad = true;
|
|
71
73
|
rangeRequests = true;
|
|
74
|
+
maxSubtitleBytes;
|
|
75
|
+
/** Aborted on dispose to cancel in-flight subtitle downloads. */
|
|
76
|
+
lifecycle = new AbortController();
|
|
72
77
|
onEvent;
|
|
73
78
|
onWarning;
|
|
74
79
|
currentRendererBackend = null;
|
|
@@ -134,6 +139,20 @@ class BaseVideoSubtitleRenderer {
|
|
|
134
139
|
this.prefetchAfter = Math.max(0, Math.floor(options.prefetchWindow?.after ?? 0));
|
|
135
140
|
this.streamingLoad = options.streamingLoad !== false;
|
|
136
141
|
this.rangeRequests = options.rangeRequests !== false;
|
|
142
|
+
this.maxSubtitleBytes = options.maxSubtitleBytes;
|
|
143
|
+
}
|
|
144
|
+
/** Fetch options bound to this renderer's lifetime and download budget. */
|
|
145
|
+
fetchOptions(preferRange, onProgress) {
|
|
146
|
+
return { preferRange, onProgress, signal: this.lifecycle.signal, maxBytes: this.maxSubtitleBytes };
|
|
147
|
+
}
|
|
148
|
+
/** Dispose this renderer's parser in the shared worker, if a session was started. */
|
|
149
|
+
releaseWorkerSession(state) {
|
|
150
|
+
state.workerReady = false;
|
|
151
|
+
const sessionId = state.sessionId;
|
|
152
|
+
if (!sessionId)
|
|
153
|
+
return;
|
|
154
|
+
const type = this.format === 'pgs' ? 'disposePgs' : this.format === 'dvb' ? 'disposeDvb' : 'disposeVobSub';
|
|
155
|
+
sendToWorker({ type, sessionId }).catch(() => { });
|
|
137
156
|
}
|
|
138
157
|
emitLoadProgress(format, progress, indexedCues) {
|
|
139
158
|
this.emitEvent({
|
|
@@ -280,6 +299,8 @@ class BaseVideoSubtitleRenderer {
|
|
|
280
299
|
startInit() {
|
|
281
300
|
this.initPromise = this.init();
|
|
282
301
|
this.initPromise.catch((error) => {
|
|
302
|
+
if (this.disposed)
|
|
303
|
+
return;
|
|
283
304
|
this.emitEvent({
|
|
284
305
|
type: 'error',
|
|
285
306
|
format: this.format,
|
|
@@ -330,10 +351,20 @@ class BaseVideoSubtitleRenderer {
|
|
|
330
351
|
/** Initialize the renderer. */
|
|
331
352
|
async init() {
|
|
332
353
|
await initWasm();
|
|
354
|
+
if (this.disposed)
|
|
355
|
+
return;
|
|
333
356
|
this.createCanvas();
|
|
334
357
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
358
|
+
if (this.disposed)
|
|
359
|
+
return;
|
|
335
360
|
await this.loadSubtitles();
|
|
361
|
+
// Loading may have created parsers or worker sessions after dispose() ran;
|
|
362
|
+
// dispose again to release them.
|
|
363
|
+
if (this.disposed)
|
|
364
|
+
return this.dispose();
|
|
336
365
|
await this.ensureWorkerOffscreenAttached();
|
|
366
|
+
if (this.disposed)
|
|
367
|
+
return this.dispose();
|
|
337
368
|
this.startRenderLoop();
|
|
338
369
|
}
|
|
339
370
|
shouldPreferWorkerOffscreen() {
|
|
@@ -432,11 +463,13 @@ class BaseVideoSubtitleRenderer {
|
|
|
432
463
|
this.lastRenderedIndex = -1;
|
|
433
464
|
this.lastRenderedTime = -1;
|
|
434
465
|
this.lastSynchronizedTime = null;
|
|
466
|
+
this.lastRenderedData = null;
|
|
435
467
|
this.offscreenFrameMetadata.clear();
|
|
436
468
|
this.onSeek();
|
|
437
469
|
this.renderPausedFrame();
|
|
438
470
|
};
|
|
439
471
|
this.video.addEventListener('loadedmetadata', this.loadedMetadataHandler);
|
|
472
|
+
this.video.addEventListener('resize', this.loadedMetadataHandler);
|
|
440
473
|
this.video.addEventListener('seeked', this.seekedHandler);
|
|
441
474
|
}
|
|
442
475
|
preferWorkerOffscreenOrCanvas2D() {
|
|
@@ -498,8 +531,6 @@ class BaseVideoSubtitleRenderer {
|
|
|
498
531
|
}
|
|
499
532
|
this.useWorkerOffscreen = true;
|
|
500
533
|
this.emitRendererBackend('worker-offscreen');
|
|
501
|
-
this.lastRenderedIndex = -1;
|
|
502
|
-
this.lastRenderedTime = -1;
|
|
503
534
|
await sendToWorker({
|
|
504
535
|
type: 'resizeOffscreenCanvas',
|
|
505
536
|
sessionId,
|
|
@@ -518,6 +549,8 @@ class BaseVideoSubtitleRenderer {
|
|
|
518
549
|
details: { reason: error instanceof Error ? error.message : String(error) }
|
|
519
550
|
}));
|
|
520
551
|
}
|
|
552
|
+
if (this.useWorkerOffscreen)
|
|
553
|
+
this.reconcileBackendPresentation();
|
|
521
554
|
}
|
|
522
555
|
recreateCanvasForMainThreadFallback() {
|
|
523
556
|
this.removeOwnedCanvas();
|
|
@@ -600,68 +633,112 @@ class BaseVideoSubtitleRenderer {
|
|
|
600
633
|
}
|
|
601
634
|
/** Initialize WebGPU renderer. */
|
|
602
635
|
async initWebGPU(allowBackendFallback = true) {
|
|
636
|
+
if (this.disposed)
|
|
637
|
+
return;
|
|
638
|
+
let renderer = null;
|
|
603
639
|
try {
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
640
|
+
renderer = new WebGPURenderer();
|
|
641
|
+
this.webgpuRenderer = renderer;
|
|
642
|
+
await renderer.init();
|
|
643
|
+
if (this.disposed || !this.canvas) {
|
|
644
|
+
renderer.destroy();
|
|
607
645
|
return;
|
|
646
|
+
}
|
|
608
647
|
const bounds = this.getVideoContentBounds();
|
|
609
648
|
const pixelRatio = this.getDevicePixelRatio();
|
|
610
649
|
const width = Math.max(1, bounds.width * pixelRatio);
|
|
611
650
|
const height = Math.max(1, bounds.height * pixelRatio);
|
|
612
|
-
await
|
|
613
|
-
this.
|
|
614
|
-
|
|
651
|
+
await renderer.setCanvas(this.canvas, width, height);
|
|
652
|
+
if (this.disposed) {
|
|
653
|
+
renderer.destroy();
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
615
656
|
}
|
|
616
657
|
catch (error) {
|
|
617
|
-
|
|
658
|
+
renderer?.destroy();
|
|
659
|
+
if (this.disposed)
|
|
660
|
+
return;
|
|
618
661
|
this.webgpuRenderer = null;
|
|
619
662
|
this.useWebGPU = false;
|
|
620
663
|
this.onWebGPUFallback?.();
|
|
664
|
+
if (this.disposed)
|
|
665
|
+
return;
|
|
621
666
|
if (allowBackendFallback && isWebGL2Supported()) {
|
|
622
667
|
void this.initWebGL2();
|
|
623
668
|
}
|
|
624
669
|
else {
|
|
625
670
|
this.preferWorkerOffscreenOrCanvas2D();
|
|
626
671
|
}
|
|
672
|
+
return;
|
|
627
673
|
}
|
|
674
|
+
this.useWebGPU = true;
|
|
675
|
+
this.emitRendererBackend('webgpu');
|
|
676
|
+
this.reconcileBackendPresentation();
|
|
628
677
|
}
|
|
629
678
|
/** Initialize WebGL2 renderer. */
|
|
630
679
|
async initWebGL2(allowBackendFallback = true) {
|
|
680
|
+
if (this.disposed)
|
|
681
|
+
return;
|
|
682
|
+
let renderer = null;
|
|
631
683
|
try {
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
684
|
+
renderer = new WebGL2Renderer();
|
|
685
|
+
this.webgl2Renderer = renderer;
|
|
686
|
+
await renderer.init();
|
|
687
|
+
if (this.disposed || !this.canvas) {
|
|
688
|
+
renderer.destroy();
|
|
635
689
|
return;
|
|
690
|
+
}
|
|
636
691
|
const bounds = this.getVideoContentBounds();
|
|
637
692
|
const pixelRatio = this.getDevicePixelRatio();
|
|
638
693
|
const width = Math.max(1, bounds.width * pixelRatio);
|
|
639
694
|
const height = Math.max(1, bounds.height * pixelRatio);
|
|
640
|
-
await
|
|
641
|
-
this.
|
|
642
|
-
|
|
695
|
+
await renderer.setCanvas(this.canvas, width, height);
|
|
696
|
+
if (this.disposed) {
|
|
697
|
+
renderer.destroy();
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
643
700
|
}
|
|
644
701
|
catch (error) {
|
|
645
|
-
|
|
702
|
+
renderer?.destroy();
|
|
703
|
+
if (this.disposed)
|
|
704
|
+
return;
|
|
646
705
|
this.webgl2Renderer = null;
|
|
647
706
|
this.useWebGL2 = false;
|
|
648
707
|
this.onWebGL2Fallback?.();
|
|
708
|
+
if (this.disposed)
|
|
709
|
+
return;
|
|
649
710
|
if (allowBackendFallback) {
|
|
650
711
|
this.preferWorkerOffscreenOrCanvas2D();
|
|
651
712
|
}
|
|
652
713
|
else {
|
|
653
714
|
this.initCanvas2D();
|
|
654
715
|
}
|
|
716
|
+
return;
|
|
655
717
|
}
|
|
718
|
+
this.useWebGL2 = true;
|
|
719
|
+
this.emitRendererBackend('webgl2');
|
|
720
|
+
this.reconcileBackendPresentation();
|
|
656
721
|
}
|
|
657
722
|
/** Initialize Canvas2D renderer. */
|
|
658
723
|
initCanvas2D() {
|
|
659
|
-
if (!this.canvas)
|
|
724
|
+
if (this.disposed || !this.canvas)
|
|
660
725
|
return;
|
|
661
726
|
this.ctx = this.canvas.getContext('2d');
|
|
662
727
|
this.useWebGPU = false;
|
|
663
728
|
this.useWebGL2 = false;
|
|
664
729
|
this.emitRendererBackend('canvas2d');
|
|
730
|
+
this.reconcileBackendPresentation();
|
|
731
|
+
}
|
|
732
|
+
/** A cue decoded before backend startup has not yet reached the canvas. */
|
|
733
|
+
reconcileBackendPresentation() {
|
|
734
|
+
if (this.disposed)
|
|
735
|
+
return;
|
|
736
|
+
this.invalidatePresentation();
|
|
737
|
+
this.lastRenderedIndex = -2;
|
|
738
|
+
this.lastRenderedTime = -1;
|
|
739
|
+
// Loading creates the temporary drawing canvas before starting presentation.
|
|
740
|
+
if (this.frameScheduler)
|
|
741
|
+
this.renderPausedFrame();
|
|
665
742
|
}
|
|
666
743
|
/** Called when video seeks. */
|
|
667
744
|
onSeek() { }
|
|
@@ -779,6 +856,13 @@ class BaseVideoSubtitleRenderer {
|
|
|
779
856
|
return;
|
|
780
857
|
this.renderSynchronizedFrame({ mediaTime: this.video.currentTime, presentedFrames: null });
|
|
781
858
|
}
|
|
859
|
+
/**
|
|
860
|
+
* Render the cue for a media time synchronously, whether or not the video is paused.
|
|
861
|
+
* Hosts call this from their own presented-frame callback so a cached cue paints on that frame.
|
|
862
|
+
*/
|
|
863
|
+
renderAtMediaTime(mediaTime) {
|
|
864
|
+
this.renderSynchronizedFrame({ mediaTime, presentedFrames: null });
|
|
865
|
+
}
|
|
782
866
|
renderSynchronizedFrame(tick) {
|
|
783
867
|
if (this.disposed || !this.isLoaded)
|
|
784
868
|
return;
|
|
@@ -853,7 +937,7 @@ class BaseVideoSubtitleRenderer {
|
|
|
853
937
|
if (this.findCurrentIndex(this.getCurrentSynchronizationTime()) !== index)
|
|
854
938
|
return;
|
|
855
939
|
this.lastRenderedIndex = -1;
|
|
856
|
-
this.
|
|
940
|
+
this.renderAtMediaTime(this.getCurrentSynchronizationTime() - this.timeOffset);
|
|
857
941
|
}, () => { });
|
|
858
942
|
}
|
|
859
943
|
recordRenderPerformance(renderTime, capturedAt, countAsDropped = true) {
|
|
@@ -904,12 +988,17 @@ class BaseVideoSubtitleRenderer {
|
|
|
904
988
|
// If data is undefined, it means async loading is in progress
|
|
905
989
|
// Keep showing the last frame only while waiting for async data
|
|
906
990
|
// Note: null means "loaded but empty" (clear screen), undefined means "still loading"
|
|
991
|
+
let stalePending = false;
|
|
907
992
|
if (data === undefined && this.lastRenderedData !== null && index >= 0) {
|
|
908
993
|
// Check if this index has a pending render (truly async loading)
|
|
909
994
|
// If not pending, it means the render returned no data immediately
|
|
910
995
|
if (this.isPendingRender(index)) {
|
|
911
|
-
|
|
912
|
-
|
|
996
|
+
if (!this.clearStaleWhilePending) {
|
|
997
|
+
// Don't clear - keep showing the last frame while loading
|
|
998
|
+
return { status: 'pending', data: null, warning: null };
|
|
999
|
+
}
|
|
1000
|
+
// The previous cue has ended: clear it below instead of holding it
|
|
1001
|
+
stalePending = true;
|
|
913
1002
|
}
|
|
914
1003
|
}
|
|
915
1004
|
const renderIssue = index >= 0 ? (this.getWorkerRendererState().renderIssues.get(index) ?? null) : null;
|
|
@@ -927,6 +1016,9 @@ class BaseVideoSubtitleRenderer {
|
|
|
927
1016
|
if (index < 0) {
|
|
928
1017
|
return { status: 'cleared', data: null, warning: null };
|
|
929
1018
|
}
|
|
1019
|
+
if (stalePending) {
|
|
1020
|
+
return { status: 'pending', data: null, warning: null };
|
|
1021
|
+
}
|
|
930
1022
|
if (warning) {
|
|
931
1023
|
return { status: 'failed', data: data ?? null, warning };
|
|
932
1024
|
}
|
|
@@ -1141,6 +1233,7 @@ class BaseVideoSubtitleRenderer {
|
|
|
1141
1233
|
/** Dispose of all resources. */
|
|
1142
1234
|
dispose() {
|
|
1143
1235
|
this.disposed = true;
|
|
1236
|
+
this.lifecycle.abort();
|
|
1144
1237
|
this.invalidatePresentation();
|
|
1145
1238
|
this.frameScheduler?.stop();
|
|
1146
1239
|
this.frameScheduler = null;
|
|
@@ -1148,6 +1241,7 @@ class BaseVideoSubtitleRenderer {
|
|
|
1148
1241
|
this.resizeObserver = null;
|
|
1149
1242
|
if (this.loadedMetadataHandler) {
|
|
1150
1243
|
this.video.removeEventListener('loadedmetadata', this.loadedMetadataHandler);
|
|
1244
|
+
this.video.removeEventListener('resize', this.loadedMetadataHandler);
|
|
1151
1245
|
this.loadedMetadataHandler = null;
|
|
1152
1246
|
}
|
|
1153
1247
|
if (this.seekedHandler) {
|
|
@@ -1222,10 +1316,7 @@ export class PgsRenderer extends BaseVideoSubtitleRenderer {
|
|
|
1222
1316
|
return;
|
|
1223
1317
|
}
|
|
1224
1318
|
if (!this.streamingLoad) {
|
|
1225
|
-
const { data, strategy, rangeSupported, total } = await fetchSubtitleAsset(this.subUrl,
|
|
1226
|
-
preferRange: this.rangeRequests,
|
|
1227
|
-
onProgress: (progress) => this.emitLoadProgress('pgs', progress, this.state.timestamps.length)
|
|
1228
|
-
});
|
|
1319
|
+
const { data, strategy, rangeSupported, total } = await fetchSubtitleAsset(this.subUrl, this.fetchOptions(this.rangeRequests, (progress) => this.emitLoadProgress('pgs', progress, this.state.timestamps.length)));
|
|
1229
1320
|
this.emitLoadProgress('pgs', { loaded: data.byteLength, total: total ?? data.byteLength, ratio: 1, rangeSupported, strategy }, 0);
|
|
1230
1321
|
await this.loadPgsBuffer(data, false);
|
|
1231
1322
|
this.onLoaded?.();
|
|
@@ -1235,11 +1326,19 @@ export class PgsRenderer extends BaseVideoSubtitleRenderer {
|
|
|
1235
1326
|
this.onLoaded?.();
|
|
1236
1327
|
}
|
|
1237
1328
|
catch (error) {
|
|
1329
|
+
if (this.disposed)
|
|
1330
|
+
return;
|
|
1238
1331
|
const resolvedError = normalizeSubtitleError(error, { format: 'pgs' });
|
|
1239
1332
|
this.emitEvent({ type: 'error', format: 'pgs', error: resolvedError });
|
|
1240
1333
|
this.onError?.(resolvedError);
|
|
1241
1334
|
}
|
|
1242
1335
|
}
|
|
1336
|
+
/** Resolve an append response, which may carry only new timestamps, to the full list. */
|
|
1337
|
+
mergePgsTimestamps(response) {
|
|
1338
|
+
if (response.timestampBase === undefined)
|
|
1339
|
+
return response.timestamps;
|
|
1340
|
+
return spliceTimestamps(this.state.timestamps, response.timestampBase, response.timestamps);
|
|
1341
|
+
}
|
|
1243
1342
|
applyPgsIndexState(metadata, timestamps, partial, usingWorker) {
|
|
1244
1343
|
this.state.metadata = metadata;
|
|
1245
1344
|
this.state.timestamps = timestamps;
|
|
@@ -1272,7 +1371,7 @@ export class PgsRenderer extends BaseVideoSubtitleRenderer {
|
|
|
1272
1371
|
}
|
|
1273
1372
|
catch (workerError) {
|
|
1274
1373
|
this.state.useWorker = false;
|
|
1275
|
-
this.state
|
|
1374
|
+
this.releaseWorkerSession(this.state);
|
|
1276
1375
|
this.state.sessionId = null;
|
|
1277
1376
|
this.emitWorkerState(false, false, null, true);
|
|
1278
1377
|
this.emitWarning(createSubtitleWarning('WORKER_FALLBACK', 'PGS worker initialization failed, falling back to main-thread rendering.', {
|
|
@@ -1323,12 +1422,13 @@ export class PgsRenderer extends BaseVideoSubtitleRenderer {
|
|
|
1323
1422
|
if (response.type !== 'pgsProgress')
|
|
1324
1423
|
throw new Error('Unexpected PGS worker response');
|
|
1325
1424
|
added = response.added;
|
|
1425
|
+
const timestamps = this.mergePgsTimestamps(response);
|
|
1326
1426
|
if (added > 0) {
|
|
1327
|
-
this.applyPgsIndexState(response.metadata,
|
|
1427
|
+
this.applyPgsIndexState(response.metadata, timestamps, true, true);
|
|
1328
1428
|
}
|
|
1329
1429
|
else {
|
|
1330
1430
|
this.state.metadata = response.metadata;
|
|
1331
|
-
this.state.timestamps =
|
|
1431
|
+
this.state.timestamps = timestamps;
|
|
1332
1432
|
}
|
|
1333
1433
|
}
|
|
1334
1434
|
else {
|
|
@@ -1426,6 +1526,7 @@ export class PgsRenderer extends BaseVideoSubtitleRenderer {
|
|
|
1426
1526
|
catch (workerError) {
|
|
1427
1527
|
this.state.useWorker = false;
|
|
1428
1528
|
this.emitWorkerState(false, false, this.state.sessionId, true);
|
|
1529
|
+
this.releaseWorkerSession(this.state);
|
|
1429
1530
|
this.emitWarning(createSubtitleWarning('WORKER_FALLBACK', 'PGS worker initialization failed, falling back to main-thread rendering.', {
|
|
1430
1531
|
format: 'pgs',
|
|
1431
1532
|
details: { reason: workerError instanceof Error ? workerError.message : String(workerError) }
|
|
@@ -1451,6 +1552,7 @@ export class PgsRenderer extends BaseVideoSubtitleRenderer {
|
|
|
1451
1552
|
this.state.useWorker = false;
|
|
1452
1553
|
usedWorker = false;
|
|
1453
1554
|
this.emitWorkerState(false, false, this.state.sessionId, true);
|
|
1555
|
+
this.releaseWorkerSession(this.state);
|
|
1454
1556
|
this.emitWarning(createSubtitleWarning('WORKER_FALLBACK', 'PGS worker initialization failed, falling back to main-thread rendering.', {
|
|
1455
1557
|
format: 'pgs',
|
|
1456
1558
|
details: { reason: workerError instanceof Error ? workerError.message : String(workerError) }
|
|
@@ -1463,10 +1565,7 @@ export class PgsRenderer extends BaseVideoSubtitleRenderer {
|
|
|
1463
1565
|
this.pgsParser.reset();
|
|
1464
1566
|
}
|
|
1465
1567
|
try {
|
|
1466
|
-
const { data, strategy, rangeSupported, total } = await fetchSubtitleAsset(url, {
|
|
1467
|
-
preferRange: this.rangeRequests,
|
|
1468
|
-
onProgress: (progress) => this.emitLoadProgress('pgs', progress, this.state.timestamps.length)
|
|
1469
|
-
}, async (chunk, progress) => {
|
|
1568
|
+
const { data, strategy, rangeSupported, total } = await fetchSubtitleAsset(url, this.fetchOptions(this.rangeRequests, (progress) => this.emitLoadProgress('pgs', progress, this.state.timestamps.length)), async (chunk, progress) => {
|
|
1470
1569
|
if (chunk.byteLength === 0)
|
|
1471
1570
|
return;
|
|
1472
1571
|
if (usedWorker && this.state.sessionId) {
|
|
@@ -1477,12 +1576,13 @@ export class PgsRenderer extends BaseVideoSubtitleRenderer {
|
|
|
1477
1576
|
data: transferable
|
|
1478
1577
|
});
|
|
1479
1578
|
if (response.type === 'pgsProgress') {
|
|
1579
|
+
const timestamps = this.mergePgsTimestamps(response);
|
|
1480
1580
|
if (response.added > 0 || !indexedOnce) {
|
|
1481
|
-
this.applyPgsIndexState(response.metadata,
|
|
1581
|
+
this.applyPgsIndexState(response.metadata, timestamps, true, true);
|
|
1482
1582
|
indexedOnce = true;
|
|
1483
1583
|
}
|
|
1484
1584
|
else {
|
|
1485
|
-
this.state.timestamps =
|
|
1585
|
+
this.state.timestamps = timestamps;
|
|
1486
1586
|
this.state.metadata = response.metadata;
|
|
1487
1587
|
}
|
|
1488
1588
|
}
|
|
@@ -1536,12 +1636,15 @@ export class PgsRenderer extends BaseVideoSubtitleRenderer {
|
|
|
1536
1636
|
if (usedWorker) {
|
|
1537
1637
|
this.state.useWorker = false;
|
|
1538
1638
|
this.emitWorkerState(false, false, this.state.sessionId, true);
|
|
1639
|
+
this.releaseWorkerSession(this.state);
|
|
1539
1640
|
}
|
|
1641
|
+
if (this.disposed)
|
|
1642
|
+
throw error;
|
|
1540
1643
|
this.emitWarning(createSubtitleWarning('RANGE_FALLBACK', 'Progressive PGS load failed; retrying with a full buffer fetch.', {
|
|
1541
1644
|
format: 'pgs',
|
|
1542
1645
|
details: { reason: error instanceof Error ? error.message : String(error) }
|
|
1543
1646
|
}));
|
|
1544
|
-
const { data } = await fetchSubtitleAsset(url,
|
|
1647
|
+
const { data } = await fetchSubtitleAsset(url, this.fetchOptions(this.rangeRequests));
|
|
1545
1648
|
await this.loadPgsBuffer(data, false);
|
|
1546
1649
|
}
|
|
1547
1650
|
}
|
|
@@ -1718,12 +1821,9 @@ export class PgsRenderer extends BaseVideoSubtitleRenderer {
|
|
|
1718
1821
|
this.state.frameCache.clear();
|
|
1719
1822
|
this.state.renderIssues.clear();
|
|
1720
1823
|
this.state.pendingRenders.clear();
|
|
1721
|
-
|
|
1722
|
-
sendToWorker({ type: 'disposePgs', sessionId: this.state.sessionId }).catch(() => { });
|
|
1723
|
-
}
|
|
1824
|
+
this.releaseWorkerSession(this.state);
|
|
1724
1825
|
this.pgsParser?.dispose();
|
|
1725
1826
|
this.pgsParser = null;
|
|
1726
|
-
this.state.sessionId = null;
|
|
1727
1827
|
}
|
|
1728
1828
|
}
|
|
1729
1829
|
/**
|
|
@@ -1739,10 +1839,8 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
1739
1839
|
onLoading;
|
|
1740
1840
|
onLoaded;
|
|
1741
1841
|
onError;
|
|
1742
|
-
//
|
|
1743
|
-
|
|
1744
|
-
cachedIndexTime = -1;
|
|
1745
|
-
pendingIndexLookup = null;
|
|
1842
|
+
// Cue end times (ms) for the main-thread index lookup in worker mode; starts live in state.timestamps
|
|
1843
|
+
cueEnds = null;
|
|
1746
1844
|
constructor(options) {
|
|
1747
1845
|
super(options, 'vobsub');
|
|
1748
1846
|
this.idxUrl =
|
|
@@ -1753,6 +1851,7 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
1753
1851
|
this.onLoading = options.onLoading;
|
|
1754
1852
|
this.onLoaded = options.onLoaded;
|
|
1755
1853
|
this.onError = options.onError;
|
|
1854
|
+
this.clearStaleWhilePending = true;
|
|
1756
1855
|
applyCacheLimit(this.state, this.cacheLimit);
|
|
1757
1856
|
this.startInit();
|
|
1758
1857
|
}
|
|
@@ -1783,6 +1882,8 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
1783
1882
|
this.onLoaded?.();
|
|
1784
1883
|
}
|
|
1785
1884
|
catch (error) {
|
|
1885
|
+
if (this.disposed)
|
|
1886
|
+
return;
|
|
1786
1887
|
const resolvedError = normalizeSubtitleError(error, { format: 'vobsub' });
|
|
1787
1888
|
this.emitEvent({ type: 'error', format: 'vobsub', error: resolvedError });
|
|
1788
1889
|
this.onError?.(resolvedError);
|
|
@@ -1813,6 +1914,7 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
1813
1914
|
catch (workerError) {
|
|
1814
1915
|
this.state.useWorker = false;
|
|
1815
1916
|
this.emitWorkerState(false, false, this.state.sessionId, true);
|
|
1917
|
+
this.releaseWorkerSession(this.state);
|
|
1816
1918
|
this.emitWarning(createSubtitleWarning('WORKER_FALLBACK', 'VobSub worker initialization failed, falling back to main-thread rendering.', {
|
|
1817
1919
|
format: 'vobsub',
|
|
1818
1920
|
details: { reason: workerError instanceof Error ? workerError.message : String(workerError) }
|
|
@@ -1845,6 +1947,7 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
1845
1947
|
this.state.workerReady = true;
|
|
1846
1948
|
this.state.metadata = loadResponse.metadata;
|
|
1847
1949
|
this.state.timestamps = loadResponse.timestamps;
|
|
1950
|
+
await this.loadCueTable();
|
|
1848
1951
|
this.isLoaded = true;
|
|
1849
1952
|
this.setParserMetadata(loadResponse.metadata);
|
|
1850
1953
|
this.emitIndexed('vobsub', loadResponse.metadata, false);
|
|
@@ -1861,6 +1964,7 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
1861
1964
|
}
|
|
1862
1965
|
this.state.useWorker = false;
|
|
1863
1966
|
this.emitWorkerState(false, false, this.state.sessionId, true);
|
|
1967
|
+
this.releaseWorkerSession(this.state);
|
|
1864
1968
|
this.emitWarning(createSubtitleWarning('WORKER_FALLBACK', 'VobSub worker load failed, falling back to main-thread rendering.', {
|
|
1865
1969
|
format: 'vobsub',
|
|
1866
1970
|
details: { reason: workerError instanceof Error ? workerError.message : String(workerError) }
|
|
@@ -1877,10 +1981,7 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
1877
1981
|
if (!this.subUrl) {
|
|
1878
1982
|
throw createSubtitleDiagnosticError('MISSING_INPUT', 'No SUB content or URL provided.', { format: 'vobsub' });
|
|
1879
1983
|
}
|
|
1880
|
-
const { data, strategy, rangeSupported, total } = await fetchSubtitleAsset(this.subUrl,
|
|
1881
|
-
preferRange: this.rangeRequests && this.streamingLoad,
|
|
1882
|
-
onProgress: (progress) => this.emitLoadProgress('vobsub', progress, this.state.timestamps.length)
|
|
1883
|
-
});
|
|
1984
|
+
const { data, strategy, rangeSupported, total } = await fetchSubtitleAsset(this.subUrl, this.fetchOptions(this.rangeRequests && this.streamingLoad, (progress) => this.emitLoadProgress('vobsub', progress, this.state.timestamps.length)));
|
|
1884
1985
|
this.emitLoadProgress('vobsub', {
|
|
1885
1986
|
loaded: data.byteLength,
|
|
1886
1987
|
total: total ?? data.byteLength,
|
|
@@ -1896,9 +1997,7 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
1896
1997
|
if (!this.idxUrl) {
|
|
1897
1998
|
throw createSubtitleDiagnosticError('MISSING_INPUT', 'No IDX content or URL provided.', { format: 'vobsub' });
|
|
1898
1999
|
}
|
|
1899
|
-
idxData = await fetchSubtitleText(this.idxUrl,
|
|
1900
|
-
onProgress: (progress) => this.emitLoadProgress('vobsub', progress, 0)
|
|
1901
|
-
});
|
|
2000
|
+
idxData = await fetchSubtitleText(this.idxUrl, this.fetchOptions(false, (progress) => this.emitLoadProgress('vobsub', progress, 0)));
|
|
1902
2001
|
}
|
|
1903
2002
|
let usedWorker = await this.ensureVobSubWorkerSession();
|
|
1904
2003
|
if (usedWorker && this.state.sessionId) {
|
|
@@ -1929,6 +2028,7 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
1929
2028
|
usedWorker = false;
|
|
1930
2029
|
this.state.useWorker = false;
|
|
1931
2030
|
this.emitWorkerState(false, false, this.state.sessionId, true);
|
|
2031
|
+
this.releaseWorkerSession(this.state);
|
|
1932
2032
|
this.emitWarning(createSubtitleWarning('WORKER_FALLBACK', 'VobSub worker IDX load failed, falling back to main-thread rendering.', {
|
|
1933
2033
|
format: 'vobsub',
|
|
1934
2034
|
details: { reason: error instanceof Error ? error.message : String(error) }
|
|
@@ -1953,10 +2053,7 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
1953
2053
|
if (!this.subUrl) {
|
|
1954
2054
|
throw createSubtitleDiagnosticError('MISSING_INPUT', 'No SUB content or URL provided.', { format: 'vobsub' });
|
|
1955
2055
|
}
|
|
1956
|
-
const fetched = await fetchSubtitleAsset(this.subUrl,
|
|
1957
|
-
preferRange: this.rangeRequests && this.streamingLoad,
|
|
1958
|
-
onProgress: (progress) => this.emitLoadProgress('vobsub', progress, this.state.timestamps.length)
|
|
1959
|
-
});
|
|
2056
|
+
const fetched = await fetchSubtitleAsset(this.subUrl, this.fetchOptions(this.rangeRequests && this.streamingLoad, (progress) => this.emitLoadProgress('vobsub', progress, this.state.timestamps.length)));
|
|
1960
2057
|
subData = fetched.data;
|
|
1961
2058
|
this.emitLoadProgress('vobsub', {
|
|
1962
2059
|
loaded: subData.byteLength,
|
|
@@ -1976,6 +2073,7 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
1976
2073
|
if (attachResponse.type === 'vobSubProgress') {
|
|
1977
2074
|
this.applyVobSubIndexState(attachResponse.metadata, attachResponse.timestamps, false, true, true);
|
|
1978
2075
|
this.state.workerReady = true;
|
|
2076
|
+
await this.loadCueTable();
|
|
1979
2077
|
this.isLoaded = true;
|
|
1980
2078
|
this.emitWorkerState(true, true, this.state.sessionId);
|
|
1981
2079
|
return;
|
|
@@ -2045,50 +2143,53 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
2045
2143
|
}
|
|
2046
2144
|
findCurrentIndex(time) {
|
|
2047
2145
|
if (this.state.useWorker && this.state.workerReady) {
|
|
2048
|
-
|
|
2049
|
-
// Only use cache if time is very close (within 1 frame)
|
|
2050
|
-
const timeDelta = timeMs - this.cachedIndexTime;
|
|
2051
|
-
const cacheValid = this.cachedIndexTime >= 0 && Math.abs(timeDelta) < 17;
|
|
2052
|
-
if (cacheValid) {
|
|
2053
|
-
return this.cachedIndex;
|
|
2054
|
-
}
|
|
2055
|
-
// Start async lookup if not already pending
|
|
2056
|
-
if (!this.pendingIndexLookup) {
|
|
2057
|
-
const presentationToken = this.getPresentationToken();
|
|
2058
|
-
const lookup = sendToWorker({
|
|
2059
|
-
type: 'findVobSubIndex',
|
|
2060
|
-
sessionId: this.state.sessionId,
|
|
2061
|
-
timeMs
|
|
2062
|
-
}).then((response) => {
|
|
2063
|
-
if (!this.isCurrentPresentation(presentationToken))
|
|
2064
|
-
return this.cachedIndex;
|
|
2065
|
-
if (response.type === 'vobSubIndex') {
|
|
2066
|
-
const newIndex = response.index;
|
|
2067
|
-
const oldIndex = this.cachedIndex;
|
|
2068
|
-
this.cachedIndex = newIndex;
|
|
2069
|
-
this.cachedIndexTime = timeMs;
|
|
2070
|
-
// Force re-render if index changed (including to -1 for clear)
|
|
2071
|
-
if (oldIndex !== newIndex) {
|
|
2072
|
-
this.lastRenderedIndex = -2; // Use -2 to force update even when new index is -1
|
|
2073
|
-
}
|
|
2074
|
-
}
|
|
2075
|
-
return this.cachedIndex;
|
|
2076
|
-
});
|
|
2077
|
-
this.pendingIndexLookup = lookup;
|
|
2078
|
-
void lookup.then(() => {
|
|
2079
|
-
if (this.pendingIndexLookup !== lookup)
|
|
2080
|
-
return;
|
|
2081
|
-
this.pendingIndexLookup = null;
|
|
2082
|
-
this.renderPausedFrame();
|
|
2083
|
-
}, () => {
|
|
2084
|
-
if (this.pendingIndexLookup === lookup)
|
|
2085
|
-
this.pendingIndexLookup = null;
|
|
2086
|
-
});
|
|
2087
|
-
}
|
|
2088
|
-
return this.cachedIndex;
|
|
2146
|
+
return this.findIndexInCueTable(time);
|
|
2089
2147
|
}
|
|
2090
2148
|
return this.vobsubParser?.findIndexAtTimestamp(time) ?? -1;
|
|
2091
2149
|
}
|
|
2150
|
+
/** Synchronous lookup against the worker's cue table; mirrors the core parser's find_index_at_timestamp. */
|
|
2151
|
+
findIndexInCueTable(time) {
|
|
2152
|
+
const starts = this.state.timestamps;
|
|
2153
|
+
const ends = this.cueEnds;
|
|
2154
|
+
if (!ends || starts.length === 0)
|
|
2155
|
+
return -1;
|
|
2156
|
+
const timeMs = Math.max(0, Math.floor(time * 1000));
|
|
2157
|
+
let low = 0;
|
|
2158
|
+
let high = starts.length;
|
|
2159
|
+
while (low < high) {
|
|
2160
|
+
const mid = low + ((high - low) >> 1);
|
|
2161
|
+
if (starts[mid] <= timeMs)
|
|
2162
|
+
low = mid + 1;
|
|
2163
|
+
else
|
|
2164
|
+
high = mid;
|
|
2165
|
+
}
|
|
2166
|
+
const index = low > 0 ? low - 1 : 0;
|
|
2167
|
+
if (timeMs < starts[index] || timeMs >= ends[index])
|
|
2168
|
+
return -1;
|
|
2169
|
+
return index;
|
|
2170
|
+
}
|
|
2171
|
+
/** Fetch cue end times once, after the worker holds the SUB data. Falls back to next-start ends. */
|
|
2172
|
+
async loadCueTable() {
|
|
2173
|
+
const starts = this.state.timestamps;
|
|
2174
|
+
let ends = null;
|
|
2175
|
+
try {
|
|
2176
|
+
const response = await sendToWorker({ type: 'getVobSubTimestamps', sessionId: this.state.sessionId });
|
|
2177
|
+
const received = response.endTimestamps;
|
|
2178
|
+
if (received && received.length === starts.length)
|
|
2179
|
+
ends = received;
|
|
2180
|
+
}
|
|
2181
|
+
catch {
|
|
2182
|
+
/* fall back below */
|
|
2183
|
+
}
|
|
2184
|
+
if (!ends) {
|
|
2185
|
+
ends = new Float64Array(starts.length);
|
|
2186
|
+
for (let i = 0; i < starts.length; i++)
|
|
2187
|
+
ends[i] = starts[i + 1] ?? starts[i] + 5000;
|
|
2188
|
+
}
|
|
2189
|
+
this.cueEnds = ends;
|
|
2190
|
+
this.lastRenderedIndex = -1;
|
|
2191
|
+
this.renderPausedFrame();
|
|
2192
|
+
}
|
|
2092
2193
|
renderAtIndex(index) {
|
|
2093
2194
|
if (this.state.frameCache.has(index)) {
|
|
2094
2195
|
return this.state.frameCache.get(index) ?? undefined;
|
|
@@ -2159,10 +2260,6 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
2159
2260
|
this.state.frameCache.clear();
|
|
2160
2261
|
this.state.renderIssues.clear();
|
|
2161
2262
|
this.state.pendingRenders.clear();
|
|
2162
|
-
// Clear cached index lookup on seek
|
|
2163
|
-
this.cachedIndex = -1;
|
|
2164
|
-
this.cachedIndexTime = -1;
|
|
2165
|
-
this.pendingIndexLookup = null;
|
|
2166
2263
|
if (this.state.useWorker && this.state.workerReady) {
|
|
2167
2264
|
sendToWorker({ type: 'clearVobSubCache', sessionId: this.state.sessionId }).catch(() => { });
|
|
2168
2265
|
}
|
|
@@ -2179,9 +2276,6 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
2179
2276
|
this.state.renderIssues.clear();
|
|
2180
2277
|
this.state.pendingRenders.clear();
|
|
2181
2278
|
this.clearOffscreenFrameMetadata();
|
|
2182
|
-
this.cachedIndex = -1;
|
|
2183
|
-
this.cachedIndexTime = -1;
|
|
2184
|
-
this.pendingIndexLookup = null;
|
|
2185
2279
|
this.lastRenderedIndex = -1;
|
|
2186
2280
|
if (this.state.useWorker && this.state.workerReady) {
|
|
2187
2281
|
sendToWorker({ type: 'clearVobSubCache', sessionId: this.state.sessionId }).catch(() => { });
|
|
@@ -2270,12 +2364,9 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
2270
2364
|
this.state.frameCache.clear();
|
|
2271
2365
|
this.state.renderIssues.clear();
|
|
2272
2366
|
this.state.pendingRenders.clear();
|
|
2273
|
-
|
|
2274
|
-
sendToWorker({ type: 'disposeVobSub', sessionId: this.state.sessionId }).catch(() => { });
|
|
2275
|
-
}
|
|
2367
|
+
this.releaseWorkerSession(this.state);
|
|
2276
2368
|
this.vobsubParser?.dispose();
|
|
2277
2369
|
this.vobsubParser = null;
|
|
2278
|
-
this.state.sessionId = null;
|
|
2279
2370
|
}
|
|
2280
2371
|
}
|
|
2281
2372
|
/** High-level DVB overlay renderer bound to a video element. */
|
|
@@ -2313,10 +2404,7 @@ export class DvbRenderer extends BaseVideoSubtitleRenderer {
|
|
|
2313
2404
|
return;
|
|
2314
2405
|
}
|
|
2315
2406
|
if (!this.streamingLoad) {
|
|
2316
|
-
const { data, strategy, rangeSupported, total } = await fetchSubtitleAsset(this.subUrl,
|
|
2317
|
-
preferRange: this.rangeRequests,
|
|
2318
|
-
onProgress: (progress) => this.emitLoadProgress('dvb', progress, this.state.timestamps.length)
|
|
2319
|
-
});
|
|
2407
|
+
const { data, strategy, rangeSupported, total } = await fetchSubtitleAsset(this.subUrl, this.fetchOptions(this.rangeRequests, (progress) => this.emitLoadProgress('dvb', progress, this.state.timestamps.length)));
|
|
2320
2408
|
this.emitLoadProgress('dvb', { loaded: data.byteLength, total: total ?? data.byteLength, ratio: 1, rangeSupported, strategy }, 0);
|
|
2321
2409
|
await this.loadDvbBuffer(data, false);
|
|
2322
2410
|
this.onLoaded?.();
|
|
@@ -2326,11 +2414,23 @@ export class DvbRenderer extends BaseVideoSubtitleRenderer {
|
|
|
2326
2414
|
this.onLoaded?.();
|
|
2327
2415
|
}
|
|
2328
2416
|
catch (error) {
|
|
2417
|
+
if (this.disposed)
|
|
2418
|
+
return;
|
|
2329
2419
|
const resolvedError = normalizeSubtitleError(error, { format: 'dvb' });
|
|
2330
2420
|
this.emitEvent({ type: 'error', format: 'dvb', error: resolvedError });
|
|
2331
2421
|
this.onError?.(resolvedError);
|
|
2332
2422
|
}
|
|
2333
2423
|
}
|
|
2424
|
+
/** Resolve an append response, which may carry only changed entries, to the full lists. */
|
|
2425
|
+
mergeDvbTimestamps(response) {
|
|
2426
|
+
const base = response.timestampBase;
|
|
2427
|
+
if (base === undefined)
|
|
2428
|
+
return { timestamps: response.timestamps, endTimestamps: response.endTimestamps };
|
|
2429
|
+
return {
|
|
2430
|
+
timestamps: spliceTimestamps(this.state.timestamps, base, response.timestamps),
|
|
2431
|
+
endTimestamps: spliceTimestamps(this.endTimestamps, base, response.endTimestamps)
|
|
2432
|
+
};
|
|
2433
|
+
}
|
|
2334
2434
|
applyDvbIndexState(metadata, timestamps, endTimestamps, partial, usingWorker) {
|
|
2335
2435
|
this.state.metadata = metadata;
|
|
2336
2436
|
this.state.timestamps = timestamps;
|
|
@@ -2364,7 +2464,7 @@ export class DvbRenderer extends BaseVideoSubtitleRenderer {
|
|
|
2364
2464
|
}
|
|
2365
2465
|
catch (workerError) {
|
|
2366
2466
|
this.state.useWorker = false;
|
|
2367
|
-
this.state
|
|
2467
|
+
this.releaseWorkerSession(this.state);
|
|
2368
2468
|
this.state.sessionId = null;
|
|
2369
2469
|
this.emitWorkerState(false, false, null, true);
|
|
2370
2470
|
this.emitWarning(createSubtitleWarning('WORKER_FALLBACK', 'DVB worker initialization failed, falling back to main-thread rendering.', {
|
|
@@ -2415,13 +2515,14 @@ export class DvbRenderer extends BaseVideoSubtitleRenderer {
|
|
|
2415
2515
|
if (response.type !== 'dvbProgress')
|
|
2416
2516
|
throw new Error('Unexpected DVB worker response');
|
|
2417
2517
|
added = response.added;
|
|
2518
|
+
const { timestamps, endTimestamps } = this.mergeDvbTimestamps(response);
|
|
2418
2519
|
if (added > 0) {
|
|
2419
|
-
this.applyDvbIndexState(response.metadata,
|
|
2520
|
+
this.applyDvbIndexState(response.metadata, timestamps, endTimestamps, true, true);
|
|
2420
2521
|
}
|
|
2421
2522
|
else {
|
|
2422
2523
|
this.state.metadata = response.metadata;
|
|
2423
|
-
this.state.timestamps =
|
|
2424
|
-
this.endTimestamps =
|
|
2524
|
+
this.state.timestamps = timestamps;
|
|
2525
|
+
this.endTimestamps = endTimestamps;
|
|
2425
2526
|
}
|
|
2426
2527
|
}
|
|
2427
2528
|
else {
|
|
@@ -2520,6 +2621,7 @@ export class DvbRenderer extends BaseVideoSubtitleRenderer {
|
|
|
2520
2621
|
catch (workerError) {
|
|
2521
2622
|
this.state.useWorker = false;
|
|
2522
2623
|
this.emitWorkerState(false, false, this.state.sessionId, true);
|
|
2624
|
+
this.releaseWorkerSession(this.state);
|
|
2523
2625
|
this.emitWarning(createSubtitleWarning('WORKER_FALLBACK', 'DVB worker initialization failed, falling back to main-thread rendering.', {
|
|
2524
2626
|
format: 'dvb',
|
|
2525
2627
|
details: { reason: workerError instanceof Error ? workerError.message : String(workerError) }
|
|
@@ -2545,6 +2647,7 @@ export class DvbRenderer extends BaseVideoSubtitleRenderer {
|
|
|
2545
2647
|
this.state.useWorker = false;
|
|
2546
2648
|
usedWorker = false;
|
|
2547
2649
|
this.emitWorkerState(false, false, this.state.sessionId, true);
|
|
2650
|
+
this.releaseWorkerSession(this.state);
|
|
2548
2651
|
this.emitWarning(createSubtitleWarning('WORKER_FALLBACK', 'DVB worker initialization failed, falling back to main-thread rendering.', {
|
|
2549
2652
|
format: 'dvb',
|
|
2550
2653
|
details: { reason: workerError instanceof Error ? workerError.message : String(workerError) }
|
|
@@ -2557,10 +2660,7 @@ export class DvbRenderer extends BaseVideoSubtitleRenderer {
|
|
|
2557
2660
|
this.dvbParser.reset();
|
|
2558
2661
|
}
|
|
2559
2662
|
try {
|
|
2560
|
-
const { data, strategy, rangeSupported, total } = await fetchSubtitleAsset(url, {
|
|
2561
|
-
preferRange: this.rangeRequests,
|
|
2562
|
-
onProgress: (progress) => this.emitLoadProgress('dvb', progress, this.state.timestamps.length)
|
|
2563
|
-
}, async (chunk, progress) => {
|
|
2663
|
+
const { data, strategy, rangeSupported, total } = await fetchSubtitleAsset(url, this.fetchOptions(this.rangeRequests, (progress) => this.emitLoadProgress('dvb', progress, this.state.timestamps.length)), async (chunk, progress) => {
|
|
2564
2664
|
if (chunk.byteLength === 0)
|
|
2565
2665
|
return;
|
|
2566
2666
|
if (usedWorker && this.state.sessionId) {
|
|
@@ -2571,13 +2671,14 @@ export class DvbRenderer extends BaseVideoSubtitleRenderer {
|
|
|
2571
2671
|
data: transferable
|
|
2572
2672
|
});
|
|
2573
2673
|
if (response.type === 'dvbProgress') {
|
|
2674
|
+
const { timestamps, endTimestamps } = this.mergeDvbTimestamps(response);
|
|
2574
2675
|
if (response.added > 0 || !indexedOnce) {
|
|
2575
|
-
this.applyDvbIndexState(response.metadata,
|
|
2676
|
+
this.applyDvbIndexState(response.metadata, timestamps, endTimestamps, true, true);
|
|
2576
2677
|
indexedOnce = true;
|
|
2577
2678
|
}
|
|
2578
2679
|
else {
|
|
2579
|
-
this.state.timestamps =
|
|
2580
|
-
this.endTimestamps =
|
|
2680
|
+
this.state.timestamps = timestamps;
|
|
2681
|
+
this.endTimestamps = endTimestamps;
|
|
2581
2682
|
this.state.metadata = response.metadata;
|
|
2582
2683
|
}
|
|
2583
2684
|
}
|
|
@@ -2632,12 +2733,15 @@ export class DvbRenderer extends BaseVideoSubtitleRenderer {
|
|
|
2632
2733
|
if (usedWorker) {
|
|
2633
2734
|
this.state.useWorker = false;
|
|
2634
2735
|
this.emitWorkerState(false, false, this.state.sessionId, true);
|
|
2736
|
+
this.releaseWorkerSession(this.state);
|
|
2635
2737
|
}
|
|
2738
|
+
if (this.disposed)
|
|
2739
|
+
throw error;
|
|
2636
2740
|
this.emitWarning(createSubtitleWarning('RANGE_FALLBACK', 'Progressive DVB load failed; retrying with a full buffer fetch.', {
|
|
2637
2741
|
format: 'dvb',
|
|
2638
2742
|
details: { reason: error instanceof Error ? error.message : String(error) }
|
|
2639
2743
|
}));
|
|
2640
|
-
const { data } = await fetchSubtitleAsset(url,
|
|
2744
|
+
const { data } = await fetchSubtitleAsset(url, this.fetchOptions(this.rangeRequests));
|
|
2641
2745
|
await this.loadDvbBuffer(data, false);
|
|
2642
2746
|
}
|
|
2643
2747
|
}
|
|
@@ -2817,13 +2921,10 @@ export class DvbRenderer extends BaseVideoSubtitleRenderer {
|
|
|
2817
2921
|
this.state.frameCache.clear();
|
|
2818
2922
|
this.state.renderIssues.clear();
|
|
2819
2923
|
this.state.pendingRenders.clear();
|
|
2820
|
-
|
|
2821
|
-
sendToWorker({ type: 'disposeDvb', sessionId: this.state.sessionId }).catch(() => { });
|
|
2822
|
-
}
|
|
2924
|
+
this.releaseWorkerSession(this.state);
|
|
2823
2925
|
this.dvbParser?.dispose();
|
|
2824
2926
|
this.dvbParser = null;
|
|
2825
2927
|
this.endTimestamps = new Float64Array(0);
|
|
2826
|
-
this.state.sessionId = null;
|
|
2827
2928
|
}
|
|
2828
2929
|
}
|
|
2829
2930
|
/**
|