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/pkg/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # libbit(map)sub
2
2
 
3
- High-performance WASM renderer for graphical subtitles (PGS, VobSub, and MKS-embedded VobSub), written in Rust.
3
+ High-performance WASM renderer for graphical subtitles (PGS, VobSub, DVB-SUB, and MKS-embedded VobSub), written in Rust.
4
4
 
5
5
  Started as a fork of Arcus92's [libpgs-js](https://github.com/Arcus92/libpgs-js), this project was reworked for higher performance and broader format support. It keeps the familiar high-level PGS-oriented API while adding a lower-level parser surface, VobSub support, GPU backends, and worker-backed rendering.
6
6
 
@@ -8,9 +8,11 @@ Started as a fork of Arcus92's [libpgs-js](https://github.com/Arcus92/libpgs-js)
8
8
 
9
9
  - PGS (Blu-ray) subtitle parsing and rendering
10
10
  - VobSub (DVD) subtitle parsing and rendering
11
+ - DVB-SUB (ETSI EN 300 743) parsing and rendering via PES/ES and `"DV"` PTS-framed dumps
11
12
  - Matroska `.mks` extraction for embedded `S_VOBSUB` tracks
12
13
  - WebGPU, WebGL2, and Canvas2D rendering with automatic fallback
13
14
  - Worker-backed parsing/rendering for large subtitle files
15
+ - HTTP Range / streaming loads for large `.sup`, `.sub`, `.idx`, and `.mks` assets (progressive indexing where feasible)
14
16
  - Rich layout controls: scale, aspect mode, horizontal/vertical offsets, alignment, bottom padding, safe area, opacity
15
17
  - Cue metadata and parser introspection APIs
16
18
  - Rendered-frame export helpers for ImageData, ImageBitmap, Blob, and custom canvas targets
@@ -18,6 +20,7 @@ Started as a fork of Arcus92's [libpgs-js](https://github.com/Arcus92/libpgs-js)
18
20
  - Automatic format detection and unified loading helpers
19
21
  - First-class diagnostics with structured error codes, warning hooks, and render/cache snapshots
20
22
  - TypeScript support with exported event and metadata types
23
+ - Optional player integrations for Video.js, Shaka Player, hls.js, and React (core stays dependency-free)
21
24
 
22
25
  ## Showcase
23
26
 
@@ -67,11 +70,11 @@ deno add jsr:@altq/libbitsub
67
70
 
68
71
  In most bundler-based projects, no manual worker setup is required. `libbitsub` now resolves the WASM asset relative to the package module URL, so bundlers such as Vite, webpack, and Rollup can emit the asset automatically.
69
72
 
70
- If your app serves package files in a way that does not expose that emitted WASM asset to the browser, you can still provide the legacy public fallback by copying the WASM file to `/libbitsub/libbitsub_bg.wasm`:
73
+ If your app serves package files in a way that does not expose that emitted WASM asset to the browser, you can still provide the legacy public fallback by copying the WASM file and its JS glue to `/libbitsub/`:
71
74
 
72
75
  ```bash
73
76
  mkdir -p public/libbitsub
74
- cp node_modules/libbitsub/pkg/libbitsub_bg.wasm public/libbitsub/
77
+ cp node_modules/libbitsub/pkg/libbitsub_bg.wasm node_modules/libbitsub/pkg/libbitsub.js public/libbitsub/
75
78
  ```
76
79
 
77
80
  The worker is still created inline. `workerUrl` remains in the option type only for compatibility and does not change runtime behavior.
@@ -110,6 +113,129 @@ const parser = new PgsParser()
110
113
 
111
114
  Calling `initWasm()` multiple times is safe (it deduplicates).
112
115
 
