libbitsub 1.5.1 → 1.7.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.
Files changed (43) hide show
  1. package/README.md +262 -223
  2. package/dist/index.d.ts +2 -2
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +2 -2
  5. package/dist/index.js.map +1 -1
  6. package/dist/ts/parsers.d.ts +18 -1
  7. package/dist/ts/parsers.d.ts.map +1 -1
  8. package/dist/ts/parsers.js +151 -0
  9. package/dist/ts/parsers.js.map +1 -1
  10. package/dist/ts/renderers.d.ts +54 -4
  11. package/dist/ts/renderers.d.ts.map +1 -1
  12. package/dist/ts/renderers.js +457 -87
  13. package/dist/ts/renderers.js.map +1 -1
  14. package/dist/ts/types.d.ts +147 -3
  15. package/dist/ts/types.d.ts.map +1 -1
  16. package/dist/ts/utils.d.ts +11 -1
  17. package/dist/ts/utils.d.ts.map +1 -1
  18. package/dist/ts/utils.js +100 -1
  19. package/dist/ts/utils.js.map +1 -1
  20. package/dist/ts/wasm.js.map +1 -1
  21. package/dist/ts/webgl2-renderer.d.ts +57 -0
  22. package/dist/ts/webgl2-renderer.d.ts.map +1 -0
  23. package/dist/ts/webgl2-renderer.js +293 -0
  24. package/dist/ts/webgl2-renderer.js.map +1 -0
  25. package/dist/ts/webgpu-renderer.d.ts +5 -1
  26. package/dist/ts/webgpu-renderer.d.ts.map +1 -1
  27. package/dist/ts/webgpu-renderer.js +64 -45
  28. package/dist/ts/webgpu-renderer.js.map +1 -1
  29. package/dist/ts/worker.d.ts.map +1 -1
  30. package/dist/ts/worker.js +145 -87
  31. package/dist/ts/worker.js.map +1 -1
  32. package/dist/wrapper.d.ts +3 -2
  33. package/dist/wrapper.d.ts.map +1 -1
  34. package/dist/wrapper.js +3 -1
  35. package/dist/wrapper.js.map +1 -1
  36. package/package.json +3 -2
  37. package/pkg/README.md +262 -223
  38. package/pkg/libbitsub.d.ts +120 -0
  39. package/pkg/libbitsub.js +251 -15
  40. package/pkg/libbitsub_bg.wasm +0 -0
  41. package/pkg/libbitsub_bg.wasm.d.ts +24 -0
  42. package/pkg/package.json +1 -1
  43. package/src/wrapper.ts +14 -1
@@ -4,13 +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
+ import { WebGL2Renderer, isWebGL2Supported } from './webgl2-renderer';
10
11
  /** Default display settings */
11
12
  const DEFAULT_DISPLAY_SETTINGS = {
12
13
  scale: 1.0,
13
- verticalOffset: 0
14
+ verticalOffset: 0,
15
+ horizontalOffset: 0,
16
+ horizontalAlign: 'center',
17
+ bottomPadding: 0,
18
+ safeArea: 0,
19
+ opacity: 1.0
14
20
  };
15
21
  /**
16
22
  * Base class for video-integrated subtitle renderers.
@@ -18,6 +24,7 @@ const DEFAULT_DISPLAY_SETTINGS = {
18
24
  */
