libbitsub 1.10.1 → 1.11.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/LICENSE +1 -0
- package/README.md +186 -36
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/integrations/hlsjs.d.ts +45 -0
- package/dist/integrations/hlsjs.d.ts.map +1 -0
- package/dist/integrations/hlsjs.js +121 -0
- package/dist/integrations/hlsjs.js.map +1 -0
- package/dist/integrations/index.d.ts +11 -0
- package/dist/integrations/index.d.ts.map +1 -0
- package/dist/integrations/index.js +11 -0
- package/dist/integrations/index.js.map +1 -0
- package/dist/integrations/react.d.ts +55 -0
- package/dist/integrations/react.d.ts.map +1 -0
- package/dist/integrations/react.js +127 -0
- package/dist/integrations/react.js.map +1 -0
- package/dist/integrations/shaka.d.ts +38 -0
- package/dist/integrations/shaka.d.ts.map +1 -0
- package/dist/integrations/shaka.js +71 -0
- package/dist/integrations/shaka.js.map +1 -0
- package/dist/integrations/shared.d.ts +41 -0
- package/dist/integrations/shared.d.ts.map +1 -0
- package/dist/integrations/shared.js +123 -0
- package/dist/integrations/shared.js.map +1 -0
- package/dist/integrations/videojs.d.ts +85 -0
- package/dist/integrations/videojs.d.ts.map +1 -0
- package/dist/integrations/videojs.js +111 -0
- package/dist/integrations/videojs.js.map +1 -0
- package/dist/ts/diagnostics.d.ts.map +1 -1
- package/dist/ts/diagnostics.js.map +1 -1
- package/dist/ts/frame-export.d.ts.map +1 -1
- package/dist/ts/frame-export.js +4 -4
- package/dist/ts/frame-export.js.map +1 -1
- package/dist/ts/parsers.d.ts +7 -6
- package/dist/ts/parsers.d.ts.map +1 -1
- package/dist/ts/parsers.js +76 -13
- package/dist/ts/parsers.js.map +1 -1
- package/dist/ts/range-loader.d.ts +31 -0
- package/dist/ts/range-loader.d.ts.map +1 -0
- package/dist/ts/range-loader.js +220 -0
- package/dist/ts/range-loader.js.map +1 -0
- package/dist/ts/renderers.d.ts +14 -0
- package/dist/ts/renderers.d.ts.map +1 -1
- package/dist/ts/renderers.js +445 -162
- package/dist/ts/renderers.js.map +1 -1
- package/dist/ts/types.d.ts +51 -1
- package/dist/ts/types.d.ts.map +1 -1
- package/dist/ts/utils.d.ts.map +1 -1
- package/dist/ts/utils.js.map +1 -1
- package/dist/ts/wasm.d.ts +2 -0
- package/dist/ts/wasm.d.ts.map +1 -1
- package/dist/ts/wasm.js +12 -0
- package/dist/ts/wasm.js.map +1 -1
- package/dist/ts/webgl2-renderer.d.ts +9 -0
- package/dist/ts/webgl2-renderer.d.ts.map +1 -1
- package/dist/ts/webgl2-renderer.js +34 -0
- package/dist/ts/webgl2-renderer.js.map +1 -1
- package/dist/ts/webgpu-renderer.d.ts +9 -0
- package/dist/ts/webgpu-renderer.d.ts.map +1 -1
- package/dist/ts/webgpu-renderer.js +70 -2
- package/dist/ts/webgpu-renderer.js.map +1 -1
- package/dist/ts/worker.d.ts +11 -0
- package/dist/ts/worker.d.ts.map +1 -1
- package/dist/ts/worker.js +235 -160
- package/dist/ts/worker.js.map +1 -1
- package/dist/wrapper.d.ts +3 -1
- package/dist/wrapper.d.ts.map +1 -1
- package/dist/wrapper.js +2 -0
- package/dist/wrapper.js.map +1 -1
- package/examples/README.md +38 -0
- package/examples/hlsjs/README.md +53 -0
- package/examples/react/README.md +60 -0
- package/examples/shaka/README.md +61 -0
- package/examples/videojs/README.md +55 -0
- package/package.json +68 -12
- package/pkg/LICENSE +1 -0
- package/pkg/README.md +186 -36
- package/pkg/libbitsub.d.ts +14 -0
- package/pkg/libbitsub.js +50 -0
- package/pkg/libbitsub_bg.wasm +0 -0
- package/pkg/libbitsub_bg.wasm.d.ts +7 -0
- package/pkg/package.json +1 -1
- package/src/integrations/hlsjs.ts +154 -0
- package/src/integrations/index.ts +36 -0
- package/src/integrations/react.tsx +170 -0
- package/src/integrations/shaka.ts +92 -0
- package/src/integrations/shared.ts +170 -0
- package/src/integrations/videojs.ts +200 -0
- package/src/wrapper.ts +12 -0
package/dist/ts/renderers.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* High-level video-integrated subtitle renderers for libbitsub.
|
|
3
3
|
* Handles canvas overlay, video sync, and subtitle fetching.
|
|
4
4
|
*/
|
|
5
|
-
import { createSubtitleDiagnosticError, createSubtitleWarning, formatSubtitleWarningForConsole, normalizeSubtitleError, warningFromRenderIssue } from './diagnostics';
|
|
5
|
+
import { SubtitleDiagnosticError, createSubtitleDiagnosticError, createSubtitleWarning, formatSubtitleWarningForConsole, normalizeSubtitleError, warningFromRenderIssue } from './diagnostics';
|
|
6
|
+
import { fetchSubtitleAsset, fetchSubtitleText } from './range-loader';
|
|
6
7
|
import { initWasm } from './wasm';
|
|
7
8
|
import { getOrCreateWorker, sendToWorker } from './worker';
|
|
8
9
|
import { binarySearchTimestamp, convertFrameData, createWorkerSessionId, createWorkerState, detectSubtitleFormat, getSubtitleBounds, isMksSource, setCacheLimit as applyCacheLimit, setCachedFrame } from './utils';
|
|
@@ -59,6 +60,8 @@ class BaseVideoSubtitleRenderer {
|
|
|
59
60
|
cacheLimit = 24;
|
|
60
61
|
prefetchBefore = 0;
|
|
61
62
|
prefetchAfter = 0;
|
|
63
|
+
streamingLoad = true;
|
|
64
|
+
rangeRequests = true;
|
|
62
65
|
onEvent;
|
|
63
66
|
onWarning;
|
|
64
67
|
currentRendererBackend = null;
|
|
@@ -98,6 +101,32 @@ class BaseVideoSubtitleRenderer {
|
|
|
98
101
|
this.cacheLimit = Math.max(0, Math.floor(options.cacheLimit ?? 24));
|
|
99
102
|
this.prefetchBefore = Math.max(0, Math.floor(options.prefetchWindow?.before ?? 0));
|
|
100
103
|
this.prefetchAfter = Math.max(0, Math.floor(options.prefetchWindow?.after ?? 0));
|
|
104
|
+
this.streamingLoad = options.streamingLoad !== false;
|
|
105
|
+
this.rangeRequests = options.rangeRequests !== false;
|
|
106
|
+
}
|
|
107
|
+
emitLoadProgress(format, progress, indexedCues) {
|
|
108
|
+
this.emitEvent({
|
|
109
|
+
type: 'load-progress',
|
|
110
|
+
format,
|
|
111
|
+
loadedBytes: progress.loaded,
|
|
112
|
+
totalBytes: progress.total,
|
|
113
|
+
ratio: progress.ratio,
|
|
114
|
+
strategy: progress.strategy,
|
|
115
|
+
rangeSupported: progress.rangeSupported,
|
|
116
|
+
indexedCues
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
emitIndexed(format, metadata, partial) {
|
|
120
|
+
this.emitEvent({ type: 'indexed', format, metadata, partial });
|
|
121
|
+
}
|
|
122
|
+
memoryProgress(byteLength) {
|
|
123
|
+
return {
|
|
124
|
+
loaded: byteLength,
|
|
125
|
+
total: byteLength,
|
|
126
|
+
ratio: 1,
|
|
127
|
+
rangeSupported: false,
|
|
128
|
+
strategy: 'memory'
|
|
129
|
+
};
|
|
101
130
|
}
|
|
102
131
|
/** Get current display settings */
|
|
103
132
|
getDisplaySettings() {
|
|
@@ -194,7 +223,11 @@ class BaseVideoSubtitleRenderer {
|
|
|
194
223
|
/** Start initialization. */
|
|
195
224
|
startInit() {
|
|
196
225
|
this.init().catch((error) => {
|
|
197
|
-
this.emitEvent({
|
|
226
|
+
this.emitEvent({
|
|
227
|
+
type: 'error',
|
|
228
|
+
format: this.format,
|
|
229
|
+
error: normalizeSubtitleError(error, { format: this.format })
|
|
230
|
+
});
|
|
198
231
|
});
|
|
199
232
|
}
|
|
200
233
|
/** Initialize the renderer. */
|
|
@@ -462,7 +495,8 @@ class BaseVideoSubtitleRenderer {
|
|
|
462
495
|
});
|
|
463
496
|
this.emitEvent({ type: 'stats', stats: this.getStats() });
|
|
464
497
|
if (currentIndex >= 0 && (this.prefetchBefore > 0 || this.prefetchAfter > 0)) {
|
|
465
|
-
const prefetch = this
|
|
498
|
+
const prefetch = this
|
|
499
|
+
.prefetchAroundTime;
|
|
466
500
|
prefetch?.call(this, currentTime).catch(() => { });
|
|
467
501
|
}
|
|
468
502
|
}
|
|
@@ -489,7 +523,7 @@ class BaseVideoSubtitleRenderer {
|
|
|
489
523
|
return { status: 'pending', data: null, warning: null };
|
|
490
524
|
}
|
|
491
525
|
}
|
|
492
|
-
const renderIssue = index >= 0 ? this.getWorkerRendererState().renderIssues.get(index) ?? null : null;
|
|
526
|
+
const renderIssue = index >= 0 ? (this.getWorkerRendererState().renderIssues.get(index) ?? null) : null;
|
|
493
527
|
const warning = warningFromRenderIssue(renderIssue, { format: this.format, cueIndex: index });
|
|
494
528
|
// Use best available renderer
|
|
495
529
|
if (this.useWebGPU && this.webgpuRenderer) {
|
|
@@ -533,9 +567,7 @@ class BaseVideoSubtitleRenderer {
|
|
|
533
567
|
let frameShiftX = 0;
|
|
534
568
|
let frameShiftY = 0;
|
|
535
569
|
if (aspectMode !== 'stretch') {
|
|
536
|
-
const uniformScale = aspectMode === 'cover'
|
|
537
|
-
? Math.max(stretchScaleX, stretchScaleY)
|
|
538
|
-
: Math.min(stretchScaleX, stretchScaleY);
|
|
570
|
+
const uniformScale = aspectMode === 'cover' ? Math.max(stretchScaleX, stretchScaleY) : Math.min(stretchScaleX, stretchScaleY);
|
|
539
571
|
baseScaleX = uniformScale;
|
|
540
572
|
baseScaleY = uniformScale;
|
|
541
573
|
frameShiftX = (this.canvas.width - safeDataWidth * uniformScale) / 2;
|
|
@@ -701,77 +733,200 @@ export class PgsRenderer extends BaseVideoSubtitleRenderer {
|
|
|
701
733
|
try {
|
|
702
734
|
this.emitEvent({ type: 'loading', format: 'pgs' });
|
|
703
735
|
this.onLoading?.();
|
|
704
|
-
let arrayBuffer;
|
|
705
736
|
if (this.subContent) {
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
throw new Error(`Failed to fetch subtitle: ${response.status}`);
|
|
712
|
-
arrayBuffer = await response.arrayBuffer();
|
|
737
|
+
const data = new Uint8Array(this.subContent);
|
|
738
|
+
this.emitLoadProgress('pgs', this.memoryProgress(data.byteLength), 0);
|
|
739
|
+
await this.loadPgsBuffer(data, true);
|
|
740
|
+
this.onLoaded?.();
|
|
741
|
+
return;
|
|
713
742
|
}
|
|
714
|
-
|
|
743
|
+
if (!this.subUrl) {
|
|
715
744
|
throw new Error('No subtitle content or URL provided');
|
|
716
745
|
}
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
this.state.
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
746
|
+
if (!this.streamingLoad) {
|
|
747
|
+
const { data, strategy, rangeSupported, total } = await fetchSubtitleAsset(this.subUrl, {
|
|
748
|
+
preferRange: this.rangeRequests,
|
|
749
|
+
onProgress: (progress) => this.emitLoadProgress('pgs', progress, this.state.timestamps.length)
|
|
750
|
+
});
|
|
751
|
+
this.emitLoadProgress('pgs', { loaded: data.byteLength, total: total ?? data.byteLength, ratio: 1, rangeSupported, strategy }, 0);
|
|
752
|
+
await this.loadPgsBuffer(data, false);
|
|
753
|
+
this.onLoaded?.();
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
await this.loadPgsStreaming(this.subUrl);
|
|
757
|
+
this.onLoaded?.();
|
|
758
|
+
}
|
|
759
|
+
catch (error) {
|
|
760
|
+
const resolvedError = normalizeSubtitleError(error, { format: 'pgs' });
|
|
761
|
+
this.emitEvent({ type: 'error', format: 'pgs', error: resolvedError });
|
|
762
|
+
this.onError?.(resolvedError);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
applyPgsIndexState(metadata, timestamps, partial, usingWorker) {
|
|
766
|
+
this.state.metadata = metadata;
|
|
767
|
+
this.state.timestamps = timestamps;
|
|
768
|
+
this.setParserMetadata(metadata);
|
|
769
|
+
this.emitIndexed('pgs', metadata, partial);
|
|
770
|
+
if (!this.isLoaded && timestamps.length > 0) {
|
|
771
|
+
this.isLoaded = true;
|
|
772
|
+
if (usingWorker) {
|
|
773
|
+
this.state.workerReady = true;
|
|
774
|
+
this.emitWorkerState(true, true, this.state.sessionId);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
async loadPgsBuffer(data, preserveSource) {
|
|
779
|
+
if (this.state.useWorker) {
|
|
780
|
+
try {
|
|
781
|
+
this.state.sessionId = createWorkerSessionId();
|
|
782
|
+
await getOrCreateWorker();
|
|
783
|
+
this.emitWorkerState(true, false, this.state.sessionId);
|
|
784
|
+
const transferableData = createTransferableBuffer(data, preserveSource);
|
|
785
|
+
const loadResponse = await sendToWorker({
|
|
786
|
+
type: 'loadPgs',
|
|
787
|
+
sessionId: this.state.sessionId,
|
|
788
|
+
data: transferableData
|
|
789
|
+
});
|
|
790
|
+
if (loadResponse.type === 'pgsLoaded') {
|
|
791
|
+
this.state.workerReady = true;
|
|
792
|
+
this.state.metadata = loadResponse.metadata;
|
|
793
|
+
this.state.timestamps = loadResponse.timestamps;
|
|
794
|
+
this.isLoaded = true;
|
|
795
|
+
this.setParserMetadata(loadResponse.metadata);
|
|
796
|
+
this.emitIndexed('pgs', loadResponse.metadata, false);
|
|
797
|
+
this.emitWorkerState(true, true, this.state.sessionId);
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
else if (loadResponse.type === 'error') {
|
|
801
|
+
throw new Error(loadResponse.message);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
catch (workerError) {
|
|
805
|
+
this.state.useWorker = false;
|
|
806
|
+
this.emitWorkerState(false, false, this.state.sessionId, true);
|
|
807
|
+
this.emitWarning(createSubtitleWarning('WORKER_FALLBACK', 'PGS worker initialization failed, falling back to main-thread rendering.', {
|
|
808
|
+
format: 'pgs',
|
|
809
|
+
details: { reason: workerError instanceof Error ? workerError.message : String(workerError) }
|
|
810
|
+
}));
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
await this.loadOnMainThread(data);
|
|
814
|
+
}
|
|
815
|
+
async loadPgsStreaming(url) {
|
|
816
|
+
let usedWorker = false;
|
|
817
|
+
let indexedOnce = false;
|
|
818
|
+
if (this.state.useWorker) {
|
|
819
|
+
try {
|
|
820
|
+
this.state.sessionId = createWorkerSessionId();
|
|
821
|
+
await getOrCreateWorker();
|
|
822
|
+
this.emitWorkerState(true, false, this.state.sessionId);
|
|
823
|
+
const begin = await sendToWorker({ type: 'beginPgs', sessionId: this.state.sessionId });
|
|
824
|
+
if (begin.type === 'error')
|
|
825
|
+
throw new Error(begin.message);
|
|
826
|
+
usedWorker = true;
|
|
827
|
+
}
|
|
828
|
+
catch (workerError) {
|
|
829
|
+
this.state.useWorker = false;
|
|
830
|
+
usedWorker = false;
|
|
831
|
+
this.emitWorkerState(false, false, this.state.sessionId, true);
|
|
832
|
+
this.emitWarning(createSubtitleWarning('WORKER_FALLBACK', 'PGS worker initialization failed, falling back to main-thread rendering.', {
|
|
833
|
+
format: 'pgs',
|
|
834
|
+
details: { reason: workerError instanceof Error ? workerError.message : String(workerError) }
|
|
835
|
+
}));
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
if (!usedWorker) {
|
|
839
|
+
await this.yieldToMain();
|
|
840
|
+
this.pgsParser = new PgsParser({ debug: this.debug, onWarning: (warning) => this.emitWarning(warning) });
|
|
841
|
+
this.pgsParser.reset();
|
|
842
|
+
}
|
|
843
|
+
try {
|
|
844
|
+
const { data, strategy, rangeSupported, total } = await fetchSubtitleAsset(url, {
|
|
845
|
+
preferRange: this.rangeRequests,
|
|
846
|
+
onProgress: (progress) => this.emitLoadProgress('pgs', progress, this.state.timestamps.length)
|
|
847
|
+
}, async (chunk, progress) => {
|
|
848
|
+
if (chunk.byteLength === 0)
|
|
849
|
+
return;
|
|
850
|
+
if (usedWorker && this.state.sessionId) {
|
|
851
|
+
const transferable = createTransferableBuffer(chunk, true);
|
|
852
|
+
const response = await sendToWorker({
|
|
853
|
+
type: 'appendPgs',
|
|
726
854
|
sessionId: this.state.sessionId,
|
|
727
|
-
data:
|
|
855
|
+
data: transferable
|
|
728
856
|
});
|
|
729
|
-
if (
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
857
|
+
if (response.type === 'pgsProgress') {
|
|
858
|
+
if (response.added > 0 || !indexedOnce) {
|
|
859
|
+
this.applyPgsIndexState(response.metadata, response.timestamps, true, true);
|
|
860
|
+
indexedOnce = true;
|
|
861
|
+
}
|
|
862
|
+
else {
|
|
863
|
+
this.state.timestamps = response.timestamps;
|
|
864
|
+
this.state.metadata = response.metadata;
|
|
865
|
+
}
|
|
738
866
|
}
|
|
739
|
-
else if (
|
|
740
|
-
throw new Error(
|
|
867
|
+
else if (response.type === 'error') {
|
|
868
|
+
throw new Error(response.message);
|
|
741
869
|
}
|
|
742
870
|
}
|
|
743
|
-
|
|
744
|
-
this.
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
}));
|
|
750
|
-
if (!this.subContent && this.subUrl && data.byteLength === 0) {
|
|
751
|
-
const response = await fetch(this.subUrl);
|
|
752
|
-
if (!response.ok)
|
|
753
|
-
throw new Error(`Failed to fetch subtitle: ${response.status}`);
|
|
754
|
-
data = new Uint8Array(await response.arrayBuffer());
|
|
871
|
+
else if (this.pgsParser) {
|
|
872
|
+
const added = this.pgsParser.feed(chunk);
|
|
873
|
+
if (added > 0 || !indexedOnce) {
|
|
874
|
+
const metadata = this.pgsParser.getMetadata();
|
|
875
|
+
this.applyPgsIndexState(metadata, this.pgsParser.getTimestamps(), true, false);
|
|
876
|
+
indexedOnce = true;
|
|
755
877
|
}
|
|
756
878
|
}
|
|
879
|
+
this.emitLoadProgress('pgs', progress, this.state.timestamps.length);
|
|
880
|
+
});
|
|
881
|
+
if (usedWorker && this.state.sessionId) {
|
|
882
|
+
const finish = await sendToWorker({ type: 'finishPgs', sessionId: this.state.sessionId });
|
|
883
|
+
if (finish.type === 'pgsProgress') {
|
|
884
|
+
this.applyPgsIndexState(finish.metadata, finish.timestamps, false, true);
|
|
885
|
+
this.state.workerReady = true;
|
|
886
|
+
this.isLoaded = true;
|
|
887
|
+
this.emitWorkerState(true, true, this.state.sessionId);
|
|
888
|
+
}
|
|
889
|
+
else if (finish.type === 'error') {
|
|
890
|
+
throw new Error(finish.message);
|
|
891
|
+
}
|
|
757
892
|
}
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
893
|
+
else if (this.pgsParser) {
|
|
894
|
+
this.pgsParser.finishFeed();
|
|
895
|
+
const metadata = this.pgsParser.getMetadata();
|
|
896
|
+
this.state.timestamps = this.pgsParser.getTimestamps();
|
|
897
|
+
this.state.metadata = metadata;
|
|
898
|
+
this.isLoaded = true;
|
|
899
|
+
this.setParserMetadata(metadata);
|
|
900
|
+
this.emitIndexed('pgs', metadata, false);
|
|
901
|
+
if (metadata.cueCount === 0) {
|
|
902
|
+
this.state.renderIssues.set(-1, 'INVALID_SUBTITLE_DATA');
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
this.emitLoadProgress('pgs', {
|
|
906
|
+
loaded: data.byteLength,
|
|
907
|
+
total: total ?? data.byteLength,
|
|
908
|
+
ratio: 1,
|
|
909
|
+
rangeSupported,
|
|
910
|
+
strategy: strategy
|
|
911
|
+
}, this.state.timestamps.length);
|
|
761
912
|
}
|
|
762
913
|
catch (error) {
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
914
|
+
if (usedWorker) {
|
|
915
|
+
this.state.useWorker = false;
|
|
916
|
+
this.emitWorkerState(false, false, this.state.sessionId, true);
|
|
917
|
+
}
|
|
918
|
+
this.emitWarning(createSubtitleWarning('RANGE_FALLBACK', 'Progressive PGS load failed; retrying with a full buffer fetch.', {
|
|
919
|
+
format: 'pgs',
|
|
920
|
+
details: { reason: error instanceof Error ? error.message : String(error) }
|
|
921
|
+
}));
|
|
922
|
+
const { data } = await fetchSubtitleAsset(url, { preferRange: this.rangeRequests });
|
|
923
|
+
await this.loadPgsBuffer(data, false);
|
|
766
924
|
}
|
|
767
925
|
}
|
|
768
926
|
async loadOnMainThread(data) {
|
|
769
|
-
// Yield to browser before heavy parsing
|
|
770
927
|
await this.yieldToMain();
|
|
771
928
|
this.pgsParser = new PgsParser({ debug: this.debug, onWarning: (warning) => this.emitWarning(warning) });
|
|
772
|
-
// Parse in a microtask to allow UI to update
|
|
773
929
|
await new Promise((resolve) => {
|
|
774
|
-
// Use requestIdleCallback if available, otherwise setTimeout
|
|
775
930
|
const scheduleTask = typeof requestIdleCallback !== 'undefined'
|
|
776
931
|
? (cb) => requestIdleCallback(() => cb(), { timeout: 1000 })
|
|
777
932
|
: (cb) => setTimeout(cb, 0);
|
|
@@ -781,6 +936,7 @@ export class PgsRenderer extends BaseVideoSubtitleRenderer {
|
|
|
781
936
|
this.state.metadata = this.pgsParser.getMetadata();
|
|
782
937
|
this.isLoaded = true;
|
|
783
938
|
this.setParserMetadata(this.state.metadata);
|
|
939
|
+
this.emitIndexed('pgs', this.state.metadata, false);
|
|
784
940
|
if (count === 0) {
|
|
785
941
|
this.state.renderIssues.set(-1, 'INVALID_SUBTITLE_DATA');
|
|
786
942
|
}
|
|
@@ -962,9 +1118,9 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
962
1118
|
pendingIndexLookup = null;
|
|
963
1119
|
constructor(options) {
|
|
964
1120
|
super(options, 'vobsub');
|
|
965
|
-
this.idxUrl =
|
|
966
|
-
|
|
967
|
-
|
|
1121
|
+
this.idxUrl =
|
|
1122
|
+
options.idxUrl ||
|
|
1123
|
+
(options.subUrl && /\.sub$/i.test(options.subUrl) ? options.subUrl.replace(/\.sub$/i, '.idx') : undefined);
|
|
968
1124
|
this.idxContent = options.idxContent;
|
|
969
1125
|
this.fileName = options.fileName;
|
|
970
1126
|
this.onLoading = options.onLoading;
|
|
@@ -977,125 +1133,251 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
977
1133
|
try {
|
|
978
1134
|
this.emitEvent({ type: 'loading', format: 'vobsub' });
|
|
979
1135
|
this.onLoading?.();
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1136
|
+
const useMksSource = !this.idxContent &&
|
|
1137
|
+
!this.idxUrl &&
|
|
1138
|
+
isMksSource({
|
|
1139
|
+
subData: this.subContent,
|
|
1140
|
+
fileName: this.fileName,
|
|
1141
|
+
subUrl: this.subUrl
|
|
1142
|
+
});
|
|
1143
|
+
if (this.subContent && (useMksSource || this.idxContent)) {
|
|
1144
|
+
const subData = new Uint8Array(this.subContent);
|
|
1145
|
+
this.emitLoadProgress('vobsub', this.memoryProgress(subData.byteLength), 0);
|
|
1146
|
+
await this.loadVobSubBuffer(subData, this.idxContent, useMksSource, true);
|
|
1147
|
+
this.onLoaded?.();
|
|
1148
|
+
return;
|
|
990
1149
|
}
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1150
|
+
if (useMksSource) {
|
|
1151
|
+
await this.loadVobSubMksStreaming();
|
|
1152
|
+
this.onLoaded?.();
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
await this.loadVobSubIdxSubStreaming();
|
|
1156
|
+
this.onLoaded?.();
|
|
1157
|
+
}
|
|
1158
|
+
catch (error) {
|
|
1159
|
+
const resolvedError = normalizeSubtitleError(error, { format: 'vobsub' });
|
|
1160
|
+
this.emitEvent({ type: 'error', format: 'vobsub', error: resolvedError });
|
|
1161
|
+
this.onError?.(resolvedError);
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
applyVobSubIndexState(metadata, timestamps, partial, renderable, usingWorker) {
|
|
1165
|
+
this.state.metadata = metadata;
|
|
1166
|
+
this.state.timestamps = timestamps;
|
|
1167
|
+
this.setParserMetadata(metadata);
|
|
1168
|
+
this.emitIndexed('vobsub', metadata, partial);
|
|
1169
|
+
if (renderable && !this.isLoaded && timestamps.length > 0) {
|
|
1170
|
+
this.isLoaded = true;
|
|
1171
|
+
if (usingWorker) {
|
|
1172
|
+
this.state.workerReady = true;
|
|
1173
|
+
this.emitWorkerState(true, true, this.state.sessionId);
|
|
994
1174
|
}
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
async ensureVobSubWorkerSession() {
|
|
1178
|
+
if (!this.state.useWorker)
|
|
1179
|
+
return false;
|
|
1180
|
+
try {
|
|
1181
|
+
this.state.sessionId = createWorkerSessionId();
|
|
1182
|
+
await getOrCreateWorker();
|
|
1183
|
+
this.emitWorkerState(true, false, this.state.sessionId);
|
|
1184
|
+
return true;
|
|
1185
|
+
}
|
|
1186
|
+
catch (workerError) {
|
|
1187
|
+
this.state.useWorker = false;
|
|
1188
|
+
this.emitWorkerState(false, false, this.state.sessionId, true);
|
|
1189
|
+
this.emitWarning(createSubtitleWarning('WORKER_FALLBACK', 'VobSub worker initialization failed, falling back to main-thread rendering.', {
|
|
1190
|
+
format: 'vobsub',
|
|
1191
|
+
details: { reason: workerError instanceof Error ? workerError.message : String(workerError) }
|
|
1192
|
+
}));
|
|
1193
|
+
return false;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
async loadVobSubBuffer(subData, idxData, useMksSource, preserveSource) {
|
|
1197
|
+
if (await this.ensureVobSubWorkerSession()) {
|
|
1198
|
+
try {
|
|
1199
|
+
const transferableSubData = createTransferableBuffer(subData, preserveSource);
|
|
1200
|
+
const loadResponse = await sendToWorker(useMksSource
|
|
1201
|
+
? {
|
|
1202
|
+
type: 'loadVobSubMks',
|
|
1203
|
+
sessionId: this.state.sessionId,
|
|
1204
|
+
subData: transferableSubData
|
|
1205
|
+
}
|
|
1206
|
+
: {
|
|
1207
|
+
type: 'loadVobSub',
|
|
1208
|
+
sessionId: this.state.sessionId,
|
|
1209
|
+
idxContent: idxData,
|
|
1210
|
+
subData: transferableSubData
|
|
1211
|
+
});
|
|
1212
|
+
if (loadResponse.type === 'vobSubLoaded') {
|
|
1213
|
+
if (!useMksSource && idxData && loadResponse.count === 0) {
|
|
1214
|
+
throw createSubtitleDiagnosticError('BAD_IDX', 'IDX metadata did not yield any subtitle timestamps.', {
|
|
1215
|
+
format: 'vobsub'
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
this.state.workerReady = true;
|
|
1219
|
+
this.state.metadata = loadResponse.metadata;
|
|
1220
|
+
this.state.timestamps = loadResponse.timestamps;
|
|
1221
|
+
this.isLoaded = true;
|
|
1222
|
+
this.setParserMetadata(loadResponse.metadata);
|
|
1223
|
+
this.emitIndexed('vobsub', loadResponse.metadata, false);
|
|
1224
|
+
this.emitWorkerState(true, true, this.state.sessionId);
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
else if (loadResponse.type === 'error') {
|
|
1228
|
+
throw new Error(loadResponse.message);
|
|
1000
1229
|
}
|
|
1001
|
-
promises.push(fetch(this.subUrl)
|
|
1002
|
-
.then((r) => {
|
|
1003
|
-
if (!r.ok)
|
|
1004
|
-
throw new Error(`Failed to fetch .sub file: ${r.status}`);
|
|
1005
|
-
return r.arrayBuffer();
|
|
1006
|
-
})
|
|
1007
|
-
.then((b) => {
|
|
1008
|
-
subArrayBuffer = b;
|
|
1009
|
-
}));
|
|
1010
1230
|
}
|
|
1011
|
-
|
|
1012
|
-
if (
|
|
1013
|
-
throw
|
|
1231
|
+
catch (workerError) {
|
|
1232
|
+
if (workerError instanceof SubtitleDiagnosticError && workerError.code === 'BAD_IDX') {
|
|
1233
|
+
throw workerError;
|
|
1014
1234
|
}
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
})
|
|
1021
|
-
.then((t) => {
|
|
1022
|
-
idxData = t;
|
|
1235
|
+
this.state.useWorker = false;
|
|
1236
|
+
this.emitWorkerState(false, false, this.state.sessionId, true);
|
|
1237
|
+
this.emitWarning(createSubtitleWarning('WORKER_FALLBACK', 'VobSub worker load failed, falling back to main-thread rendering.', {
|
|
1238
|
+
format: 'vobsub',
|
|
1239
|
+
details: { reason: workerError instanceof Error ? workerError.message : String(workerError) }
|
|
1023
1240
|
}));
|
|
1024
1241
|
}
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1242
|
+
}
|
|
1243
|
+
await this.loadOnMainThread(subData, idxData, useMksSource);
|
|
1244
|
+
}
|
|
1245
|
+
async loadVobSubMksStreaming() {
|
|
1246
|
+
if (this.subContent) {
|
|
1247
|
+
await this.loadVobSubBuffer(new Uint8Array(this.subContent), undefined, true, true);
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
if (!this.subUrl) {
|
|
1251
|
+
throw createSubtitleDiagnosticError('MISSING_INPUT', 'No SUB content or URL provided.', { format: 'vobsub' });
|
|
1252
|
+
}
|
|
1253
|
+
const { data, strategy, rangeSupported, total } = await fetchSubtitleAsset(this.subUrl, {
|
|
1254
|
+
preferRange: this.rangeRequests && this.streamingLoad,
|
|
1255
|
+
onProgress: (progress) => this.emitLoadProgress('vobsub', progress, this.state.timestamps.length)
|
|
1256
|
+
});
|
|
1257
|
+
this.emitLoadProgress('vobsub', {
|
|
1258
|
+
loaded: data.byteLength,
|
|
1259
|
+
total: total ?? data.byteLength,
|
|
1260
|
+
ratio: 1,
|
|
1261
|
+
rangeSupported,
|
|
1262
|
+
strategy
|
|
1263
|
+
}, 0);
|
|
1264
|
+
await this.loadVobSubBuffer(data, undefined, true, false);
|
|
1265
|
+
}
|
|
1266
|
+
async loadVobSubIdxSubStreaming() {
|
|
1267
|
+
let idxData = this.idxContent;
|
|
1268
|
+
if (!idxData) {
|
|
1269
|
+
if (!this.idxUrl) {
|
|
1270
|
+
throw createSubtitleDiagnosticError('MISSING_INPUT', 'No IDX content or URL provided.', { format: 'vobsub' });
|
|
1030
1271
|
}
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
idxContent: idxData,
|
|
1048
|
-
subData: transferableSubData
|
|
1272
|
+
idxData = await fetchSubtitleText(this.idxUrl, {
|
|
1273
|
+
onProgress: (progress) => this.emitLoadProgress('vobsub', progress, 0)
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
let usedWorker = await this.ensureVobSubWorkerSession();
|
|
1277
|
+
if (usedWorker && this.state.sessionId) {
|
|
1278
|
+
try {
|
|
1279
|
+
const idxResponse = await sendToWorker({
|
|
1280
|
+
type: 'loadVobSubIdx',
|
|
1281
|
+
sessionId: this.state.sessionId,
|
|
1282
|
+
idxContent: idxData
|
|
1283
|
+
});
|
|
1284
|
+
if (idxResponse.type === 'vobSubProgress') {
|
|
1285
|
+
if (idxResponse.count === 0) {
|
|
1286
|
+
throw createSubtitleDiagnosticError('BAD_IDX', 'IDX metadata did not yield any subtitle timestamps.', {
|
|
1287
|
+
format: 'vobsub'
|
|
1049
1288
|
});
|
|
1050
|
-
if (loadResponse.type === 'vobSubLoaded') {
|
|
1051
|
-
if (!useMksSource && idxData && loadResponse.count === 0) {
|
|
1052
|
-
throw createSubtitleDiagnosticError('BAD_IDX', 'IDX metadata did not yield any subtitle timestamps.', {
|
|
1053
|
-
format: 'vobsub'
|
|
1054
|
-
});
|
|
1055
|
-
}
|
|
1056
|
-
this.state.workerReady = true;
|
|
1057
|
-
this.state.metadata = loadResponse.metadata;
|
|
1058
|
-
this.state.timestamps = loadResponse.timestamps;
|
|
1059
|
-
this.isLoaded = true;
|
|
1060
|
-
this.setParserMetadata(loadResponse.metadata);
|
|
1061
|
-
this.emitWorkerState(true, true, this.state.sessionId);
|
|
1062
|
-
this.onLoaded?.();
|
|
1063
|
-
return; // Success, don't fall through to main thread
|
|
1064
|
-
}
|
|
1065
|
-
else if (loadResponse.type === 'error') {
|
|
1066
|
-
throw new Error(loadResponse.message);
|
|
1067
1289
|
}
|
|
1290
|
+
this.applyVobSubIndexState(idxResponse.metadata, idxResponse.timestamps, true, false, true);
|
|
1291
|
+
this.state.workerReady = true;
|
|
1292
|
+
this.emitWorkerState(true, true, this.state.sessionId);
|
|
1068
1293
|
}
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
this.emitWorkerState(false, false, this.state.sessionId, true);
|
|
1072
|
-
this.emitWarning(createSubtitleWarning('WORKER_FALLBACK', 'VobSub worker initialization failed, falling back to main-thread rendering.', {
|
|
1073
|
-
format: 'vobsub',
|
|
1074
|
-
details: { reason: workerError instanceof Error ? workerError.message : String(workerError) }
|
|
1075
|
-
}));
|
|
1076
|
-
if (!this.subContent && this.subUrl && subData.byteLength === 0) {
|
|
1077
|
-
const response = await fetch(this.subUrl);
|
|
1078
|
-
if (!response.ok)
|
|
1079
|
-
throw new Error(`Failed to fetch .sub file: ${response.status}`);
|
|
1080
|
-
subData = new Uint8Array(await response.arrayBuffer());
|
|
1081
|
-
}
|
|
1294
|
+
else if (idxResponse.type === 'error') {
|
|
1295
|
+
throw new Error(idxResponse.message);
|
|
1082
1296
|
}
|
|
1083
1297
|
}
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1298
|
+
catch (error) {
|
|
1299
|
+
if (error instanceof SubtitleDiagnosticError && error.code === 'BAD_IDX') {
|
|
1300
|
+
throw error;
|
|
1301
|
+
}
|
|
1302
|
+
usedWorker = false;
|
|
1303
|
+
this.state.useWorker = false;
|
|
1304
|
+
this.emitWorkerState(false, false, this.state.sessionId, true);
|
|
1305
|
+
this.emitWarning(createSubtitleWarning('WORKER_FALLBACK', 'VobSub worker IDX load failed, falling back to main-thread rendering.', {
|
|
1306
|
+
format: 'vobsub',
|
|
1307
|
+
details: { reason: error instanceof Error ? error.message : String(error) }
|
|
1308
|
+
}));
|
|
1309
|
+
}
|
|
1087
1310
|
}
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
this.
|
|
1091
|
-
|
|
1311
|
+
if (!usedWorker) {
|
|
1312
|
+
await this.yieldToMain();
|
|
1313
|
+
this.vobsubParser = new VobSubParserLowLevel({
|
|
1314
|
+
debug: this.debug,
|
|
1315
|
+
onWarning: (warning) => this.emitWarning(warning)
|
|
1316
|
+
});
|
|
1317
|
+
this.vobsubParser.loadFromIdx(idxData);
|
|
1318
|
+
this.applyVobSubIndexState(this.vobsubParser.getMetadata(), this.vobsubParser.getTimestamps(), true, false, false);
|
|
1092
1319
|
}
|
|
1320
|
+
let subData;
|
|
1321
|
+
if (this.subContent) {
|
|
1322
|
+
subData = new Uint8Array(this.subContent);
|
|
1323
|
+
this.emitLoadProgress('vobsub', this.memoryProgress(subData.byteLength), this.state.timestamps.length);
|
|
1324
|
+
}
|
|
1325
|
+
else {
|
|
1326
|
+
if (!this.subUrl) {
|
|
1327
|
+
throw createSubtitleDiagnosticError('MISSING_INPUT', 'No SUB content or URL provided.', { format: 'vobsub' });
|
|
1328
|
+
}
|
|
1329
|
+
const fetched = await fetchSubtitleAsset(this.subUrl, {
|
|
1330
|
+
preferRange: this.rangeRequests && this.streamingLoad,
|
|
1331
|
+
onProgress: (progress) => this.emitLoadProgress('vobsub', progress, this.state.timestamps.length)
|
|
1332
|
+
});
|
|
1333
|
+
subData = fetched.data;
|
|
1334
|
+
this.emitLoadProgress('vobsub', {
|
|
1335
|
+
loaded: subData.byteLength,
|
|
1336
|
+
total: fetched.total ?? subData.byteLength,
|
|
1337
|
+
ratio: 1,
|
|
1338
|
+
rangeSupported: fetched.rangeSupported,
|
|
1339
|
+
strategy: fetched.strategy
|
|
1340
|
+
}, this.state.timestamps.length);
|
|
1341
|
+
}
|
|
1342
|
+
if (usedWorker && this.state.sessionId) {
|
|
1343
|
+
const transferable = createTransferableBuffer(subData, Boolean(this.subContent));
|
|
1344
|
+
const attachResponse = await sendToWorker({
|
|
1345
|
+
type: 'attachVobSubData',
|
|
1346
|
+
sessionId: this.state.sessionId,
|
|
1347
|
+
subData: transferable
|
|
1348
|
+
});
|
|
1349
|
+
if (attachResponse.type === 'vobSubProgress') {
|
|
1350
|
+
this.applyVobSubIndexState(attachResponse.metadata, attachResponse.timestamps, false, true, true);
|
|
1351
|
+
this.state.workerReady = true;
|
|
1352
|
+
this.isLoaded = true;
|
|
1353
|
+
this.emitWorkerState(true, true, this.state.sessionId);
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1356
|
+
else if (attachResponse.type === 'error') {
|
|
1357
|
+
throw new Error(attachResponse.message);
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
if (!this.vobsubParser) {
|
|
1361
|
+
this.vobsubParser = new VobSubParserLowLevel({
|
|
1362
|
+
debug: this.debug,
|
|
1363
|
+
onWarning: (warning) => this.emitWarning(warning)
|
|
1364
|
+
});
|
|
1365
|
+
this.vobsubParser.loadFromIdx(idxData);
|
|
1366
|
+
}
|
|
1367
|
+
this.vobsubParser.attachSubData(subData);
|
|
1368
|
+
const metadata = this.vobsubParser.getMetadata();
|
|
1369
|
+
this.state.timestamps = this.vobsubParser.getTimestamps();
|
|
1370
|
+
this.state.metadata = metadata;
|
|
1371
|
+
this.isLoaded = true;
|
|
1372
|
+
this.setParserMetadata(metadata);
|
|
1373
|
+
this.emitIndexed('vobsub', metadata, false);
|
|
1093
1374
|
}
|
|
1094
1375
|
async loadOnMainThread(subData, idxData, useMksSource = false) {
|
|
1095
|
-
// Yield to browser before heavy parsing
|
|
1096
1376
|
await this.yieldToMain();
|
|
1097
|
-
this.vobsubParser = new VobSubParserLowLevel({
|
|
1098
|
-
|
|
1377
|
+
this.vobsubParser = new VobSubParserLowLevel({
|
|
1378
|
+
debug: this.debug,
|
|
1379
|
+
onWarning: (warning) => this.emitWarning(warning)
|
|
1380
|
+
});
|
|
1099
1381
|
await new Promise((resolve) => {
|
|
1100
1382
|
const scheduleTask = typeof requestIdleCallback !== 'undefined'
|
|
1101
1383
|
? (cb) => requestIdleCallback(() => cb(), { timeout: 1000 })
|
|
@@ -1114,6 +1396,7 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
1114
1396
|
this.state.metadata = this.vobsubParser.getMetadata();
|
|
1115
1397
|
this.isLoaded = true;
|
|
1116
1398
|
this.setParserMetadata(this.state.metadata);
|
|
1399
|
+
this.emitIndexed('vobsub', this.state.metadata, false);
|
|
1117
1400
|
resolve();
|
|
1118
1401
|
});
|
|
1119
1402
|
});
|