libbitsub 1.12.0 → 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 +6 -4
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/integrations/hlsjs.d.ts +6 -0
- package/dist/integrations/hlsjs.d.ts.map +1 -1
- package/dist/integrations/hlsjs.js +3 -0
- package/dist/integrations/hlsjs.js.map +1 -1
- package/dist/integrations/index.d.ts +10 -2
- package/dist/integrations/index.d.ts.map +1 -1
- package/dist/integrations/index.js +10 -2
- package/dist/integrations/index.js.map +1 -1
- package/dist/integrations/react.d.ts +10 -1
- package/dist/integrations/react.d.ts.map +1 -1
- package/dist/integrations/react.js +4 -1
- package/dist/integrations/react.js.map +1 -1
- package/dist/integrations/shaka.d.ts +6 -0
- package/dist/integrations/shaka.d.ts.map +1 -1
- package/dist/integrations/shaka.js +3 -0
- package/dist/integrations/shaka.js.map +1 -1
- package/dist/integrations/shared.d.ts +2 -0
- package/dist/integrations/shared.d.ts.map +1 -1
- package/dist/integrations/shared.js.map +1 -1
- package/dist/integrations/videojs.d.ts +9 -0
- package/dist/integrations/videojs.d.ts.map +1 -1
- package/dist/integrations/videojs.js +3 -0
- package/dist/integrations/videojs.js.map +1 -1
- package/dist/ts/capabilities.d.ts +6 -0
- package/dist/ts/capabilities.d.ts.map +1 -1
- package/dist/ts/capabilities.js +6 -0
- package/dist/ts/capabilities.js.map +1 -1
- package/dist/ts/diagnostics.d.ts +6 -0
- package/dist/ts/diagnostics.d.ts.map +1 -1
- package/dist/ts/diagnostics.js +6 -0
- package/dist/ts/diagnostics.js.map +1 -1
- package/dist/ts/frame-export.d.ts +4 -0
- package/dist/ts/frame-export.d.ts.map +1 -1
- package/dist/ts/frame-export.js +14 -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 +8 -0
- package/dist/ts/range-loader.d.ts.map +1 -1
- package/dist/ts/range-loader.js +102 -21
- package/dist/ts/range-loader.js.map +1 -1
- package/dist/ts/renderers.d.ts +27 -4
- package/dist/ts/renderers.d.ts.map +1 -1
- package/dist/ts/renderers.js +225 -126
- package/dist/ts/renderers.js.map +1 -1
- package/dist/ts/types.d.ts +45 -0
- package/dist/ts/types.d.ts.map +1 -1
- package/dist/ts/utils.d.ts +16 -0
- package/dist/ts/utils.d.ts.map +1 -1
- package/dist/ts/utils.js +70 -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 +6 -1
- package/pkg/README.md +6 -4
- 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/src/integrations/hlsjs.ts +6 -0
- package/src/integrations/index.ts +10 -2
- package/src/integrations/react.tsx +10 -1
- package/src/integrations/shaka.ts +6 -0
- package/src/integrations/shared.ts +2 -0
- package/src/integrations/videojs.ts +9 -0
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,14 +2361,12 @@ 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
|
}
|
|
2369
|
+
/** High-level DVB overlay renderer bound to a video element. */
|
|
2281
2370
|
export class DvbRenderer extends BaseVideoSubtitleRenderer {
|
|
2282
2371
|
dvbParser = null;
|
|
2283
2372
|
endTimestamps = new Float64Array(0);
|
|
@@ -2312,10 +2401,7 @@ export class DvbRenderer extends BaseVideoSubtitleRenderer {
|
|
|
2312
2401
|
return;
|
|
2313
2402
|
}
|
|
2314
2403
|
if (!this.streamingLoad) {
|
|
2315
|
-
const { data, strategy, rangeSupported, total } = await fetchSubtitleAsset(this.subUrl,
|
|
2316
|
-
preferRange: this.rangeRequests,
|
|
2317
|
-
onProgress: (progress) => this.emitLoadProgress('dvb', progress, this.state.timestamps.length)
|
|
2318
|
-
});
|
|
2404
|
+
const { data, strategy, rangeSupported, total } = await fetchSubtitleAsset(this.subUrl, this.fetchOptions(this.rangeRequests, (progress) => this.emitLoadProgress('dvb', progress, this.state.timestamps.length)));
|
|
2319
2405
|
this.emitLoadProgress('dvb', { loaded: data.byteLength, total: total ?? data.byteLength, ratio: 1, rangeSupported, strategy }, 0);
|
|
2320
2406
|
await this.loadDvbBuffer(data, false);
|
|
2321
2407
|
this.onLoaded?.();
|
|
@@ -2325,11 +2411,23 @@ export class DvbRenderer extends BaseVideoSubtitleRenderer {
|
|
|
2325
2411
|
this.onLoaded?.();
|
|
2326
2412
|
}
|
|
2327
2413
|
catch (error) {
|
|
2414
|
+
if (this.disposed)
|
|
2415
|
+
return;
|
|
2328
2416
|
const resolvedError = normalizeSubtitleError(error, { format: 'dvb' });
|
|
2329
2417
|
this.emitEvent({ type: 'error', format: 'dvb', error: resolvedError });
|
|
2330
2418
|
this.onError?.(resolvedError);
|
|
2331
2419
|
}
|
|
2332
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
|
+
}
|
|
2333
2431
|
applyDvbIndexState(metadata, timestamps, endTimestamps, partial, usingWorker) {
|
|
2334
2432
|
this.state.metadata = metadata;
|
|
2335
2433
|
this.state.timestamps = timestamps;
|
|
@@ -2363,7 +2461,7 @@ export class DvbRenderer extends BaseVideoSubtitleRenderer {
|
|
|
2363
2461
|
}
|
|
2364
2462
|
catch (workerError) {
|
|
2365
2463
|
this.state.useWorker = false;
|
|
2366
|
-
this.state
|
|
2464
|
+
this.releaseWorkerSession(this.state);
|
|
2367
2465
|
this.state.sessionId = null;
|
|
2368
2466
|
this.emitWorkerState(false, false, null, true);
|
|
2369
2467
|
this.emitWarning(createSubtitleWarning('WORKER_FALLBACK', 'DVB worker initialization failed, falling back to main-thread rendering.', {
|
|
@@ -2414,13 +2512,14 @@ export class DvbRenderer extends BaseVideoSubtitleRenderer {
|
|
|
2414
2512
|
if (response.type !== 'dvbProgress')
|
|
2415
2513
|
throw new Error('Unexpected DVB worker response');
|
|
2416
2514
|
added = response.added;
|
|
2515
|
+
const { timestamps, endTimestamps } = this.mergeDvbTimestamps(response);
|
|
2417
2516
|
if (added > 0) {
|
|
2418
|
-
this.applyDvbIndexState(response.metadata,
|
|
2517
|
+
this.applyDvbIndexState(response.metadata, timestamps, endTimestamps, true, true);
|
|
2419
2518
|
}
|
|
2420
2519
|
else {
|
|
2421
2520
|
this.state.metadata = response.metadata;
|
|
2422
|
-
this.state.timestamps =
|
|
2423
|
-
this.endTimestamps =
|
|
2521
|
+
this.state.timestamps = timestamps;
|
|
2522
|
+
this.endTimestamps = endTimestamps;
|
|
2424
2523
|
}
|
|
2425
2524
|
}
|
|
2426
2525
|
else {
|
|
@@ -2519,6 +2618,7 @@ export class DvbRenderer extends BaseVideoSubtitleRenderer {
|
|
|
2519
2618
|
catch (workerError) {
|
|
2520
2619
|
this.state.useWorker = false;
|
|
2521
2620
|
this.emitWorkerState(false, false, this.state.sessionId, true);
|
|
2621
|
+
this.releaseWorkerSession(this.state);
|
|
2522
2622
|
this.emitWarning(createSubtitleWarning('WORKER_FALLBACK', 'DVB worker initialization failed, falling back to main-thread rendering.', {
|
|
2523
2623
|
format: 'dvb',
|
|
2524
2624
|
details: { reason: workerError instanceof Error ? workerError.message : String(workerError) }
|
|
@@ -2544,6 +2644,7 @@ export class DvbRenderer extends BaseVideoSubtitleRenderer {
|
|
|
2544
2644
|
this.state.useWorker = false;
|
|
2545
2645
|
usedWorker = false;
|
|
2546
2646
|
this.emitWorkerState(false, false, this.state.sessionId, true);
|
|
2647
|
+
this.releaseWorkerSession(this.state);
|
|
2547
2648
|
this.emitWarning(createSubtitleWarning('WORKER_FALLBACK', 'DVB worker initialization failed, falling back to main-thread rendering.', {
|
|
2548
2649
|
format: 'dvb',
|
|
2549
2650
|
details: { reason: workerError instanceof Error ? workerError.message : String(workerError) }
|
|
@@ -2556,10 +2657,7 @@ export class DvbRenderer extends BaseVideoSubtitleRenderer {
|
|
|
2556
2657
|
this.dvbParser.reset();
|
|
2557
2658
|
}
|
|
2558
2659
|
try {
|
|
2559
|
-
const { data, strategy, rangeSupported, total } = await fetchSubtitleAsset(url, {
|
|
2560
|
-
preferRange: this.rangeRequests,
|
|
2561
|
-
onProgress: (progress) => this.emitLoadProgress('dvb', progress, this.state.timestamps.length)
|
|
2562
|
-
}, 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) => {
|
|
2563
2661
|
if (chunk.byteLength === 0)
|
|
2564
2662
|
return;
|
|
2565
2663
|
if (usedWorker && this.state.sessionId) {
|
|
@@ -2570,13 +2668,14 @@ export class DvbRenderer extends BaseVideoSubtitleRenderer {
|
|
|
2570
2668
|
data: transferable
|
|
2571
2669
|
});
|
|
2572
2670
|
if (response.type === 'dvbProgress') {
|
|
2671
|
+
const { timestamps, endTimestamps } = this.mergeDvbTimestamps(response);
|
|
2573
2672
|
if (response.added > 0 || !indexedOnce) {
|
|
2574
|
-
this.applyDvbIndexState(response.metadata,
|
|
2673
|
+
this.applyDvbIndexState(response.metadata, timestamps, endTimestamps, true, true);
|
|
2575
2674
|
indexedOnce = true;
|
|
2576
2675
|
}
|
|
2577
2676
|
else {
|
|
2578
|
-
this.state.timestamps =
|
|
2579
|
-
this.endTimestamps =
|
|
2677
|
+
this.state.timestamps = timestamps;
|
|
2678
|
+
this.endTimestamps = endTimestamps;
|
|
2580
2679
|
this.state.metadata = response.metadata;
|
|
2581
2680
|
}
|
|
2582
2681
|
}
|
|
@@ -2631,12 +2730,15 @@ export class DvbRenderer extends BaseVideoSubtitleRenderer {
|
|
|
2631
2730
|
if (usedWorker) {
|
|
2632
2731
|
this.state.useWorker = false;
|
|
2633
2732
|
this.emitWorkerState(false, false, this.state.sessionId, true);
|
|
2733
|
+
this.releaseWorkerSession(this.state);
|
|
2634
2734
|
}
|
|
2735
|
+
if (this.disposed)
|
|
2736
|
+
throw error;
|
|
2635
2737
|
this.emitWarning(createSubtitleWarning('RANGE_FALLBACK', 'Progressive DVB load failed; retrying with a full buffer fetch.', {
|
|
2636
2738
|
format: 'dvb',
|
|
2637
2739
|
details: { reason: error instanceof Error ? error.message : String(error) }
|
|
2638
2740
|
}));
|
|
2639
|
-
const { data } = await fetchSubtitleAsset(url,
|
|
2741
|
+
const { data } = await fetchSubtitleAsset(url, this.fetchOptions(this.rangeRequests));
|
|
2640
2742
|
await this.loadDvbBuffer(data, false);
|
|
2641
2743
|
}
|
|
2642
2744
|
}
|
|
@@ -2816,13 +2918,10 @@ export class DvbRenderer extends BaseVideoSubtitleRenderer {
|
|
|
2816
2918
|
this.state.frameCache.clear();
|
|
2817
2919
|
this.state.renderIssues.clear();
|
|
2818
2920
|
this.state.pendingRenders.clear();
|
|
2819
|
-
|
|
2820
|
-
sendToWorker({ type: 'disposeDvb', sessionId: this.state.sessionId }).catch(() => { });
|
|
2821
|
-
}
|
|
2921
|
+
this.releaseWorkerSession(this.state);
|
|
2822
2922
|
this.dvbParser?.dispose();
|
|
2823
2923
|
this.dvbParser = null;
|
|
2824
2924
|
this.endTimestamps = new Float64Array(0);
|
|
2825
|
-
this.state.sessionId = null;
|
|
2826
2925
|
}
|
|
2827
2926
|
}
|
|
2828
2927
|
/**
|