19
25
  class BaseVideoSubtitleRenderer {
20
26
  video;
27
+ format;
21
28
  subUrl;
22
29
  subContent;
23
30
  canvas = null;
@@ -31,13 +38,26 @@ class BaseVideoSubtitleRenderer {
31
38
  tempCanvas = null;
32
39
  tempCtx = null;
33
40
  lastRenderedData = null;
41
+ lastCueIndex = null;
42
+ currentCueMetadata = null;
43
+ parserMetadata = null;
34
44
  /** Display settings for subtitle rendering */
35
45
  displaySettings = { ...DEFAULT_DISPLAY_SETTINGS };
36
- // WebGPU renderer (optional, falls back to Canvas2D if unavailable)
46
+ cacheLimit = 24;
47
+ prefetchBefore = 0;
48
+ prefetchAfter = 0;
49
+ onEvent;
50
+ currentRendererBackend = null;
51
+ loadedMetadataHandler = null;
52
+ seekedHandler = null;
53
+ // WebGPU renderer (optional, falls back to WebGL2 then Canvas2D)
37
54
  webgpuRenderer = null;
38
55
  useWebGPU = false;
39
- preferWebGPU = true;
40
56
  onWebGPUFallback;
57
+ // WebGL2 renderer (optional, falls back to Canvas2D)
58
+ webgl2Renderer = null;
59
+ useWebGL2 = false;
60
+ onWebGL2Fallback;
41
61
  // Performance tracking
42
62
  perfStats = {
43
63
  framesRendered: 0,
@@ -47,17 +67,39 @@ class BaseVideoSubtitleRenderer {
47
67
  fpsTimestamps: [],
48
68
  lastFrameTime: 0
49
69
  };
50
- constructor(options) {
70
+ constructor(options, format) {
51
71
  this.video = options.video;
72
+ this.format = format;
52
73
  this.subUrl = options.subUrl;
53
74
  this.subContent = options.subContent;
54
- this.preferWebGPU = options.preferWebGPU !== false; // Default to true
55
75
  this.onWebGPUFallback = options.onWebGPUFallback;
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));
56
82
  }
57
83
  /** Get current display settings */
58
84
  getDisplaySettings() {
59
85
  return { ...this.displaySettings };
60
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
+ }
61
103
  /** Get base stats common to all renderers */
62
104
  getBaseStats() {
63
105
  const now = performance.now();
@@ -80,13 +122,18 @@ class BaseVideoSubtitleRenderer {
80
122
  }
81
123
  /** Set display settings and force re-render */
82
124
  setDisplaySettings(settings) {
83
- const changed = settings.scale !== this.displaySettings.scale || settings.verticalOffset !== this.displaySettings.verticalOffset;
84
- if (settings.scale !== undefined) {
85
- this.displaySettings.scale = Math.max(0.1, Math.min(3.0, settings.scale));
86
- }
87
- if (settings.verticalOffset !== undefined) {
88
- this.displaySettings.verticalOffset = Math.max(-50, Math.min(50, settings.verticalOffset));
89
- }
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;
90
137
  // Force re-render if settings changed
91
138
  if (changed) {
92
139
  this.lastRenderedIndex = -1;
@@ -101,7 +148,9 @@ class BaseVideoSubtitleRenderer {
101
148
  }
102
149
  /** Start initialization. */
103
150
  startInit() {
104
- 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
+ });
105
154
  }
106
155
  /** Initialize the renderer. */
107
156
  async init() {
@@ -126,22 +175,57 @@ class BaseVideoSubtitleRenderer {
126
175
  }
127
176
  parent.appendChild(this.canvas);
128
177
  }
129
- // Try WebGPU first if preferred
130
- if (this.preferWebGPU && isWebGPUSupported()) {
178
+ // Try WebGPU first, then WebGL2, then Canvas2D
179
+ if (isWebGPUSupported()) {
131
180
  this.initWebGPU();
132
181
  }
182
+ else if (isWebGL2Supported()) {
183
+ this.initWebGL2();
184
+ }
133
185
  else {
134
186
  this.initCanvas2D();
135
187
  }
136
188
  this.updateCanvasSize();
137
189
  this.resizeObserver = new ResizeObserver(() => this.updateCanvasSize());
138
190
  this.resizeObserver.observe(this.video);
139
- this.video.addEventListener('loadedmetadata', () => this.updateCanvasSize());
140
- this.video.addEventListener('seeked', () => {
191
+ this.loadedMetadataHandler = () => this.updateCanvasSize();
192
+ this.seekedHandler = () => {
141
193
  this.lastRenderedIndex = -1;
142
194
  this.lastRenderedTime = -1;
143
195
  this.onSeek();
144
- });
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 });
145
229
  }
146
230
  /** Initialize WebGPU renderer. */
147
231
  async initWebGPU() {
@@ -155,15 +239,42 @@ class BaseVideoSubtitleRenderer {
155
239
  const height = Math.max(1, bounds.height * window.devicePixelRatio);
156
240
  await this.webgpuRenderer.setCanvas(this.canvas, width, height);
157
241
  this.useWebGPU = true;
158
- console.log('[libbitsub] Using WebGPU renderer');
242
+ this.emitRendererBackend('webgpu');
159
243
  }
160
244
  catch (error) {
161
- console.warn('[libbitsub] WebGPU init failed, falling back to Canvas2D:', error);
162
245
  this.webgpuRenderer?.destroy();
163
246
  this.webgpuRenderer = null;
164
247
  this.useWebGPU = false;
165
- this.initCanvas2D();
166
248
  this.onWebGPUFallback?.();
249
+ // Try WebGL2 before Canvas2D
250
+ if (isWebGL2Supported()) {
251
+ this.initWebGL2();
252
+ }
253
+ else {
254
+ this.initCanvas2D();
255
+ }
256
+ }
257
+ }
258
+ /** Initialize WebGL2 renderer. */
259
+ async initWebGL2() {
260
+ try {
261
+ this.webgl2Renderer = new WebGL2Renderer();
262
+ await this.webgl2Renderer.init();
263
+ if (!this.canvas)
264
+ return;
265
+ const bounds = this.getVideoContentBounds();
266
+ const width = Math.max(1, bounds.width * window.devicePixelRatio);
267
+ const height = Math.max(1, bounds.height * window.devicePixelRatio);
268
+ await this.webgl2Renderer.setCanvas(this.canvas, width, height);
269
+ this.useWebGL2 = true;
270
+ this.emitRendererBackend('webgl2');
271
+ }
272
+ catch (error) {
273
+ this.webgl2Renderer?.destroy();
274
+ this.webgl2Renderer = null;
275
+ this.useWebGL2 = false;
276
+ this.onWebGL2Fallback?.();
277
+ this.initCanvas2D();
167
278
  }
168
279
  }
169
280
  /** Initialize Canvas2D renderer. */
@@ -172,7 +283,8 @@ class BaseVideoSubtitleRenderer {
172
283
  return;
173
284
  this.ctx = this.canvas.getContext('2d');
174
285
  this.useWebGPU = false;
175
- console.log('[libbitsub] Using Canvas2D renderer');
286
+ this.useWebGL2 = false;
287
+ this.emitRendererBackend('canvas2d');
176
288
  }
177
289
  /** Called when video seeks. */
178
290
  onSeek() { }
@@ -227,10 +339,13 @@ class BaseVideoSubtitleRenderer {
227
339
  this.canvas.style.top = `${bounds.y}px`;
228
340
  this.canvas.style.width = `${bounds.width}px`;
229
341
  this.canvas.style.height = `${bounds.height}px`;
230
- // Update WebGPU renderer size if active
342
+ // Update GPU renderer size if active
231
343
  if (this.useWebGPU && this.webgpuRenderer) {
232
344
  this.webgpuRenderer.updateSize(pixelWidth, pixelHeight);
233
345
  }
346
+ else if (this.useWebGL2 && this.webgl2Renderer) {
347
+ this.webgl2Renderer.updateSize(pixelWidth, pixelHeight);
348
+ }
234
349
  this.lastRenderedIndex = -1;
235
350
  this.lastRenderedTime = -1;
236
351
  }
@@ -267,6 +382,12 @@ class BaseVideoSubtitleRenderer {
267
382
  }
268
383
  this.lastRenderedIndex = currentIndex;
269
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
+ }
270
391
  }