116
+ ### Worker prewarm (TV / players)
117
+
118
+ On devices where the first subtitle track switch should not pay worker + WASM startup cost (for example TV apps), prewarm the shared parsing worker during app startup:
119
+
120
+ ```ts
121
+ import { warmup, ready, PgsRenderer } from 'libbitsub'
122
+
123
+ // Fire-and-forget at app boot (safe to call multiple times).
124
+ void warmup()
125
+
126
+ // Before the user changes tracks, ensure the worker is ready.
127
+ await ready()
128
+
129
+ const renderer = new PgsRenderer({ video: videoElement, subContent: trackBytes })
130
+ ```
131
+
132
+ `warmup()` and `ready()` share a single worker-init promise with renderer creation. The shared worker is only published after in-worker WASM initialization succeeds. When Web Workers are unavailable, both resolve immediately and renderers use the main thread.
133
+
134
+ ### Range / streaming loads (TV / slow networks)
135
+
136
+ URL-based loads (`subUrl` / `idxUrl`) use progressive fetch by default:
137
+
138
+ - Probe HTTP Range support and, for large assets, download with Range chunks when the origin allows it
139
+ - Stream the response body otherwise and report `load-progress` events
140
+ - **PGS (`.sup`)**: feed chunks into the parser as they arrive so cues can be indexed before the full file finishes
141
+ - **VobSub (`.idx` + `.sub`)**: parse the small `.idx` first for timestamps, then stream/Range-download the `.sub` body and attach packets
142
+ - **MKS**: stream/Range-download the container first (Matroska extraction still needs the assembled payload), then parse
143
+ - **In-memory** `subContent` / `idxContent` remains the simple full-buffer path
144
+
145
+ ```ts
146
+ import { PgsRenderer } from 'libbitsub'
147
+
148
+ const renderer = new PgsRenderer({
149
+ video: videoElement,
150
+ subUrl: '/subtitles/movie.sup',
151
+ // streamingLoad: true, // default
152
+ // rangeRequests: true, // default
153
+ onEvent: (event) => {
154
+ if (event.type === 'load-progress') {
155
+ console.log(event.strategy, event.loadedBytes, event.totalBytes, event.indexedCues)
156
+ }
157
+ if (event.type === 'indexed') {
158
+ console.log('cues ready', event.metadata.cueCount, event.partial ? '(partial)' : '(final)')
159
+ }
160
+ }
161
+ })
162
+ ```
163
+
164
+ Disable progressive behavior when you want a single blocking GET:
165
+
166
+ ```ts
167
+ new PgsRenderer({ video, subUrl, streamingLoad: false, rangeRequests: false })
168
+ ```
169
+
170
+ Low-level helpers are also exported: `probeRangeSupport()`, `fetchSubtitleAsset()`, and `fetchSubtitleText()`.
171
+
172
+ ## Player integrations
173
+
174
+ Optional adapters ship as **subpath exports**. They wrap the high-level renderers for common players while keeping the core package free of player/React dependencies (peer deps are optional and only needed when you import that adapter).
175
+
176
+ | Import | Adapter | Optional peer |
177
+ | ------------------------ | ---------------------------------------- | ------------------- |
178
+ | `libbitsub/videojs` | Video.js plugin (`registerBitSubPlugin`) | `video.js` >= 7 |
179
+ | `libbitsub/shaka` | `attachBitSubToShaka(player, options)` | `shaka-player` >= 4 |
180
+ | `libbitsub/hlsjs` | `attachBitSubToHls(hls, options)` | `hls.js` >= 1 |
181
+ | `libbitsub/react` | `useBitSub` / `BitSubOverlay` | `react` >= 18 |
182
+ | `libbitsub/integrations` | Shared `attachBitSub` controller | — |
183
+
184
+ Copy-paste recipes live under [`examples/`](./examples).
185
+
186
+ ### Video.js
187
+
188
+ ```ts
189
+ import videojs from 'video.js'
190
+ import { registerBitSubPlugin } from 'libbitsub/videojs'
191
+
192
+ registerBitSubPlugin(videojs)
193
+ const player = videojs('my-video')
194
+ const bitsub = player.bitsub({ subUrl: '/subs/movie.sup' })
195
+ bitsub.load({ subUrl: '/subs/other.sup' })
196
+ ```
197
+
198
+ ### Shaka Player
199
+
200
+ ```ts
201
+ import { attachBitSubToShaka } from 'libbitsub/shaka'
202
+
203
+ const bitsub = attachBitSubToShaka(player, { subUrl: '/subs/movie.sup' })
204
+ // later
205
+ bitsub.dispose()
206
+ ```
207
+
208
+ ### hls.js
209
+
210
+ ```ts
211
+ import Hls from 'hls.js'
212
+ import { attachBitSubToHls } from 'libbitsub/hlsjs'
213
+
214
+ const hls = new Hls()
215
+ hls.loadSource(url)
216
+ hls.attachMedia(video)
217
+ const bitsub = attachBitSubToHls(hls, { subUrl: '/subs/movie.sup' })
218
+ ```
219
+
220
+ ### React
221
+
222
+ ```tsx
223
+ import { useRef } from 'react'
224
+ import { useBitSub } from 'libbitsub/react'
225
+
226
+ function Player({ src, subUrl }: { src: string; subUrl: string }) {
227
+ const videoRef = useRef<HTMLVideoElement>(null)
228
+ useBitSub(videoRef, { subUrl })
229
+ return (
230
+ <div style={{ position: 'relative' }}>
231
+ <video ref={videoRef} src={src} controls playsInline />
232
+ </div>
233
+ )
234
+ }
235
+ ```
236
+
237
+ Bitmap PGS/VobSub/MKS tracks are external canvas overlays, not native HTML/TextTrack entries. Prefer deep imports so unused adapters stay out of your bundle.
238
+
113
239
  ## High-level video renderers
