libbitsub 1.10.2 → 1.12.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 (98) hide show
  1. package/README.md +296 -49
  2. package/dist/index.d.ts +3 -3
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +3 -3
  5. package/dist/index.js.map +1 -1
  6. package/dist/integrations/hlsjs.d.ts +45 -0
  7. package/dist/integrations/hlsjs.d.ts.map +1 -0
  8. package/dist/integrations/hlsjs.js +121 -0
  9. package/dist/integrations/hlsjs.js.map +1 -0
  10. package/dist/integrations/index.d.ts +11 -0
  11. package/dist/integrations/index.d.ts.map +1 -0
  12. package/dist/integrations/index.js +11 -0
  13. package/dist/integrations/index.js.map +1 -0
  14. package/dist/integrations/react.d.ts +55 -0
  15. package/dist/integrations/react.d.ts.map +1 -0
  16. package/dist/integrations/react.js +133 -0
  17. package/dist/integrations/react.js.map +1 -0
  18. package/dist/integrations/shaka.d.ts +38 -0
  19. package/dist/integrations/shaka.d.ts.map +1 -0
  20. package/dist/integrations/shaka.js +71 -0
  21. package/dist/integrations/shaka.js.map +1 -0
  22. package/dist/integrations/shared.d.ts +41 -0
  23. package/dist/integrations/shared.d.ts.map +1 -0
  24. package/dist/integrations/shared.js +123 -0
  25. package/dist/integrations/shared.js.map +1 -0
  26. package/dist/integrations/videojs.d.ts +85 -0
  27. package/dist/integrations/videojs.d.ts.map +1 -0
  28. package/dist/integrations/videojs.js +111 -0
  29. package/dist/integrations/videojs.js.map +1 -0
  30. package/dist/ts/capabilities.d.ts +8 -0
  31. package/dist/ts/capabilities.d.ts.map +1 -0
  32. package/dist/ts/capabilities.js +105 -0
  33. package/dist/ts/capabilities.js.map +1 -0
  34. package/dist/ts/diagnostics.d.ts.map +1 -1
  35. package/dist/ts/diagnostics.js.map +1 -1
  36. package/dist/ts/frame-export.d.ts.map +1 -1
  37. package/dist/ts/frame-export.js +4 -4
  38. package/dist/ts/frame-export.js.map +1 -1
  39. package/dist/ts/parsers.d.ts +46 -8
  40. package/dist/ts/parsers.d.ts.map +1 -1
  41. package/dist/ts/parsers.js +305 -14
  42. package/dist/ts/parsers.js.map +1 -1
  43. package/dist/ts/range-loader.d.ts +31 -0
  44. package/dist/ts/range-loader.d.ts.map +1 -0
  45. package/dist/ts/range-loader.js +220 -0
  46. package/dist/ts/range-loader.js.map +1 -0
  47. package/dist/ts/renderers.d.ts +140 -8
  48. package/dist/ts/renderers.d.ts.map +1 -1
  49. package/dist/ts/renderers.js +1773 -287
  50. package/dist/ts/renderers.js.map +1 -1
  51. package/dist/ts/types.d.ts +210 -7
  52. package/dist/ts/types.d.ts.map +1 -1
  53. package/dist/ts/utils.d.ts.map +1 -1
  54. package/dist/ts/utils.js +98 -2
  55. package/dist/ts/utils.js.map +1 -1
  56. package/dist/ts/video-frame-scheduler.d.ts +35 -0
  57. package/dist/ts/video-frame-scheduler.d.ts.map +1 -0
  58. package/dist/ts/video-frame-scheduler.js +98 -0
  59. package/dist/ts/video-frame-scheduler.js.map +1 -0
  60. package/dist/ts/wasm.d.ts +2 -0
  61. package/dist/ts/wasm.d.ts.map +1 -1
  62. package/dist/ts/wasm.js +12 -0
  63. package/dist/ts/wasm.js.map +1 -1
  64. package/dist/ts/webgl2-renderer.d.ts +9 -0
  65. package/dist/ts/webgl2-renderer.d.ts.map +1 -1
  66. package/dist/ts/webgl2-renderer.js +34 -0
  67. package/dist/ts/webgl2-renderer.js.map +1 -1
  68. package/dist/ts/webgpu-renderer.d.ts +9 -0
  69. package/dist/ts/webgpu-renderer.d.ts.map +1 -1
  70. package/dist/ts/webgpu-renderer.js +70 -2
  71. package/dist/ts/webgpu-renderer.js.map +1 -1
  72. package/dist/ts/worker.d.ts +11 -0
  73. package/dist/ts/worker.d.ts.map +1 -1
  74. package/dist/ts/worker.js +620 -158
  75. package/dist/ts/worker.js.map +1 -1
  76. package/dist/wrapper.d.ts +6 -3
  77. package/dist/wrapper.d.ts.map +1 -1
  78. package/dist/wrapper.js +5 -2
  79. package/dist/wrapper.js.map +1 -1
  80. package/examples/README.md +38 -0
  81. package/examples/hlsjs/README.md +53 -0
  82. package/examples/react/README.md +60 -0
  83. package/examples/shaka/README.md +61 -0
  84. package/examples/videojs/README.md +55 -0
  85. package/package.json +66 -8
  86. package/pkg/README.md +296 -49
  87. package/pkg/libbitsub.d.ts +65 -1
  88. package/pkg/libbitsub.js +228 -3
  89. package/pkg/libbitsub_bg.wasm +0 -0
  90. package/pkg/libbitsub_bg.wasm.d.ts +28 -0
  91. package/pkg/package.json +4 -4
  92. package/src/integrations/hlsjs.ts +154 -0
  93. package/src/integrations/index.ts +36 -0
  94. package/src/integrations/react.tsx +176 -0
  95. package/src/integrations/shaka.ts +92 -0
  96. package/src/integrations/shared.ts +171 -0
  97. package/src/integrations/videojs.ts +200 -0
  98. package/src/wrapper.ts +34 -2
package/dist/ts/worker.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * Web Worker management for libbitsub.
3
3
  * Handles off-main-thread subtitle parsing and rendering.