271
392
  }
272
393
  this.animationFrameId = requestAnimationFrame(render);
@@ -290,14 +411,70 @@ class BaseVideoSubtitleRenderer {
290
411
  return;
291
412
  }
292
413
  }
293
- // Use WebGPU or Canvas2D based on availability
414
+ // Use best available renderer
294
415
  if (this.useWebGPU && this.webgpuRenderer) {
295
416
  this.renderFrameWebGPU(data, index);
296
417
  }
418
+ else if (this.useWebGL2 && this.webgl2Renderer) {
419
+ this.renderFrameWebGL2(data, index);
420
+ }
297
421
  else {
298
422
  this.renderFrameCanvas2D(data, index);
299
423
  }
300
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
+ }
301
478
  /** Render using WebGPU. */
302
479
  renderFrameWebGPU(data, index) {
303
480
  if (!this.webgpuRenderer || !this.canvas)
@@ -311,14 +488,21 @@ class BaseVideoSubtitleRenderer {
311
488
  // Store for potential reuse
312
489
  this.lastRenderedData = data;
313
490
  // Calculate base scale factors
314
- const baseScaleX = this.canvas.width / data.width;
315
- const baseScaleY = this.canvas.height / data.height;
316
- // Apply display settings
317
- const { scale, verticalOffset } = this.displaySettings;
318
- const scaleX = baseScaleX * scale;
319
- const scaleY = baseScaleY * scale;
320
- const offsetY = (verticalOffset / 100) * this.canvas.height;
321
- 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);
493
+ }
494
+ /** Render using WebGL2. */
495
+ renderFrameWebGL2(data, index) {
496
+ if (!this.webgl2Renderer || !this.canvas)
497
+ return;
498
+ if (index < 0 || !data || data.compositionData.length === 0) {
499
+ this.webgl2Renderer.clear();
500
+ this.lastRenderedData = null;
501
+ return;
502
+ }
503
+ this.lastRenderedData = data;
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);
322
506
  }