114
240
 
115
241
  The high-level API manages subtitle loading, canvas overlay creation, playback sync, resize handling, worker usage, and renderer fallback.
@@ -131,6 +257,7 @@ const renderer = new PgsRenderer({
131
257
  },
132
258
  cacheLimit: 32,
133
259
  prefetchWindow: { before: 1, after: 2 },
260
+ frameAwareSync: true,
134
261
  onWarning: (warning) => {
135
262
  console.warn(warning.code, warning.message, warning.details)
136
263
  },
@@ -144,6 +271,33 @@ const renderer = new PgsRenderer({
144
271
  renderer.dispose()
145
272
  ```
146
273
 
274
+ Mount into a fullscreen shell or Shadow DOM root, reuse a player-owned canvas,
275
+ cap high-DPI backing-store cost, or bypass automatic backend selection:
276
+
277
+ ```ts
278
+ const renderer = new PgsRenderer({
279
+ video: videoElement,
280
+ subUrl: '/subtitles/movie.sup',
281
+ container: shadowRoot, // HTMLElement or ShadowRoot; defaults to video.parentElement
282
+ canvas: playerOverlayCanvas, // optional; remains mounted after dispose()
283
+ devicePixelRatioCap: 2,
284
+ backend: 'webgl2' // 'auto' | 'webgpu' | 'webgl2' | 'worker-offscreen' | 'canvas2d'
285
+ })
286
+ ```
287
+
288
+ An explicitly requested GPU or worker backend falls back to Canvas2D if it cannot
289
+ initialize. A supplied `container` takes precedence over the canvas's current parent;
290
+ without one, an already-mounted caller canvas stays in its existing host. Automatic
291
+ selection does not transfer a caller-owned canvas to a worker; request
292
+ `backend: 'worker-offscreen'` explicitly to allow that ownership transfer.
293
+
294
+ Frame-aware synchronization is enabled by default. On browsers with
295
+ `requestVideoFrameCallback()`, cue lookup uses the `mediaTime` of the frame sent
296
+ to the compositor instead of polling `video.currentTime`. Older browsers fall
297
+ back to `requestAnimationFrame()` automatically; set `frameAwareSync: false` to
298
+ force that compatibility path. The active path is available as
299
+ `renderer.getSynchronizationMode()` and as `getStats().syncMode`.
300
+
147
301
  ### VobSub renderer
148
302
 
149
303
  ```ts
@@ -165,6 +319,45 @@ renderer.setDebandThreshold(64)
165
319
  renderer.setDebandRange(15)
166
320
  ```
167
321
 
322
+ ### DVB renderer
323
+
324
+ ```ts
325
+ import { DvbRenderer } from 'libbitsub'
326
+
327
+ const renderer = new DvbRenderer({
328
+ video: videoElement,
329
+ subUrl: '/subtitles/track.dvb'
330
+ })
331
+ ```
332
+
333
+ DVB dumps use libbitsub framing:
334
+
335
+ ```
336
+ "DV" | pts_90k_be_u32 | payload_len_be_u32 | payload…
337
+ ```
338
+
339
+ `payload` is a PES data field (`0x20 0x00 …`) or raw segments (`sync_byte 0x0F`). Concatenated MPEG PES packets (`00 00 01 BD`) with PTS are also accepted.
340
+
341
+ The DVB decoder renders bitmap object coding method 0. Character-string and reserved object coding methods are ignored.
342
+
343
+ ### Live / push streams
344
+
345
+ PGS and DVB renderers can start without a URL or in-memory subtitle file. Push transport or demuxer chunks as they arrive; calls are processed in order even when they are made before renderer initialization completes.
346
+
347
+ ```ts
348
+ import { DvbRenderer } from 'libbitsub'
349
+
350
+ const renderer = new DvbRenderer({ video: videoElement })
351
+
352
+ const added = await renderer.append(dvbPesChunk) // newly indexed cues
353
+ const total = await renderer.flush() // finalize trailing input; total indexed cues
354
+
355
+ await renderer.reset() // clear cues/caches/display, then reuse the same renderer
356
+ await renderer.append(nextChannelChunk)
357
+ ```
358
+
359
+ `append()` accepts `Uint8Array` or `ArrayBuffer` without detaching the caller's buffer. `flush()` discards incomplete trailing bytes; later appends are still accepted. `reset()` keeps the canvas, worker session, render loop, and configuration alive while clearing the stream state. VobSub does not expose the push API because it depends on its `.idx`/container index.
360
+
168
361
  ### Automatic format detection
169
362
 
170
363
  ```ts
@@ -177,7 +370,7 @@ const renderer = createAutoSubtitleRenderer({
177
370
  })
178
371
  ```
179
372
 
180
- Automatic detection uses file hints when available and otherwise inspects the binary payload. `.mks` sources are treated as VobSub only when they contain an embedded `S_VOBSUB` track. If the format cannot be identified confidently, it throws instead of silently forcing a parser.
373
+ Automatic detection uses file hints when available and otherwise inspects the binary payload. `.idx` / `idxContent` still means VobSub. Bare `.sub` is content-probed for DVB before VobSub. `.mks` sources are treated as VobSub only when they contain an embedded `S_VOBSUB` track. If the format cannot be identified confidently, it throws instead of silently forcing a parser.
181
374
 
182
375
  ## Layout controls
183
376
 
@@ -207,16 +400,16 @@ renderer.resetDisplaySettings()
207
400
 
208
401
  `SubtitleDisplaySettings`:
209
402
 
210
- | Field | Type | Range / values | Meaning |
211
- | --- | --- | --- | --- |
212
- | `scale` | number | `0.1` to `3.0` | Overall subtitle scale |
213
- | `aspectMode` | `'stretch' \| 'contain' \| 'cover'` | fixed set | How subtitle screen coordinates map into the visible video box |
214
- | `verticalOffset` | number | `-50` to `50` | Vertical movement as percent of video height |
215
- | `horizontalOffset` | number | `-50` to `50` | Horizontal movement as percent of video width |
216
- | `horizontalAlign` | `'left' \| 'center' \| 'right'` | fixed set | Anchor used when scaling subtitle groups |
217
- | `bottomPadding` | number | `0` to `50` | Extra padding from the bottom edge |
218
- | `safeArea` | number | `0` to `25` | Clamp subtitles inside a video-safe area |
219
- | `opacity` | number | `0.0` to `1.0` | Global subtitle opacity |
403
+ | Field | Type | Range / values | Meaning |
404
+ | ------------------ | ----------------------------------- | -------------- | -------------------------------------------------------------- |
405
+ | `scale` | number | `0.1` to `3.0` | Overall subtitle scale |
406
+ | `aspectMode` | `'stretch' \| 'contain' \| 'cover'` | fixed set | How subtitle screen coordinates map into the visible video box |
407
+ | `verticalOffset` | number | `-50` to `50` | Vertical movement as percent of video height |
408
+ | `horizontalOffset` | number | `-50` to `50` | Horizontal movement as percent of video width |
409
+ | `horizontalAlign` | `'left' \| 'center' \| 'right'` | fixed set | Anchor used when scaling subtitle groups |
410
+ | `bottomPadding` | number | `0` to `50` | Extra padding from the bottom edge |
411
+ | `safeArea` | number | `0` to `25` | Clamp subtitles inside a video-safe area |
412
+ | `opacity` | number | `0.0` to `1.0` | Global subtitle opacity |
220
413
 
221
414
  ## Metadata and introspection
222
415
 
@@ -291,14 +484,7 @@ Like `UnifiedSubtitleParser.loadAuto()`, this is a low-level in-memory API: pass
291
484
  Low-level parser output can be flattened into a single exportable frame for previews, editors, snapshots, fixture generation, or visual diffing.
292
485
 
293
486
  ```ts
294
- import {
295
- PgsParser,
296
- initWasm,
297
- renderFrameData,
298
- toBlob,
299
- toCanvas,
300
- toImageBitmap
301
- } from 'libbitsub'
487
+ import { PgsParser, initWasm, renderFrameData, toBlob, toCanvas, toImageBitmap } from 'libbitsub'
302
488
 
303
489
  await initWasm()
304
490
 
@@ -385,7 +571,7 @@ Structured diagnostic error codes currently include:
385
571
  - `WORKER_FALLBACK`
386
572
  - `UNKNOWN`
387
573
 
388
- Warnings use a smaller code set focused on non-fatal conditions such as malformed bitmap buffers, worker fallback, and missing PGS palettes during render.
574
+ Warnings use a smaller code set focused on non-fatal conditions such as malformed bitmap buffers, worker fallback, progressive/Range fallback (`RANGE_FALLBACK`), and missing PGS palettes during render.
389
575
 
390
576
  ## MKS security and corruption checks
391
577
 
@@ -415,6 +601,8 @@ const renderer = new PgsRenderer({
415
601
  onEvent: (event) => {
416
602
  switch (event.type) {
417
603
  case 'loading':
604
+ case 'load-progress':
605
+ case 'indexed':
418
606
  case 'loaded':
419
607
  case 'error':
420
608
  case 'warning':
@@ -482,17 +670,17 @@ renderer.dispose()
482
670
 
483
671
  Emitted events:
484
672
 
485
- | Event | Payload |
486
- | --- | --- |
487
- | `loading` | subtitle format |
488
- | `loaded` | subtitle format and parser metadata |
489
- | `error` | subtitle format and `SubtitleDiagnosticErrorLike` |
490
- | `warning` | `SubtitleDiagnosticWarning` |
491
- | `renderer-change` | active backend: `webgpu`, `webgl2`, or `canvas2d` |
492
- | `worker-state` | whether worker mode is enabled, ready, fallback status, and the active session ID |
493
- | `cache-change` | cached frame count, pending renders, and configured cache limit |
494
- | `cue-change` | current cue metadata or `null` when nothing is displayed |
495
- | `stats` | periodic renderer stats snapshot |
673
+ | Event | Payload |
674
+ | ----------------- | --------------------------------------------------------------------------------- |
675
+ | `loading` | subtitle format |
676
+ | `loaded` | subtitle format and parser metadata |
677
+ | `error` | subtitle format and `SubtitleDiagnosticErrorLike` |
678
+ | `warning` | `SubtitleDiagnosticWarning` |
679
+ | `renderer-change` | active backend: `webgpu`, `webgl2`, `worker-offscreen`, or `canvas2d` |
680
+ | `worker-state` | whether worker mode is enabled, ready, fallback status, and the active session ID |
681
+ | `cache-change` | cached frame count, pending renders, and configured cache limit |
682
+ | `cue-change` | current cue metadata or `null` when nothing is displayed |
683
+ | `stats` | periodic renderer stats snapshot |
496
684
 
497
685
  ## Performance stats
498
686
 
@@ -514,6 +702,7 @@ const stats = renderer.getStats()
514
702
  - `pendingRenders`
515
703
  - `totalEntries`
516
704
  - `currentIndex`
705
+ - `syncMode`
517
706
 
518
707
  `getCacheStats()` adds worker/session state for the current renderer cache, while `getLastRenderInfo()` exposes the most recent render outcome with a status of `rendered`, `cleared`, `pending`, `empty`, or `failed`.
519
708
 
@@ -563,23 +752,50 @@ const parser = new UnifiedSubtitleParser({
563
752
  parser.loadAuto({ data: subtitleBytes, fileName: 'track.sup' })
564
753
  ```
565
754
 
566
- ## GPU backends
755
+ ## GPU / present backends
567
756
 
568
757
  libbitsub prefers:
569
758
 
570
- 1. WebGPU
571
- 2. WebGL2
572
- 3. Canvas2D
759
+ 1. WebGPU (main-thread present)
760
+ 2. WebGL2 (main-thread present)
761
+ 3. Worker OffscreenCanvas (decode + Canvas2D present off the UI thread)
762
+ 4. Canvas2D (main-thread fallback for older TVs)
573
763
 
574
764
  ```ts
575
- import { isWebGPUSupported } from 'libbitsub'
765
+ import { getRuntimeCapabilities, isWebGPUSupported } from 'libbitsub'
576
766
 
577
767
  console.log({
578
- webgpu: isWebGPUSupported()
768
+ webgpu: isWebGPUSupported(),
769
+ capabilities: getRuntimeCapabilities()
579
770
  })
580
771
  ```
581
772
 
582
- WebGL2 and Canvas2D fallback remain automatic. Use `onWebGPUFallback`, `onWebGL2Fallback`, `onEvent`, or `onWarning` if you need to observe backend changes.
773
+ When Worker OffscreenCanvas is selected, composition/present moves into the shared worker —
774
+ not only parsing — which reduces UI stalls during subtitle track switches on constrained devices.
775
+ The existing transferable RGBA path remains for main-thread GPU/Canvas2D present.
776
+
777
+ Use `onWebGPUFallback`, `onWebGL2Fallback`, `onEvent`, `onWarning`, or `getRuntimeCapabilities()`
778
+ to observe or explain the active backend path. Set `offscreenRender: false` to force main-thread
779
+ Canvas2D on the software tier.
780
+
781
+ ## Compatibility & visual regression
782
+
783
+ A formal suite locks decoder edge cases and backend pixel parity:
784
+
785
+ - Malformed PGS / VobSub / MKS fixtures
786
+ - Palette edge cases and zero-length RLE runs
787
+ - Alternate display sizes (SD → 4K)
788
+ - Slow worker startup / shared init
789
+ - Pixel-level goldens across software, Canvas2D, WebGL2, and WebGPU
790
+
791
+ ```bash
792
+ bun run test # Rust core (includes compatibility fixtures)
793
+ bun run test:ts # TypeScript fixtures, goldens, worker startup
794
+ bun run test:visual # Headless Chromium backend parity
795
+ bun run test:all # All of the above
796
+ ```
797
+
798
+ Browser/device support matrix (including webOS TV): [docs/COMPATIBILITY.md](./docs/COMPATIBILITY.md).
583
799
 
584
800
  ## Notes
585
801
 
@@ -594,10 +810,13 @@ WebGL2 and Canvas2D fallback remain automatic. Use `onWebGPUFallback`, `onWebGL2
594
810
 
595
811
  - `initWasm(): Promise<void>` initializes the WASM module. Called automatically by high-level renderers and on first import in browser environments. Safe to call multiple times. Only needed explicitly for low-level parser usage.
596
812
  - `isWasmInitialized(): boolean` reports whether initialization has completed.
813
+ - `warmup(): Promise<void>` / `ready(): Promise<void>` pre-initialize the shared subtitle worker (including in-worker WASM). Safe to call multiple times; concurrent callers share one init promise. Prefer these in TV/player apps before the first track switch.
814
+ - `isWorkerAvailable(): boolean` / `isWorkerReady(): boolean` report worker support and whether the shared worker has finished initializing.
597
815
  - `isWebGPUSupported(): boolean` checks WebGPU support.
598
- - `detectSubtitleFormat(source: AutoSubtitleSource): 'pgs' | 'vobsub' | null` detects the bitmap subtitle format from file hints or binary data.
599
- - `createAutoSubtitleRenderer(options: AutoVideoSubtitleOptions): PgsRenderer | VobSubRenderer` creates a high-level renderer after format detection.
816
+ - `detectSubtitleFormat(source: AutoSubtitleSource): 'pgs' | 'vobsub' | 'dvb' | null` detects the bitmap subtitle format from file hints or binary data.
817
+ - `createAutoSubtitleRenderer(options: AutoVideoSubtitleOptions): PgsRenderer | VobSubRenderer | DvbRenderer` creates a high-level renderer after format detection.
600
818
  - `openSubtitles(source, options?): Promise<OpenedSubtitles>` initializes WASM, auto-detects the low-level format, and returns a normalized parser handle.
819
+ - `probeRangeSupport(url, options?)` / `fetchSubtitleAsset(url, options?, onChunk?)` / `fetchSubtitleText(url, options?)` perform Range-aware progressive asset downloads for custom loaders.
601
820
  - `renderFrameData(frame, options?): SubtitleRenderedFrameData | null` composes a `SubtitleData` frame into exportable pixels.
602
821
  - `toCanvas(frame, target?, options?): HTMLCanvasElement | OffscreenCanvas` draws a rendered frame to a new or existing canvas/context.
603
822
  - `toImageBitmap(frame, options?): Promise<ImageBitmap>` creates an `ImageBitmap` from a subtitle frame export.
@@ -605,6 +824,7 @@ WebGL2 and Canvas2D fallback remain automatic. Use `onWebGPUFallback`, `onWebGL2
605
824
  - `SubtitleDiagnosticError` is the structured error class libbitsub uses for coded diagnostics.
606
825
  - `createSubtitleDiagnosticError(...)` and `normalizeSubtitleError(...)` are exported for integrations that want to preserve libbitsub-style error codes.
607
826
  - Legacy aliases remain exported: `PGSRenderer`, `VobsubRenderer`, `UnifiedSubtitleRenderer`.
827
+ - Optional integration entry points: `libbitsub/videojs`, `libbitsub/shaka`, `libbitsub/hlsjs`, `libbitsub/react`, `libbitsub/integrations` (see [examples/](./examples) and [references/api.md](./references/api.md)).
608
828
 
609
829
  ### High-level renderers
610
830
 
@@ -625,21 +845,30 @@ WebGL2 and Canvas2D fallback remain automatic. Use `onWebGPUFallback`, `onWebGL2
625
845
  - `clearFrameCache(): void` clears the renderer-side and parser-side frame cache.
626
846
  - `prefetchRange(startIndex: number, endIndex: number): Promise<void>` prefetches decoded frames for a cue range.
627
847
  - `prefetchAroundTime(time: number, before?: number, after?: number): Promise<void>` prefetches around a playback time in seconds.
848
+ - `append(data: Uint8Array | ArrayBuffer): Promise<number>` pushes live bytes and returns newly indexed cues.
849
+ - `flush(): Promise<number>` finalizes trailing input and returns the total cue count.
850
+ - `reset(): Promise<void>` clears live parser, cue, cache, and presentation state for reuse.
628
851
  - `dispose(): void` releases DOM, parser, and worker resources.
629
852
 
630
853
  #### `VobSubRenderer`
631
854
 
632
- - Supports all `PgsRenderer` methods above.
855
+ - Supports the common `PgsRenderer` methods above, except `append()`, `flush()`, and `reset()`.
633
856
  - `setDebandEnabled(enabled: boolean): void` enables or disables debanding.
634
857
  - `setDebandThreshold(threshold: number): void` updates the deband threshold.
635
858
  - `setDebandRange(range: number): void` updates the deband sample range.
636
859
  - `debandEnabled: boolean` reports whether debanding is enabled.
637
860
 
861
+ #### `DvbRenderer`
862
+
863
+ - Supports all `PgsRenderer` methods above, including the live push API (no debanding).
864
+
638
865
  ### Low-level parsers
639
866
 
640
867
  #### `PgsParser`
641
868
 
642
- - `load(data: Uint8Array): number` loads PGS data and returns the cue count.
869
+ - `load(data: Uint8Array): number` loads PGS data and returns the cue count (simple in-memory path).
870
+ - `reset(): void` / `feed(data: Uint8Array): number` / `finishFeed(): number` progressive indexing APIs for streamed chunks.
871
+ - `pendingLen: number` incomplete trailing bytes retained between `feed()` calls.
643
872
  - `getTimestamps(): Float64Array` returns cue timestamps in milliseconds.
644
873
  - `count: number` returns the number of cues.
645
874
  - `findIndexAtTimestamp(timeSeconds: number): number` finds the cue index for a playback time in seconds.
@@ -650,9 +879,18 @@ WebGL2 and Canvas2D fallback remain automatic. Use `onWebGPUFallback`, `onWebGL2
650
879
  - `clearCache(): void` clears parser-side caches.
651
880
  - `dispose(): void` frees parser resources.
652
881
 
882
+ #### `DvbParser`
883
+
884
+ - Same progressive surface as `PgsParser` for `"DV"`-framed dumps and MPEG PES streams.
885
+ - `load(data: Uint8Array): number` / `feed` / `finishFeed` / `renderAtIndex` / `renderAtTimestamp` / `getMetadata` / `getCueMetadata` / `dispose`.
886
+ - `getEndTimestamps(): Float64Array` returns the page-timeout-aware cue end timestamps in milliseconds.
887
+
653
888
  #### `VobSubParserLowLevel`
654
889
 
655
- - `loadFromData(idxContent: string, subData: Uint8Array): void` loads IDX and SUB data.
890
+ - `loadFromData(idxContent: string, subData: Uint8Array): void` loads IDX and SUB data (simple in-memory path).
891
+ - `loadFromIdx(idxContent: string): void` indexes timestamps from IDX before packet bytes arrive.
892
+ - `attachSubData(subData: Uint8Array): void` attaches `.sub` packets after an IDX-first load.
893
+ - `hasSubData: boolean` whether packet bytes are currently attached.
656
894
  - `loadFromSubOnly(subData: Uint8Array): void` loads SUB-only VobSub data.
657
895
  - `getTimestamps(): Float64Array`, `count`, `findIndexAtTimestamp()`, `renderAtIndex()`, `renderAtTimestamp()`, `getMetadata()`, `getCueMetadata()`, `clearCache()`, and `dispose()` behave like `PgsParser`.
658
896
  - `setDebandEnabled(enabled: boolean): void`, `setDebandThreshold(threshold: number): void`, `setDebandRange(range: number): void`, and `debandEnabled` control debanding.
@@ -662,8 +900,9 @@ WebGL2 and Canvas2D fallback remain automatic. Use `onWebGPUFallback`, `onWebGL2
662
900
  - `loadPgs(data: Uint8Array): number` loads PGS data.
663
901
  - `loadVobSub(idxContent: string, subData: Uint8Array): void` loads VobSub from IDX and SUB.
664
902
  - `loadVobSubOnly(subData: Uint8Array): void` loads SUB-only VobSub data.
903
+ - `loadDvb(data: Uint8Array): number` loads DVB data.
665
904
  - `loadAuto(source: AutoSubtitleSource): SubtitleFormatName` detects and loads a supported bitmap subtitle format.
666
- - `format: 'pgs' | 'vobsub' | null` returns the active format.
905
+ - `format: 'pgs' | 'vobsub' | 'dvb' | null` returns the active format.
667
906
  - `getTimestamps()`, `count`, `findIndexAtTimestamp()`, `renderAtIndex()`, `renderAtTimestamp()`, `getMetadata()`, `getCueMetadata()`, `clearCache()`, and `dispose()` are available as on the format-specific parsers.
668
907
 
669
908
  ### Core option and data types
@@ -673,6 +912,10 @@ WebGL2 and Canvas2D fallback remain automatic. Use `onWebGPUFallback`, `onWebGL2
673
912
  ```ts
674
913
  interface VideoSubtitleOptions {
675
914
  video: HTMLVideoElement
915
+ container?: HTMLElement | ShadowRoot
916
+ canvas?: HTMLCanvasElement
917
+ devicePixelRatioCap?: number
918
+ backend?: 'auto' | 'webgpu' | 'webgl2' | 'worker-offscreen' | 'canvas2d'
676
919
  subUrl?: string
677
920
  subContent?: ArrayBuffer
678
921
  workerUrl?: string
@@ -681,12 +924,16 @@ interface VideoSubtitleOptions {
681
924
  onError?: (error: Error) => void
682
925
  onWebGPUFallback?: () => void
683
926
  onWebGL2Fallback?: () => void
927
+ offscreenRender?: boolean // default true — Worker OffscreenCanvas on Canvas2D tier
928
+ frameAwareSync?: boolean // default true — sync to presented-frame mediaTime when available
684
929
  displaySettings?: Partial<SubtitleDisplaySettings>
685
930
  cacheLimit?: number
686
931
  prefetchWindow?: {
687
932
  before?: number
688
933
  after?: number
689
934
  }
935
+ streamingLoad?: boolean // default true — progressive URL loads
936
+ rangeRequests?: boolean // default true — use HTTP Range when supported
690
937
  onEvent?: (event: SubtitleRendererEvent) => void
691
938
  }
692
939
  ```
@@ -733,7 +980,7 @@ type SubtitleRendererEvent =
733
980
  | { type: 'loaded'; format: SubtitleFormatName; metadata: SubtitleParserMetadata }
734
981
  | { type: 'error'; format: SubtitleFormatName; error: SubtitleDiagnosticErrorLike }
735
982
  | { type: 'warning'; warning: SubtitleDiagnosticWarning }
736
- | { type: 'renderer-change'; renderer: 'webgpu' | 'webgl2' | 'canvas2d' }
983
+ | { type: 'renderer-change'; renderer: 'webgpu' | 'webgl2' | 'worker-offscreen' | 'canvas2d' }
737
984
  | { type: 'worker-state'; enabled: boolean; ready: boolean; sessionId: string | null; fallback?: boolean }
738
985
  | { type: 'cache-change'; cachedFrames: number; pendingRenders: number; cacheLimit: number }
739
986
  | { type: 'cue-change'; cue: SubtitleCueMetadata | null }
@@ -766,7 +1013,7 @@ Related diagnostics shapes:
766
1013
 
767
1014
  ```ts
768
1015
  interface SubtitleParserMetadata {
769
- format: 'pgs' | 'vobsub'
1016
+ format: 'pgs' | 'vobsub' | 'dvb'
770
1017
  cueCount: number
771
1018
  screenWidth: number
772
1019
  screenHeight: number