libbitsub 1.6.0 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +407 -378
- 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/parsers.d.ts +18 -1
- package/dist/ts/parsers.d.ts.map +1 -1
- package/dist/ts/parsers.js +151 -0
- package/dist/ts/parsers.js.map +1 -1
- package/dist/ts/renderers.d.ts +47 -4
- package/dist/ts/renderers.d.ts.map +1 -1
- package/dist/ts/renderers.js +391 -87
- package/dist/ts/renderers.js.map +1 -1
- package/dist/ts/types.d.ts +144 -0
- package/dist/ts/types.d.ts.map +1 -1
- package/dist/ts/utils.d.ts +11 -1
- package/dist/ts/utils.d.ts.map +1 -1
- package/dist/ts/utils.js +100 -1
- package/dist/ts/utils.js.map +1 -1
- package/dist/ts/wasm.js.map +1 -1
- package/dist/ts/webgl2-renderer.d.ts +2 -1
- package/dist/ts/webgl2-renderer.d.ts.map +1 -1
- package/dist/ts/webgl2-renderer.js +10 -7
- package/dist/ts/webgl2-renderer.js.map +1 -1
- package/dist/ts/webgpu-renderer.d.ts +4 -1
- package/dist/ts/webgpu-renderer.d.ts.map +1 -1
- package/dist/ts/webgpu-renderer.js +82 -32
- package/dist/ts/webgpu-renderer.js.map +1 -1
- package/dist/ts/worker.d.ts.map +1 -1
- package/dist/ts/worker.js +145 -87
- package/dist/ts/worker.js.map +1 -1
- package/dist/wrapper.d.ts +3 -2
- package/dist/wrapper.d.ts.map +1 -1
- package/dist/wrapper.js +3 -1
- package/dist/wrapper.js.map +1 -1
- package/package.json +3 -2
- package/pkg/README.md +407 -378
- package/pkg/libbitsub.d.ts +121 -1
- package/pkg/libbitsub.js +251 -15
- package/pkg/libbitsub_bg.wasm +0 -0
- package/pkg/libbitsub_bg.wasm.d.ts +25 -1
- package/pkg/package.json +1 -1
- package/src/wrapper.ts +14 -1
package/dist/ts/renderers.js
CHANGED
|
@@ -4,14 +4,19 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { initWasm } from './wasm';
|
|
6
6
|
import { getOrCreateWorker, sendToWorker } from './worker';
|
|
7
|
-
import { binarySearchTimestamp, convertFrameData, createWorkerState } from './utils';
|
|
7
|
+
import { binarySearchTimestamp, convertFrameData, createWorkerSessionId, createWorkerState, detectSubtitleFormat, getSubtitleBounds, setCacheLimit as applyCacheLimit, setCachedFrame } from './utils';
|
|
8
8
|
import { PgsParser, VobSubParserLowLevel } from './parsers';
|
|
9
9
|
import { WebGPURenderer, isWebGPUSupported } from './webgpu-renderer';
|
|
10
10
|
import { WebGL2Renderer, isWebGL2Supported } from './webgl2-renderer';
|
|
11
11
|
/** Default display settings */
|
|
12
12
|
const DEFAULT_DISPLAY_SETTINGS = {
|
|
13
13
|
scale: 1.0,
|
|
14
|
-
verticalOffset: 0
|
|
14
|
+
verticalOffset: 0,
|
|
15
|
+
horizontalOffset: 0,
|
|
16
|
+
horizontalAlign: 'center',
|
|
17
|
+
bottomPadding: 0,
|
|
18
|
+
safeArea: 0,
|
|
19
|
+
opacity: 1.0
|
|
15
20
|
};
|
|
16
21
|
/**
|
|
17
22
|
* Base class for video-integrated subtitle renderers.
|
|
@@ -19,6 +24,7 @@ const DEFAULT_DISPLAY_SETTINGS = {
|
|
|
19
24
|
*/
|
|
20
25
|
class BaseVideoSubtitleRenderer {
|
|
21
26
|
video;
|
|
27
|
+
format;
|
|
22
28
|
subUrl;
|
|
23
29
|
subContent;
|
|
24
30
|
canvas = null;
|
|
@@ -32,8 +38,18 @@ class BaseVideoSubtitleRenderer {
|
|
|
32
38
|
tempCanvas = null;
|
|
33
39
|
tempCtx = null;
|
|
34
40
|
lastRenderedData = null;
|
|
41
|
+
lastCueIndex = null;
|
|
42
|
+
currentCueMetadata = null;
|
|
43
|
+
parserMetadata = null;
|
|
35
44
|
/** Display settings for subtitle rendering */
|
|
36
45
|
displaySettings = { ...DEFAULT_DISPLAY_SETTINGS };
|
|
46
|
+
cacheLimit = 24;
|
|
47
|
+
prefetchBefore = 0;
|
|
48
|
+
prefetchAfter = 0;
|
|
49
|
+
onEvent;
|
|
50
|
+
currentRendererBackend = null;
|
|
51
|
+
loadedMetadataHandler = null;
|
|
52
|
+
seekedHandler = null;
|
|
37
53
|
// WebGPU renderer (optional, falls back to WebGL2 then Canvas2D)
|
|
38
54
|
webgpuRenderer = null;
|
|
39
55
|
useWebGPU = false;
|
|
@@ -51,17 +67,39 @@ class BaseVideoSubtitleRenderer {
|
|
|
51
67
|
fpsTimestamps: [],
|
|
52
68
|
lastFrameTime: 0
|
|
53
69
|
};
|
|
54
|
-
constructor(options) {
|
|
70
|
+
constructor(options, format) {
|
|
55
71
|
this.video = options.video;
|
|
72
|
+
this.format = format;
|
|
56
73
|
this.subUrl = options.subUrl;
|
|
57
74
|
this.subContent = options.subContent;
|
|
58
75
|
this.onWebGPUFallback = options.onWebGPUFallback;
|
|
59
76
|
this.onWebGL2Fallback = options.onWebGL2Fallback;
|
|
77
|
+
this.onEvent = options.onEvent;
|
|
78
|
+
this.displaySettings = { ...DEFAULT_DISPLAY_SETTINGS, ...options.displaySettings };
|
|
79
|
+
this.cacheLimit = Math.max(0, Math.floor(options.cacheLimit ?? 24));
|
|
80
|
+
this.prefetchBefore = Math.max(0, Math.floor(options.prefetchWindow?.before ?? 0));
|
|
81
|
+
this.prefetchAfter = Math.max(0, Math.floor(options.prefetchWindow?.after ?? 0));
|
|
60
82
|
}
|
|
61
83
|
/** Get current display settings */
|
|
62
84
|
getDisplaySettings() {
|
|
63
85
|
return { ...this.displaySettings };
|
|
64
86
|
}
|
|
87
|
+
/** Get parser metadata for the active subtitle track. */
|
|
88
|
+
getMetadata() {
|
|
89
|
+
return this.parserMetadata;
|
|
90
|
+
}
|
|
91
|
+
/** Get the most recently displayed cue metadata. */
|
|
92
|
+
getCurrentCueMetadata() {
|
|
93
|
+
return this.currentCueMetadata;
|
|
94
|
+
}
|
|
95
|
+
/** Get cue metadata for the specified index. */
|
|
96
|
+
getCueMetadata(index) {
|
|
97
|
+
return this.buildCueMetadata(index);
|
|
98
|
+
}
|
|
99
|
+
/** Get the configured frame-cache limit. */
|
|
100
|
+
getCacheLimit() {
|
|
101
|
+
return this.cacheLimit;
|
|
102
|
+
}
|
|
65
103
|
/** Get base stats common to all renderers */
|
|
66
104
|
getBaseStats() {
|
|
67
105
|
const now = performance.now();
|
|
@@ -84,13 +122,18 @@ class BaseVideoSubtitleRenderer {
|
|
|
84
122
|
}
|
|
85
123
|
/** Set display settings and force re-render */
|
|
86
124
|
setDisplaySettings(settings) {
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
125
|
+
const nextSettings = {
|
|
126
|
+
...this.displaySettings,
|
|
127
|
+
...settings
|
|
128
|
+
};
|
|
129
|
+
nextSettings.scale = Math.max(0.1, Math.min(3.0, nextSettings.scale));
|
|
130
|
+
nextSettings.verticalOffset = Math.max(-50, Math.min(50, nextSettings.verticalOffset));
|
|
131
|
+
nextSettings.horizontalOffset = Math.max(-50, Math.min(50, nextSettings.horizontalOffset));
|
|
132
|
+
nextSettings.bottomPadding = Math.max(0, Math.min(50, nextSettings.bottomPadding));
|
|
133
|
+
nextSettings.safeArea = Math.max(0, Math.min(25, nextSettings.safeArea));
|
|
134
|
+
nextSettings.opacity = Math.max(0, Math.min(1, nextSettings.opacity));
|
|
135
|
+
const changed = JSON.stringify(nextSettings) !== JSON.stringify(this.displaySettings);
|
|
136
|
+
this.displaySettings = nextSettings;
|
|
94
137
|
// Force re-render if settings changed
|
|
95
138
|
if (changed) {
|
|
96
139
|
this.lastRenderedIndex = -1;
|
|
@@ -105,7 +148,9 @@ class BaseVideoSubtitleRenderer {
|
|
|
105
148
|
}
|
|
106
149
|
/** Start initialization. */
|
|
107
150
|
startInit() {
|
|
108
|
-
this.init()
|
|
151
|
+
this.init().catch((error) => {
|
|
152
|
+
this.emitEvent({ type: 'error', format: this.format, error: error instanceof Error ? error : new Error(String(error)) });
|
|
153
|
+
});
|
|
109
154
|
}
|
|
110
155
|
/** Initialize the renderer. */
|
|
111
156
|
async init() {
|
|
@@ -143,12 +188,44 @@ class BaseVideoSubtitleRenderer {
|
|
|
143
188
|
this.updateCanvasSize();
|
|
144
189
|
this.resizeObserver = new ResizeObserver(() => this.updateCanvasSize());
|
|
145
190
|
this.resizeObserver.observe(this.video);
|
|
146
|
-
this.
|
|
147
|
-
this.
|
|
191
|
+
this.loadedMetadataHandler = () => this.updateCanvasSize();
|
|
192
|
+
this.seekedHandler = () => {
|
|
148
193
|
this.lastRenderedIndex = -1;
|
|
149
194
|
this.lastRenderedTime = -1;
|
|
150
195
|
this.onSeek();
|
|
151
|
-
}
|
|
196
|
+
};
|
|
197
|
+
this.video.addEventListener('loadedmetadata', this.loadedMetadataHandler);
|
|
198
|
+
this.video.addEventListener('seeked', this.seekedHandler);
|
|
199
|
+
}
|
|
200
|
+
emitEvent(event) {
|
|
201
|
+
this.onEvent?.(event);
|
|
202
|
+
}
|
|
203
|
+
setParserMetadata(metadata) {
|
|
204
|
+
this.parserMetadata = metadata;
|
|
205
|
+
if (metadata) {
|
|
206
|
+
this.emitEvent({ type: 'loaded', format: this.format, metadata });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
emitWorkerState(enabled, ready, sessionId, fallback = false) {
|
|
210
|
+
this.emitEvent({ type: 'worker-state', enabled, ready, sessionId, fallback });
|
|
211
|
+
}
|
|
212
|
+
emitCacheChange(cachedFrames, pendingRenders) {
|
|
213
|
+
this.emitEvent({ type: 'cache-change', cachedFrames, pendingRenders, cacheLimit: this.cacheLimit });
|
|
214
|
+
}
|
|
215
|
+
emitCueChange(cue) {
|
|
216
|
+
if (this.lastCueIndex === cue?.index && cue?.index !== undefined) {
|
|
217
|
+
this.currentCueMetadata = cue;
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
this.lastCueIndex = cue?.index ?? null;
|
|
221
|
+
this.currentCueMetadata = cue;
|
|
222
|
+
this.emitEvent({ type: 'cue-change', cue });
|
|
223
|
+
}
|
|
224
|
+
emitRendererBackend(renderer) {
|
|
225
|
+
if (this.currentRendererBackend === renderer)
|
|
226
|
+
return;
|
|
227
|
+
this.currentRendererBackend = renderer;
|
|
228
|
+
this.emitEvent({ type: 'renderer-change', renderer });
|
|
152
229
|
}
|
|
153
230
|
/** Initialize WebGPU renderer. */
|
|
154
231
|
async initWebGPU() {
|
|
@@ -162,10 +239,9 @@ class BaseVideoSubtitleRenderer {
|
|
|
162
239
|
const height = Math.max(1, bounds.height * window.devicePixelRatio);
|
|
163
240
|
await this.webgpuRenderer.setCanvas(this.canvas, width, height);
|
|
164
241
|
this.useWebGPU = true;
|
|
165
|
-
|
|
242
|
+
this.emitRendererBackend('webgpu');
|
|
166
243
|
}
|
|
167
244
|
catch (error) {
|
|
168
|
-
console.warn('[libbitsub] WebGPU init failed, falling back to WebGL2:', error);
|
|
169
245
|
this.webgpuRenderer?.destroy();
|
|
170
246
|
this.webgpuRenderer = null;
|
|
171
247
|
this.useWebGPU = false;
|
|
@@ -191,10 +267,9 @@ class BaseVideoSubtitleRenderer {
|
|
|
191
267
|
const height = Math.max(1, bounds.height * window.devicePixelRatio);
|
|
192
268
|
await this.webgl2Renderer.setCanvas(this.canvas, width, height);
|
|
193
269
|
this.useWebGL2 = true;
|
|
194
|
-
|
|
270
|
+
this.emitRendererBackend('webgl2');
|
|
195
271
|
}
|
|
196
272
|
catch (error) {
|
|
197
|
-
console.warn('[libbitsub] WebGL2 init failed, falling back to Canvas2D:', error);
|
|
198
273
|
this.webgl2Renderer?.destroy();
|
|
199
274
|
this.webgl2Renderer = null;
|
|
200
275
|
this.useWebGL2 = false;
|
|
@@ -209,7 +284,7 @@ class BaseVideoSubtitleRenderer {
|
|
|
209
284
|
this.ctx = this.canvas.getContext('2d');
|
|
210
285
|
this.useWebGPU = false;
|
|
211
286
|
this.useWebGL2 = false;
|
|
212
|
-
|
|
287
|
+
this.emitRendererBackend('canvas2d');
|
|
213
288
|
}
|
|
214
289
|
/** Called when video seeks. */
|
|
215
290
|
onSeek() { }
|
|
@@ -307,6 +382,12 @@ class BaseVideoSubtitleRenderer {
|
|
|
307
382
|
}
|
|
308
383
|
this.lastRenderedIndex = currentIndex;
|
|
309
384
|
this.lastRenderedTime = currentTime;
|
|
385
|
+
this.emitCueChange(currentIndex >= 0 ? this.buildCueMetadata(currentIndex) : null);
|
|
386
|
+
this.emitEvent({ type: 'stats', stats: this.getStats() });
|
|
387
|
+
if (currentIndex >= 0 && (this.prefetchBefore > 0 || this.prefetchAfter > 0)) {
|
|
388
|
+
const prefetch = this.prefetchAroundTime;
|
|
389
|
+
prefetch?.call(this, currentTime).catch(() => { });
|
|
390
|
+
}
|
|
310
391
|
}
|
|
311
392
|
}
|
|
312
393
|
this.animationFrameId = requestAnimationFrame(render);
|
|
@@ -341,6 +422,59 @@ class BaseVideoSubtitleRenderer {
|
|
|
341
422
|
this.renderFrameCanvas2D(data, index);
|
|
342
423
|
}
|
|
343
424
|
}
|
|
425
|
+
computeLayout(data) {
|
|
426
|
+
if (!this.canvas) {
|
|
427
|
+
return { scaleX: 1, scaleY: 1, shiftX: 0, shiftY: 0, opacity: this.displaySettings.opacity };
|
|
428
|
+
}
|
|
429
|
+
const baseScaleX = this.canvas.width / data.width;
|
|
430
|
+
const baseScaleY = this.canvas.height / data.height;
|
|
431
|
+
const bounds = getSubtitleBounds(data);
|
|
432
|
+
const { scale, verticalOffset, horizontalOffset, horizontalAlign, bottomPadding, safeArea, opacity } = this.displaySettings;
|
|
433
|
+
if (!bounds) {
|
|
434
|
+
return {
|
|
435
|
+
scaleX: baseScaleX * scale,
|
|
436
|
+
scaleY: baseScaleY * scale,
|
|
437
|
+
shiftX: (horizontalOffset / 100) * this.canvas.width,
|
|
438
|
+
shiftY: (verticalOffset / 100) * this.canvas.height - (bottomPadding / 100) * this.canvas.height,
|
|
439
|
+
opacity
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
const groupWidth = bounds.width * baseScaleX;
|
|
443
|
+
const groupHeight = bounds.height * baseScaleY;
|
|
444
|
+
const scaledGroupWidth = groupWidth * scale;
|
|
445
|
+
const scaledGroupHeight = groupHeight * scale;
|
|
446
|
+
let anchorShiftX = 0;
|
|
447
|
+
if (horizontalAlign === 'center') {
|
|
448
|
+
anchorShiftX = (groupWidth - scaledGroupWidth) / 2;
|
|
449
|
+
}
|
|
450
|
+
else if (horizontalAlign === 'right') {
|
|
451
|
+
anchorShiftX = groupWidth - scaledGroupWidth;
|
|
452
|
+
}
|
|
453
|
+
let shiftX = anchorShiftX + (horizontalOffset / 100) * this.canvas.width;
|
|
454
|
+
let shiftY = groupHeight - scaledGroupHeight + (verticalOffset / 100) * this.canvas.height;
|
|
455
|
+
shiftY -= (bottomPadding / 100) * this.canvas.height;
|
|
456
|
+
const safeX = (safeArea / 100) * this.canvas.width;
|
|
457
|
+
const safeY = (safeArea / 100) * this.canvas.height;
|
|
458
|
+
const finalMinX = bounds.x * baseScaleX + shiftX;
|
|
459
|
+
const finalMinY = bounds.y * baseScaleY + shiftY;
|
|
460
|
+
const finalMaxX = finalMinX + scaledGroupWidth;
|
|
461
|
+
const finalMaxY = finalMinY + scaledGroupHeight;
|
|
462
|
+
if (finalMinX < safeX)
|
|
463
|
+
shiftX += safeX - finalMinX;
|
|
464
|
+
if (finalMaxX > this.canvas.width - safeX)
|
|
465
|
+
shiftX -= finalMaxX - (this.canvas.width - safeX);
|
|
466
|
+
if (finalMinY < safeY)
|
|
467
|
+
shiftY += safeY - finalMinY;
|
|
468
|
+
if (finalMaxY > this.canvas.height - safeY)
|
|
469
|
+
shiftY -= finalMaxY - (this.canvas.height - safeY);
|
|
470
|
+
return {
|
|
471
|
+
scaleX: baseScaleX * scale,
|
|
472
|
+
scaleY: baseScaleY * scale,
|
|
473
|
+
shiftX,
|
|
474
|
+
shiftY,
|
|
475
|
+
opacity
|
|
476
|
+
};
|
|
477
|
+
}
|
|
344
478
|
/** Render using WebGPU. */
|
|
345
479
|
renderFrameWebGPU(data, index) {
|
|
346
480
|
if (!this.webgpuRenderer || !this.canvas)
|
|
@@ -354,14 +488,8 @@ class BaseVideoSubtitleRenderer {
|
|
|
354
488
|
// Store for potential reuse
|
|
355
489
|
this.lastRenderedData = data;
|
|
356
490
|
// Calculate base scale factors
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
// Apply display settings
|
|
360
|
-
const { scale, verticalOffset } = this.displaySettings;
|
|
361
|
-
const scaleX = baseScaleX * scale;
|
|
362
|
-
const scaleY = baseScaleY * scale;
|
|
363
|
-
const offsetY = (verticalOffset / 100) * this.canvas.height;
|
|
364
|
-
this.webgpuRenderer.render(data.compositionData, data.width, data.height, scaleX, scaleY, offsetY);
|
|
491
|
+
const layout = this.computeLayout(data);
|
|
492
|
+
this.webgpuRenderer.render(data.compositionData, data.width, data.height, layout.scaleX, layout.scaleY, layout.shiftX, layout.shiftY, layout.opacity);
|
|
365
493
|
}
|
|
366
494
|
/** Render using WebGL2. */
|
|
367
495
|
renderFrameWebGL2(data, index) {
|
|
@@ -373,13 +501,8 @@ class BaseVideoSubtitleRenderer {
|
|
|
373
501
|
return;
|
|
374
502
|
}
|
|
375
503
|
this.lastRenderedData = data;
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
const { scale, verticalOffset } = this.displaySettings;
|
|
379
|
-
const scaleX = baseScaleX * scale;
|
|
380
|
-
const scaleY = baseScaleY * scale;
|
|
381
|
-
const offsetY = (verticalOffset / 100) * this.canvas.height;
|
|
382
|
-
this.webgl2Renderer.render(data.compositionData, data.width, data.height, scaleX, scaleY, offsetY);
|
|
504
|
+
const layout = this.computeLayout(data);
|
|
505
|
+
this.webgl2Renderer.render(data.compositionData, data.width, data.height, layout.scaleX, layout.scaleY, layout.shiftX, layout.shiftY, layout.opacity);
|
|
383
506
|
}
|
|
384
507
|
/** Render using Canvas2D. */
|
|
385
508
|
renderFrameCanvas2D(data, index) {
|
|
@@ -394,14 +517,9 @@ class BaseVideoSubtitleRenderer {
|
|
|
394
517
|
}
|
|
395
518
|
// Store for potential reuse
|
|
396
519
|
this.lastRenderedData = data;
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
// Apply display settings
|
|
401
|
-
const { scale, verticalOffset } = this.displaySettings;
|
|
402
|
-
const scaleX = baseScaleX * scale;
|
|
403
|
-
const scaleY = baseScaleY * scale;
|
|
404
|
-
const offsetY = (verticalOffset / 100) * this.canvas.height;
|
|
520
|
+
const layout = this.computeLayout(data);
|
|
521
|
+
this.ctx.save();
|
|
522
|
+
this.ctx.globalAlpha = layout.opacity;
|
|
405
523
|
for (const comp of data.compositionData) {
|
|
406
524
|
if (!this.tempCanvas || !this.tempCtx)
|
|
407
525
|
continue;
|
|
@@ -413,14 +531,13 @@ class BaseVideoSubtitleRenderer {
|
|
|
413
531
|
this.tempCtx.putImageData(comp.pixelData, 0, 0);
|
|
414
532
|
// Calculate position with scale and offset applied
|
|
415
533
|
// Center the scaled content horizontally
|
|
416
|
-
const scaledWidth = comp.pixelData.width * scaleX;
|
|
417
|
-
const scaledHeight = comp.pixelData.height * scaleY;
|
|
418
|
-
const
|
|
419
|
-
const
|
|
420
|
-
|
|
421
|
-
const adjustedY = baseY + offsetY + (comp.pixelData.height * baseScaleY - scaledHeight);
|
|
422
|
-
this.ctx.drawImage(this.tempCanvas, centeredX, adjustedY, scaledWidth, scaledHeight);
|
|
534
|
+
const scaledWidth = comp.pixelData.width * layout.scaleX;
|
|
535
|
+
const scaledHeight = comp.pixelData.height * layout.scaleY;
|
|
536
|
+
const adjustedX = comp.x * (this.canvas.width / data.width) + layout.shiftX;
|
|
537
|
+
const adjustedY = comp.y * (this.canvas.height / data.height) + layout.shiftY;
|
|
538
|
+
this.ctx.drawImage(this.tempCanvas, adjustedX, adjustedY, scaledWidth, scaledHeight);
|
|
423
539
|
}
|
|
540
|
+
this.ctx.restore();
|
|
424
541
|
}
|
|
425
542
|
/** Dispose of all resources. */
|
|
426
543
|
dispose() {
|
|
@@ -431,6 +548,14 @@ class BaseVideoSubtitleRenderer {
|
|
|
431
548
|
}
|
|
432
549
|
this.resizeObserver?.disconnect();
|
|
433
550
|
this.resizeObserver = null;
|
|
551
|
+
if (this.loadedMetadataHandler) {
|
|
552
|
+
this.video.removeEventListener('loadedmetadata', this.loadedMetadataHandler);
|
|
553
|
+
this.loadedMetadataHandler = null;
|
|
554
|
+
}
|
|
555
|
+
if (this.seekedHandler) {
|
|
556
|
+
this.video.removeEventListener('seeked', this.seekedHandler);
|
|
557
|
+
this.seekedHandler = null;
|
|
558
|
+
}
|
|
434
559
|
// Clean up GPU renderers
|
|
435
560
|
if (this.webgpuRenderer) {
|
|
436
561
|
this.webgpuRenderer.destroy();
|
|
@@ -446,6 +571,8 @@ class BaseVideoSubtitleRenderer {
|
|
|
446
571
|
this.tempCanvas = null;
|
|
447
572
|
this.tempCtx = null;
|
|
448
573
|
this.lastRenderedData = null;
|
|
574
|
+
this.currentCueMetadata = null;
|
|
575
|
+
this.parserMetadata = null;
|
|
449
576
|
this.useWebGPU = false;
|
|
450
577
|
this.useWebGL2 = false;
|
|
451
578
|
}
|
|
@@ -461,14 +588,16 @@ export class PgsRenderer extends BaseVideoSubtitleRenderer {
|
|
|
461
588
|
onLoaded;
|
|
462
589
|
onError;
|
|
463
590
|
constructor(options) {
|
|
464
|
-
super(options);
|
|
591
|
+
super(options, 'pgs');
|
|
465
592
|
this.onLoading = options.onLoading;
|
|
466
593
|
this.onLoaded = options.onLoaded;
|
|
467
594
|
this.onError = options.onError;
|
|
595
|
+
applyCacheLimit(this.state, this.cacheLimit);
|
|
468
596
|
this.startInit();
|
|
469
597
|
}
|
|
470
598
|
async loadSubtitles() {
|
|
471
599
|
try {
|
|
600
|
+
this.emitEvent({ type: 'loading', format: 'pgs' });
|
|
472
601
|
this.onLoading?.();
|
|
473
602
|
let arrayBuffer;
|
|
474
603
|
if (this.subContent) {
|
|
@@ -486,16 +615,24 @@ export class PgsRenderer extends BaseVideoSubtitleRenderer {
|
|
|
486
615
|
const data = new Uint8Array(arrayBuffer);
|
|
487
616
|
if (this.state.useWorker) {
|
|
488
617
|
try {
|
|
618
|
+
this.state.sessionId = createWorkerSessionId();
|
|
489
619
|
await getOrCreateWorker();
|
|
490
|
-
|
|
620
|
+
this.emitWorkerState(true, false, this.state.sessionId);
|
|
621
|
+
const loadResponse = await sendToWorker({
|
|
622
|
+
type: 'loadPgs',
|
|
623
|
+
sessionId: this.state.sessionId,
|
|
624
|
+
data: data.buffer.slice(0)
|
|
625
|
+
});
|
|
491
626
|
if (loadResponse.type === 'pgsLoaded') {
|
|
492
627
|
this.state.workerReady = true;
|
|
493
|
-
|
|
628
|
+
this.state.metadata = loadResponse.metadata;
|
|
629
|
+
const tsResponse = await sendToWorker({ type: 'getPgsTimestamps', sessionId: this.state.sessionId });
|
|
494
630
|
if (tsResponse.type === 'pgsTimestamps') {
|
|
495
631
|
this.state.timestamps = tsResponse.timestamps;
|
|
496
632
|
}
|
|
497
633
|
this.isLoaded = true;
|
|
498
|
-
|
|
634
|
+
this.setParserMetadata(loadResponse.metadata);
|
|
635
|
+
this.emitWorkerState(true, true, this.state.sessionId);
|
|
499
636
|
this.onLoaded?.();
|
|
500
637
|
return; // Success, don't fall through to main thread
|
|
501
638
|
}
|
|
@@ -504,8 +641,8 @@ export class PgsRenderer extends BaseVideoSubtitleRenderer {
|
|
|
504
641
|
}
|
|
505
642
|
}
|
|
506
643
|
catch (workerError) {
|
|
507
|
-
console.warn('[libbitsub] Worker failed, falling back to main thread:', workerError);
|
|
508
644
|
this.state.useWorker = false;
|
|
645
|
+
this.emitWorkerState(false, false, this.state.sessionId, true);
|
|
509
646
|
}
|
|
510
647
|
}
|
|
511
648
|
// Main thread fallback - use idle callback to avoid blocking UI
|
|
@@ -513,8 +650,9 @@ export class PgsRenderer extends BaseVideoSubtitleRenderer {
|
|
|
513
650
|
this.onLoaded?.();
|
|
514
651
|
}
|
|
515
652
|
catch (error) {
|
|
516
|
-
|
|
517
|
-
this.
|
|
653
|
+
const resolvedError = error instanceof Error ? error : new Error(String(error));
|
|
654
|
+
this.emitEvent({ type: 'error', format: 'pgs', error: resolvedError });
|
|
655
|
+
this.onError?.(resolvedError);
|
|
518
656
|
}
|
|
519
657
|
}
|
|
520
658
|
async loadOnMainThread(data) {
|
|
@@ -530,8 +668,9 @@ export class PgsRenderer extends BaseVideoSubtitleRenderer {
|
|
|
530
668
|
scheduleTask(() => {
|
|
531
669
|
const count = this.pgsParser.load(data);
|
|
532
670
|
this.state.timestamps = this.pgsParser.getTimestamps();
|
|
671
|
+
this.state.metadata = this.pgsParser.getMetadata();
|
|
533
672
|
this.isLoaded = true;
|
|
534
|
-
|
|
673
|
+
this.setParserMetadata(this.state.metadata);
|
|
535
674
|
resolve();
|
|
536
675
|
});
|
|
537
676
|
});
|
|
@@ -557,16 +696,22 @@ export class PgsRenderer extends BaseVideoSubtitleRenderer {
|
|
|
557
696
|
return this.pgsParser?.findIndexAtTimestamp(time) ?? -1;
|
|
558
697
|
}
|
|
559
698
|
renderAtIndex(index) {
|
|
699
|
+
if (this.state.frameCache.has(index)) {
|
|
700
|
+
return this.state.frameCache.get(index) ?? undefined;
|
|
701
|
+
}
|
|
560
702
|
if (this.state.useWorker && this.state.workerReady) {
|
|
561
|
-
if (this.state.frameCache.has(index)) {
|
|
562
|
-
return this.state.frameCache.get(index) ?? undefined;
|
|
563
|
-
}
|
|
564
703
|
if (!this.state.pendingRenders.has(index)) {
|
|
565
|
-
const renderPromise = sendToWorker({
|
|
704
|
+
const renderPromise = sendToWorker({
|
|
705
|
+
type: 'renderPgsAtIndex',
|
|
706
|
+
sessionId: this.state.sessionId,
|
|
707
|
+
index
|
|
708
|
+
}).then((response) => (response.type === 'pgsFrame' && response.frame ? convertFrameData(response.frame) : null));
|
|
566
709
|
this.state.pendingRenders.set(index, renderPromise);
|
|
710
|
+
this.emitCacheChange(this.state.frameCache.size, this.state.pendingRenders.size);
|
|
567
711
|
renderPromise.then((result) => {
|
|
568
|
-
this.state
|
|
712
|
+
setCachedFrame(this.state, index, result);
|
|
569
713
|
this.state.pendingRenders.delete(index);
|
|
714
|
+
this.emitCacheChange(this.state.frameCache.size, this.state.pendingRenders.size);
|
|
570
715
|
// Force re-render on next frame by resetting lastRenderedIndex
|
|
571
716
|
if (this.findCurrentIndex(this.video.currentTime) === index) {
|
|
572
717
|
this.lastRenderedIndex = -1;
|
|
@@ -576,7 +721,32 @@ export class PgsRenderer extends BaseVideoSubtitleRenderer {
|
|
|
576
721
|
// Return undefined to indicate async loading in progress
|
|
577
722
|
return undefined;
|
|
578
723
|
}
|
|
579
|
-
|
|
724
|
+
const rendered = this.pgsParser?.renderAtIndex(index) ?? null;
|
|
725
|
+
setCachedFrame(this.state, index, rendered);
|
|
726
|
+
this.emitCacheChange(this.state.frameCache.size, this.state.pendingRenders.size);
|
|
727
|
+
return rendered ?? undefined;
|
|
728
|
+
}
|
|
729
|
+
buildCueMetadata(index) {
|
|
730
|
+
if (this.pgsParser) {
|
|
731
|
+
return this.pgsParser.getCueMetadata(index);
|
|
732
|
+
}
|
|
733
|
+
const metadata = this.state.metadata;
|
|
734
|
+
if (!metadata || index < 0 || index >= this.state.timestamps.length)
|
|
735
|
+
return null;
|
|
736
|
+
const startTime = this.state.timestamps[index];
|
|
737
|
+
const endTime = this.state.timestamps[index + 1] ?? startTime + 5000;
|
|
738
|
+
const frame = this.state.frameCache.get(index) ?? null;
|
|
739
|
+
return {
|
|
740
|
+
index,
|
|
741
|
+
format: 'pgs',
|
|
742
|
+
startTime,
|
|
743
|
+
endTime,
|
|
744
|
+
duration: Math.max(0, endTime - startTime),
|
|
745
|
+
screenWidth: metadata.screenWidth,
|
|
746
|
+
screenHeight: metadata.screenHeight,
|
|
747
|
+
bounds: frame ? getSubtitleBounds(frame) : null,
|
|
748
|
+
compositionCount: frame?.compositionData.length ?? 0
|
|
749
|
+
};
|
|
580
750
|
}
|
|
581
751
|
isPendingRender(index) {
|
|
582
752
|
return this.state.pendingRenders.has(index);
|
|
@@ -584,10 +754,43 @@ export class PgsRenderer extends BaseVideoSubtitleRenderer {
|
|
|
584
754
|
onSeek() {
|
|
585
755
|
this.state.frameCache.clear();
|
|
586
756
|
this.state.pendingRenders.clear();
|
|
757
|
+
this.emitCacheChange(this.state.frameCache.size, this.state.pendingRenders.size);
|
|
758
|
+
if (this.state.useWorker && this.state.workerReady) {
|
|
759
|
+
sendToWorker({ type: 'clearPgsCache', sessionId: this.state.sessionId }).catch(() => { });
|
|
760
|
+
}
|
|
761
|
+
this.pgsParser?.clearCache();
|
|
762
|
+
}
|
|
763
|
+
setCacheLimit(limit) {
|
|
764
|
+
this.cacheLimit = applyCacheLimit(this.state, limit);
|
|
765
|
+
this.emitCacheChange(this.state.frameCache.size, this.state.pendingRenders.size);
|
|
766
|
+
}
|
|
767
|
+
clearFrameCache() {
|
|
768
|
+
this.state.frameCache.clear();
|
|
769
|
+
this.state.pendingRenders.clear();
|
|
770
|
+
this.lastRenderedIndex = -1;
|
|
587
771
|
if (this.state.useWorker && this.state.workerReady) {
|
|
588
|
-
sendToWorker({ type: 'clearPgsCache' }).catch(() => { });
|
|
772
|
+
sendToWorker({ type: 'clearPgsCache', sessionId: this.state.sessionId }).catch(() => { });
|
|
589
773
|
}
|
|
590
774
|
this.pgsParser?.clearCache();
|
|
775
|
+
this.emitCacheChange(this.state.frameCache.size, this.state.pendingRenders.size);
|
|
776
|
+
}
|
|
777
|
+
async prefetchRange(startIndex, endIndex) {
|
|
778
|
+
const safeStart = Math.max(0, Math.min(startIndex, endIndex));
|
|
779
|
+
const safeEnd = Math.min(Math.max(startIndex, endIndex), this.state.timestamps.length - 1);
|
|
780
|
+
for (let index = safeStart; index <= safeEnd; index++) {
|
|
781
|
+
if (this.state.frameCache.has(index))
|
|
782
|
+
continue;
|
|
783
|
+
const result = this.renderAtIndex(index);
|
|
784
|
+
if (result === undefined && this.state.pendingRenders.has(index)) {
|
|
785
|
+
await this.state.pendingRenders.get(index);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
async prefetchAroundTime(time, before = this.prefetchBefore, after = this.prefetchAfter) {
|
|
790
|
+
const currentIndex = this.findCurrentIndex(time);
|
|
791
|
+
if (currentIndex < 0)
|
|
792
|
+
return;
|
|
793
|
+
await this.prefetchRange(currentIndex - before, currentIndex + after);
|
|
591
794
|
}
|
|
592
795
|
/** Get performance statistics for PGS renderer */
|
|
593
796
|
getStats() {
|
|
@@ -605,10 +808,11 @@ export class PgsRenderer extends BaseVideoSubtitleRenderer {
|
|
|
605
808
|
this.state.frameCache.clear();
|
|
606
809
|
this.state.pendingRenders.clear();
|
|
607
810
|
if (this.state.useWorker && this.state.workerReady) {
|
|
608
|
-
sendToWorker({ type: 'disposePgs' }).catch(() => { });
|
|
811
|
+
sendToWorker({ type: 'disposePgs', sessionId: this.state.sessionId }).catch(() => { });
|
|
609
812
|
}
|
|
610
813
|
this.pgsParser?.dispose();
|
|
611
814
|
this.pgsParser = null;
|
|
815
|
+
this.state.sessionId = null;
|
|
612
816
|
}
|
|
613
817
|
}
|
|
614
818
|
/**
|
|
@@ -628,18 +832,19 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
628
832
|
cachedIndexTime = -1;
|
|
629
833
|
pendingIndexLookup = null;
|
|
630
834
|
constructor(options) {
|
|
631
|
-
super(options);
|
|
835
|
+
super(options, 'vobsub');
|
|
632
836
|
this.idxUrl = options.idxUrl || (options.subUrl ? options.subUrl.replace(/\\.sub$/i, '.idx') : undefined);
|
|
633
837
|
this.idxContent = options.idxContent;
|
|
634
838
|
this.onLoading = options.onLoading;
|
|
635
839
|
this.onLoaded = options.onLoaded;
|
|
636
840
|
this.onError = options.onError;
|
|
841
|
+
applyCacheLimit(this.state, this.cacheLimit);
|
|
637
842
|
this.startInit();
|
|
638
843
|
}
|
|
639
844
|
async loadSubtitles() {
|
|
640
845
|
try {
|
|
846
|
+
this.emitEvent({ type: 'loading', format: 'vobsub' });
|
|
641
847
|
this.onLoading?.();
|
|
642
|
-
console.log(`[libbitsub] Loading VobSub`);
|
|
643
848
|
let subArrayBuffer;
|
|
644
849
|
let idxData;
|
|
645
850
|
// Resolve SUB content
|
|
@@ -685,23 +890,27 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
685
890
|
throw new Error('Failed to load VobSub data');
|
|
686
891
|
}
|
|
687
892
|
const subData = new Uint8Array(subArrayBuffer);
|
|
688
|
-
console.log(`[libbitsub] VobSub files loaded: .sub=${subArrayBuffer.byteLength} bytes, .idx=${idxData.length} chars`);
|
|
689
893
|
if (this.state.useWorker) {
|
|
690
894
|
try {
|
|
895
|
+
this.state.sessionId = createWorkerSessionId();
|
|
691
896
|
await getOrCreateWorker();
|
|
897
|
+
this.emitWorkerState(true, false, this.state.sessionId);
|
|
692
898
|
const loadResponse = await sendToWorker({
|
|
693
899
|
type: 'loadVobSub',
|
|
900
|
+
sessionId: this.state.sessionId,
|
|
694
901
|
idxContent: idxData,
|
|
695
902
|
subData: subData.buffer.slice(0)
|
|
696
903
|
});
|
|
697
904
|
if (loadResponse.type === 'vobSubLoaded') {
|
|
698
905
|
this.state.workerReady = true;
|
|
699
|
-
|
|
906
|
+
this.state.metadata = loadResponse.metadata;
|
|
907
|
+
const tsResponse = await sendToWorker({ type: 'getVobSubTimestamps', sessionId: this.state.sessionId });
|
|
700
908
|
if (tsResponse.type === 'vobSubTimestamps') {
|
|
701
909
|
this.state.timestamps = tsResponse.timestamps;
|
|
702
910
|
}
|
|
703
911
|
this.isLoaded = true;
|
|
704
|
-
|
|
912
|
+
this.setParserMetadata(loadResponse.metadata);
|
|
913
|
+
this.emitWorkerState(true, true, this.state.sessionId);
|
|
705
914
|
this.onLoaded?.();
|
|
706
915
|
return; // Success, don't fall through to main thread
|
|
707
916
|
}
|
|
@@ -710,8 +919,8 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
710
919
|
}
|
|
711
920
|
}
|
|
712
921
|
catch (workerError) {
|
|
713
|
-
console.warn('[libbitsub] Worker failed, falling back to main thread:', workerError);
|
|
714
922
|
this.state.useWorker = false;
|
|
923
|
+
this.emitWorkerState(false, false, this.state.sessionId, true);
|
|
715
924
|
}
|
|
716
925
|
}
|
|
717
926
|
// Main thread fallback
|
|
@@ -719,8 +928,9 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
719
928
|
this.onLoaded?.();
|
|
720
929
|
}
|
|
721
930
|
catch (error) {
|
|
722
|
-
|
|
723
|
-
this.
|
|
931
|
+
const resolvedError = error instanceof Error ? error : new Error(String(error));
|
|
932
|
+
this.emitEvent({ type: 'error', format: 'vobsub', error: resolvedError });
|
|
933
|
+
this.onError?.(resolvedError);
|
|
724
934
|
}
|
|
725
935
|
}
|
|
726
936
|
async loadOnMainThread(idxData, subData) {
|
|
@@ -735,8 +945,9 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
735
945
|
scheduleTask(() => {
|
|
736
946
|
this.vobsubParser.loadFromData(idxData, subData);
|
|
737
947
|
this.state.timestamps = this.vobsubParser.getTimestamps();
|
|
738
|
-
|
|
948
|
+
this.state.metadata = this.vobsubParser.getMetadata();
|
|
739
949
|
this.isLoaded = true;
|
|
950
|
+
this.setParserMetadata(this.state.metadata);
|
|
740
951
|
resolve();
|
|
741
952
|
});
|
|
742
953
|
});
|
|
@@ -764,7 +975,11 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
764
975
|
}
|
|
765
976
|
// Start async lookup if not already pending
|
|
766
977
|
if (!this.pendingIndexLookup) {
|
|
767
|
-
this.pendingIndexLookup = sendToWorker({
|
|
978
|
+
this.pendingIndexLookup = sendToWorker({
|
|
979
|
+
type: 'findVobSubIndex',
|
|
980
|
+
sessionId: this.state.sessionId,
|
|
981
|
+
timeMs
|
|
982
|
+
}).then((response) => {
|
|
768
983
|
if (response.type === 'vobSubIndex') {
|
|
769
984
|
const newIndex = response.index;
|
|
770
985
|
const oldIndex = this.cachedIndex;
|
|
@@ -784,18 +999,23 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
784
999
|
return this.vobsubParser?.findIndexAtTimestamp(time) ?? -1;
|
|
785
1000
|
}
|
|
786
1001
|
renderAtIndex(index) {
|
|
1002
|
+
if (this.state.frameCache.has(index)) {
|
|
1003
|
+
return this.state.frameCache.get(index) ?? undefined;
|
|
1004
|
+
}
|
|
787
1005
|
if (this.state.useWorker && this.state.workerReady) {
|
|
788
|
-
// Return cached frame immediately if available
|
|
789
|
-
if (this.state.frameCache.has(index)) {
|
|
790
|
-
return this.state.frameCache.get(index) ?? undefined;
|
|
791
|
-
}
|
|
792
1006
|
// Start async render if not already pending
|
|
793
1007
|
if (!this.state.pendingRenders.has(index)) {
|
|
794
|
-
const renderPromise = sendToWorker({
|
|
1008
|
+
const renderPromise = sendToWorker({
|
|
1009
|
+
type: 'renderVobSubAtIndex',
|
|
1010
|
+
sessionId: this.state.sessionId,
|
|
1011
|
+
index
|
|
1012
|
+
}).then((response) => (response.type === 'vobSubFrame' && response.frame ? convertFrameData(response.frame) : null));
|
|
795
1013
|
this.state.pendingRenders.set(index, renderPromise);
|
|
1014
|
+
this.emitCacheChange(this.state.frameCache.size, this.state.pendingRenders.size);
|
|
796
1015
|
renderPromise.then((result) => {
|
|
797
|
-
this.state
|
|
1016
|
+
setCachedFrame(this.state, index, result);
|
|
798
1017
|
this.state.pendingRenders.delete(index);
|
|
1018
|
+
this.emitCacheChange(this.state.frameCache.size, this.state.pendingRenders.size);
|
|
799
1019
|
// Force re-render on next frame by resetting lastRenderedIndex
|
|
800
1020
|
if (this.findCurrentIndex(this.video.currentTime) === index) {
|
|
801
1021
|
this.lastRenderedIndex = -1;
|
|
@@ -805,7 +1025,34 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
805
1025
|
// Return undefined to indicate async loading in progress
|
|
806
1026
|
return undefined;
|
|
807
1027
|
}
|
|
808
|
-
|
|
1028
|
+
const rendered = this.vobsubParser?.renderAtIndex(index) ?? null;
|
|
1029
|
+
setCachedFrame(this.state, index, rendered);
|
|
1030
|
+
this.emitCacheChange(this.state.frameCache.size, this.state.pendingRenders.size);
|
|
1031
|
+
return rendered ?? undefined;
|
|
1032
|
+
}
|
|
1033
|
+
buildCueMetadata(index) {
|
|
1034
|
+
if (this.vobsubParser) {
|
|
1035
|
+
return this.vobsubParser.getCueMetadata(index);
|
|
1036
|
+
}
|
|
1037
|
+
const metadata = this.state.metadata;
|
|
1038
|
+
if (!metadata || index < 0 || index >= this.state.timestamps.length)
|
|
1039
|
+
return null;
|
|
1040
|
+
const startTime = this.state.timestamps[index];
|
|
1041
|
+
const endTime = this.state.timestamps[index + 1] ?? startTime + 5000;
|
|
1042
|
+
const frame = this.state.frameCache.get(index) ?? null;
|
|
1043
|
+
return {
|
|
1044
|
+
index,
|
|
1045
|
+
format: 'vobsub',
|
|
1046
|
+
startTime,
|
|
1047
|
+
endTime,
|
|
1048
|
+
duration: Math.max(0, endTime - startTime),
|
|
1049
|
+
screenWidth: metadata.screenWidth,
|
|
1050
|
+
screenHeight: metadata.screenHeight,
|
|
1051
|
+
bounds: frame ? getSubtitleBounds(frame) : null,
|
|
1052
|
+
compositionCount: frame?.compositionData.length ?? 0,
|
|
1053
|
+
language: metadata.language ?? null,
|
|
1054
|
+
trackId: metadata.trackId ?? null
|
|
1055
|
+
};
|
|
809
1056
|
}
|
|
810
1057
|
isPendingRender(index) {
|
|
811
1058
|
return this.state.pendingRenders.has(index);
|
|
@@ -818,9 +1065,45 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
818
1065
|
this.cachedIndexTime = -1;
|
|
819
1066
|
this.pendingIndexLookup = null;
|
|
820
1067
|
if (this.state.useWorker && this.state.workerReady) {
|
|
821
|
-
sendToWorker({ type: 'clearVobSubCache' }).catch(() => { });
|
|
1068
|
+
sendToWorker({ type: 'clearVobSubCache', sessionId: this.state.sessionId }).catch(() => { });
|
|
1069
|
+
}
|
|
1070
|
+
this.vobsubParser?.clearCache();
|
|
1071
|
+
this.emitCacheChange(this.state.frameCache.size, this.state.pendingRenders.size);
|
|
1072
|
+
}
|
|
1073
|
+
setCacheLimit(limit) {
|
|
1074
|
+
this.cacheLimit = applyCacheLimit(this.state, limit);
|
|
1075
|
+
this.emitCacheChange(this.state.frameCache.size, this.state.pendingRenders.size);
|
|
1076
|
+
}
|
|
1077
|
+
clearFrameCache() {
|
|
1078
|
+
this.state.frameCache.clear();
|
|
1079
|
+
this.state.pendingRenders.clear();
|
|
1080
|
+
this.cachedIndex = -1;
|
|
1081
|
+
this.cachedIndexTime = -1;
|
|
1082
|
+
this.pendingIndexLookup = null;
|
|
1083
|
+
this.lastRenderedIndex = -1;
|
|
1084
|
+
if (this.state.useWorker && this.state.workerReady) {
|
|
1085
|
+
sendToWorker({ type: 'clearVobSubCache', sessionId: this.state.sessionId }).catch(() => { });
|
|
822
1086
|
}
|
|
823
1087
|
this.vobsubParser?.clearCache();
|
|
1088
|
+
this.emitCacheChange(this.state.frameCache.size, this.state.pendingRenders.size);
|
|
1089
|
+
}
|
|
1090
|
+
async prefetchRange(startIndex, endIndex) {
|
|
1091
|
+
const safeStart = Math.max(0, Math.min(startIndex, endIndex));
|
|
1092
|
+
const safeEnd = Math.min(Math.max(startIndex, endIndex), this.state.timestamps.length - 1);
|
|
1093
|
+
for (let index = safeStart; index <= safeEnd; index++) {
|
|
1094
|
+
if (this.state.frameCache.has(index))
|
|
1095
|
+
continue;
|
|
1096
|
+
const result = this.renderAtIndex(index);
|
|
1097
|
+
if (result === undefined && this.state.pendingRenders.has(index)) {
|
|
1098
|
+
await this.state.pendingRenders.get(index);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
async prefetchAroundTime(time, before = this.prefetchBefore, after = this.prefetchAfter) {
|
|
1103
|
+
const currentIndex = this.findCurrentIndex(time);
|
|
1104
|
+
if (currentIndex < 0)
|
|
1105
|
+
return;
|
|
1106
|
+
await this.prefetchRange(currentIndex - before, currentIndex + after);
|
|
824
1107
|
}
|
|
825
1108
|
/** Get performance statistics for VobSub renderer */
|
|
826
1109
|
getStats() {
|
|
@@ -836,32 +1119,35 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
836
1119
|
/** Enable or disable debanding filter */
|
|
837
1120
|
setDebandEnabled(enabled) {
|
|
838
1121
|
if (this.state.useWorker && this.state.workerReady) {
|
|
839
|
-
sendToWorker({ type: 'setVobSubDebandEnabled', enabled }).catch(() => { });
|
|
1122
|
+
sendToWorker({ type: 'setVobSubDebandEnabled', sessionId: this.state.sessionId, enabled }).catch(() => { });
|
|
840
1123
|
}
|
|
841
1124
|
this.vobsubParser?.setDebandEnabled(enabled);
|
|
842
1125
|
// Clear cache to force re-render with new settings
|
|
843
1126
|
this.state.frameCache.clear();
|
|
844
1127
|
this.lastRenderedIndex = -1;
|
|
1128
|
+
this.emitCacheChange(this.state.frameCache.size, this.state.pendingRenders.size);
|
|
845
1129
|
}
|
|
846
1130
|
/** Set debanding threshold (0-255, default: 64) */
|
|
847
1131
|
setDebandThreshold(threshold) {
|
|
848
1132
|
if (this.state.useWorker && this.state.workerReady) {
|
|
849
|
-
sendToWorker({ type: 'setVobSubDebandThreshold', threshold }).catch(() => { });
|
|
1133
|
+
sendToWorker({ type: 'setVobSubDebandThreshold', sessionId: this.state.sessionId, threshold }).catch(() => { });
|
|
850
1134
|
}
|
|
851
1135
|
this.vobsubParser?.setDebandThreshold(threshold);
|
|
852
1136
|
// Clear cache to force re-render with new settings
|
|
853
1137
|
this.state.frameCache.clear();
|
|
854
1138
|
this.lastRenderedIndex = -1;
|
|
1139
|
+
this.emitCacheChange(this.state.frameCache.size, this.state.pendingRenders.size);
|
|
855
1140
|
}
|
|
856
1141
|
/** Set debanding sample range in pixels (1-64, default: 15) */
|
|
857
1142
|
setDebandRange(range) {
|
|
858
1143
|
if (this.state.useWorker && this.state.workerReady) {
|
|
859
|
-
sendToWorker({ type: 'setVobSubDebandRange', range }).catch(() => { });
|
|
1144
|
+
sendToWorker({ type: 'setVobSubDebandRange', sessionId: this.state.sessionId, range }).catch(() => { });
|
|
860
1145
|
}
|
|
861
1146
|
this.vobsubParser?.setDebandRange(range);
|
|
862
1147
|
// Clear cache to force re-render with new settings
|
|
863
1148
|
this.state.frameCache.clear();
|
|
864
1149
|
this.lastRenderedIndex = -1;
|
|
1150
|
+
this.emitCacheChange(this.state.frameCache.size, this.state.pendingRenders.size);
|
|
865
1151
|
}
|
|
866
1152
|
/** Check if debanding is enabled */
|
|
867
1153
|
get debandEnabled() {
|
|
@@ -872,10 +1158,28 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
|
|
|
872
1158
|
this.state.frameCache.clear();
|
|
873
1159
|
this.state.pendingRenders.clear();
|
|
874
1160
|
if (this.state.useWorker && this.state.workerReady) {
|
|
875
|
-
sendToWorker({ type: 'disposeVobSub' }).catch(() => { });
|
|
1161
|
+
sendToWorker({ type: 'disposeVobSub', sessionId: this.state.sessionId }).catch(() => { });
|
|
876
1162
|
}
|
|
877
1163
|
this.vobsubParser?.dispose();
|
|
878
1164
|
this.vobsubParser = null;
|
|
1165
|
+
this.state.sessionId = null;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
/** Create a video subtitle renderer with automatic format detection. */
|
|
1169
|
+
export function createAutoSubtitleRenderer(options) {
|
|
1170
|
+
const format = detectSubtitleFormat({
|
|
1171
|
+
data: options.subContent,
|
|
1172
|
+
idxContent: options.idxContent,
|
|
1173
|
+
fileName: options.fileName,
|
|
1174
|
+
subUrl: options.subUrl,
|
|
1175
|
+
idxUrl: options.idxUrl
|
|
1176
|
+
});
|
|
1177
|
+
if (format === 'pgs') {
|
|
1178
|
+
return new PgsRenderer(options);
|
|
1179
|
+
}
|
|
1180
|
+
if (format === 'vobsub') {
|
|
1181
|
+
return new VobSubRenderer(options);
|
|
879
1182
|
}
|
|
1183
|
+
throw new Error('Unable to detect subtitle format for video renderer');
|
|
880
1184
|
}
|
|
881
1185
|
//# sourceMappingURL=renderers.js.map
|