323
507
  /** Render using Canvas2D. */
324
508
  renderFrameCanvas2D(data, index) {
@@ -333,14 +517,9 @@ class BaseVideoSubtitleRenderer {
333
517
  }
334
518
  // Store for potential reuse
335
519
  this.lastRenderedData = data;
336
- // Calculate base scale factors
337
- const baseScaleX = this.canvas.width / data.width;
338
- const baseScaleY = this.canvas.height / data.height;
339
- // Apply display settings
340
- const { scale, verticalOffset } = this.displaySettings;
341
- const scaleX = baseScaleX * scale;
342
- const scaleY = baseScaleY * scale;
343
- const offsetY = (verticalOffset / 100) * this.canvas.height;
520
+ const layout = this.computeLayout(data);
521
+ this.ctx.save();
522
+ this.ctx.globalAlpha = layout.opacity;
344
523
  for (const comp of data.compositionData) {
345
524
  if (!this.tempCanvas || !this.tempCtx)
346
525
  continue;
@@ -352,14 +531,13 @@ class BaseVideoSubtitleRenderer {
352
531
  this.tempCtx.putImageData(comp.pixelData, 0, 0);
353
532
  // Calculate position with scale and offset applied
354
533
  // Center the scaled content horizontally
355
- const scaledWidth = comp.pixelData.width * scaleX;
356
- const scaledHeight = comp.pixelData.height * scaleY;
357
- const baseX = comp.x * baseScaleX;
358
- const baseY = comp.y * baseScaleY;
359
- const centeredX = baseX + (comp.pixelData.width * baseScaleX - scaledWidth) / 2;
360
- const adjustedY = baseY + offsetY + (comp.pixelData.height * baseScaleY - scaledHeight);
361
- 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);
362
539
  }
540
+ this.ctx.restore();
363
541
  }
364
542
  /** Dispose of all resources. */
365
543
  dispose() {
@@ -370,18 +548,33 @@ class BaseVideoSubtitleRenderer {
370
548
  }
371
549
  this.resizeObserver?.disconnect();
372
550
  this.resizeObserver = null;
373
- // Clean up WebGPU renderer
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
+ }
559
+ // Clean up GPU renderers
374
560
  if (this.webgpuRenderer) {
375
561
  this.webgpuRenderer.destroy();
376
562
  this.webgpuRenderer = null;
377
563
  }
564
+ if (this.webgl2Renderer) {
565
+ this.webgl2Renderer.destroy();
566
+ this.webgl2Renderer = null;
567
+ }
378
568
  this.canvas?.parentElement?.removeChild(this.canvas);
379
569
  this.canvas = null;
380
570
  this.ctx = null;
381
571
  this.tempCanvas = null;
382
572
  this.tempCtx = null;
383
573
  this.lastRenderedData = null;
574
+ this.currentCueMetadata = null;
575
+ this.parserMetadata = null;
384
576
  this.useWebGPU = false;
577
+ this.useWebGL2 = false;
385
578
  }
386
579
  }
