libbitsub 1.8.2 → 1.10.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 +187 -9
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/ts/diagnostics.d.ts +35 -0
- package/dist/ts/diagnostics.d.ts.map +1 -0
- package/dist/ts/diagnostics.js +102 -0
- package/dist/ts/diagnostics.js.map +1 -0
- package/dist/ts/frame-export.d.ts +6 -0
- package/dist/ts/frame-export.d.ts.map +1 -0
- package/dist/ts/frame-export.js +235 -0
- package/dist/ts/frame-export.js.map +1 -0
- package/dist/ts/parsers.d.ts +44 -4
- package/dist/ts/parsers.d.ts.map +1 -1
- package/dist/ts/parsers.js +272 -67
- package/dist/ts/parsers.js.map +1 -1
- package/dist/ts/renderers.d.ts +19 -2
- package/dist/ts/renderers.d.ts.map +1 -1
- package/dist/ts/renderers.js +180 -40
- package/dist/ts/renderers.js.map +1 -1
- package/dist/ts/types.d.ts +112 -1
- package/dist/ts/types.d.ts.map +1 -1
- package/dist/ts/utils.d.ts +2 -2
- package/dist/ts/utils.d.ts.map +1 -1
- package/dist/ts/utils.js +26 -11
- package/dist/ts/utils.js.map +1 -1
- package/dist/ts/worker.d.ts.map +1 -1
- package/dist/ts/worker.js +32 -14
- package/dist/ts/worker.js.map +1 -1
- package/dist/wrapper.d.ts +4 -2
- package/dist/wrapper.d.ts.map +1 -1
- package/dist/wrapper.js +4 -1
- package/dist/wrapper.js.map +1 -1
- package/package.json +1 -1
- package/pkg/README.md +187 -9
- package/pkg/libbitsub.d.ts +15 -0
- package/pkg/libbitsub.js +48 -7
- package/pkg/libbitsub_bg.wasm +0 -0
- package/pkg/libbitsub_bg.wasm.d.ts +3 -0
- package/pkg/package.json +1 -1
- package/src/wrapper.ts +20 -1
package/dist/ts/renderers.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* High-level video-integrated subtitle renderers for libbitsub.
|
|
3
3
|
* Handles canvas overlay, video sync, and subtitle fetching.
|
|
4
4
|
*/
|
|
5
|
-
import type { AutoVideoSubtitleOptions, SubtitleCueMetadata, SubtitleData, SubtitleDisplaySettings, SubtitleParserMetadata, SubtitleRendererBackend, SubtitleRendererEvent, VideoSubtitleOptions, VideoVobSubOptions } from './types';
|
|
5
|
+
import type { AutoVideoSubtitleOptions, SubtitleCacheStats, SubtitleCueMetadata, SubtitleData, SubtitleDiagnosticWarning, SubtitleDisplaySettings, SubtitleLastRenderInfo, SubtitleParserMetadata, SubtitleRendererBackend, SubtitleRendererEvent, VideoSubtitleOptions, VideoVobSubOptions, WorkerRendererState } from './types';
|
|
6
6
|
import { WebGPURenderer } from './webgpu-renderer';
|
|
7
7
|
import { WebGL2Renderer } from './webgl2-renderer';
|
|
8
8
|
interface SubtitleRenderLayout {
|
|
@@ -12,6 +12,11 @@ interface SubtitleRenderLayout {
|
|
|
12
12
|
shiftY: number;
|
|
13
13
|
opacity: number;
|
|
14
14
|
}
|
|
15
|
+
interface RenderFrameOutcome {
|
|
16
|
+
status: 'rendered' | 'cleared' | 'pending' | 'empty' | 'failed';
|
|
17
|
+
data: SubtitleData | null;
|
|
18
|
+
warning: SubtitleDiagnosticWarning | null;
|
|
19
|
+
}
|
|
15
20
|
/** Performance statistics for subtitle renderer */
|
|
16
21
|
export interface SubtitleRendererStats {
|
|
17
22
|
/** Total frames rendered since initialization */
|
|
@@ -70,7 +75,10 @@ declare abstract class BaseVideoSubtitleRenderer {
|
|
|
70
75
|
protected prefetchBefore: number;
|
|
71
76
|
protected prefetchAfter: number;
|
|
72
77
|
protected onEvent?: (event: SubtitleRendererEvent) => void;
|
|
78
|
+
protected onWarning?: (warning: SubtitleDiagnosticWarning) => void;
|
|
73
79
|
protected currentRendererBackend: SubtitleRendererBackend | null;
|
|
80
|
+
protected readonly debug: boolean;
|
|
81
|
+
protected lastRenderInfo: SubtitleLastRenderInfo | null;
|
|
74
82
|
private loadedMetadataHandler;
|
|
75
83
|
private seekedHandler;
|
|
76
84
|
protected webgpuRenderer: WebGPURenderer | null;
|
|
@@ -92,6 +100,10 @@ declare abstract class BaseVideoSubtitleRenderer {
|
|
|
92
100
|
getDisplaySettings(): SubtitleDisplaySettings;
|
|
93
101
|
/** Get performance statistics */
|
|
94
102
|
abstract getStats(): SubtitleRendererStats;
|
|
103
|
+
/** Get cache-related diagnostics for the active renderer session. */
|
|
104
|
+
getCacheStats(): SubtitleCacheStats;
|
|
105
|
+
/** Get the most recent render attempt when debug mode is enabled. */
|
|
106
|
+
getLastRenderInfo(): SubtitleLastRenderInfo | null;
|
|
95
107
|
/** Get parser metadata for the active subtitle track. */
|
|
96
108
|
getMetadata(): SubtitleParserMetadata | null;
|
|
97
109
|
/** Get the most recently displayed cue metadata. */
|
|
@@ -113,11 +125,13 @@ declare abstract class BaseVideoSubtitleRenderer {
|
|
|
113
125
|
/** Create the canvas overlay positioned over the video. */
|
|
114
126
|
protected createCanvas(): void;
|
|
115
127
|
protected emitEvent(event: SubtitleRendererEvent): void;
|
|
128
|
+
protected emitWarning(warning: SubtitleDiagnosticWarning): void;
|
|
116
129
|
protected setParserMetadata(metadata: SubtitleParserMetadata | null): void;
|
|
117
130
|
protected emitWorkerState(enabled: boolean, ready: boolean, sessionId: string | null, fallback?: boolean): void;
|
|
118
131
|
protected emitCacheChange(cachedFrames: number, pendingRenders: number): void;
|
|
119
132
|
protected emitCueChange(cue: SubtitleCueMetadata | null): void;
|
|
120
133
|
protected emitRendererBackend(renderer: SubtitleRendererBackend): void;
|
|
134
|
+
protected recordLastRenderInfo(info: SubtitleLastRenderInfo): void;
|
|
121
135
|
private initWebGPU;
|
|
122
136
|
private initWebGL2;
|
|
123
137
|
/** Initialize Canvas2D renderer. */
|
|
@@ -138,12 +152,13 @@ declare abstract class BaseVideoSubtitleRenderer {
|
|
|
138
152
|
protected abstract findCurrentIndex(time: number): number;
|
|
139
153
|
protected abstract renderAtIndex(index: number): SubtitleData | undefined;
|
|
140
154
|
protected abstract buildCueMetadata(index: number): SubtitleCueMetadata | null;
|
|
155
|
+
protected abstract getWorkerRendererState(): WorkerRendererState;
|
|
141
156
|
/** Check if a render is pending for the given index (async loading in progress) */
|
|
142
157
|
protected abstract isPendingRender(index: number): boolean;
|
|
143
158
|
/** Start the render loop. */
|
|
144
159
|
protected startRenderLoop(): void;
|
|
145
160
|
/** Render a subtitle frame to the canvas. */
|
|
146
|
-
protected renderFrame(time: number, index: number):
|
|
161
|
+
protected renderFrame(time: number, index: number): RenderFrameOutcome;
|
|
147
162
|
protected computeLayout(data: SubtitleData): SubtitleRenderLayout;
|
|
148
163
|
/** Render using WebGPU. */
|
|
149
164
|
private renderFrameWebGPU;
|
|
@@ -167,6 +182,7 @@ export declare class PgsRenderer extends BaseVideoSubtitleRenderer {
|
|
|
167
182
|
constructor(options: VideoSubtitleOptions);
|
|
168
183
|
protected loadSubtitles(): Promise<void>;
|
|
169
184
|
private loadOnMainThread;
|
|
185
|
+
protected getWorkerRendererState(): WorkerRendererState;
|
|
170
186
|
/** Yield to main thread to prevent UI blocking */
|
|
171
187
|
private yieldToMain;
|
|
172
188
|
protected renderAtTime(time: number): SubtitleData | undefined;
|
|
@@ -202,6 +218,7 @@ export declare class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
202
218
|
constructor(options: VideoVobSubOptions);
|
|
203
219
|
protected loadSubtitles(): Promise<void>;
|
|
204
220
|
private loadOnMainThread;
|
|
221
|
+
protected getWorkerRendererState(): WorkerRendererState;
|
|
205
222
|
/** Yield to main thread to prevent UI blocking */
|
|
206
223
|
private yieldToMain;
|
|
207
224
|
protected renderAtTime(time: number): SubtitleData | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"renderers.d.ts","sourceRoot":"","sources":["../../src/ts/renderers.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EACV,wBAAwB,EACxB,mBAAmB,EACnB,YAAY,EACZ,uBAAuB,EACvB,sBAAsB,EACtB,uBAAuB,EACvB,qBAAqB,EACrB,oBAAoB,EACpB,kBAAkB,
|
|
1
|
+
{"version":3,"file":"renderers.d.ts","sourceRoot":"","sources":["../../src/ts/renderers.ts"],"names":[],"mappings":"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"}
|
package/dist/ts/renderers.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
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
6
|
import { initWasm } from './wasm';
|
|
6
7
|
import { getOrCreateWorker, sendToWorker } from './worker';
|
|
7
8
|
import { binarySearchTimestamp, convertFrameData, createWorkerSessionId, createWorkerState, detectSubtitleFormat, getSubtitleBounds, isMksSource, setCacheLimit as applyCacheLimit, setCachedFrame } from './utils';
|
|
@@ -19,6 +20,15 @@ const DEFAULT_DISPLAY_SETTINGS = {
|
|
|
19
20
|
safeArea: 0,
|
|
20
21
|
opacity: 1.0
|
|
21
22
|
};
|
|
23
|
+
function createTransferableBuffer(data, preserveSource) {
|
|
24
|
+
if (!preserveSource &&
|
|
25
|
+
data.buffer instanceof ArrayBuffer &&
|
|
26
|
+
data.byteOffset === 0 &&
|
|
27
|
+
data.byteLength === data.buffer.byteLength) {
|
|
28
|
+
return data.buffer;
|
|
29
|
+
}
|
|
30
|
+
return data.slice().buffer;
|
|
31
|
+
}
|
|
22
32
|
/**
|
|
23
33
|
* Base class for video-integrated subtitle renderers.
|
|
24
34
|
* Handles canvas overlay, video sync, and subtitle fetching.
|
|
@@ -50,7 +60,10 @@ class BaseVideoSubtitleRenderer {
|
|
|
50
60
|
prefetchBefore = 0;
|
|
51
61
|
prefetchAfter = 0;
|
|
52
62
|
onEvent;
|
|
63
|
+
onWarning;
|
|
53
64
|
currentRendererBackend = null;
|
|
65
|
+
debug;
|
|
66
|
+
lastRenderInfo = null;
|
|
54
67
|
loadedMetadataHandler = null;
|
|
55
68
|
seekedHandler = null;
|
|
56
69
|
// WebGPU renderer (optional, falls back to WebGL2 then Canvas2D)
|
|
@@ -78,6 +91,8 @@ class BaseVideoSubtitleRenderer {
|
|
|
78
91
|
this.onWebGPUFallback = options.onWebGPUFallback;
|
|
79
92
|
this.onWebGL2Fallback = options.onWebGL2Fallback;
|
|
80
93
|
this.onEvent = options.onEvent;
|
|
94
|
+
this.onWarning = options.onWarning;
|
|
95
|
+
this.debug = Boolean(options.debug);
|
|
81
96
|
this.displaySettings = { ...DEFAULT_DISPLAY_SETTINGS, ...options.displaySettings };
|
|
82
97
|
this.timeOffset = options.timeOffset ?? 0;
|
|
83
98
|
this.cacheLimit = Math.max(0, Math.floor(options.cacheLimit ?? 24));
|
|
@@ -88,6 +103,29 @@ class BaseVideoSubtitleRenderer {
|
|
|
88
103
|
getDisplaySettings() {
|
|
89
104
|
return { ...this.displaySettings };
|
|
90
105
|
}
|
|
106
|
+
/** Get cache-related diagnostics for the active renderer session. */
|
|
107
|
+
getCacheStats() {
|
|
108
|
+
const state = this.getWorkerRendererState();
|
|
109
|
+
return {
|
|
110
|
+
cacheLimit: this.cacheLimit,
|
|
111
|
+
cachedFrames: state.frameCache.size,
|
|
112
|
+
pendingRenders: state.pendingRenders.size,
|
|
113
|
+
totalEntries: state.timestamps.length,
|
|
114
|
+
usingWorker: state.useWorker && state.workerReady,
|
|
115
|
+
workerReady: state.workerReady,
|
|
116
|
+
sessionId: state.sessionId
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/** Get the most recent render attempt when debug mode is enabled. */
|
|
120
|
+
getLastRenderInfo() {
|
|
121
|
+
if (!this.lastRenderInfo)
|
|
122
|
+
return null;
|
|
123
|
+
return {
|
|
124
|
+
...this.lastRenderInfo,
|
|
125
|
+
cache: { ...this.lastRenderInfo.cache },
|
|
126
|
+
cue: this.lastRenderInfo.cue ? { ...this.lastRenderInfo.cue } : null
|
|
127
|
+
};
|
|
128
|
+
}
|
|
91
129
|
/** Get parser metadata for the active subtitle track. */
|
|
92
130
|
getMetadata() {
|
|
93
131
|
return this.parserMetadata;
|
|
@@ -156,7 +194,7 @@ class BaseVideoSubtitleRenderer {
|
|
|
156
194
|
/** Start initialization. */
|
|
157
195
|
startInit() {
|
|
158
196
|
this.init().catch((error) => {
|
|
159
|
-
this.emitEvent({ type: 'error', format: this.format, error: error
|
|
197
|
+
this.emitEvent({ type: 'error', format: this.format, error: normalizeSubtitleError(error, { format: this.format }) });
|
|
160
198
|
});
|
|
161
199
|
}
|
|
162
200
|
/** Initialize the renderer. */
|
|
@@ -207,6 +245,13 @@ class BaseVideoSubtitleRenderer {
|
|
|
207
245
|
emitEvent(event) {
|
|
208
246
|
this.onEvent?.(event);
|
|
209
247
|
}
|
|
248
|
+
emitWarning(warning) {
|
|
249
|
+
this.onWarning?.(warning);
|
|
250
|
+
this.emitEvent({ type: 'warning', warning });
|
|
251
|
+
if (this.debug && !this.onWarning) {
|
|
252
|
+
console.warn(formatSubtitleWarningForConsole(warning), warning.details ?? {});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
210
255
|
setParserMetadata(metadata) {
|
|
211
256
|
this.parserMetadata = metadata;
|
|
212
257
|
if (metadata) {
|
|
@@ -234,6 +279,11 @@ class BaseVideoSubtitleRenderer {
|
|
|
234
279
|
this.currentRendererBackend = renderer;
|
|
235
280
|
this.emitEvent({ type: 'renderer-change', renderer });
|
|
236
281
|
}
|
|
282
|
+
recordLastRenderInfo(info) {
|
|
283
|
+
if (!this.debug)
|
|
284
|
+
return;
|
|
285
|
+
this.lastRenderInfo = info;
|
|
286
|
+
}
|
|
237
287
|
/** Initialize WebGPU renderer. */
|
|
238
288
|
async initWebGPU() {
|
|
239
289
|
try {
|
|
@@ -369,8 +419,9 @@ class BaseVideoSubtitleRenderer {
|
|
|
369
419
|
const currentIndex = this.findCurrentIndex(currentTime);
|
|
370
420
|
// Only re-render if index changed
|
|
371
421
|
if (currentIndex !== this.lastRenderedIndex) {
|
|
422
|
+
const cacheHit = currentIndex >= 0 && this.getWorkerRendererState().frameCache.has(currentIndex);
|
|
372
423
|
const startTime = performance.now();
|
|
373
|
-
this.renderFrame(currentTime, currentIndex);
|
|
424
|
+
const outcome = this.renderFrame(currentTime, currentIndex);
|
|
374
425
|
const endTime = performance.now();
|
|
375
426
|
// Track performance
|
|
376
427
|
const renderTime = endTime - startTime;
|
|
@@ -387,9 +438,28 @@ class BaseVideoSubtitleRenderer {
|
|
|
387
438
|
if (renderTime > frameBudget) {
|
|
388
439
|
this.perfStats.framesDropped++;
|
|
389
440
|
}
|
|
441
|
+
if (outcome.warning) {
|
|
442
|
+
this.emitWarning(outcome.warning);
|
|
443
|
+
}
|
|
390
444
|
this.lastRenderedIndex = currentIndex;
|
|
391
445
|
this.lastRenderedTime = currentTime;
|
|
392
|
-
|
|
446
|
+
const cue = currentIndex >= 0 ? this.buildCueMetadata(currentIndex) : null;
|
|
447
|
+
this.emitCueChange(cue);
|
|
448
|
+
this.recordLastRenderInfo({
|
|
449
|
+
time: currentTime,
|
|
450
|
+
index: currentIndex,
|
|
451
|
+
status: outcome.status,
|
|
452
|
+
backend: this.currentRendererBackend,
|
|
453
|
+
usingWorker: this.getCacheStats().usingWorker,
|
|
454
|
+
cacheHit,
|
|
455
|
+
renderDuration: Math.round(renderTime * 100) / 100,
|
|
456
|
+
frameWidth: outcome.data?.width ?? null,
|
|
457
|
+
frameHeight: outcome.data?.height ?? null,
|
|
458
|
+
compositionCount: outcome.data?.compositionData.length ?? 0,
|
|
459
|
+
cue,
|
|
460
|
+
cache: this.getCacheStats(),
|
|
461
|
+
capturedAt: endTime
|
|
462
|
+
});
|
|
393
463
|
this.emitEvent({ type: 'stats', stats: this.getStats() });
|
|
394
464
|
if (currentIndex >= 0 && (this.prefetchBefore > 0 || this.prefetchAfter > 0)) {
|
|
395
465
|
const prefetch = this.prefetchAroundTime;
|
|
@@ -403,8 +473,9 @@ class BaseVideoSubtitleRenderer {
|
|
|
403
473
|
}
|
|
404
474
|
/** Render a subtitle frame to the canvas. */
|
|
405
475
|
renderFrame(time, index) {
|
|
406
|
-
if (!this.canvas)
|
|
407
|
-
return;
|
|
476
|
+
if (!this.canvas) {
|
|
477
|
+
return { status: 'failed', data: null, warning: null };
|
|
478
|
+
}
|
|
408
479
|
// Get the data for this index
|
|
409
480
|
const data = index >= 0 ? this.renderAtIndex(index) : undefined;
|
|
410
481
|
// If data is undefined, it means async loading is in progress
|
|
@@ -415,9 +486,11 @@ class BaseVideoSubtitleRenderer {
|
|
|
415
486
|
// If not pending, it means the render returned no data immediately
|
|
416
487
|
if (this.isPendingRender(index)) {
|
|
417
488
|
// Don't clear - keep showing the last frame while loading
|
|
418
|
-
return;
|
|
489
|
+
return { status: 'pending', data: null, warning: null };
|
|
419
490
|
}
|
|
420
491
|
}
|
|
492
|
+
const renderIssue = index >= 0 ? this.getWorkerRendererState().renderIssues.get(index) ?? null : null;
|
|
493
|
+
const warning = warningFromRenderIssue(renderIssue, { format: this.format, cueIndex: index });
|
|
421
494
|
// Use best available renderer
|
|
422
495
|
if (this.useWebGPU && this.webgpuRenderer) {
|
|
423
496
|
this.renderFrameWebGPU(data, index);
|
|
@@ -428,6 +501,16 @@ class BaseVideoSubtitleRenderer {
|
|
|
428
501
|
else {
|
|
429
502
|
this.renderFrameCanvas2D(data, index);
|
|
430
503
|
}
|
|
504
|
+
if (index < 0) {
|
|
505
|
+
return { status: 'cleared', data: null, warning: null };
|
|
506
|
+
}
|
|
507
|
+
if (warning) {
|
|
508
|
+
return { status: 'failed', data: data ?? null, warning };
|
|
509
|
+
}
|
|
510
|
+
if (!data || data.compositionData.length === 0) {
|
|
511
|
+
return { status: 'empty', data: data ?? null, warning: null };
|
|
512
|
+
}
|
|
513
|
+
return { status: 'rendered', data, warning: null };
|
|
431
514
|
}
|
|
432
515
|
computeLayout(data) {
|
|
433
516
|
if (!this.canvas) {
|
|
@@ -591,6 +674,7 @@ class BaseVideoSubtitleRenderer {
|
|
|
591
674
|
this.lastRenderedData = null;
|
|
592
675
|
this.currentCueMetadata = null;
|
|
593
676
|
this.parserMetadata = null;
|
|
677
|
+
this.lastRenderInfo = null;
|
|
594
678
|
this.useWebGPU = false;
|
|
595
679
|
this.useWebGL2 = false;
|
|
596
680
|
}
|
|
@@ -630,24 +714,22 @@ export class PgsRenderer extends BaseVideoSubtitleRenderer {
|
|
|
630
714
|
else {
|
|
631
715
|
throw new Error('No subtitle content or URL provided');
|
|
632
716
|
}
|
|
633
|
-
|
|
717
|
+
let data = new Uint8Array(arrayBuffer);
|
|
634
718
|
if (this.state.useWorker) {
|
|
635
719
|
try {
|
|
636
720
|
this.state.sessionId = createWorkerSessionId();
|
|
637
721
|
await getOrCreateWorker();
|
|
638
722
|
this.emitWorkerState(true, false, this.state.sessionId);
|
|
723
|
+
const transferableData = createTransferableBuffer(data, Boolean(this.subContent));
|
|
639
724
|
const loadResponse = await sendToWorker({
|
|
640
725
|
type: 'loadPgs',
|
|
641
726
|
sessionId: this.state.sessionId,
|
|
642
|
-
data:
|
|
727
|
+
data: transferableData
|
|
643
728
|
});
|
|
644
729
|
if (loadResponse.type === 'pgsLoaded') {
|
|
645
730
|
this.state.workerReady = true;
|
|
646
731
|
this.state.metadata = loadResponse.metadata;
|
|
647
|
-
|
|
648
|
-
if (tsResponse.type === 'pgsTimestamps') {
|
|
649
|
-
this.state.timestamps = tsResponse.timestamps;
|
|
650
|
-
}
|
|
732
|
+
this.state.timestamps = loadResponse.timestamps;
|
|
651
733
|
this.isLoaded = true;
|
|
652
734
|
this.setParserMetadata(loadResponse.metadata);
|
|
653
735
|
this.emitWorkerState(true, true, this.state.sessionId);
|
|
@@ -661,6 +743,16 @@ export class PgsRenderer extends BaseVideoSubtitleRenderer {
|
|
|
661
743
|
catch (workerError) {
|
|
662
744
|
this.state.useWorker = false;
|
|
663
745
|
this.emitWorkerState(false, false, this.state.sessionId, true);
|
|
746
|
+
this.emitWarning(createSubtitleWarning('WORKER_FALLBACK', 'PGS worker initialization failed, falling back to main-thread rendering.', {
|
|
747
|
+
format: 'pgs',
|
|
748
|
+
details: { reason: workerError instanceof Error ? workerError.message : String(workerError) }
|
|
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());
|
|
755
|
+
}
|
|
664
756
|
}
|
|
665
757
|
}
|
|
666
758
|
// Main thread fallback - use idle callback to avoid blocking UI
|
|
@@ -668,7 +760,7 @@ export class PgsRenderer extends BaseVideoSubtitleRenderer {
|
|
|
668
760
|
this.onLoaded?.();
|
|
669
761
|
}
|
|
670
762
|
catch (error) {
|
|
671
|
-
const resolvedError = error
|
|
763
|
+
const resolvedError = normalizeSubtitleError(error, { format: 'pgs' });
|
|
672
764
|
this.emitEvent({ type: 'error', format: 'pgs', error: resolvedError });
|
|
673
765
|
this.onError?.(resolvedError);
|
|
674
766
|
}
|
|
@@ -676,7 +768,7 @@ export class PgsRenderer extends BaseVideoSubtitleRenderer {
|
|
|
676
768
|
async loadOnMainThread(data) {
|
|
677
769
|
// Yield to browser before heavy parsing
|
|
678
770
|
await this.yieldToMain();
|
|
679
|
-
this.pgsParser = new PgsParser();
|
|
771
|
+
this.pgsParser = new PgsParser({ debug: this.debug, onWarning: (warning) => this.emitWarning(warning) });
|
|
680
772
|
// Parse in a microtask to allow UI to update
|
|
681
773
|
await new Promise((resolve) => {
|
|
682
774
|
// Use requestIdleCallback if available, otherwise setTimeout
|
|
@@ -689,10 +781,16 @@ export class PgsRenderer extends BaseVideoSubtitleRenderer {
|
|
|
689
781
|
this.state.metadata = this.pgsParser.getMetadata();
|
|
690
782
|
this.isLoaded = true;
|
|
691
783
|
this.setParserMetadata(this.state.metadata);
|
|
784
|
+
if (count === 0) {
|
|
785
|
+
this.state.renderIssues.set(-1, 'INVALID_SUBTITLE_DATA');
|
|
786
|
+
}
|
|
692
787
|
resolve();
|
|
693
788
|
});
|
|
694
789
|
});
|
|
695
790
|
}
|
|
791
|
+
getWorkerRendererState() {
|
|
792
|
+
return this.state;
|
|
793
|
+
}
|
|
696
794
|
/** Yield to main thread to prevent UI blocking */
|
|
697
795
|
yieldToMain() {
|
|
698
796
|
// Use scheduler.yield if available (Chrome 115+)
|
|
@@ -719,19 +817,28 @@ export class PgsRenderer extends BaseVideoSubtitleRenderer {
|
|
|
719
817
|
}
|
|
720
818
|
if (this.state.useWorker && this.state.workerReady) {
|
|
721
819
|
if (!this.state.pendingRenders.has(index)) {
|
|
722
|
-
const
|
|
820
|
+
const renderTask = sendToWorker({
|
|
723
821
|
type: 'renderPgsAtIndex',
|
|
724
822
|
sessionId: this.state.sessionId,
|
|
725
823
|
index
|
|
726
|
-
}).then((response) =>
|
|
824
|
+
}).then((response) => {
|
|
825
|
+
if (response.type === 'pgsFrame') {
|
|
826
|
+
return {
|
|
827
|
+
frame: response.frame ? convertFrameData(response.frame) : null,
|
|
828
|
+
renderIssue: response.renderIssue?.trim() || null
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
return { frame: null, renderIssue: null };
|
|
832
|
+
});
|
|
833
|
+
const renderPromise = renderTask.then(({ frame }) => frame);
|
|
727
834
|
this.state.pendingRenders.set(index, renderPromise);
|
|
728
835
|
this.emitCacheChange(this.state.frameCache.size, this.state.pendingRenders.size);
|
|
729
|
-
|
|
730
|
-
setCachedFrame(this.state, index,
|
|
836
|
+
renderTask.then(({ frame, renderIssue }) => {
|
|
837
|
+
setCachedFrame(this.state, index, frame, renderIssue);
|
|
731
838
|
this.state.pendingRenders.delete(index);
|
|
732
839
|
this.emitCacheChange(this.state.frameCache.size, this.state.pendingRenders.size);
|
|
733
840
|
// Force re-render on next frame by resetting lastRenderedIndex
|
|
734
|
-
if (this.findCurrentIndex(this.video.currentTime) === index) {
|
|
841
|
+
if (this.findCurrentIndex(this.video.currentTime + this.timeOffset) === index) {
|
|
735
842
|
this.lastRenderedIndex = -1;
|
|
736
843
|
}
|
|
737
844
|
});
|
|
@@ -740,7 +847,7 @@ export class PgsRenderer extends BaseVideoSubtitleRenderer {
|
|
|
740
847
|
return undefined;
|
|
741
848
|
}
|
|
742
849
|
const rendered = this.pgsParser?.renderAtIndex(index) ?? null;
|
|
743
|
-
setCachedFrame(this.state, index, rendered);
|
|
850
|
+
setCachedFrame(this.state, index, rendered, this.pgsParser?.getLastRenderIssue() ?? null);
|
|
744
851
|
this.emitCacheChange(this.state.frameCache.size, this.state.pendingRenders.size);
|
|
745
852
|
return rendered ?? undefined;
|
|
746
853
|
}
|
|
@@ -771,6 +878,7 @@ export class PgsRenderer extends BaseVideoSubtitleRenderer {
|
|
|
771
878
|
}
|
|
772
879
|
onSeek() {
|
|
773
880
|
this.state.frameCache.clear();
|
|
881
|
+
this.state.renderIssues.clear();
|
|
774
882
|
this.state.pendingRenders.clear();
|
|
775
883
|
this.emitCacheChange(this.state.frameCache.size, this.state.pendingRenders.size);
|
|
776
884
|
if (this.state.useWorker && this.state.workerReady) {
|
|
@@ -784,6 +892,7 @@ export class PgsRenderer extends BaseVideoSubtitleRenderer {
|
|
|
784
892
|
}
|
|
785
893
|
clearFrameCache() {
|
|
786
894
|
this.state.frameCache.clear();
|
|
895
|
+
this.state.renderIssues.clear();
|
|
787
896
|
this.state.pendingRenders.clear();
|
|
788
897
|
this.lastRenderedIndex = -1;
|
|
789
898
|
if (this.state.useWorker && this.state.workerReady) {
|
|
@@ -824,6 +933,7 @@ export class PgsRenderer extends BaseVideoSubtitleRenderer {
|
|
|
824
933
|
dispose() {
|
|
825
934
|
super.dispose();
|
|
826
935
|
this.state.frameCache.clear();
|
|
936
|
+
this.state.renderIssues.clear();
|
|
827
937
|
this.state.pendingRenders.clear();
|
|
828
938
|
if (this.state.useWorker && this.state.workerReady) {
|
|
829
939
|
sendToWorker({ type: 'disposePgs', sessionId: this.state.sessionId }).catch(() => { });
|
|
@@ -885,8 +995,9 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
885
995
|
// Fetch missing parts
|
|
886
996
|
const promises = [];
|
|
887
997
|
if (!subArrayBuffer) {
|
|
888
|
-
if (!this.subUrl)
|
|
889
|
-
throw
|
|
998
|
+
if (!this.subUrl) {
|
|
999
|
+
throw createSubtitleDiagnosticError('MISSING_INPUT', 'No SUB content or URL provided.', { format: 'vobsub' });
|
|
1000
|
+
}
|
|
890
1001
|
promises.push(fetch(this.subUrl)
|
|
891
1002
|
.then((r) => {
|
|
892
1003
|
if (!r.ok)
|
|
@@ -898,8 +1009,9 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
898
1009
|
}));
|
|
899
1010
|
}
|
|
900
1011
|
if (!useMksSource && !idxData) {
|
|
901
|
-
if (!this.idxUrl)
|
|
902
|
-
throw
|
|
1012
|
+
if (!this.idxUrl) {
|
|
1013
|
+
throw createSubtitleDiagnosticError('MISSING_INPUT', 'No IDX content or URL provided.', { format: 'vobsub' });
|
|
1014
|
+
}
|
|
903
1015
|
promises.push(fetch(this.idxUrl)
|
|
904
1016
|
.then((r) => {
|
|
905
1017
|
if (!r.ok)
|
|
@@ -914,33 +1026,36 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
914
1026
|
await Promise.all(promises);
|
|
915
1027
|
}
|
|
916
1028
|
if (!subArrayBuffer || (!useMksSource && !idxData)) {
|
|
917
|
-
throw
|
|
1029
|
+
throw createSubtitleDiagnosticError('MISSING_INPUT', 'Failed to load VobSub data.', { format: 'vobsub' });
|
|
918
1030
|
}
|
|
919
|
-
|
|
1031
|
+
let subData = new Uint8Array(subArrayBuffer);
|
|
920
1032
|
if (this.state.useWorker) {
|
|
921
1033
|
try {
|
|
922
1034
|
this.state.sessionId = createWorkerSessionId();
|
|
923
1035
|
await getOrCreateWorker();
|
|
924
1036
|
this.emitWorkerState(true, false, this.state.sessionId);
|
|
1037
|
+
const transferableSubData = createTransferableBuffer(subData, Boolean(this.subContent));
|
|
925
1038
|
const loadResponse = await sendToWorker(useMksSource
|
|
926
1039
|
? {
|
|
927
1040
|
type: 'loadVobSubMks',
|
|
928
1041
|
sessionId: this.state.sessionId,
|
|
929
|
-
subData:
|
|
1042
|
+
subData: transferableSubData
|
|
930
1043
|
}
|
|
931
1044
|
: {
|
|
932
1045
|
type: 'loadVobSub',
|
|
933
1046
|
sessionId: this.state.sessionId,
|
|
934
1047
|
idxContent: idxData,
|
|
935
|
-
subData:
|
|
1048
|
+
subData: transferableSubData
|
|
936
1049
|
});
|
|
937
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
|
+
}
|
|
938
1056
|
this.state.workerReady = true;
|
|
939
1057
|
this.state.metadata = loadResponse.metadata;
|
|
940
|
-
|
|
941
|
-
if (tsResponse.type === 'vobSubTimestamps') {
|
|
942
|
-
this.state.timestamps = tsResponse.timestamps;
|
|
943
|
-
}
|
|
1058
|
+
this.state.timestamps = loadResponse.timestamps;
|
|
944
1059
|
this.isLoaded = true;
|
|
945
1060
|
this.setParserMetadata(loadResponse.metadata);
|
|
946
1061
|
this.emitWorkerState(true, true, this.state.sessionId);
|
|
@@ -954,6 +1069,16 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
954
1069
|
catch (workerError) {
|
|
955
1070
|
this.state.useWorker = false;
|
|
956
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
|
+
}
|
|
957
1082
|
}
|
|
958
1083
|
}
|
|
959
1084
|
// Main thread fallback
|
|
@@ -961,7 +1086,7 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
961
1086
|
this.onLoaded?.();
|
|
962
1087
|
}
|
|
963
1088
|
catch (error) {
|
|
964
|
-
const resolvedError = error
|
|
1089
|
+
const resolvedError = normalizeSubtitleError(error, { format: 'vobsub' });
|
|
965
1090
|
this.emitEvent({ type: 'error', format: 'vobsub', error: resolvedError });
|
|
966
1091
|
this.onError?.(resolvedError);
|
|
967
1092
|
}
|
|
@@ -969,7 +1094,7 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
969
1094
|
async loadOnMainThread(subData, idxData, useMksSource = false) {
|
|
970
1095
|
// Yield to browser before heavy parsing
|
|
971
1096
|
await this.yieldToMain();
|
|
972
|
-
this.vobsubParser = new VobSubParserLowLevel();
|
|
1097
|
+
this.vobsubParser = new VobSubParserLowLevel({ debug: this.debug, onWarning: (warning) => this.emitWarning(warning) });
|
|
973
1098
|
// Parse in a microtask to allow UI to update
|
|
974
1099
|
await new Promise((resolve) => {
|
|
975
1100
|
const scheduleTask = typeof requestIdleCallback !== 'undefined'
|
|
@@ -993,6 +1118,9 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
993
1118
|
});
|
|
994
1119
|
});
|
|
995
1120
|
}
|
|
1121
|
+
getWorkerRendererState() {
|
|
1122
|
+
return this.state;
|
|
1123
|
+
}
|
|
996
1124
|
/** Yield to main thread to prevent UI blocking */
|
|
997
1125
|
yieldToMain() {
|
|
998
1126
|
const globalScheduler = globalThis.scheduler;
|
|
@@ -1046,19 +1174,28 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
1046
1174
|
if (this.state.useWorker && this.state.workerReady) {
|
|
1047
1175
|
// Start async render if not already pending
|
|
1048
1176
|
if (!this.state.pendingRenders.has(index)) {
|
|
1049
|
-
const
|
|
1177
|
+
const renderTask = sendToWorker({
|
|
1050
1178
|
type: 'renderVobSubAtIndex',
|
|
1051
1179
|
sessionId: this.state.sessionId,
|
|
1052
1180
|
index
|
|
1053
|
-
}).then((response) =>
|
|
1181
|
+
}).then((response) => {
|
|
1182
|
+
if (response.type === 'vobSubFrame') {
|
|
1183
|
+
return {
|
|
1184
|
+
frame: response.frame ? convertFrameData(response.frame) : null,
|
|
1185
|
+
renderIssue: response.renderIssue?.trim() || null
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
return { frame: null, renderIssue: null };
|
|
1189
|
+
});
|
|
1190
|
+
const renderPromise = renderTask.then(({ frame }) => frame);
|
|
1054
1191
|
this.state.pendingRenders.set(index, renderPromise);
|
|
1055
1192
|
this.emitCacheChange(this.state.frameCache.size, this.state.pendingRenders.size);
|
|
1056
|
-
|
|
1057
|
-
setCachedFrame(this.state, index,
|
|
1193
|
+
renderTask.then(({ frame, renderIssue }) => {
|
|
1194
|
+
setCachedFrame(this.state, index, frame, renderIssue);
|
|
1058
1195
|
this.state.pendingRenders.delete(index);
|
|
1059
1196
|
this.emitCacheChange(this.state.frameCache.size, this.state.pendingRenders.size);
|
|
1060
1197
|
// Force re-render on next frame by resetting lastRenderedIndex
|
|
1061
|
-
if (this.findCurrentIndex(this.video.currentTime) === index) {
|
|
1198
|
+
if (this.findCurrentIndex(this.video.currentTime + this.timeOffset) === index) {
|
|
1062
1199
|
this.lastRenderedIndex = -1;
|
|
1063
1200
|
}
|
|
1064
1201
|
});
|
|
@@ -1067,7 +1204,7 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
1067
1204
|
return undefined;
|
|
1068
1205
|
}
|
|
1069
1206
|
const rendered = this.vobsubParser?.renderAtIndex(index) ?? null;
|
|
1070
|
-
setCachedFrame(this.state, index, rendered);
|
|
1207
|
+
setCachedFrame(this.state, index, rendered, this.vobsubParser?.getLastRenderIssue() ?? null);
|
|
1071
1208
|
this.emitCacheChange(this.state.frameCache.size, this.state.pendingRenders.size);
|
|
1072
1209
|
return rendered ?? undefined;
|
|
1073
1210
|
}
|
|
@@ -1100,6 +1237,7 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
1100
1237
|
}
|
|
1101
1238
|
onSeek() {
|
|
1102
1239
|
this.state.frameCache.clear();
|
|
1240
|
+
this.state.renderIssues.clear();
|
|
1103
1241
|
this.state.pendingRenders.clear();
|
|
1104
1242
|
// Clear cached index lookup on seek
|
|
1105
1243
|
this.cachedIndex = -1;
|
|
@@ -1117,6 +1255,7 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
1117
1255
|
}
|
|
1118
1256
|
clearFrameCache() {
|
|
1119
1257
|
this.state.frameCache.clear();
|
|
1258
|
+
this.state.renderIssues.clear();
|
|
1120
1259
|
this.state.pendingRenders.clear();
|
|
1121
1260
|
this.cachedIndex = -1;
|
|
1122
1261
|
this.cachedIndexTime = -1;
|
|
@@ -1197,6 +1336,7 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
1197
1336
|
dispose() {
|
|
1198
1337
|
super.dispose();
|
|
1199
1338
|
this.state.frameCache.clear();
|
|
1339
|
+
this.state.renderIssues.clear();
|
|
1200
1340
|
this.state.pendingRenders.clear();
|
|
1201
1341
|
if (this.state.useWorker && this.state.workerReady) {
|
|
1202
1342
|
sendToWorker({ type: 'disposeVobSub', sessionId: this.state.sessionId }).catch(() => { });
|