4
4
  */
5
- import { getWasmUrl } from './wasm';
5
+ import { getWasmGlueUrl, getWasmUrl } from './wasm';
6
6
  let sharedWorker = null;
7
7
  let workerInitPromise = null;
8
8
  let messageId = 0;
@@ -15,60 +15,10 @@ export function isWorkerAvailable() {
15
15
  function createWorkerScript() {
16
16
  return `
17
17
  let wasmModule = null;
18
- let wasm;
19
- let cachedUint8Memory = null;
20
- let WASM_VECTOR_LEN = 0;
21
- let cachedTextEncoder = new TextEncoder();
22
- let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
23
18
  const pgsParsers = new Map();
19
+ const dvbParsers = new Map();
24
20
  const vobSubParsers = new Map();
25
-
26
- function getUint8Memory() {
27
- if (cachedUint8Memory === null || cachedUint8Memory.byteLength === 0) {
28
- cachedUint8Memory = new Uint8Array(wasm.memory.buffer);
29
- }
30
- return cachedUint8Memory;
31
- }
32
-
33
- function passArray8ToWasm(arg) {
34
- const ptr = wasm.__wbindgen_malloc(arg.length);
35
- getUint8Memory().set(arg, ptr);
36
- WASM_VECTOR_LEN = arg.length;
37
- return ptr;
38
- }
39
-
40
- const encodeString = typeof cachedTextEncoder.encodeInto === 'function'
41
- ? function(arg, view) { return cachedTextEncoder.encodeInto(arg, view); }
42
- : function(arg, view) {
43
- const buf = cachedTextEncoder.encode(arg);
44
- view.set(buf);
45
- return { read: arg.length, written: buf.length };
46
- };
47
-
48
- function passStringToWasm(arg) {
49
- let len = arg.length;
50
- let ptr = wasm.__wbindgen_malloc(len);
51
- const mem = getUint8Memory();
52
- let offset = 0;
53
- for (; offset < len; offset++) {
54
- const code = arg.charCodeAt(offset);
55
- if (code > 0x7F) break;
56
- mem[ptr + offset] = code;
57
- }
58
- if (offset !== len) {
59
- if (offset !== 0) arg = arg.slice(offset);
60
- ptr = wasm.__wbindgen_realloc(ptr, len, len = offset + arg.length * 3);
61
- const view = getUint8Memory().subarray(ptr + offset, ptr + len);
62
- const ret = encodeString(arg, view);
63
- offset += ret.written;
64
- }
65
- WASM_VECTOR_LEN = offset;
66
- return ptr;
67
- }
68
-
69
- function getStringFromWasm(ptr, len) {
70
- return cachedTextDecoder.decode(getUint8Memory().subarray(ptr, ptr + len));
71
- }
21
+ const offscreenSurfaces = new Map();
72
22
 
73
23
  function buildPgsMetadata(parser) {
74
24
  return {
@@ -79,6 +29,15 @@ function buildPgsMetadata(parser) {
79
29
  };
80
30
  }
81
31
 
32
+ function buildDvbMetadata(parser) {
33
+ return {
34
+ format: 'dvb',
35
+ cueCount: parser.count,
36
+ screenWidth: parser.screenWidth || 0,
37
+ screenHeight: parser.screenHeight || 0
38
+ };
39
+ }
40
+
82
41
  function buildVobSubMetadata(parser) {
83
42
  return {
84
43
  format: 'vobsub',
@@ -91,87 +50,162 @@ function buildVobSubMetadata(parser) {
91
50
  };
92
51
  }
93
52
 
53
+ function detachOffscreenSurface(sessionId) {
54
+ offscreenSurfaces.delete(sessionId);
55
+ }
56
+
94
57
  function disposeSession(sessionId) {
95
58
  const pgsParser = pgsParsers.get(sessionId);
96
59
  if (pgsParser) {
97
60
  pgsParser.free();
98
61
  pgsParsers.delete(sessionId);
99
62
  }
63
+ const dvbParser = dvbParsers.get(sessionId);
64
+ if (dvbParser) {
65
+ dvbParser.free();
66
+ dvbParsers.delete(sessionId);
67
+ }
100
68
  const vobSubParser = vobSubParsers.get(sessionId);
101
69
  if (vobSubParser) {
102
70
  vobSubParser.free();
103
71
  vobSubParsers.delete(sessionId);
104
72
  }
73
+ detachOffscreenSurface(sessionId);
74
+ }
75
+
76
+ function getCompositionBounds(compositions) {
77
+ if (!compositions || compositions.length === 0) return null;
78
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
79
+ for (const comp of compositions) {
80
+ minX = Math.min(minX, comp.x);
81
+ minY = Math.min(minY, comp.y);
82
+ maxX = Math.max(maxX, comp.x + comp.width);
83
+ maxY = Math.max(maxY, comp.y + comp.height);
84
+ }
85
+ if (!Number.isFinite(minX) || !Number.isFinite(minY)) return null;
86
+ return { x: minX, y: minY, width: Math.max(0, maxX - minX), height: Math.max(0, maxY - minY) };
105
87
  }
106
88
 
107
- async function initWasm(wasmUrl) {
108
- if (wasm) return;
89
+ function computeOffscreenLayout(frame, canvasWidth, canvasHeight, settings) {
90
+ const safeDataWidth = frame.width > 0 ? frame.width : canvasWidth;
91
+ const safeDataHeight = frame.height > 0 ? frame.height : canvasHeight;
92
+ const stretchScaleX = canvasWidth / safeDataWidth;
93
+ const stretchScaleY = canvasHeight / safeDataHeight;
94
+ const bounds = getCompositionBounds(frame.compositions) || {
95
+ x: 0, y: 0, width: safeDataWidth, height: safeDataHeight
96
+ };
97
+
98
+ const scale = settings.scale;
99
+ const aspectMode = settings.aspectMode;
100
+ const verticalOffset = settings.verticalOffset;
101
+ const horizontalOffset = settings.horizontalOffset;
102
+ const horizontalAlign = settings.horizontalAlign;
103
+ const bottomPadding = settings.bottomPadding;
104
+ const safeArea = settings.safeArea;
105
+ const opacity = settings.opacity;
109
106
 
110
- const response = await fetch(wasmUrl);
111
- if (!response.ok) {
112
- throw new Error('Failed to fetch WASM: ' + response.status);
107
+ let baseScaleX = stretchScaleX;
108
+ let baseScaleY = stretchScaleY;
109
+ let frameShiftX = 0;
110
+ let frameShiftY = 0;
111
+
112
+ if (aspectMode !== 'stretch') {
113
+ const uniformScale = aspectMode === 'cover'
114
+ ? Math.max(stretchScaleX, stretchScaleY)
115
+ : Math.min(stretchScaleX, stretchScaleY);
116
+ baseScaleX = uniformScale;
117
+ baseScaleY = uniformScale;
118
+ frameShiftX = (canvasWidth - safeDataWidth * uniformScale) / 2;
119
+ frameShiftY = (canvasHeight - safeDataHeight * uniformScale) / 2;
113
120
  }
114
121
 
115
- const wasmBytes = await response.arrayBuffer();
116
- const result = await WebAssembly.instantiate(wasmBytes, {
117
- __wbindgen_placeholder__: {
118
- __wbindgen_throw: function(ptr, len) {
119
- throw new Error(getStringFromWasm(ptr, len));
120
- }
121
- }
122
- });
123
- wasm = result.instance.exports;
124
-
125
- wasmModule = {
126
- PgsParser: class {
127
- constructor() { this.ptr = wasm.pgsparser_new(); }
128
- parse(data) {
129
- const ptr = passArray8ToWasm(data);
130
- return wasm.pgsparser_parse(this.ptr, ptr, WASM_VECTOR_LEN);
131
- }
132
- getTimestamps() { return wasm.pgsparser_getTimestamps(this.ptr); }
133
- renderAtIndex(idx) { return wasm.pgsparser_renderAtIndex(this.ptr, idx); }
134
- findIndexAtTimestamp(ts) { return wasm.pgsparser_findIndexAtTimestamp(this.ptr, ts); }
135
- clearCache() { wasm.pgsparser_clearCache(this.ptr); }
136
- free() { wasm.pgsparser_free(this.ptr); }
137
- get count() { return wasm.pgsparser_count(this.ptr); }
138
- get screenWidth() { return wasm.pgsparser_screenWidth(this.ptr); }
139
- get screenHeight() { return wasm.pgsparser_screenHeight(this.ptr); }
140
- },
141
- VobSubParser: class {
142
- constructor() { this.ptr = wasm.vobsubparser_new(); }
143
- loadFromData(idx, sub) {
144
- const idxPtr = passStringToWasm(idx);
145
- const idxLen = WASM_VECTOR_LEN;
146
- const subPtr = passArray8ToWasm(sub);
147
- wasm.vobsubparser_loadFromData(this.ptr, idxPtr, idxLen, subPtr, sub.length);
148
- }
149
- loadFromMks(sub) {
150
- const ptr = passArray8ToWasm(sub);
151
- wasm.vobsubparser_loadFromMks(this.ptr, ptr, WASM_VECTOR_LEN);
152
- }
153
- loadFromSubOnly(sub) {
154
- const ptr = passArray8ToWasm(sub);
155
- wasm.vobsubparser_loadFromSubOnly(this.ptr, ptr, WASM_VECTOR_LEN);
156
- }
157
- getTimestamps() { return wasm.vobsubparser_getTimestamps(this.ptr); }
158
- renderAtIndex(idx) { return wasm.vobsubparser_renderAtIndex(this.ptr, idx); }
159
- findIndexAtTimestamp(ts) { return wasm.vobsubparser_findIndexAtTimestamp(this.ptr, ts); }
160
- clearCache() { wasm.vobsubparser_clearCache(this.ptr); }
161
- free() { wasm.vobsubparser_free(this.ptr); }
162
- setDebandEnabled(enabled) { wasm.vobsubparser_setDebandEnabled(this.ptr, enabled); }
163
- setDebandThreshold(threshold) { wasm.vobsubparser_setDebandThreshold(this.ptr, threshold); }
164
- setDebandRange(range) { wasm.vobsubparser_setDebandRange(this.ptr, range); }
165
- get count() { return wasm.vobsubparser_count(this.ptr); }
166
- get screenWidth() { return wasm.vobsubparser_screenWidth(this.ptr); }
167
- get screenHeight() { return wasm.vobsubparser_screenHeight(this.ptr); }
168
- get language() { return wasm.vobsubparser_language(this.ptr); }
169
- get trackId() { return wasm.vobsubparser_trackId(this.ptr); }
170
- get hasIdxMetadata() { return !!wasm.vobsubparser_hasIdxMetadata(this.ptr); }
122
+ const anchorX = horizontalAlign === 'left'
123
+ ? bounds.x
124
+ : horizontalAlign === 'right'
125
+ ? bounds.x + bounds.width
126
+ : bounds.x + bounds.width / 2;
127
+ const anchorY = bounds.y + bounds.height;
128
+
129
+ const scaleX = baseScaleX * scale;
130
+ const scaleY = baseScaleY * scale;
131
+ const anchorShiftX = frameShiftX + anchorX * baseScaleX * (1 - scale);
132
+ const anchorShiftY = frameShiftY + anchorY * baseScaleY * (1 - scale);
133
+
134
+ let shiftX = anchorShiftX + (horizontalOffset / 100) * canvasWidth;
135
+ let shiftY = anchorShiftY + (verticalOffset / 100) * canvasHeight;
136
+ shiftY -= (bottomPadding / 100) * canvasHeight;
137
+
138
+ const safeX = (safeArea / 100) * canvasWidth;
139
+ const safeY = (safeArea / 100) * canvasHeight;
140
+ const finalMinX = bounds.x * scaleX + shiftX;
141
+ const finalMinY = bounds.y * scaleY + shiftY;
142
+ const finalMaxX = (bounds.x + bounds.width) * scaleX + shiftX;
143
+ const finalMaxY = (bounds.y + bounds.height) * scaleY + shiftY;
144
+
145
+ if (finalMinX < safeX) shiftX += safeX - finalMinX;
146
+ if (finalMaxX > canvasWidth - safeX) shiftX -= finalMaxX - (canvasWidth - safeX);
147
+ if (finalMinY < safeY) shiftY += safeY - finalMinY;
148
+ if (finalMaxY > canvasHeight - safeY) shiftY -= finalMaxY - (canvasHeight - safeY);
149
+
150
+ return { scaleX, scaleY, shiftX, shiftY, opacity };
151
+ }
152
+
153
+ function presentFrameToOffscreen(surface, frame, canvasWidth, canvasHeight, settings) {
154
+ const ctx = surface.ctx;
155
+ const canvas = surface.canvas;
156
+ if (canvas.width !== canvasWidth) canvas.width = canvasWidth;
157
+ if (canvas.height !== canvasHeight) canvas.height = canvasHeight;
158
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
159
+
160
+ if (!frame || !frame.compositions || frame.compositions.length === 0) {
161
+ return { status: frame ? 'empty' : 'cleared', width: frame?.width || 0, height: frame?.height || 0, compositionCount: 0 };
162
+ }
163
+
164
+ const layout = computeOffscreenLayout(frame, canvas.width, canvas.height, settings);
165
+ ctx.save();
166
+ ctx.globalAlpha = layout.opacity;
167
+
168
+ for (const comp of frame.compositions) {
169
+ if (!comp.width || !comp.height || !comp.rgba) continue;
170
+ if (surface.buffer.width !== comp.width || surface.buffer.height !== comp.height) {
171
+ surface.buffer.width = comp.width;
172
+ surface.buffer.height = comp.height;
171
173
  }
174
+ const pixels = comp.rgba instanceof Uint8ClampedArray
175
+ ? comp.rgba
176
+ : new Uint8ClampedArray(comp.rgba.buffer, comp.rgba.byteOffset, comp.rgba.byteLength);
177
+ surface.bufferCtx.putImageData(new ImageData(pixels, comp.width, comp.height), 0, 0);
178
+ const scaledWidth = comp.width * layout.scaleX;
179
+ const scaledHeight = comp.height * layout.scaleY;
180
+ const adjustedX = comp.x * layout.scaleX + layout.shiftX;
181
+ const adjustedY = comp.y * layout.scaleY + layout.shiftY;
182
+ ctx.drawImage(surface.buffer, adjustedX, adjustedY, scaledWidth, scaledHeight);
183
+ }
184
+
185
+ ctx.restore();
186
+ return {
187
+ status: 'rendered',
188
+ width: frame.width,
189
+ height: frame.height,
190
+ compositionCount: frame.compositions.length,
191
+ bounds: getCompositionBounds(frame.compositions)
172
192
  };
173
193
  }
174
194
 
195
+ async function initWasm(wasmUrl, glueUrl) {
196
+ if (wasmModule) return;
197
+
198
+ let jsGlueUrl = glueUrl;
199
+ if (!jsGlueUrl) {
200
+ const derivedUrl = new URL(wasmUrl);
201
+ derivedUrl.pathname = derivedUrl.pathname.replace(/_bg\.wasm$/, '.js');
202
+ jsGlueUrl = derivedUrl.href;
203
+ }
204
+ const mod = await import(jsGlueUrl);
205
+ await mod.default({ module_or_path: wasmUrl });
206
+ wasmModule = mod;
207
+ }
208
+
175
209
  function convertFrame(frame, isVobSub) {
176
210
  const compositions = [];
177
211
  if (isVobSub) {
@@ -205,7 +239,7 @@ self.onmessage = async function(event) {
205
239
  try {
206
240
  switch (request.type) {
207
241
  case 'init': {
208
- await initWasm(request.wasmUrl);
242
+ await initWasm(request.wasmUrl, request.glueUrl);
209
243
  postResponse({ type: 'initComplete', success: true }, [], _id);
210
244
  break;
211
245
  }
@@ -222,6 +256,145 @@ self.onmessage = async function(event) {
222
256
  );
223
257
  break;
224
258
  }
259
+ case 'beginPgs': {
260
+ disposeSession(request.sessionId);
261
+ const parser = new wasmModule.PgsParser();
262
+ parser.reset();
263
+ pgsParsers.set(request.sessionId, parser);
264
+ const timestamps = parser.getTimestamps();
265
+ postResponse(
266
+ { type: 'pgsProgress', count: 0, added: 0, partial: true, metadata: buildPgsMetadata(parser), timestamps },
267
+ [timestamps.buffer],
268
+ _id
269
+ );
270
+ break;
271
+ }
272
+ case 'appendPgs': {
273
+ let parser = pgsParsers.get(request.sessionId);
274
+ if (!parser) {
275
+ parser = new wasmModule.PgsParser();
276
+ parser.reset();
277
+ pgsParsers.set(request.sessionId, parser);
278
+ }
279
+ const added = parser.feed(new Uint8Array(request.data));
280
+ const timestamps = parser.getTimestamps();
281
+ postResponse(
282
+ { type: 'pgsProgress', count: parser.count, added, partial: true, metadata: buildPgsMetadata(parser), timestamps },
283
+ [timestamps.buffer],
284
+ _id
285
+ );
286
+ break;
287
+ }
288
+ case 'finishPgs': {
289
+ const parser = pgsParsers.get(request.sessionId);
290
+ if (!parser) {
291
+ postResponse({ type: 'error', message: 'PGS session not found for finishPgs' }, [], _id);
292
+ break;
293
+ }
294
+ const count = parser.finishFeed();
295
+ const timestamps = parser.getTimestamps();
296
+ postResponse(
297
+ { type: 'pgsProgress', count, added: 0, partial: false, metadata: buildPgsMetadata(parser), timestamps },
298
+ [timestamps.buffer],
299
+ _id
300
+ );
301
+ break;
302
+ }
303
+ case 'resetPgs': {
304
+ let parser = pgsParsers.get(request.sessionId);
305
+ if (!parser) {
306
+ parser = new wasmModule.PgsParser();
307
+ pgsParsers.set(request.sessionId, parser);
308
+ }
309
+ parser.reset();
310
+ const timestamps = parser.getTimestamps();
311
+ postResponse(
312
+ { type: 'pgsProgress', count: 0, added: 0, partial: true, metadata: buildPgsMetadata(parser), timestamps },
313
+ [timestamps.buffer],
314
+ _id
315
+ );
316
+ break;
317
+ }
318
+ case 'loadDvb': {
319
+ disposeSession(request.sessionId);
320
+ const parser = new wasmModule.DvbParser();
321
+ const count = parser.parse(new Uint8Array(request.data));
322
+ const timestamps = parser.getTimestamps();
323
+ const endTimestamps = parser.getEndTimestamps();
324
+ dvbParsers.set(request.sessionId, parser);
325
+ postResponse(
326
+ { type: 'dvbLoaded', count, byteLength: request.data.byteLength, metadata: buildDvbMetadata(parser), timestamps, endTimestamps },
327
+ [timestamps.buffer, endTimestamps.buffer],
328
+ _id
329
+ );
330
+ break;
331
+ }
332
+ case 'beginDvb': {
333
+ disposeSession(request.sessionId);
334
+ const parser = new wasmModule.DvbParser();
335
+ parser.reset();
336
+ dvbParsers.set(request.sessionId, parser);
337
+ const timestamps = parser.getTimestamps();
338
+ const endTimestamps = parser.getEndTimestamps();
339
+ postResponse(
340
+ { type: 'dvbProgress', count: 0, added: 0, partial: true, metadata: buildDvbMetadata(parser), timestamps, endTimestamps },
341
+ [timestamps.buffer, endTimestamps.buffer],
342
+ _id
343
+ );
344
+ break;
345
+ }
346
+ case 'appendDvb': {
347
+ let parser = dvbParsers.get(request.sessionId);
348
+ if (!parser) {
349
+ parser = new wasmModule.DvbParser();
350
+ parser.reset();
351
+ dvbParsers.set(request.sessionId, parser);
352
+ }
353
+ const added = parser.feed(new Uint8Array(request.data));
354
+ const timestamps = parser.getTimestamps();
355
+ const endTimestamps = parser.getEndTimestamps();
356
+ postResponse(
357
+ { type: 'dvbProgress', count: parser.count, added, partial: true, metadata: buildDvbMetadata(parser), timestamps, endTimestamps },
358
+ [timestamps.buffer, endTimestamps.buffer],
359
+ _id
360
+ );
361
+ break;
362
+ }
363
+ case 'finishDvb': {
364
+ const parser = dvbParsers.get(request.sessionId);
365
+ if (!parser) {
366
+ postResponse({ type: 'error', message: 'DVB session not found for finishDvb' }, [], _id);
367
+ break;
368
+ }
369
+ const previousCount = parser.count;
370
+ parser.finishFeed();
371
+ const count = parser.count;
372
+ const added = count - previousCount;
373
+ const timestamps = parser.getTimestamps();
374
+ const endTimestamps = parser.getEndTimestamps();
375
+ postResponse(
376
+ { type: 'dvbProgress', count, added, partial: false, metadata: buildDvbMetadata(parser), timestamps, endTimestamps },
377
+ [timestamps.buffer, endTimestamps.buffer],
378
+ _id
379
+ );
380
+ break;
381
+ }
382
+ case 'resetDvb': {
383
+ let parser = dvbParsers.get(request.sessionId);
384
+ if (!parser) {
385
+ parser = new wasmModule.DvbParser();
386
+ dvbParsers.set(request.sessionId, parser);
387
+ }
388
+ parser.reset();
389
+ const timestamps = parser.getTimestamps();
390
+ const endTimestamps = parser.getEndTimestamps();
391
+ postResponse(
392
+ { type: 'dvbProgress', count: 0, added: 0, partial: true, metadata: buildDvbMetadata(parser), timestamps, endTimestamps },
393
+ [timestamps.buffer, endTimestamps.buffer],
394
+ _id
395
+ );
396
+ break;
397
+ }
225
398
  case 'loadVobSub': {
226
399
  disposeSession(request.sessionId);
227
400
  const parser = new wasmModule.VobSubParser();
@@ -235,6 +408,48 @@ self.onmessage = async function(event) {
235
408
  );
236
409
  break;
237
410
  }
411
+ case 'loadVobSubIdx': {
412
+ disposeSession(request.sessionId);
413
+ const parser = new wasmModule.VobSubParser();
414
+ parser.loadFromIdx(request.idxContent);
415
+ const timestamps = parser.getTimestamps();
416
+ vobSubParsers.set(request.sessionId, parser);
417
+ postResponse(
418
+ {
419
+ type: 'vobSubProgress',
420
+ count: parser.count,
421
+ partial: true,
422
+ hasSubData: !!parser.hasSubData,
423
+ metadata: buildVobSubMetadata(parser),
424
+ timestamps
425
+ },
426
+ [timestamps.buffer],
427
+ _id
428
+ );
429
+ break;
430
+ }
431
+ case 'attachVobSubData': {
432
+ const parser = vobSubParsers.get(request.sessionId);
433
+ if (!parser) {
434
+ postResponse({ type: 'error', message: 'VobSub session not found for attachVobSubData' }, [], _id);
435
+ break;
436
+ }
437
+ parser.attachSubData(new Uint8Array(request.subData));
438
+ const timestamps = parser.getTimestamps();
439
+ postResponse(
440
+ {
441
+ type: 'vobSubProgress',
442
+ count: parser.count,
443
+ partial: false,
444
+ hasSubData: !!parser.hasSubData,
445
+ metadata: buildVobSubMetadata(parser),
446
+ timestamps
447
+ },
448
+ [timestamps.buffer],
449
+ _id
450
+ );
451
+ break;
452
+ }
238
453
  case 'loadVobSubMks': {
239
454
  disposeSession(request.sessionId);
240
455
  const parser = new wasmModule.VobSubParser();
@@ -271,6 +486,16 @@ self.onmessage = async function(event) {
271
486
  postResponse({ type: 'pgsFrame', frame: frameData, renderIssue }, frameData.compositions.map((c) => c.rgba.buffer), _id);
272
487
  break;
273
488
  }
489
+ case 'renderDvbAtIndex': {
490
+ const parser = dvbParsers.get(request.sessionId);
491
+ if (!parser) { postResponse({ type: 'dvbFrame', frame: null }, [], _id); break; }
492
+ const frame = parser.renderAtIndex(request.index);
493
+ const renderIssue = parser.lastRenderIssue || '';
494
+ if (!frame) { postResponse({ type: 'dvbFrame', frame: null, renderIssue }, [], _id); break; }
495
+ const frameData = convertFrame(frame, false);
496
+ postResponse({ type: 'dvbFrame', frame: frameData, renderIssue }, frameData.compositions.map((c) => c.rgba.buffer), _id);
497
+ break;
498
+ }
274
499
  case 'renderVobSubAtIndex': {
275
500
  const parser = vobSubParsers.get(request.sessionId);
276
501
  if (!parser) { postResponse({ type: 'vobSubFrame', frame: null }, [], _id); break; }
@@ -286,6 +511,11 @@ self.onmessage = async function(event) {
286
511
  postResponse({ type: 'pgsIndex', index: parser ? parser.findIndexAtTimestamp(request.timeMs) : -1 }, [], _id);
287
512
  break;
288
513
  }
514
+ case 'findDvbIndex': {
515
+ const parser = dvbParsers.get(request.sessionId);
516
+ postResponse({ type: 'dvbIndex', index: parser ? parser.findIndexAtTimestamp(request.timeMs) : -1 }, [], _id);
517
+ break;
518
+ }
289
519
  case 'findVobSubIndex': {
290
520
  const parser = vobSubParsers.get(request.sessionId);
291
521
  postResponse({ type: 'vobSubIndex', index: parser ? parser.findIndexAtTimestamp(request.timeMs) : -1 }, [], _id);
@@ -296,6 +526,11 @@ self.onmessage = async function(event) {
296
526
  postResponse({ type: 'pgsTimestamps', timestamps: parser ? parser.getTimestamps() : new Float64Array(0) }, [], _id);
297
527
  break;
298
528
  }
529
+ case 'getDvbTimestamps': {
530
+ const parser = dvbParsers.get(request.sessionId);
531
+ postResponse({ type: 'dvbTimestamps', timestamps: parser ? parser.getTimestamps() : new Float64Array(0) }, [], _id);
532
+ break;
533
+ }
299
534
  case 'getVobSubTimestamps': {
300
535
  const parser = vobSubParsers.get(request.sessionId);
301
536
  postResponse({ type: 'vobSubTimestamps', timestamps: parser ? parser.getTimestamps() : new Float64Array(0) }, [], _id);
@@ -306,6 +541,11 @@ self.onmessage = async function(event) {
306
541
  postResponse({ type: 'cleared' }, [], _id);
307
542
  break;
308
543
  }
544
+ case 'clearDvbCache': {
545
+ dvbParsers.get(request.sessionId)?.clearCache();
546
+ postResponse({ type: 'cleared' }, [], _id);
547
+ break;
548
+ }
309
549
  case 'clearVobSubCache': {
310
550
  vobSubParsers.get(request.sessionId)?.clearCache();
311
551
  postResponse({ type: 'cleared' }, [], _id);
@@ -320,6 +560,15 @@ self.onmessage = async function(event) {
320
560
  postResponse({ type: 'disposed' }, [], _id);
321
561
  break;
322
562
  }
563
+ case 'disposeDvb': {
564
+ const parser = dvbParsers.get(request.sessionId);
565
+ if (parser) {
566
+ parser.free();
567
+ dvbParsers.delete(request.sessionId);
568
+ }
569
+ postResponse({ type: 'disposed' }, [], _id);
570
+ break;
571
+ }
323
572
  case 'disposeVobSub': {
324
573
  const parser = vobSubParsers.get(request.sessionId);
325
574
  if (parser) {
@@ -344,63 +593,237 @@ self.onmessage = async function(event) {
344
593
  postResponse({ type: 'debandSet' }, [], _id);
345
594
  break;
346
595
  }
596
+ case 'attachOffscreenCanvas': {
597
+ const canvas = request.canvas;
598
+ if (!canvas || typeof canvas.getContext !== 'function') {
599
+ throw new Error('OffscreenCanvas attach requires a transferable OffscreenCanvas');
600
+ }
601
+ if (offscreenSurfaces.has(request.sessionId)) {
602
+ throw new Error('OffscreenCanvas already attached for this session');
603
+ }
604
+ const ctx = canvas.getContext('2d', { alpha: true, desynchronized: true });
605
+ if (!ctx) {
606
+ throw new Error('OffscreenCanvas 2D context unavailable');
607
+ }
608
+ const buffer = new OffscreenCanvas(1, 1);
609
+ const bufferCtx = buffer.getContext('2d', { alpha: true, desynchronized: true });
610
+ if (!bufferCtx) {
611
+ throw new Error('OffscreenCanvas buffer 2D context unavailable');
612
+ }
613
+ offscreenSurfaces.set(request.sessionId, { canvas, ctx, buffer, bufferCtx });
614
+ postResponse({ type: 'offscreenAttached' }, [], _id);
615
+ break;
616
+ }
617
+ case 'resizeOffscreenCanvas': {
618
+ const surface = offscreenSurfaces.get(request.sessionId);
619
+ if (surface) {
620
+ const width = Math.max(1, request.width | 0);
621
+ const height = Math.max(1, request.height | 0);
622
+ if (surface.canvas.width !== width) surface.canvas.width = width;
623
+ if (surface.canvas.height !== height) surface.canvas.height = height;
624
+ }
625
+ postResponse({ type: 'offscreenResized' }, [], _id);
626
+ break;
627
+ }
628
+ case 'detachOffscreenCanvas': {
629
+ detachOffscreenSurface(request.sessionId);
630
+ postResponse({ type: 'offscreenDetached' }, [], _id);
631
+ break;
632
+ }
633
+ case 'clearOffscreenCanvas': {
634
+ const surface = offscreenSurfaces.get(request.sessionId);
635
+ if (surface) {
636
+ surface.ctx.clearRect(0, 0, surface.canvas.width, surface.canvas.height);
637
+ }
638
+ postResponse({ type: 'offscreenCleared' }, [], _id);
639
+ break;
640
+ }
641
+ case 'presentOffscreen': {
642
+ const surface = offscreenSurfaces.get(request.sessionId);
643
+ if (!surface) {
644
+ postResponse({ type: 'offscreenPresented', status: 'failed', fatal: true, renderIssue: 'OFFSCREEN_SURFACE_MISSING' }, [], _id);
645
+ break;
646
+ }
647
+
648
+ const canvasWidth = Math.max(1, request.canvasWidth | 0);
649
+ const canvasHeight = Math.max(1, request.canvasHeight | 0);
650
+ const settings = request.displaySettings || {
651
+ scale: 1, aspectMode: 'stretch', verticalOffset: 0, horizontalOffset: 0,
652
+ horizontalAlign: 'center', bottomPadding: 0, safeArea: 0, opacity: 1
653
+ };
654
+
655
+ if (request.index < 0) {
656
+ if (surface.canvas.width !== canvasWidth) surface.canvas.width = canvasWidth;
657
+ if (surface.canvas.height !== canvasHeight) surface.canvas.height = canvasHeight;
658
+ surface.ctx.clearRect(0, 0, surface.canvas.width, surface.canvas.height);
659
+ postResponse({ type: 'offscreenPresented', status: 'cleared', compositionCount: 0 }, [], _id);
660
+ break;
661
+ }
662
+
663
+ let frame = null;
664
+ let renderIssue = '';
665
+ if (request.format === 'pgs') {
666
+ const parser = pgsParsers.get(request.sessionId);
667
+ if (!parser) {
668
+ postResponse({ type: 'offscreenPresented', status: 'failed', fatal: true, renderIssue: 'PARSER_MISSING' }, [], _id);
669
+ break;
670
+ }
671
+ const rendered = parser.renderAtIndex(request.index);
672
+ renderIssue = parser.lastRenderIssue || '';
673
+ frame = rendered ? convertFrame(rendered, false) : null;
674
+ } else if (request.format === 'dvb') {
675
+ const parser = dvbParsers.get(request.sessionId);
676
+ if (!parser) {
677
+ postResponse({ type: 'offscreenPresented', status: 'failed', fatal: true, renderIssue: 'PARSER_MISSING' }, [], _id);
678
+ break;
679
+ }
680
+ const rendered = parser.renderAtIndex(request.index);
681
+ renderIssue = parser.lastRenderIssue || '';
682
+ frame = rendered ? convertFrame(rendered, false) : null;
683
+ } else {
684
+ const parser = vobSubParsers.get(request.sessionId);
685
+ if (!parser) {
686
+ postResponse({ type: 'offscreenPresented', status: 'failed', fatal: true, renderIssue: 'PARSER_MISSING' }, [], _id);
687
+ break;
688
+ }
689
+ const rendered = parser.renderAtIndex(request.index);
690
+ renderIssue = parser.lastRenderIssue || '';
691
+ frame = rendered ? convertFrame(rendered, true) : null;
692
+ }
693
+
694
+ if (!frame) {
695
+ if (surface.canvas.width !== canvasWidth) surface.canvas.width = canvasWidth;
696
+ if (surface.canvas.height !== canvasHeight) surface.canvas.height = canvasHeight;
697
+ surface.ctx.clearRect(0, 0, surface.canvas.width, surface.canvas.height);
698
+ postResponse({
699
+ type: 'offscreenPresented',
700
+ status: renderIssue ? 'failed' : 'empty',
701
+ renderIssue,
702
+ compositionCount: 0
703
+ }, [], _id);
704
+ break;
705
+ }
706
+
707
+ const presented = presentFrameToOffscreen(surface, frame, canvasWidth, canvasHeight, settings);
708
+ postResponse({
709
+ type: 'offscreenPresented',
710
+ status: presented.status,
711
+ renderIssue,
712
+ width: presented.width,
713
+ height: presented.height,
714
+ compositionCount: presented.compositionCount,
715
+ bounds: presented.bounds || null
716
+ }, [], _id);
717
+ break;
718
+ }
347
719
  }
348
720
  } catch (error) {
349
721
  postResponse({ type: 'error', message: error instanceof Error ? error.message : String(error) }, [], _id);
350
722
  }
351
723
  };`;
352
724
  }
725
+ /** Whether the shared worker has finished WASM initialization. */
726
+ export function isWorkerReady() {
727
+ return sharedWorker !== null;
728
+ }
353
729
  /** Create or get the shared worker instance. */
354
730
  export function getOrCreateWorker() {
355
731
  if (sharedWorker)
356
732
  return Promise.resolve(sharedWorker);
357
733
  if (workerInitPromise)
358
734
  return workerInitPromise;
359
- workerInitPromise = new Promise((resolve, reject) => {
735
+ const initPromise = initializeWorker();
736
+ workerInitPromise = initPromise;
737
+ void initPromise.then(() => {
738
+ if (workerInitPromise === initPromise)
739
+ workerInitPromise = null;
740
+ }, () => {
741
+ if (workerInitPromise === initPromise)
742
+ workerInitPromise = null;
743
+ });
744
+ return initPromise;
745
+ }
746
+ /** Pre-initialize the shared subtitle worker. */
747
+ export function warmup() {
748
+ return ready();
749
+ }
750
+ /** Wait until the shared worker is ready for parse/render requests. */
751
+ export async function ready() {
752
+ if (!isWorkerAvailable())
753
+ return;
754
+ if (sharedWorker)
755
+ return;
756
+ await getOrCreateWorker();
757
+ }
758
+ async function initializeWorker() {
759
+ const blob = new Blob([createWorkerScript()], { type: 'application/javascript' });
760
+ const workerUrl = URL.createObjectURL(blob);
761
+ let worker;
762
+ try {
763
+ worker = new Worker(workerUrl, { type: 'module' });
764
+ }
765
+ catch (error) {
766
+ URL.revokeObjectURL(workerUrl);
767
+ throw error instanceof Error ? error : new Error(String(error));
768
+ }
769
+ worker.onmessage = (event) => {
770
+ const { _id, ...response } = event.data;
771
+ if (_id === undefined)
772
+ return;
773
+ const callback = pendingCallbacks.get(_id);
774
+ if (!callback)
775
+ return;
776
+ pendingCallbacks.delete(_id);
777
+ if (response.type === 'error' && callback.requestType === 'init') {
778
+ callback.reject(new Error(response.message));
779
+ }
780
+ else {
781
+ callback.resolve(response);
782
+ }
783
+ };
784
+ worker.onerror = (event) => {
785
+ const error = event instanceof ErrorEvent ? new Error(event.message) : new Error(String(event));
786
+ rejectWorkerCallbacks(worker, error);
787
+ if (sharedWorker === worker)
788
+ sharedWorker = null;
360
789
  try {
361
- const blob = new Blob([createWorkerScript()], { type: 'application/javascript' });
362
- const workerUrl = URL.createObjectURL(blob);
363
- const worker = new Worker(workerUrl, { type: 'module' });
364
- let initialized = false;
365
- worker.onmessage = (event) => {
366
- const { _id, ...response } = event.data;
367
- if (_id !== undefined) {
368
- const callback = pendingCallbacks.get(_id);
369
- if (callback) {
370
- pendingCallbacks.delete(_id);
371
- callback.resolve(response);
372
- }
373
- }
374
- };
375
- worker.onerror = (error) => {
376
- if (!initialized) {
377
- URL.revokeObjectURL(workerUrl);
378
- worker.terminate();
379
- workerInitPromise = null;
380
- reject(error instanceof ErrorEvent ? new Error(error.message) : new Error(String(error)));
381
- }
382
- };
383
- sendToWorkerInstance(worker, { type: 'init', wasmUrl: getWasmUrl() })
384
- .then(() => {
385
- URL.revokeObjectURL(workerUrl);
386
- initialized = true;
387
- sharedWorker = worker;
388
- workerInitPromise = null;
389
- resolve(worker);
390
- })
391
- .catch((err) => {
392
- URL.revokeObjectURL(workerUrl);
393
- worker.terminate();
394
- workerInitPromise = null;
395
- reject(err);
396
- });
790
+ worker.terminate();
397
791
  }
398
- catch (error) {
399
- workerInitPromise = null;
400
- reject(error instanceof Error ? error : new Error(String(error)));
792
+ catch {
793
+ /* ignore */
401
794
  }
402
- });
403
- return workerInitPromise;
795
+ };
796
+ try {
797
+ const initResponse = await sendToWorkerInstance(worker, {
798
+ type: 'init',
799
+ wasmUrl: getWasmUrl(),
800
+ glueUrl: getWasmGlueUrl()
801
+ });
802
+ if (initResponse.type === 'error') {
803
+ throw new Error(initResponse.message);
804
+ }
805
+ if (initResponse.type !== 'initComplete' || !initResponse.success) {
806
+ throw new Error('Worker WASM initialization failed');
807
+ }
808
+ sharedWorker = worker;
809
+ return worker;
810
+ }
811
+ catch (error) {
812
+ const workerError = error instanceof Error ? error : new Error(String(error));
813
+ rejectWorkerCallbacks(worker, workerError);
814
+ if (sharedWorker === worker)
815
+ sharedWorker = null;
816
+ try {
817
+ worker.terminate();
818
+ }
819
+ catch {
820
+ /* ignore */
821
+ }
822
+ throw workerError;
823
+ }
824
+ finally {
825
+ URL.revokeObjectURL(workerUrl);
826
+ }
404
827
  }
405
828
  /** Default timeout for worker operations (30 seconds for large files) */
406
829
  const WORKER_TIMEOUT = 30000;
@@ -419,6 +842,8 @@ function sendToWorkerInstance(worker, request, timeout = WORKER_TIMEOUT) {
419
842
  reject(new Error(`Worker operation timed out after ${timeout}ms`));
420
843
  }, timeout);
421
844
  pendingCallbacks.set(id, {
845
+ worker,
846
+ requestType: request.type,
422
847
  resolve: (response) => {
423
848
  clearTimeout(timeoutId);
424
849
  resolve(response);
@@ -433,7 +858,44 @@ function sendToWorkerInstance(worker, request, timeout = WORKER_TIMEOUT) {
433
858
  transfers.push(request.data);
434
859
  if ('subData' in request && request.subData instanceof ArrayBuffer)
435
860
  transfers.push(request.subData);
436
- worker.postMessage({ ...request, _id: id }, transfers);
861
+ if ('canvas' in request && request.canvas)
862
+ transfers.push(request.canvas);
863
+ try {
864
+ worker.postMessage({ ...request, _id: id }, transfers);
865
+ }
866
+ catch (error) {
867
+ pendingCallbacks.delete(id);
868
+ clearTimeout(timeoutId);
869
+ reject(error instanceof Error ? error : new Error(String(error)));
870
+ }
437
871
  });
438
872
  }
873
+ function rejectWorkerCallbacks(worker, error) {
874
+ for (const [id, callback] of pendingCallbacks) {
875
+ if (callback.worker !== worker)
876
+ continue;
877
+ pendingCallbacks.delete(id);
878
+ callback.reject(error);
879
+ }
880
+ }
881
+ /**
882
+ * Test-only helper: tear down shared worker state between Bun test files.
883
+ * Not part of the public runtime API.
884
+ */
885
+ export function resetWorkerForTests() {
886
+ if (sharedWorker) {
887
+ try {
888
+ sharedWorker.terminate();
889
+ }
890
+ catch {
891
+ /* ignore */
892
+ }
893
+ }
894
+ sharedWorker = null;
895
+ workerInitPromise = null;
896
+ for (const [, callback] of pendingCallbacks) {
897
+ callback.reject(new Error('Worker reset for tests'));
898
+ }
899
+ pendingCallbacks.clear();
900
+ }
439
901
  //# sourceMappingURL=worker.js.map