387
580
  /**
@@ -395,14 +588,16 @@ export class PgsRenderer extends BaseVideoSubtitleRenderer {
395
588
  onLoaded;
396
589
  onError;
397
590
  constructor(options) {
398
- super(options);
591
+ super(options, 'pgs');
399
592
  this.onLoading = options.onLoading;
400
593
  this.onLoaded = options.onLoaded;
401
594
  this.onError = options.onError;
595
+ applyCacheLimit(this.state, this.cacheLimit);
402
596
  this.startInit();
403
597
  }
404
598
  async loadSubtitles() {
405
599
  try {
600
+ this.emitEvent({ type: 'loading', format: 'pgs' });
406
601
  this.onLoading?.();
407
602
  let arrayBuffer;
408
603
  if (this.subContent) {
@@ -420,16 +615,24 @@ export class PgsRenderer extends BaseVideoSubtitleRenderer {
420
615
  const data = new Uint8Array(arrayBuffer);
421
616
  if (this.state.useWorker) {
422
617
  try {
618
+ this.state.sessionId = createWorkerSessionId();
423
619
  await getOrCreateWorker();
424
- const loadResponse = await sendToWorker({ type: 'loadPgs', data: data.buffer.slice(0) });
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
+ });
425
626
  if (loadResponse.type === 'pgsLoaded') {
426
627
  this.state.workerReady = true;
427
- const tsResponse = await sendToWorker({ type: 'getPgsTimestamps' });
628
+ this.state.metadata = loadResponse.metadata;
629
+ const tsResponse = await sendToWorker({ type: 'getPgsTimestamps', sessionId: this.state.sessionId });
428
630
  if (tsResponse.type === 'pgsTimestamps') {
429
631
  this.state.timestamps = tsResponse.timestamps;
430
632
  }
431
633
  this.isLoaded = true;
432
- console.log(`[libbitsub] PGS loaded (worker): ${loadResponse.count} display sets from ${loadResponse.byteLength} bytes`);
634
+ this.setParserMetadata(loadResponse.metadata);
635
+ this.emitWorkerState(true, true, this.state.sessionId);
433
636
  this.onLoaded?.();
434
637
  return; // Success, don't fall through to main thread
435
638
  }
@@ -438,8 +641,8 @@ export class PgsRenderer extends BaseVideoSubtitleRenderer {
438
641
  }
439
642
  }
440
643
  catch (workerError) {
441
- console.warn('[libbitsub] Worker failed, falling back to main thread:', workerError);
442
644
  this.state.useWorker = false;
645
+ this.emitWorkerState(false, false, this.state.sessionId, true);
443
646
  }
444
647
  }
445
648
  // Main thread fallback - use idle callback to avoid blocking UI
@@ -447,8 +650,9 @@ export class PgsRenderer extends BaseVideoSubtitleRenderer {
447
650
  this.onLoaded?.();
448
651
  }
449
652
  catch (error) {
450
- console.error('Failed to load PGS subtitles:', error);
451
- this.onError?.(error instanceof Error ? error : new Error(String(error)));
653
+ const resolvedError = error instanceof Error ? error : new Error(String(error));
654
+ this.emitEvent({ type: 'error', format: 'pgs', error: resolvedError });
655
+ this.onError?.(resolvedError);
452
656
  }
453
657
  }
454
658
  async loadOnMainThread(data) {
@@ -464,8 +668,9 @@ export class PgsRenderer extends BaseVideoSubtitleRenderer {
464
668
  scheduleTask(() => {
465
669
  const count = this.pgsParser.load(data);
466
670
  this.state.timestamps = this.pgsParser.getTimestamps();
671
+ this.state.metadata = this.pgsParser.getMetadata();
467
672
  this.isLoaded = true;
468
- console.log(`[libbitsub] PGS loaded (main thread): ${count} display sets from ${data.byteLength} bytes`);
673
+ this.setParserMetadata(this.state.metadata);
469
674
  resolve();
470
675
  });
471
676
  });
@@ -491,16 +696,22 @@ export class PgsRenderer extends BaseVideoSubtitleRenderer {
491
696
  return this.pgsParser?.findIndexAtTimestamp(time) ?? -1;
492
697
  }
493
698
  renderAtIndex(index) {
699
+ if (this.state.frameCache.has(index)) {
700
+ return this.state.frameCache.get(index) ?? undefined;
701
+ }
494
702
  if (this.state.useWorker && this.state.workerReady) {
495
- if (this.state.frameCache.has(index)) {
496
- return this.state.frameCache.get(index) ?? undefined;
497
- }
498
703
  if (!this.state.pendingRenders.has(index)) {
499
- const renderPromise = sendToWorker({ type: 'renderPgsAtIndex', index }).then((response) => response.type === 'pgsFrame' && response.frame ? convertFrameData(response.frame) : null);
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));
500
709
  this.state.pendingRenders.set(index, renderPromise);
710
+ this.emitCacheChange(this.state.frameCache.size, this.state.pendingRenders.size);
501
711
  renderPromise.then((result) => {
502
- this.state.frameCache.set(index, result);
712
+ setCachedFrame(this.state, index, result);
503
713
  this.state.pendingRenders.delete(index);
714
+ this.emitCacheChange(this.state.frameCache.size, this.state.pendingRenders.size);
504
715
  // Force re-render on next frame by resetting lastRenderedIndex
505
716
  if (this.findCurrentIndex(this.video.currentTime) === index) {
506
717
  this.lastRenderedIndex = -1;
@@ -510,7 +721,32 @@ export class PgsRenderer extends BaseVideoSubtitleRenderer {
510
721
  // Return undefined to indicate async loading in progress
511
722
  return undefined;
512
723
  }
513
- return this.pgsParser?.renderAtIndex(index);
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
+ };
514
750
  }
515
751
  isPendingRender(index) {
516
752
  return this.state.pendingRenders.has(index);
@@ -518,10 +754,43 @@ export class PgsRenderer extends BaseVideoSubtitleRenderer {
518
754
  onSeek() {
519
755
  this.state.frameCache.clear();
520
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;
521
771
  if (this.state.useWorker && this.state.workerReady) {
522
- sendToWorker({ type: 'clearPgsCache' }).catch(() => { });
772
+ sendToWorker({ type: 'clearPgsCache', sessionId: this.state.sessionId }).catch(() => { });
523
773
  }
524
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);
525
794
  }
526
795
  /** Get performance statistics for PGS renderer */
