libbitsub 1.12.1 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- 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 +224 -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 +5 -3
- 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() {
|
|
@@ -498,8 +529,6 @@ class BaseVideoSubtitleRenderer {
|
|
|
498
529
|
}
|
|
499
530
|
this.useWorkerOffscreen = true;
|
|
500
531
|
this.emitRendererBackend('worker-offscreen');
|
|
501
|
-
this.lastRenderedIndex = -1;
|
|
502
|
-
this.lastRenderedTime = -1;
|
|
503
532
|
await sendToWorker({
|
|
504
533
|
type: 'resizeOffscreenCanvas',
|
|
505
534
|
sessionId,
|
|
@@ -518,6 +547,8 @@ class BaseVideoSubtitleRenderer {
|
|
|
518
547
|
details: { reason: error instanceof Error ? error.message : String(error) }
|
|
519
548
|
}));
|
|
520
549
|
}
|
|
550
|
+
if (this.useWorkerOffscreen)
|
|
551
|
+
this.reconcileBackendPresentation();
|
|
521
552
|
}
|
|
522
553
|
recreateCanvasForMainThreadFallback() {
|
|
523
554
|
this.removeOwnedCanvas();
|
|
@@ -600,68 +631,112 @@ class BaseVideoSubtitleRenderer {
|
|
|
600
631
|
}
|
|
601
632
|
/** Initialize WebGPU renderer. */
|
|
602
633
|
async initWebGPU(allowBackendFallback = true) {
|
|
634
|
+
if (this.disposed)
|
|
635
|
+
return;
|
|
636
|
+
let renderer = null;
|
|
603
637
|
try {
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
638
|
+
renderer = new WebGPURenderer();
|
|
639
|
+
this.webgpuRenderer = renderer;
|
|
640
|
+
await renderer.init();
|
|
641
|
+
if (this.disposed || !this.canvas) {
|
|
642
|
+
renderer.destroy();
|
|
607
643
|
return;
|
|
644
|
+
}
|
|
608
645
|
const bounds = this.getVideoContentBounds();
|
|
609
646
|
const pixelRatio = this.getDevicePixelRatio();
|
|
610
647
|
const width = Math.max(1, bounds.width * pixelRatio);
|
|
611
648
|
const height = Math.max(1, bounds.height * pixelRatio);
|
|
612
|
-
await
|
|
613
|
-
this.
|
|
614
|
-
|
|
649
|
+
await renderer.setCanvas(this.canvas, width, height);
|
|
650
|
+
if (this.disposed) {
|
|
651
|
+
renderer.destroy();
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
615
654
|
}
|
|
616
655
|
catch (error) {
|
|
617
|
-
|
|
656
|
+
renderer?.destroy();
|
|
657
|
+
if (this.disposed)
|
|
658
|
+
return;
|
|
618
659
|
this.webgpuRenderer = null;
|
|
619
660
|
this.useWebGPU = false;
|
|
620
661
|
this.onWebGPUFallback?.();
|
|
662
|
+
if (this.disposed)
|
|
663
|
+
return;
|
|
621
664
|
if (allowBackendFallback && isWebGL2Supported()) {
|
|
622
665
|
void this.initWebGL2();
|
|
623
666
|
}
|
|
624
667
|
else {
|
|
625
668
|
this.preferWorkerOffscreenOrCanvas2D();
|
|
626
669
|
}
|
|
670
|
+
return;
|
|
627
671
|
}
|
|
672
|
+
this.useWebGPU = true;
|
|
673
|
+
this.emitRendererBackend('webgpu');
|
|
674
|
+
this.reconcileBackendPresentation();
|
|
628
675
|
}
|
|
629
676
|
/** Initialize WebGL2 renderer. */
|
|
630
677
|
async initWebGL2(allowBackendFallback = true) {
|
|
678
|
+
if (this.disposed)
|
|
679
|
+
return;
|
|
680
|
+
let renderer = null;
|
|
631
681
|
try {
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
682
|
+
renderer = new WebGL2Renderer();
|
|
683
|
+
this.webgl2Renderer = renderer;
|
|
684
|
+
await renderer.init();
|
|
685
|
+
if (this.disposed || !this.canvas) {
|
|
686
|
+
renderer.destroy();
|
|
635
687
|
return;
|
|
688
|
+
}
|
|
636
689
|
const bounds = this.getVideoContentBounds();
|
|
637
690
|
const pixelRatio = this.getDevicePixelRatio();
|
|
638
691
|
const width = Math.max(1, bounds.width * pixelRatio);
|
|
639
692
|
const height = Math.max(1, bounds.height * pixelRatio);
|
|
640
|
-
await
|
|
641
|
-
this.
|
|
642
|
-
|
|
693
|
+
await renderer.setCanvas(this.canvas, width, height);
|
|
694
|
+
if (this.disposed) {
|
|
695
|
+
renderer.destroy();
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
643
698
|
}
|
|
644
699
|
catch (error) {
|
|
645
|
-
|
|
700
|
+
renderer?.destroy();
|
|
701
|
+
if (this.disposed)
|
|
702
|
+
return;
|
|
646
703
|
this.webgl2Renderer = null;
|
|
647
704
|
this.useWebGL2 = false;
|
|
648
705
|
this.onWebGL2Fallback?.();
|
|
706
|
+
if (this.disposed)
|
|
707
|
+
return;
|
|
649
708
|
if (allowBackendFallback) {
|
|
650
709
|
this.preferWorkerOffscreenOrCanvas2D();
|
|
651
710
|
}
|
|
652
711
|
else {
|
|
653
712
|
this.initCanvas2D();
|
|
654
713
|
}
|
|
714
|
+
return;
|
|
655
715
|
}
|
|
716
|
+
this.useWebGL2 = true;
|
|
717
|
+
this.emitRendererBackend('webgl2');
|
|
718
|
+
this.reconcileBackendPresentation();
|
|
656
719
|
}
|
|
657
720
|
/** Initialize Canvas2D renderer. */
|
|
658
721
|
initCanvas2D() {
|
|
659
|
-
if (!this.canvas)
|
|
722
|
+
if (this.disposed || !this.canvas)
|
|
660
723
|
return;
|
|
661
724
|
this.ctx = this.canvas.getContext('2d');
|
|
662
725
|
this.useWebGPU = false;
|
|
663
726
|
this.useWebGL2 = false;
|
|
664
727
|
this.emitRendererBackend('canvas2d');
|
|
728
|
+
this.reconcileBackendPresentation();
|
|
729
|
+
}
|
|
730
|
+
/** A cue decoded before backend startup has not yet reached the canvas. */
|
|
731
|
+
reconcileBackendPresentation() {
|
|
732
|
+
if (this.disposed)
|
|
733
|
+
return;
|
|
734
|
+
this.invalidatePresentation();
|
|
735
|
+
this.lastRenderedIndex = -2;
|
|
736
|
+
this.lastRenderedTime = -1;
|
|
737
|
+
// Loading creates the temporary drawing canvas before starting presentation.
|
|
738
|
+
if (this.frameScheduler)
|
|
739
|
+
this.renderPausedFrame();
|
|
665
740
|
}
|
|
666
741
|
/** Called when video seeks. */
|
|
667
742
|
onSeek() { }
|
|
@@ -779,6 +854,13 @@ class BaseVideoSubtitleRenderer {
|
|
|
779
854
|
return;
|
|
780
855
|
this.renderSynchronizedFrame({ mediaTime: this.video.currentTime, presentedFrames: null });
|
|
781
856
|
}
|
|
857
|
+
/**
|
|
858
|
+
* Render the cue for a media time synchronously, whether or not the video is paused.
|
|
859
|
+
* Hosts call this from their own presented-frame callback so a cached cue paints on that frame.
|
|
860
|
+
*/
|
|
861
|
+
renderAtMediaTime(mediaTime) {
|
|
862
|
+
this.renderSynchronizedFrame({ mediaTime, presentedFrames: null });
|
|
863
|
+
}
|
|
782
864
|
renderSynchronizedFrame(tick) {
|
|
783
865
|
if (this.disposed || !this.isLoaded)
|
|
784
866
|
return;
|
|
@@ -853,7 +935,7 @@ class BaseVideoSubtitleRenderer {
|
|
|
853
935
|
if (this.findCurrentIndex(this.getCurrentSynchronizationTime()) !== index)
|
|
854
936
|
return;
|
|
855
937
|
this.lastRenderedIndex = -1;
|
|
856
|
-
this.
|
|
938
|
+
this.renderAtMediaTime(this.getCurrentSynchronizationTime() - this.timeOffset);
|
|
857
939
|
}, () => { });
|
|
858
940
|
}
|
|
859
941
|
recordRenderPerformance(renderTime, capturedAt, countAsDropped = true) {
|
|
@@ -904,12 +986,17 @@ class BaseVideoSubtitleRenderer {
|
|
|
904
986
|
// If data is undefined, it means async loading is in progress
|
|
905
987
|
// Keep showing the last frame only while waiting for async data
|
|
906
988
|
// Note: null means "loaded but empty" (clear screen), undefined means "still loading"
|
|
989
|
+
let stalePending = false;
|
|
907
990
|
if (data === undefined && this.lastRenderedData !== null && index >= 0) {
|
|
908
991
|
// Check if this index has a pending render (truly async loading)
|
|
909
992
|
// If not pending, it means the render returned no data immediately
|
|
910
993
|
if (this.isPendingRender(index)) {
|
|
911
|
-
|
|
912
|
-
|
|
994
|
+
if (!this.clearStaleWhilePending) {
|
|
995
|
+
// Don't clear - keep showing the last frame while loading
|
|
996
|
+
return { status: 'pending', data: null, warning: null };
|
|
997
|
+
}
|
|
998
|
+
// The previous cue has ended: clear it below instead of holding it
|
|
999
|
+
stalePending = true;
|
|
913
1000
|
}
|
|
914
1001
|
}
|
|
915
1002
|
const renderIssue = index >= 0 ? (this.getWorkerRendererState().renderIssues.get(index) ?? null) : null;
|
|
@@ -927,6 +1014,9 @@ class BaseVideoSubtitleRenderer {
|
|
|
927
1014
|
if (index < 0) {
|
|
928
1015
|
return { status: 'cleared', data: null, warning: null };
|
|
929
1016
|
}
|
|
1017
|
+
if (stalePending) {
|
|
1018
|
+
return { status: 'pending', data: null, warning: null };
|
|
1019
|
+
}
|
|
930
1020
|
if (warning) {
|
|
931
1021
|
return { status: 'failed', data: data ?? null, warning };
|
|
932
1022
|
}
|
|
@@ -1141,6 +1231,7 @@ class BaseVideoSubtitleRenderer {
|
|
|
1141
1231
|
/** Dispose of all resources. */
|
|
1142
1232
|
dispose() {
|
|
1143
1233
|
this.disposed = true;
|
|
1234
|
+
this.lifecycle.abort();
|
|
1144
1235
|
this.invalidatePresentation();
|
|
1145
1236
|
this.frameScheduler?.stop();
|
|
1146
1237
|
this.frameScheduler = null;
|
|
@@ -1222,10 +1313,7 @@ export class PgsRenderer extends BaseVideoSubtitleRenderer {
|
|
|
1222
1313
|
return;
|
|
1223
1314
|
}
|
|
1224
1315
|
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
|
-
});
|
|
1316
|
+
const { data, strategy, rangeSupported, total } = await fetchSubtitleAsset(this.subUrl, this.fetchOptions(this.rangeRequests, (progress) => this.emitLoadProgress('pgs', progress, this.state.timestamps.length)));
|
|
1229
1317
|
this.emitLoadProgress('pgs', { loaded: data.byteLength, total: total ?? data.byteLength, ratio: 1, rangeSupported, strategy }, 0);
|
|
1230
1318
|
await this.loadPgsBuffer(data, false);
|
|
1231
1319
|
this.onLoaded?.();
|
|
@@ -1235,11 +1323,19 @@ export class PgsRenderer extends BaseVideoSubtitleRenderer {
|
|
|
1235
1323
|
this.onLoaded?.();
|
|
1236
1324
|
}
|
|
1237
1325
|
catch (error) {
|
|
1326
|
+
if (this.disposed)
|
|
1327
|
+
return;
|
|
1238
1328
|
const resolvedError = normalizeSubtitleError(error, { format: 'pgs' });
|
|
1239
1329
|
this.emitEvent({ type: 'error', format: 'pgs', error: resolvedError });
|
|
1240
1330
|
this.onError?.(resolvedError);
|
|
1241
1331
|
}
|
|
1242
1332
|
}
|
|
1333
|
+
/** Resolve an append response, which may carry only new timestamps, to the full list. */
|
|
1334
|
+
mergePgsTimestamps(response) {
|
|
1335
|
+
if (response.timestampBase === undefined)
|
|
1336
|
+
return response.timestamps;
|
|
1337
|
+
return spliceTimestamps(this.state.timestamps, response.timestampBase, response.timestamps);
|
|
1338
|
+
}
|
|
1243
1339
|
applyPgsIndexState(metadata, timestamps, partial, usingWorker) {
|
|
1244
1340
|
this.state.metadata = metadata;
|
|
1245
1341
|
this.state.timestamps = timestamps;
|
|
@@ -1272,7 +1368,7 @@ export class PgsRenderer extends BaseVideoSubtitleRenderer {
|
|
|
1272
1368
|
}
|
|
1273
1369
|
catch (workerError) {
|
|
1274
1370
|
this.state.useWorker = false;
|
|
1275
|
-
this.state
|
|
1371
|
+
this.releaseWorkerSession(this.state);
|
|
1276
1372
|
this.state.sessionId = null;
|
|
1277
1373
|
this.emitWorkerState(false, false, null, true);
|
|
1278
1374
|
this.emitWarning(createSubtitleWarning('WORKER_FALLBACK', 'PGS worker initialization failed, falling back to main-thread rendering.', {
|
|
@@ -1323,12 +1419,13 @@ export class PgsRenderer extends BaseVideoSubtitleRenderer {
|
|
|
1323
1419
|
if (response.type !== 'pgsProgress')
|
|
1324
1420
|
throw new Error('Unexpected PGS worker response');
|
|
1325
1421
|
added = response.added;
|
|
1422
|
+
const timestamps = this.mergePgsTimestamps(response);
|
|
1326
1423
|
if (added > 0) {
|
|
1327
|
-
this.applyPgsIndexState(response.metadata,
|
|
1424
|
+
this.applyPgsIndexState(response.metadata, timestamps, true, true);
|
|
1328
1425
|
}
|
|
1329
1426
|
else {
|
|
1330
1427
|
this.state.metadata = response.metadata;
|
|
1331
|
-
this.state.timestamps =
|
|
1428
|
+
this.state.timestamps = timestamps;
|
|
1332
1429
|
}
|
|
1333
1430
|
}
|
|
1334
1431
|
else {
|
|
@@ -1426,6 +1523,7 @@ export class PgsRenderer extends BaseVideoSubtitleRenderer {
|
|
|
1426
1523
|
catch (workerError) {
|
|
1427
1524
|
this.state.useWorker = false;
|
|
1428
1525
|
this.emitWorkerState(false, false, this.state.sessionId, true);
|
|
1526
|
+
this.releaseWorkerSession(this.state);
|
|
1429
1527
|
this.emitWarning(createSubtitleWarning('WORKER_FALLBACK', 'PGS worker initialization failed, falling back to main-thread rendering.', {
|
|
1430
1528
|
format: 'pgs',
|
|
1431
1529
|
details: { reason: workerError instanceof Error ? workerError.message : String(workerError) }
|
|
@@ -1451,6 +1549,7 @@ export class PgsRenderer extends BaseVideoSubtitleRenderer {
|
|
|
1451
1549
|
this.state.useWorker = false;
|
|
1452
1550
|
usedWorker = false;
|
|
1453
1551
|
this.emitWorkerState(false, false, this.state.sessionId, true);
|
|
1552
|
+
this.releaseWorkerSession(this.state);
|
|
1454
1553
|
this.emitWarning(createSubtitleWarning('WORKER_FALLBACK', 'PGS worker initialization failed, falling back to main-thread rendering.', {
|
|
1455
1554
|
format: 'pgs',
|
|
1456
1555
|
details: { reason: workerError instanceof Error ? workerError.message : String(workerError) }
|
|
@@ -1463,10 +1562,7 @@ export class PgsRenderer extends BaseVideoSubtitleRenderer {
|
|
|
1463
1562
|
this.pgsParser.reset();
|
|
1464
1563
|
}
|
|
1465
1564
|
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) => {
|
|
1565
|
+
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
1566
|
if (chunk.byteLength === 0)
|
|
1471
1567
|
return;
|
|
1472
1568
|
if (usedWorker && this.state.sessionId) {
|
|
@@ -1477,12 +1573,13 @@ export class PgsRenderer extends BaseVideoSubtitleRenderer {
|
|
|
1477
1573
|
data: transferable
|
|
1478
1574
|
});
|
|
1479
1575
|
if (response.type === 'pgsProgress') {
|
|
1576
|
+
const timestamps = this.mergePgsTimestamps(response);
|
|
1480
1577
|
if (response.added > 0 || !indexedOnce) {
|
|
1481
|
-
this.applyPgsIndexState(response.metadata,
|
|
1578
|
+
this.applyPgsIndexState(response.metadata, timestamps, true, true);
|
|
1482
1579
|
indexedOnce = true;
|
|
1483
1580
|
}
|
|
1484
1581
|
else {
|
|
1485
|
-
this.state.timestamps =
|
|
1582
|
+
this.state.timestamps = timestamps;
|
|
1486
1583
|
this.state.metadata = response.metadata;
|
|
1487
1584
|
}
|
|
1488
1585
|
}
|
|
@@ -1536,12 +1633,15 @@ export class PgsRenderer extends BaseVideoSubtitleRenderer {
|
|
|
1536
1633
|
if (usedWorker) {
|
|
1537
1634
|
this.state.useWorker = false;
|
|
1538
1635
|
this.emitWorkerState(false, false, this.state.sessionId, true);
|
|
1636
|
+
this.releaseWorkerSession(this.state);
|
|
1539
1637
|
}
|
|
1638
|
+
if (this.disposed)
|
|
1639
|
+
throw error;
|
|
1540
1640
|
this.emitWarning(createSubtitleWarning('RANGE_FALLBACK', 'Progressive PGS load failed; retrying with a full buffer fetch.', {
|
|
1541
1641
|
format: 'pgs',
|
|
1542
1642
|
details: { reason: error instanceof Error ? error.message : String(error) }
|
|
1543
1643
|
}));
|
|
1544
|
-
const { data } = await fetchSubtitleAsset(url,
|
|
1644
|
+
const { data } = await fetchSubtitleAsset(url, this.fetchOptions(this.rangeRequests));
|
|
1545
1645
|
await this.loadPgsBuffer(data, false);
|
|
1546
1646
|
}
|
|
1547
1647
|
}
|
|
@@ -1718,12 +1818,9 @@ export class PgsRenderer extends BaseVideoSubtitleRenderer {
|
|
|
1718
1818
|
this.state.frameCache.clear();
|
|
1719
1819
|
this.state.renderIssues.clear();
|
|
1720
1820
|
this.state.pendingRenders.clear();
|
|
1721
|
-
|
|
1722
|
-
sendToWorker({ type: 'disposePgs', sessionId: this.state.sessionId }).catch(() => { });
|
|
1723
|
-
}
|
|
1821
|
+
this.releaseWorkerSession(this.state);
|
|
1724
1822
|
this.pgsParser?.dispose();
|
|
1725
1823
|
this.pgsParser = null;
|
|
1726
|
-
this.state.sessionId = null;
|
|
1727
1824
|
}
|
|
1728
1825
|
}
|
|
1729
1826
|
/**
|
|
@@ -1739,10 +1836,8 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
1739
1836
|
onLoading;
|
|
1740
1837
|
onLoaded;
|
|
1741
1838
|
onError;
|
|
1742
|
-
//
|
|
1743
|
-
|
|
1744
|
-
cachedIndexTime = -1;
|
|
1745
|
-
pendingIndexLookup = null;
|
|
1839
|
+
// Cue end times (ms) for the main-thread index lookup in worker mode; starts live in state.timestamps
|
|
1840
|
+
cueEnds = null;
|
|
1746
1841
|
constructor(options) {
|
|
1747
1842
|
super(options, 'vobsub');
|
|
1748
1843
|
this.idxUrl =
|
|
@@ -1753,6 +1848,7 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
1753
1848
|
this.onLoading = options.onLoading;
|
|
1754
1849
|
this.onLoaded = options.onLoaded;
|
|
1755
1850
|
this.onError = options.onError;
|
|
1851
|
+
this.clearStaleWhilePending = true;
|
|
1756
1852
|
applyCacheLimit(this.state, this.cacheLimit);
|
|
1757
1853
|
this.startInit();
|
|
1758
1854
|
}
|
|
@@ -1783,6 +1879,8 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
1783
1879
|
this.onLoaded?.();
|
|
1784
1880
|
}
|
|
1785
1881
|
catch (error) {
|
|
1882
|
+
if (this.disposed)
|
|
1883
|
+
return;
|
|
1786
1884
|
const resolvedError = normalizeSubtitleError(error, { format: 'vobsub' });
|
|
1787
1885
|
this.emitEvent({ type: 'error', format: 'vobsub', error: resolvedError });
|
|
1788
1886
|
this.onError?.(resolvedError);
|
|
@@ -1813,6 +1911,7 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
1813
1911
|
catch (workerError) {
|
|
1814
1912
|
this.state.useWorker = false;
|
|
1815
1913
|
this.emitWorkerState(false, false, this.state.sessionId, true);
|
|
1914
|
+
this.releaseWorkerSession(this.state);
|
|
1816
1915
|
this.emitWarning(createSubtitleWarning('WORKER_FALLBACK', 'VobSub worker initialization failed, falling back to main-thread rendering.', {
|
|
1817
1916
|
format: 'vobsub',
|
|
1818
1917
|
details: { reason: workerError instanceof Error ? workerError.message : String(workerError) }
|
|
@@ -1845,6 +1944,7 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
1845
1944
|
this.state.workerReady = true;
|
|
1846
1945
|
this.state.metadata = loadResponse.metadata;
|
|
1847
1946
|
this.state.timestamps = loadResponse.timestamps;
|
|
1947
|
+
await this.loadCueTable();
|
|
1848
1948
|
this.isLoaded = true;
|
|
1849
1949
|
this.setParserMetadata(loadResponse.metadata);
|
|
1850
1950
|
this.emitIndexed('vobsub', loadResponse.metadata, false);
|
|
@@ -1861,6 +1961,7 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
1861
1961
|
}
|
|
1862
1962
|
this.state.useWorker = false;
|
|
1863
1963
|
this.emitWorkerState(false, false, this.state.sessionId, true);
|
|
1964
|
+
this.releaseWorkerSession(this.state);
|
|
1864
1965
|
this.emitWarning(createSubtitleWarning('WORKER_FALLBACK', 'VobSub worker load failed, falling back to main-thread rendering.', {
|
|
1865
1966
|
format: 'vobsub',
|
|
1866
1967
|
details: { reason: workerError instanceof Error ? workerError.message : String(workerError) }
|
|
@@ -1877,10 +1978,7 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
1877
1978
|
if (!this.subUrl) {
|
|
1878
1979
|
throw createSubtitleDiagnosticError('MISSING_INPUT', 'No SUB content or URL provided.', { format: 'vobsub' });
|
|
1879
1980
|
}
|
|
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
|
-
});
|
|
1981
|
+
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
1982
|
this.emitLoadProgress('vobsub', {
|
|
1885
1983
|
loaded: data.byteLength,
|
|
1886
1984
|
total: total ?? data.byteLength,
|
|
@@ -1896,9 +1994,7 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
1896
1994
|
if (!this.idxUrl) {
|
|
1897
1995
|
throw createSubtitleDiagnosticError('MISSING_INPUT', 'No IDX content or URL provided.', { format: 'vobsub' });
|
|
1898
1996
|
}
|
|
1899
|
-
idxData = await fetchSubtitleText(this.idxUrl,
|
|
1900
|
-
onProgress: (progress) => this.emitLoadProgress('vobsub', progress, 0)
|
|
1901
|
-
});
|
|
1997
|
+
idxData = await fetchSubtitleText(this.idxUrl, this.fetchOptions(false, (progress) => this.emitLoadProgress('vobsub', progress, 0)));
|
|
1902
1998
|
}
|
|
1903
1999
|
let usedWorker = await this.ensureVobSubWorkerSession();
|
|
1904
2000
|
if (usedWorker && this.state.sessionId) {
|
|
@@ -1929,6 +2025,7 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
1929
2025
|
usedWorker = false;
|
|
1930
2026
|
this.state.useWorker = false;
|
|
1931
2027
|
this.emitWorkerState(false, false, this.state.sessionId, true);
|
|
2028
|
+
this.releaseWorkerSession(this.state);
|
|
1932
2029
|
this.emitWarning(createSubtitleWarning('WORKER_FALLBACK', 'VobSub worker IDX load failed, falling back to main-thread rendering.', {
|
|
1933
2030
|
format: 'vobsub',
|
|
1934
2031
|
details: { reason: error instanceof Error ? error.message : String(error) }
|
|
@@ -1953,10 +2050,7 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
1953
2050
|
if (!this.subUrl) {
|
|
1954
2051
|
throw createSubtitleDiagnosticError('MISSING_INPUT', 'No SUB content or URL provided.', { format: 'vobsub' });
|
|
1955
2052
|
}
|
|
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
|
-
});
|
|
2053
|
+
const fetched = await fetchSubtitleAsset(this.subUrl, this.fetchOptions(this.rangeRequests && this.streamingLoad, (progress) => this.emitLoadProgress('vobsub', progress, this.state.timestamps.length)));
|
|
1960
2054
|
subData = fetched.data;
|
|
1961
2055
|
this.emitLoadProgress('vobsub', {
|
|
1962
2056
|
loaded: subData.byteLength,
|
|
@@ -1976,6 +2070,7 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
1976
2070
|
if (attachResponse.type === 'vobSubProgress') {
|
|
1977
2071
|
this.applyVobSubIndexState(attachResponse.metadata, attachResponse.timestamps, false, true, true);
|
|
1978
2072
|
this.state.workerReady = true;
|
|
2073
|
+
await this.loadCueTable();
|
|
1979
2074
|
this.isLoaded = true;
|
|
1980
2075
|
this.emitWorkerState(true, true, this.state.sessionId);
|
|
1981
2076
|
return;
|
|
@@ -2045,50 +2140,53 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
2045
2140
|
}
|
|
2046
2141
|
findCurrentIndex(time) {
|
|
2047
2142
|
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;
|
|
2143
|
+
return this.findIndexInCueTable(time);
|
|
2089
2144
|
}
|
|
2090
2145
|
return this.vobsubParser?.findIndexAtTimestamp(time) ?? -1;
|
|
2091
2146
|
}
|
|
2147
|
+
/** Synchronous lookup against the worker's cue table; mirrors the core parser's find_index_at_timestamp. */
|
|
2148
|
+
findIndexInCueTable(time) {
|
|
2149
|
+
const starts = this.state.timestamps;
|
|
2150
|
+
const ends = this.cueEnds;
|
|
2151
|
+
if (!ends || starts.length === 0)
|
|
2152
|
+
return -1;
|
|
2153
|
+
const timeMs = Math.max(0, Math.floor(time * 1000));
|
|
2154
|
+
let low = 0;
|
|
2155
|
+
let high = starts.length;
|
|
2156
|
+
while (low < high) {
|
|
2157
|
+
const mid = low + ((high - low) >> 1);
|
|
2158
|
+
if (starts[mid] <= timeMs)
|
|
2159
|
+
low = mid + 1;
|
|
2160
|
+
else
|
|
2161
|
+
high = mid;
|
|
2162
|
+
}
|
|
2163
|
+
const index = low > 0 ? low - 1 : 0;
|
|
2164
|
+
if (timeMs < starts[index] || timeMs >= ends[index])
|
|
2165
|
+
return -1;
|
|
2166
|
+
return index;
|
|
2167
|
+
}
|
|
2168
|
+
/** Fetch cue end times once, after the worker holds the SUB data. Falls back to next-start ends. */
|
|
2169
|
+
async loadCueTable() {
|
|
2170
|
+
const starts = this.state.timestamps;
|
|
2171
|
+
let ends = null;
|
|
2172
|
+
try {
|
|
2173
|
+
const response = await sendToWorker({ type: 'getVobSubTimestamps', sessionId: this.state.sessionId });
|
|
2174
|
+
const received = response.endTimestamps;
|
|
2175
|
+
if (received && received.length === starts.length)
|
|
2176
|
+
ends = received;
|
|
2177
|
+
}
|
|
2178
|
+
catch {
|
|
2179
|
+
/* fall back below */
|
|
2180
|
+
}
|
|
2181
|
+
if (!ends) {
|
|
2182
|
+
ends = new Float64Array(starts.length);
|
|
2183
|
+
for (let i = 0; i < starts.length; i++)
|
|
2184
|
+
ends[i] = starts[i + 1] ?? starts[i] + 5000;
|
|
2185
|
+
}
|
|
2186
|
+
this.cueEnds = ends;
|
|
2187
|
+
this.lastRenderedIndex = -1;
|
|
2188
|
+
this.renderPausedFrame();
|
|
2189
|
+
}
|
|
2092
2190
|
renderAtIndex(index) {
|
|
2093
2191
|
if (this.state.frameCache.has(index)) {
|
|
2094
2192
|
return this.state.frameCache.get(index) ?? undefined;
|
|
@@ -2159,10 +2257,6 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
2159
2257
|
this.state.frameCache.clear();
|
|
2160
2258
|
this.state.renderIssues.clear();
|
|
2161
2259
|
this.state.pendingRenders.clear();
|
|
2162
|
-
// Clear cached index lookup on seek
|
|
2163
|
-
this.cachedIndex = -1;
|
|
2164
|
-
this.cachedIndexTime = -1;
|
|
2165
|
-
this.pendingIndexLookup = null;
|
|
2166
2260
|
if (this.state.useWorker && this.state.workerReady) {
|
|
2167
2261
|
sendToWorker({ type: 'clearVobSubCache', sessionId: this.state.sessionId }).catch(() => { });
|
|
2168
2262
|
}
|
|
@@ -2179,9 +2273,6 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
2179
2273
|
this.state.renderIssues.clear();
|
|
2180
2274
|
this.state.pendingRenders.clear();
|
|
2181
2275
|
this.clearOffscreenFrameMetadata();
|
|
2182
|
-
this.cachedIndex = -1;
|
|
2183
|
-
this.cachedIndexTime = -1;
|
|
2184
|
-
this.pendingIndexLookup = null;
|
|
2185
2276
|
this.lastRenderedIndex = -1;
|
|
2186
2277
|
if (this.state.useWorker && this.state.workerReady) {
|
|
2187
2278
|
sendToWorker({ type: 'clearVobSubCache', sessionId: this.state.sessionId }).catch(() => { });
|
|
@@ -2270,12 +2361,9 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
2270
2361
|
this.state.frameCache.clear();
|
|
2271
2362
|
this.state.renderIssues.clear();
|
|
2272
2363
|
this.state.pendingRenders.clear();
|
|
2273
|
-
|
|
2274
|
-
sendToWorker({ type: 'disposeVobSub', sessionId: this.state.sessionId }).catch(() => { });
|
|
2275
|
-
}
|
|
2364
|
+
this.releaseWorkerSession(this.state);
|
|
2276
2365
|
this.vobsubParser?.dispose();
|
|
2277
2366
|
this.vobsubParser = null;
|
|
2278
|
-
this.state.sessionId = null;
|
|
2279
2367
|
}
|
|
2280
2368
|
}
|
|
2281
2369
|
/** High-level DVB overlay renderer bound to a video element. */
|
|
@@ -2313,10 +2401,7 @@ export class DvbRenderer extends BaseVideoSubtitleRenderer {
|
|
|
2313
2401
|
return;
|
|
2314
2402
|
}
|
|
2315
2403
|
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
|
-
});
|
|
2404
|
+
const { data, strategy, rangeSupported, total } = await fetchSubtitleAsset(this.subUrl, this.fetchOptions(this.rangeRequests, (progress) => this.emitLoadProgress('dvb', progress, this.state.timestamps.length)));
|
|
2320
2405
|
this.emitLoadProgress('dvb', { loaded: data.byteLength, total: total ?? data.byteLength, ratio: 1, rangeSupported, strategy }, 0);
|
|
2321
2406
|
await this.loadDvbBuffer(data, false);
|
|
2322
2407
|
this.onLoaded?.();
|
|
@@ -2326,11 +2411,23 @@ export class DvbRenderer extends BaseVideoSubtitleRenderer {
|
|
|
2326
2411
|
this.onLoaded?.();
|
|
2327
2412
|
}
|
|
2328
2413
|
catch (error) {
|
|
2414
|
+
if (this.disposed)
|
|
2415
|
+
return;
|
|
2329
2416
|
const resolvedError = normalizeSubtitleError(error, { format: 'dvb' });
|
|
2330
2417
|
this.emitEvent({ type: 'error', format: 'dvb', error: resolvedError });
|
|
2331
2418
|
this.onError?.(resolvedError);
|
|
2332
2419
|
}
|
|
2333
2420
|
}
|
|
2421
|
+
/** Resolve an append response, which may carry only changed entries, to the full lists. */
|
|
2422
|
+
mergeDvbTimestamps(response) {
|
|
2423
|
+
const base = response.timestampBase;
|
|
2424
|
+
if (base === undefined)
|
|
2425
|
+
return { timestamps: response.timestamps, endTimestamps: response.endTimestamps };
|
|
2426
|
+
return {
|
|
2427
|
+
timestamps: spliceTimestamps(this.state.timestamps, base, response.timestamps),
|
|
2428
|
+
endTimestamps: spliceTimestamps(this.endTimestamps, base, response.endTimestamps)
|
|
2429
|
+
};
|
|
2430
|
+
}
|
|
2334
2431
|
applyDvbIndexState(metadata, timestamps, endTimestamps, partial, usingWorker) {
|
|
2335
2432
|
this.state.metadata = metadata;
|
|
2336
2433
|
this.state.timestamps = timestamps;
|
|
@@ -2364,7 +2461,7 @@ export class DvbRenderer extends BaseVideoSubtitleRenderer {
|
|
|
2364
2461
|
}
|
|
2365
2462
|
catch (workerError) {
|
|
2366
2463
|
this.state.useWorker = false;
|
|
2367
|
-
this.state
|
|
2464
|
+
this.releaseWorkerSession(this.state);
|
|
2368
2465
|
this.state.sessionId = null;
|
|
2369
2466
|
this.emitWorkerState(false, false, null, true);
|
|
2370
2467
|
this.emitWarning(createSubtitleWarning('WORKER_FALLBACK', 'DVB worker initialization failed, falling back to main-thread rendering.', {
|
|
@@ -2415,13 +2512,14 @@ export class DvbRenderer extends BaseVideoSubtitleRenderer {
|
|
|
2415
2512
|
if (response.type !== 'dvbProgress')
|
|
2416
2513
|
throw new Error('Unexpected DVB worker response');
|
|
2417
2514
|
added = response.added;
|
|
2515
|
+
const { timestamps, endTimestamps } = this.mergeDvbTimestamps(response);
|
|
2418
2516
|
if (added > 0) {
|
|
2419
|
-
this.applyDvbIndexState(response.metadata,
|
|
2517
|
+
this.applyDvbIndexState(response.metadata, timestamps, endTimestamps, true, true);
|
|
2420
2518
|
}
|
|
2421
2519
|
else {
|
|
2422
2520
|
this.state.metadata = response.metadata;
|
|
2423
|
-
this.state.timestamps =
|
|
2424
|
-
this.endTimestamps =
|
|
2521
|
+
this.state.timestamps = timestamps;
|
|
2522
|
+
this.endTimestamps = endTimestamps;
|
|
2425
2523
|
}
|
|
2426
2524
|
}
|
|
2427
2525
|
else {
|
|
@@ -2520,6 +2618,7 @@ export class DvbRenderer extends BaseVideoSubtitleRenderer {
|
|
|
2520
2618
|
catch (workerError) {
|
|
2521
2619
|
this.state.useWorker = false;
|
|
2522
2620
|
this.emitWorkerState(false, false, this.state.sessionId, true);
|
|
2621
|
+
this.releaseWorkerSession(this.state);
|
|
2523
2622
|
this.emitWarning(createSubtitleWarning('WORKER_FALLBACK', 'DVB worker initialization failed, falling back to main-thread rendering.', {
|
|
2524
2623
|
format: 'dvb',
|
|
2525
2624
|
details: { reason: workerError instanceof Error ? workerError.message : String(workerError) }
|
|
@@ -2545,6 +2644,7 @@ export class DvbRenderer extends BaseVideoSubtitleRenderer {
|
|
|
2545
2644
|
this.state.useWorker = false;
|
|
2546
2645
|
usedWorker = false;
|
|
2547
2646
|
this.emitWorkerState(false, false, this.state.sessionId, true);
|
|
2647
|
+
this.releaseWorkerSession(this.state);
|
|
2548
2648
|
this.emitWarning(createSubtitleWarning('WORKER_FALLBACK', 'DVB worker initialization failed, falling back to main-thread rendering.', {
|
|
2549
2649
|
format: 'dvb',
|
|
2550
2650
|
details: { reason: workerError instanceof Error ? workerError.message : String(workerError) }
|
|
@@ -2557,10 +2657,7 @@ export class DvbRenderer extends BaseVideoSubtitleRenderer {
|
|
|
2557
2657
|
this.dvbParser.reset();
|
|
2558
2658
|
}
|
|
2559
2659
|
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) => {
|
|
2660
|
+
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
2661
|
if (chunk.byteLength === 0)
|
|
2565
2662
|
return;
|
|
2566
2663
|
if (usedWorker && this.state.sessionId) {
|
|
@@ -2571,13 +2668,14 @@ export class DvbRenderer extends BaseVideoSubtitleRenderer {
|
|
|
2571
2668
|
data: transferable
|
|
2572
2669
|
});
|
|
2573
2670
|
if (response.type === 'dvbProgress') {
|
|
2671
|
+
const { timestamps, endTimestamps } = this.mergeDvbTimestamps(response);
|
|
2574
2672
|
if (response.added > 0 || !indexedOnce) {
|
|
2575
|
-
this.applyDvbIndexState(response.metadata,
|
|
2673
|
+
this.applyDvbIndexState(response.metadata, timestamps, endTimestamps, true, true);
|
|
2576
2674
|
indexedOnce = true;
|
|
2577
2675
|
}
|
|
2578
2676
|
else {
|
|
2579
|
-
this.state.timestamps =
|
|
2580
|
-
this.endTimestamps =
|
|
2677
|
+
this.state.timestamps = timestamps;
|
|
2678
|
+
this.endTimestamps = endTimestamps;
|
|
2581
2679
|
this.state.metadata = response.metadata;
|
|
2582
2680
|
}
|
|
2583
2681
|
}
|
|
@@ -2632,12 +2730,15 @@ export class DvbRenderer extends BaseVideoSubtitleRenderer {
|
|
|
2632
2730
|
if (usedWorker) {
|
|
2633
2731
|
this.state.useWorker = false;
|
|
2634
2732
|
this.emitWorkerState(false, false, this.state.sessionId, true);
|
|
2733
|
+
this.releaseWorkerSession(this.state);
|
|
2635
2734
|
}
|
|
2735
|
+
if (this.disposed)
|
|
2736
|
+
throw error;
|
|
2636
2737
|
this.emitWarning(createSubtitleWarning('RANGE_FALLBACK', 'Progressive DVB load failed; retrying with a full buffer fetch.', {
|
|
2637
2738
|
format: 'dvb',
|
|
2638
2739
|
details: { reason: error instanceof Error ? error.message : String(error) }
|
|
2639
2740
|
}));
|
|
2640
|
-
const { data } = await fetchSubtitleAsset(url,
|
|
2741
|
+
const { data } = await fetchSubtitleAsset(url, this.fetchOptions(this.rangeRequests));
|
|
2641
2742
|
await this.loadDvbBuffer(data, false);
|
|
2642
2743
|
}
|
|
2643
2744
|
}
|
|
@@ -2817,13 +2918,10 @@ export class DvbRenderer extends BaseVideoSubtitleRenderer {
|
|
|
2817
2918
|
this.state.frameCache.clear();
|
|
2818
2919
|
this.state.renderIssues.clear();
|
|
2819
2920
|
this.state.pendingRenders.clear();
|
|
2820
|
-
|
|
2821
|
-
sendToWorker({ type: 'disposeDvb', sessionId: this.state.sessionId }).catch(() => { });
|
|
2822
|
-
}
|
|
2921
|
+
this.releaseWorkerSession(this.state);
|
|
2823
2922
|
this.dvbParser?.dispose();
|
|
2824
2923
|
this.dvbParser = null;
|
|
2825
2924
|
this.endTimestamps = new Float64Array(0);
|
|
2826
|
-
this.state.sessionId = null;
|
|
2827
2925
|
}
|
|
2828
2926
|
}
|
|
2829
2927
|
/**
|