527
796
  getStats() {
@@ -539,10 +808,11 @@ export class PgsRenderer extends BaseVideoSubtitleRenderer {
539
808
  this.state.frameCache.clear();
540
809
  this.state.pendingRenders.clear();
541
810
  if (this.state.useWorker && this.state.workerReady) {
542
- sendToWorker({ type: 'disposePgs' }).catch(() => { });
811
+ sendToWorker({ type: 'disposePgs', sessionId: this.state.sessionId }).catch(() => { });
543
812
  }
544
813
  this.pgsParser?.dispose();
545
814
  this.pgsParser = null;
815
+ this.state.sessionId = null;
546
816
  }
547
817
  }
548
818
  /**
@@ -562,18 +832,19 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
562
832
  cachedIndexTime = -1;
563
833
  pendingIndexLookup = null;
564
834
  constructor(options) {
565
- super(options);
835
+ super(options, 'vobsub');
566
836
  this.idxUrl = options.idxUrl || (options.subUrl ? options.subUrl.replace(/\\.sub$/i, '.idx') : undefined);
567
837
  this.idxContent = options.idxContent;
568
838
  this.onLoading = options.onLoading;
569
839
  this.onLoaded = options.onLoaded;
570
840
  this.onError = options.onError;
841
+ applyCacheLimit(this.state, this.cacheLimit);
571
842
  this.startInit();
572
843
  }
573
844
  async loadSubtitles() {
574
845
  try {
846
+ this.emitEvent({ type: 'loading', format: 'vobsub' });
575
847
  this.onLoading?.();
576
- console.log(`[libbitsub] Loading VobSub`);
577
848
  let subArrayBuffer;
578
849
  let idxData;
579
850
  // Resolve SUB content
@@ -619,23 +890,27 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
619
890
  throw new Error('Failed to load VobSub data');
620
891
  }
621
892
  const subData = new Uint8Array(subArrayBuffer);
622
- console.log(`[libbitsub] VobSub files loaded: .sub=${subArrayBuffer.byteLength} bytes, .idx=${idxData.length} chars`);
623
893
  if (this.state.useWorker) {
624
894
  try {
895
+ this.state.sessionId = createWorkerSessionId();
625
896
  await getOrCreateWorker();
897
+ this.emitWorkerState(true, false, this.state.sessionId);
626
898
  const loadResponse = await sendToWorker({
627
899
  type: 'loadVobSub',
900
+ sessionId: this.state.sessionId,
628
901
  idxContent: idxData,
629
902
  subData: subData.buffer.slice(0)
630
903
  });
631
904
  if (loadResponse.type === 'vobSubLoaded') {
632
905
  this.state.workerReady = true;
633
- const tsResponse = await sendToWorker({ type: 'getVobSubTimestamps' });
906
+ this.state.metadata = loadResponse.metadata;
907
+ const tsResponse = await sendToWorker({ type: 'getVobSubTimestamps', sessionId: this.state.sessionId });
634
908
  if (tsResponse.type === 'vobSubTimestamps') {
635
909
  this.state.timestamps = tsResponse.timestamps;
636
910
  }
637
911
  this.isLoaded = true;
638
- console.log(`[libbitsub] VobSub loaded (worker): ${loadResponse.count} subtitle entries`);
912
+ this.setParserMetadata(loadResponse.metadata);
913
+ this.emitWorkerState(true, true, this.state.sessionId);
639
914
  this.onLoaded?.();
640
915
  return; // Success, don't fall through to main thread
641
916
  }
@@ -644,8 +919,8 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
644
919
  }
645
920
  }
646
921
  catch (workerError) {
647
- console.warn('[libbitsub] Worker failed, falling back to main thread:', workerError);
648
922
  this.state.useWorker = false;
923
+ this.emitWorkerState(false, false, this.state.sessionId, true);
649
924
  }
650
925
  }
651
926
  // Main thread fallback
@@ -653,8 +928,9 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
653
928
  this.onLoaded?.();
654
929
  }
655
930
  catch (error) {
656
- console.error('Failed to load VobSub subtitles:', error);
657
- this.onError?.(error instanceof Error ? error : new Error(String(error)));
931
+ const resolvedError = error instanceof Error ? error : new Error(String(error));
932
+ this.emitEvent({ type: 'error', format: 'vobsub', error: resolvedError });
933
+ this.onError?.(resolvedError);
658
934
  }
659
935
  }
660
936
  async loadOnMainThread(idxData, subData) {
@@ -669,8 +945,9 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
669
945
  scheduleTask(() => {
670
946
  this.vobsubParser.loadFromData(idxData, subData);
671
947
  this.state.timestamps = this.vobsubParser.getTimestamps();
672
- console.log(`[libbitsub] VobSub loaded (main thread): ${this.vobsubParser.count} subtitle entries`);
948
+ this.state.metadata = this.vobsubParser.getMetadata();
673
949
  this.isLoaded = true;
950
+ this.setParserMetadata(this.state.metadata);
674
951
  resolve();
675
952
  });
676
953
  });
@@ -698,7 +975,11 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
698
975
  }
699
976
  // Start async lookup if not already pending
700
977
  if (!this.pendingIndexLookup) {
701
- this.pendingIndexLookup = sendToWorker({ type: 'findVobSubIndex', timeMs }).then((response) => {
978
+ this.pendingIndexLookup = sendToWorker({
979
+ type: 'findVobSubIndex',
980
+ sessionId: this.state.sessionId,
981
+ timeMs
982
+ }).then((response) => {
702
983
  if (response.type === 'vobSubIndex') {
703
984
  const newIndex = response.index;
704
985
  const oldIndex = this.cachedIndex;
@@ -718,18 +999,23 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
718
999
  return this.vobsubParser?.findIndexAtTimestamp(time) ?? -1;
719
1000
  }
720
1001
  renderAtIndex(index) {
1002
+ if (this.state.frameCache.has(index)) {
1003
+ return this.state.frameCache.get(index) ?? undefined;
1004
+ }
721
1005
  if (this.state.useWorker && this.state.workerReady) {
722
- // Return cached frame immediately if available
723
- if (this.state.frameCache.has(index)) {
724
- return this.state.frameCache.get(index) ?? undefined;
725
- }
726
1006
  // Start async render if not already pending
727
1007
  if (!this.state.pendingRenders.has(index)) {
728
- const renderPromise = sendToWorker({ type: 'renderVobSubAtIndex', index }).then((response) => response.type === 'vobSubFrame' && response.frame ? convertFrameData(response.frame) : null);
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));
729
1013
  this.state.pendingRenders.set(index, renderPromise);
1014
+ this.emitCacheChange(this.state.frameCache.size, this.state.pendingRenders.size);
730
1015
  renderPromise.then((result) => {
731
- this.state.frameCache.set(index, result);
1016
+ setCachedFrame(this.state, index, result);
732
1017
  this.state.pendingRenders.delete(index);
1018
+ this.emitCacheChange(this.state.frameCache.size, this.state.pendingRenders.size);
733
1019
  // Force re-render on next frame by resetting lastRenderedIndex
734
1020
  if (this.findCurrentIndex(this.video.currentTime) === index) {
735
1021
  this.lastRenderedIndex = -1;
@@ -739,7 +1025,34 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
739
1025
  // Return undefined to indicate async loading in progress
740
1026
  return undefined;
741
1027
  }
742
- return this.vobsubParser?.renderAtIndex(index);
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
+ };
743
1056
  }
744
1057
  isPendingRender(index) {
745
1058
  return this.state.pendingRenders.has(index);
@@ -752,9 +1065,45 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
752
1065
  this.cachedIndexTime = -1;
753
1066
  this.pendingIndexLookup = null;
754
1067
  if (this.state.useWorker && this.state.workerReady) {
755
- 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(() => { });
756
1086
  }
757
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);
758
1107
  }
759
1108
  /** Get performance statistics for VobSub renderer */
760
1109
  getStats() {
@@ -770,32 +1119,35 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
770
1119
  /** Enable or disable debanding filter */
771
1120
  setDebandEnabled(enabled) {
772
1121
  if (this.state.useWorker && this.state.workerReady) {
773
- sendToWorker({ type: 'setVobSubDebandEnabled', enabled }).catch(() => { });
1122
+ sendToWorker({ type: 'setVobSubDebandEnabled', sessionId: this.state.sessionId, enabled }).catch(() => { });
774
1123
  }
775
1124
  this.vobsubParser?.setDebandEnabled(enabled);
776
1125
  // Clear cache to force re-render with new settings
777
1126
  this.state.frameCache.clear();
778
1127
  this.lastRenderedIndex = -1;
1128
+ this.emitCacheChange(this.state.frameCache.size, this.state.pendingRenders.size);
779
1129
  }
780
1130
  /** Set debanding threshold (0-255, default: 64) */
781
1131
  setDebandThreshold(threshold) {
782
1132
  if (this.state.useWorker && this.state.workerReady) {
783
- sendToWorker({ type: 'setVobSubDebandThreshold', threshold }).catch(() => { });
1133
+ sendToWorker({ type: 'setVobSubDebandThreshold', sessionId: this.state.sessionId, threshold }).catch(() => { });
784
1134
  }
785
1135
  this.vobsubParser?.setDebandThreshold(threshold);
786
1136
  // Clear cache to force re-render with new settings
787
1137
  this.state.frameCache.clear();
788
1138
  this.lastRenderedIndex = -1;
1139
+ this.emitCacheChange(this.state.frameCache.size, this.state.pendingRenders.size);
789
1140
  }
790
1141
  /** Set debanding sample range in pixels (1-64, default: 15) */
791
1142
  setDebandRange(range) {
792
1143
  if (this.state.useWorker && this.state.workerReady) {
793
- sendToWorker({ type: 'setVobSubDebandRange', range }).catch(() => { });
1144
+ sendToWorker({ type: 'setVobSubDebandRange', sessionId: this.state.sessionId, range }).catch(() => { });
794
1145
  }
795
1146
  this.vobsubParser?.setDebandRange(range);
796
1147
  // Clear cache to force re-render with new settings
797
1148
  this.state.frameCache.clear();
798
1149
  this.lastRenderedIndex = -1;
1150
+ this.emitCacheChange(this.state.frameCache.size, this.state.pendingRenders.size);
799
1151
  }
800
1152
  /** Check if debanding is enabled */
801
1153
  get debandEnabled() {
@@ -806,10 +1158,28 @@ export class VobSubRenderer extends BaseVideoSubtitleRenderer {
806
1158
  this.state.frameCache.clear();
807
1159
  this.state.pendingRenders.clear();
808
1160
  if (this.state.useWorker && this.state.workerReady) {
809
- sendToWorker({ type: 'disposeVobSub' }).catch(() => { });
1161
+ sendToWorker({ type: 'disposeVobSub', sessionId: this.state.sessionId }).catch(() => { });
810
1162
  }
811
1163
  this.vobsubParser?.dispose();
812
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);
813
1182
  }
1183
+ throw new Error('Unable to detect subtitle format for video renderer');
814
1184
  }
815
1185
  //# sourceMappingURL=renderers.js.map