libbitsub 1.10.1 → 1.11.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 (91) hide show
  1. package/LICENSE +1 -0
  2. package/README.md +186 -36
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +1 -1
  6. package/dist/index.js.map +1 -1
  7. package/dist/integrations/hlsjs.d.ts +45 -0
  8. package/dist/integrations/hlsjs.d.ts.map +1 -0
  9. package/dist/integrations/hlsjs.js +121 -0
  10. package/dist/integrations/hlsjs.js.map +1 -0
  11. package/dist/integrations/index.d.ts +11 -0
  12. package/dist/integrations/index.d.ts.map +1 -0
  13. package/dist/integrations/index.js +11 -0
  14. package/dist/integrations/index.js.map +1 -0
  15. package/dist/integrations/react.d.ts +55 -0
  16. package/dist/integrations/react.d.ts.map +1 -0
  17. package/dist/integrations/react.js +127 -0
  18. package/dist/integrations/react.js.map +1 -0
  19. package/dist/integrations/shaka.d.ts +38 -0
  20. package/dist/integrations/shaka.d.ts.map +1 -0
  21. package/dist/integrations/shaka.js +71 -0
  22. package/dist/integrations/shaka.js.map +1 -0
  23. package/dist/integrations/shared.d.ts +41 -0
  24. package/dist/integrations/shared.d.ts.map +1 -0
  25. package/dist/integrations/shared.js +123 -0
  26. package/dist/integrations/shared.js.map +1 -0
  27. package/dist/integrations/videojs.d.ts +85 -0
  28. package/dist/integrations/videojs.d.ts.map +1 -0
  29. package/dist/integrations/videojs.js +111 -0
  30. package/dist/integrations/videojs.js.map +1 -0
  31. package/dist/ts/diagnostics.d.ts.map +1 -1
  32. package/dist/ts/diagnostics.js.map +1 -1
  33. package/dist/ts/frame-export.d.ts.map +1 -1
  34. package/dist/ts/frame-export.js +4 -4
  35. package/dist/ts/frame-export.js.map +1 -1
  36. package/dist/ts/parsers.d.ts +7 -6
  37. package/dist/ts/parsers.d.ts.map +1 -1
  38. package/dist/ts/parsers.js +76 -13
  39. package/dist/ts/parsers.js.map +1 -1
  40. package/dist/ts/range-loader.d.ts +31 -0
  41. package/dist/ts/range-loader.d.ts.map +1 -0
  42. package/dist/ts/range-loader.js +220 -0
  43. package/dist/ts/range-loader.js.map +1 -0
  44. package/dist/ts/renderers.d.ts +14 -0
  45. package/dist/ts/renderers.d.ts.map +1 -1
  46. package/dist/ts/renderers.js +445 -162
  47. package/dist/ts/renderers.js.map +1 -1
  48. package/dist/ts/types.d.ts +51 -1
  49. package/dist/ts/types.d.ts.map +1 -1
  50. package/dist/ts/utils.d.ts.map +1 -1
  51. package/dist/ts/utils.js.map +1 -1
  52. package/dist/ts/wasm.d.ts +2 -0
  53. package/dist/ts/wasm.d.ts.map +1 -1
  54. package/dist/ts/wasm.js +12 -0
  55. package/dist/ts/wasm.js.map +1 -1
  56. package/dist/ts/webgl2-renderer.d.ts +9 -0
  57. package/dist/ts/webgl2-renderer.d.ts.map +1 -1
  58. package/dist/ts/webgl2-renderer.js +34 -0
  59. package/dist/ts/webgl2-renderer.js.map +1 -1
  60. package/dist/ts/webgpu-renderer.d.ts +9 -0
  61. package/dist/ts/webgpu-renderer.d.ts.map +1 -1
  62. package/dist/ts/webgpu-renderer.js +70 -2
  63. package/dist/ts/webgpu-renderer.js.map +1 -1
  64. package/dist/ts/worker.d.ts +11 -0
  65. package/dist/ts/worker.d.ts.map +1 -1
  66. package/dist/ts/worker.js +235 -160
  67. package/dist/ts/worker.js.map +1 -1
  68. package/dist/wrapper.d.ts +3 -1
  69. package/dist/wrapper.d.ts.map +1 -1
  70. package/dist/wrapper.js +2 -0
  71. package/dist/wrapper.js.map +1 -1
  72. package/examples/README.md +38 -0
  73. package/examples/hlsjs/README.md +53 -0
  74. package/examples/react/README.md +60 -0
  75. package/examples/shaka/README.md +61 -0
  76. package/examples/videojs/README.md +55 -0
  77. package/package.json +68 -12
  78. package/pkg/LICENSE +1 -0
  79. package/pkg/README.md +186 -36
  80. package/pkg/libbitsub.d.ts +14 -0
  81. package/pkg/libbitsub.js +50 -0
  82. package/pkg/libbitsub_bg.wasm +0 -0
  83. package/pkg/libbitsub_bg.wasm.d.ts +7 -0
  84. package/pkg/package.json +1 -1
  85. package/src/integrations/hlsjs.ts +154 -0
  86. package/src/integrations/index.ts +36 -0
  87. package/src/integrations/react.tsx +170 -0
  88. package/src/integrations/shaka.ts +92 -0
  89. package/src/integrations/shared.ts +170 -0
  90. package/src/integrations/videojs.ts +200 -0
  91. package/src/wrapper.ts +12 -0
@@ -0,0 +1,60 @@
1
+ # React + libbitsub
2
+
3
+ ## Install
4
+
5
+ ```bash
6
+ npm install libbitsub react react-dom
7
+ ```
8
+
9
+ ## Hook recipe
10
+
11
+ ```tsx
12
+ import { useRef } from 'react'
13
+ import { useBitSub } from 'libbitsub/react'
14
+
15
+ export function VideoWithBitmaps({ src, subUrl }: { src: string; subUrl: string }) {
16
+ const videoRef = useRef<HTMLVideoElement>(null)
17
+ const { error } = useBitSub(videoRef, {
18
+ subUrl,
19
+ fileName: subUrl,
20
+ displaySettings: { scale: 1.05, bottomPadding: 4 },
21
+ onEvent: (event) => {
22
+ if (event.type === 'cue-change') {
23
+ console.log('cue', event.cue)
24
+ }
25
+ }
26
+ })
27
+
28
+ return (
29
+ <div style={{ position: 'relative' }}>
30
+ <video ref={videoRef} src={src} controls playsInline style={{ width: '100%' }} />
31
+ {error ? <p role='alert'>{error.message}</p> : null}
32
+ </div>
33
+ )
34
+ }
35
+ ```
36
+
37
+ ## Overlay component recipe
38
+
39
+ ```tsx
40
+ import { useRef } from 'react'
41
+ import { BitSubOverlay } from 'libbitsub/react'
42
+
43
+ export function Player({ src, subUrl }: { src: string; subUrl: string }) {
44
+ const videoRef = useRef<HTMLVideoElement>(null)
45
+
46
+ return (
47
+ <div style={{ position: 'relative' }}>
48
+ <video ref={videoRef} src={src} controls playsInline />
49
+ <BitSubOverlay videoRef={videoRef} subUrl={subUrl} displaySettings={{ aspectMode: 'cover' }} />
50
+ </div>
51
+ )
52
+ }
53
+ ```
54
+
55
+ ## Notes
56
+
57
+ - Prefer **either** `useBitSub` **or** `BitSubOverlay` on the same video — not both.
58
+ - Parent should be non-static (`relative` is enough) so the canvas overlay can position correctly.
59
+ - Changing `subUrl` / content identity disposes and recreates the renderer; layout-only `displaySettings` updates are applied in place.
60
+ - Call sites that need imperative control can read `controller` from `useBitSub` (`load`, `clear`, `setDisplaySettings`).
@@ -0,0 +1,61 @@
1
+ # Shaka Player + libbitsub
2
+
3
+ ## Install
4
+
5
+ ```bash
6
+ npm install libbitsub shaka-player
7
+ ```
8
+
9
+ ## Recipe
10
+
11
+ ```ts
12
+ import shaka from 'shaka-player'
13
+ import { attachBitSubToShaka } from 'libbitsub/shaka'
14
+
15
+ shaka.polyfill.installAll()
16
+
17
+ const video = document.getElementById('video') as HTMLVideoElement
18
+ const player = new shaka.Player()
19
+ await player.attach(video)
20
+ await player.load('/media/manifest.mpd')
21
+
22
+ // Bitmap tracks are external overlays (not Shaka TextTrack objects).
23
+ const bitsub = attachBitSubToShaka(player, {
24
+ subUrl: '/subs/movie.sup',
25
+ prefetchWindow: { before: 1, after: 2 },
26
+ onError: (error) => console.error(error)
27
+ })
28
+
29
+ // Switch tracks at runtime:
30
+ bitsub.load({
31
+ subUrl: '/subs/movie.idx', // companion .sub inferred / provide subUrl to .sub
32
+ idxUrl: '/subs/movie.idx',
33
+ fileName: 'movie.sub'
34
+ })
35
+
36
+ // Prefer explicit VobSub pair:
37
+ bitsub.load({
38
+ subUrl: '/subs/movie.sub',
39
+ idxUrl: '/subs/movie.idx'
40
+ })
41
+
42
+ bitsub.setDisplaySettings({ safeArea: 5, opacity: 0.95 })
43
+
44
+ // Cleanup
45
+ bitsub.dispose()
46
+ await player.destroy()
47
+ ```
48
+
49
+ ## HTML sketch
50
+
51
+ ```html
52
+ <div style="position: relative">
53
+ <video id="video" controls playsinline></video>
54
+ </div>
55
+ ```
56
+
57
+ ## Notes
58
+
59
+ - Uses `player.getMediaElement()` and requires an `HTMLVideoElement`.
60
+ - Listens for Shaka `loaded` / `unloading` when available to rebind or clear.
61
+ - Keep using Shaka's native text pipeline for WebVTT/TTML; use libbitsub only for PGS/VobSub/MKS bitmaps.
@@ -0,0 +1,55 @@
1
+ # Video.js + libbitsub
2
+
3
+ ## Install
4
+
5
+ ```bash
6
+ npm install libbitsub video.js
7
+ ```
8
+
9
+ ## Recipe
10
+
11
+ ```ts
12
+ import videojs from 'video.js'
13
+ import 'video.js/dist/video-js.css'
14
+ import { registerBitSubPlugin } from 'libbitsub/videojs'
15
+
16
+ registerBitSubPlugin(videojs)
17
+
18
+ const player = videojs('my-video', {
19
+ controls: true,
20
+ sources: [{ src: '/media/movie.mp4', type: 'video/mp4' }]
21
+ })
22
+
23
+ // Autoload a PGS track when the plugin starts:
24
+ const bitsub = player.bitsub({
25
+ subUrl: '/subs/movie.sup',
26
+ displaySettings: { aspectMode: 'cover', bottomPadding: 6 },
27
+ onEvent: (event) => console.log('bitsub', event)
28
+ })
29
+
30
+ // Or load later (e.g. after the user picks a language):
31
+ bitsub.load({
32
+ subUrl: '/subs/movie.eng.sup',
33
+ fileName: 'movie.eng.sup'
34
+ })
35
+
36
+ bitsub.setDisplaySettings({ scale: 1.1 })
37
+ bitsub.clear()
38
+
39
+ // Plugin resources are released with the player:
40
+ // player.dispose()
41
+ ```
42
+
43
+ ## HTML sketch
44
+
45
+ ```html
46
+ <video id="my-video" class="video-js vjs-big-play-centered" playsinline></video>
47
+ ```
48
+
49
+ Ensure the player container can host an absolutely positioned canvas (Video.js already uses non-static positioning on its root).
50
+
51
+ ## Notes
52
+
53
+ - Resolves the tech `<video>` element via `player.tech(true)` with a DOM fallback.
54
+ - Rebinds on `loadeddata` if the tech element is replaced.
55
+ - Emits `bitsubload` / `bitsubclear` on the player for light app wiring.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libbitsub",
3
- "version": "1.10.1",
3
+ "version": "1.11.0",
4
4
  "author": "altqx",
5
5
  "license": "MIT",
6
6
  "description": "High-performance WASM renderer for graphical subtitles (PGS and VobSub)",
@@ -17,48 +17,104 @@
17
17
  "rust",
18
18
  "rendering",
19
19
  "video",
20
- "player"
20
+ "player",
21
+ "videojs",
22
+ "shaka",
23
+ "hls.js",
24
+ "react"
21
25
  ],
22
26
  "main": "dist/index.js",
23
27
  "module": "dist/index.js",
24
28
  "types": "dist/index.d.ts",
25
29
  "exports": {
26
30
  ".": {
31
+ "types": "./dist/index.d.ts",
27
32
  "import": "./dist/index.js",
28
33
  "require": "./dist/index.js",
29
- "types": "./dist/index.d.ts"
34
+ "default": "./dist/index.js"
30
35
  },
31
36
  "./pkg": {
37
+ "types": "./pkg/libbitsub.d.ts",
32
38
  "import": "./pkg/libbitsub.js",
33
- "types": "./pkg/libbitsub.d.ts"
39
+ "default": "./pkg/libbitsub.js"
40
+ },
41
+ "./videojs": {
42
+ "types": "./dist/integrations/videojs.d.ts",
43
+ "import": "./dist/integrations/videojs.js",
44
+ "default": "./dist/integrations/videojs.js"
45
+ },
46
+ "./shaka": {
47
+ "types": "./dist/integrations/shaka.d.ts",
48
+ "import": "./dist/integrations/shaka.js",
49
+ "default": "./dist/integrations/shaka.js"
50
+ },
51
+ "./hlsjs": {
52
+ "types": "./dist/integrations/hlsjs.d.ts",
53
+ "import": "./dist/integrations/hlsjs.js",
54
+ "default": "./dist/integrations/hlsjs.js"
55
+ },
56
+ "./react": {
57
+ "types": "./dist/integrations/react.d.ts",
58
+ "import": "./dist/integrations/react.js",
59
+ "default": "./dist/integrations/react.js"
60
+ },
61
+ "./integrations": {
62
+ "types": "./dist/integrations/index.d.ts",
63
+ "import": "./dist/integrations/index.js",
64
+ "default": "./dist/integrations/index.js"
34
65
  }
35
66
  },
36
67
  "files": [
37
68
  "dist",
38
69
  "pkg",
39
- "src/wrapper.ts"
70
+ "src/wrapper.ts",
71
+ "src/integrations",
72
+ "examples"
40
73
  ],
41
74
  "scripts": {
42
75
  "build:wasm": "wasm-pack build --target web --out-dir pkg",
43
76
  "build:wasm:release": "wasm-pack build --release --target web --out-dir pkg",
44
77
  "build:wasm:bundler": "wasm-pack build --target bundler --out-dir pkg-bundler",
45
- "build:ts": "tsgo",
78
+ "build:ts": "tsc",
46
79
  "build": "bun run build:wasm:release && bun run build:ts",
47
80
  "clean": "rm -rf pkg pkg-bundler dist target",
48
- "test": "cargo test",
81
+ "test": "cargo test --workspace",
82
+ "test:ts": "bun test src/ts",
83
+ "test:visual": "bun tests/visual/run.mjs",
84
+ "test:all": "bun run test && bun run test:ts && bun run test:visual",
49
85
  "test:wasm": "wasm-pack test --headless --chrome",
50
86
  "format": "cargo fmt && prettier . --write"
51
87
  },
52
88
  "devDependencies": {
53
- "@typescript/native-preview": "^7.0.0-dev.20260524.1",
54
- "prettier": "^3.8.3",
55
- "typescript": "^6.0.3"
89
+ "@types/react": "^19.2.17",
90
+ "prettier": "^3.9.6",
91
+ "react": "^19.2.8",
92
+ "typescript": "^7.0.2"
93
+ },
94
+ "peerDependencies": {
95
+ "hls.js": ">=1.0.0",
96
+ "react": ">=18.0.0",
97
+ "shaka-player": ">=4.0.0",
98
+ "video.js": ">=7.0.0"
99
+ },
100
+ "peerDependenciesMeta": {
101
+ "hls.js": {
102
+ "optional": true
103
+ },
104
+ "react": {
105
+ "optional": true
106
+ },
107
+ "shaka-player": {
108
+ "optional": true
109
+ },
110
+ "video.js": {
111
+ "optional": true
112
+ }
56
113
  },
57
- "peerDependencies": {},
58
114
  "engines": {
59
115
  "node": ">=18"
60
116
  },
61
117
  "dependencies": {
62
- "@webgpu/types": "^0.1.70"
118
+ "@webgpu/types": "^0.1.71"
63
119
  }
64
120
  }
package/pkg/LICENSE CHANGED
@@ -1,6 +1,7 @@
1
1
  MIT License
2
2
 
3
3
  Copyright (c) 2026 Jaruwit Jaiya
4
+ Copyright (c) 2024 David Schulte, and Libpgs contributors
4
5
 
5
6
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
7
  of this software and associated documentation files (the "Software"), to deal
package/pkg/README.md CHANGED
@@ -11,6 +11,7 @@ Started as a fork of Arcus92's [libpgs-js](https://github.com/Arcus92/libpgs-js)
11
11
  - Matroska `.mks` extraction for embedded `S_VOBSUB` tracks
12
12
  - WebGPU, WebGL2, and Canvas2D rendering with automatic fallback
13
13
  - Worker-backed parsing/rendering for large subtitle files
14
+ - HTTP Range / streaming loads for large `.sup`, `.sub`, `.idx`, and `.mks` assets (progressive indexing where feasible)
14
15
  - Rich layout controls: scale, aspect mode, horizontal/vertical offsets, alignment, bottom padding, safe area, opacity
15
16
  - Cue metadata and parser introspection APIs
16
17
  - Rendered-frame export helpers for ImageData, ImageBitmap, Blob, and custom canvas targets
@@ -18,6 +19,7 @@ Started as a fork of Arcus92's [libpgs-js](https://github.com/Arcus92/libpgs-js)
18
19
  - Automatic format detection and unified loading helpers
19
20
  - First-class diagnostics with structured error codes, warning hooks, and render/cache snapshots
20
21
  - TypeScript support with exported event and metadata types
22
+ - Optional player integrations for Video.js, Shaka Player, hls.js, and React (core stays dependency-free)
21
23
 
22
24
  ## Showcase
23
25
 
@@ -39,14 +41,14 @@ For pure Rust parser/rendering logic:
39
41
 
40
42
  ```toml
41
43
  [dependencies]
42
- libbitsub-core = "1.10.0"
44
+ libbitsub-core = "1.10.1"
43
45
  ```
44
46
 
45
47
  For WASM bindings from Rust:
46
48
 
47
49
  ```toml
48
50
  [dependencies]
49
- libbitsub = "1.10.0"
51
+ libbitsub = "1.10.1"
50
52
  ```
51
53
 
52
54
  For JavaScript package managers:
@@ -67,11 +69,11 @@ deno add jsr:@altq/libbitsub
67
69
 
68
70
  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
71
 
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`:
72
+ 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
73
 
72
74
  ```bash
73
75
  mkdir -p public/libbitsub
74
- cp node_modules/libbitsub/pkg/libbitsub_bg.wasm public/libbitsub/
76
+ cp node_modules/libbitsub/pkg/libbitsub_bg.wasm node_modules/libbitsub/pkg/libbitsub.js public/libbitsub/
75
77
  ```
76
78
 
77
79
  The worker is still created inline. `workerUrl` remains in the option type only for compatibility and does not change runtime behavior.
@@ -110,6 +112,129 @@ const parser = new PgsParser()
110
112
 
111
113
  Calling `initWasm()` multiple times is safe (it deduplicates).
112
114
 
115
+ ### Worker prewarm (TV / players)
116
+
117
+ 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:
118
+
119
+ ```ts
120
+ import { warmup, ready, PgsRenderer } from 'libbitsub'
121
+
122
+ // Fire-and-forget at app boot (safe to call multiple times).
123
+ void warmup()
124
+
125
+ // Before the user changes tracks, ensure the worker is ready.
126
+ await ready()
127
+
128
+ const renderer = new PgsRenderer({ video: videoElement, subContent: trackBytes })
129
+ ```
130
+
131
+ `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.
132
+
133
+ ### Range / streaming loads (TV / slow networks)
134
+
135
+ URL-based loads (`subUrl` / `idxUrl`) use progressive fetch by default:
136
+
137
+ - Probe HTTP Range support and, for large assets, download with Range chunks when the origin allows it
138
+ - Stream the response body otherwise and report `load-progress` events
139
+ - **PGS (`.sup`)**: feed chunks into the parser as they arrive so cues can be indexed before the full file finishes
140
+ - **VobSub (`.idx` + `.sub`)**: parse the small `.idx` first for timestamps, then stream/Range-download the `.sub` body and attach packets
141
+ - **MKS**: stream/Range-download the container first (Matroska extraction still needs the assembled payload), then parse
142
+ - **In-memory** `subContent` / `idxContent` remains the simple full-buffer path
143
+
144
+ ```ts
145
+ import { PgsRenderer } from 'libbitsub'
146
+
147
+ const renderer = new PgsRenderer({
148
+ video: videoElement,
149
+ subUrl: '/subtitles/movie.sup',
150
+ // streamingLoad: true, // default
151
+ // rangeRequests: true, // default
152
+ onEvent: (event) => {
153
+ if (event.type === 'load-progress') {
154
+ console.log(event.strategy, event.loadedBytes, event.totalBytes, event.indexedCues)
155
+ }
156
+ if (event.type === 'indexed') {
157
+ console.log('cues ready', event.metadata.cueCount, event.partial ? '(partial)' : '(final)')
158
+ }
159
+ }
160
+ })
161
+ ```
162
+
163
+ Disable progressive behavior when you want a single blocking GET:
164
+
165
+ ```ts
166
+ new PgsRenderer({ video, subUrl, streamingLoad: false, rangeRequests: false })
167
+ ```
168
+
169
+ Low-level helpers are also exported: `probeRangeSupport()`, `fetchSubtitleAsset()`, and `fetchSubtitleText()`.
170
+
171
+ ## Player integrations
172
+
173
+ 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).
174
+
175
+ | Import | Adapter | Optional peer |
176
+ | ------------------------ | ---------------------------------------- | ------------------- |
177
+ | `libbitsub/videojs` | Video.js plugin (`registerBitSubPlugin`) | `video.js` >= 7 |
178
+ | `libbitsub/shaka` | `attachBitSubToShaka(player, options)` | `shaka-player` >= 4 |
179
+ | `libbitsub/hlsjs` | `attachBitSubToHls(hls, options)` | `hls.js` >= 1 |
180
+ | `libbitsub/react` | `useBitSub` / `BitSubOverlay` | `react` >= 18 |
181
+ | `libbitsub/integrations` | Shared `attachBitSub` controller | — |
182
+
183
+ Copy-paste recipes live under [`examples/`](./examples).
184
+
185
+ ### Video.js
186
+
187
+ ```ts
188
+ import videojs from 'video.js'
189
+ import { registerBitSubPlugin } from 'libbitsub/videojs'
190
+
191
+ registerBitSubPlugin(videojs)
192
+ const player = videojs('my-video')
193
+ const bitsub = player.bitsub({ subUrl: '/subs/movie.sup' })
194
+ bitsub.load({ subUrl: '/subs/other.sup' })
195
+ ```
196
+
197
+ ### Shaka Player
198
+
199
+ ```ts
200
+ import { attachBitSubToShaka } from 'libbitsub/shaka'
201
+
202
+ const bitsub = attachBitSubToShaka(player, { subUrl: '/subs/movie.sup' })
203
+ // later
204
+ bitsub.dispose()
205
+ ```
206
+
207
+ ### hls.js
208
+
209
+ ```ts
210
+ import Hls from 'hls.js'
211
+ import { attachBitSubToHls } from 'libbitsub/hlsjs'
212
+
213
+ const hls = new Hls()
214
+ hls.loadSource(url)
215
+ hls.attachMedia(video)
216
+ const bitsub = attachBitSubToHls(hls, { subUrl: '/subs/movie.sup' })
217
+ ```
218
+
219
+ ### React
220
+
221
+ ```tsx
222
+ import { useRef } from 'react'
223
+ import { useBitSub } from 'libbitsub/react'
224
+
225
+ function Player({ src, subUrl }: { src: string; subUrl: string }) {
226
+ const videoRef = useRef<HTMLVideoElement>(null)
227
+ useBitSub(videoRef, { subUrl })
228
+ return (
229
+ <div style={{ position: 'relative' }}>
230
+ <video ref={videoRef} src={src} controls playsInline />
231
+ </div>
232
+ )
233
+ }
234
+ ```
235
+
236
+ 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.
237
+
113
238
  ## High-level video renderers
114
239
 
115
240
  The high-level API manages subtitle loading, canvas overlay creation, playback sync, resize handling, worker usage, and renderer fallback.
@@ -207,16 +332,16 @@ renderer.resetDisplaySettings()
207
332
 
208
333
  `SubtitleDisplaySettings`:
209
334
 
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 |
335
+ | Field | Type | Range / values | Meaning |
336
+ | ------------------ | ----------------------------------- | -------------- | -------------------------------------------------------------- |
337
+ | `scale` | number | `0.1` to `3.0` | Overall subtitle scale |
338
+ | `aspectMode` | `'stretch' \| 'contain' \| 'cover'` | fixed set | How subtitle screen coordinates map into the visible video box |
339
+ | `verticalOffset` | number | `-50` to `50` | Vertical movement as percent of video height |
340
+ | `horizontalOffset` | number | `-50` to `50` | Horizontal movement as percent of video width |
341
+ | `horizontalAlign` | `'left' \| 'center' \| 'right'` | fixed set | Anchor used when scaling subtitle groups |
342
+ | `bottomPadding` | number | `0` to `50` | Extra padding from the bottom edge |
343
+ | `safeArea` | number | `0` to `25` | Clamp subtitles inside a video-safe area |
344
+ | `opacity` | number | `0.0` to `1.0` | Global subtitle opacity |
220
345
 
221
346
  ## Metadata and introspection
222
347
 
@@ -291,14 +416,7 @@ Like `UnifiedSubtitleParser.loadAuto()`, this is a low-level in-memory API: pass
291
416
  Low-level parser output can be flattened into a single exportable frame for previews, editors, snapshots, fixture generation, or visual diffing.
292
417
 
293
418
  ```ts
294
- import {
295
- PgsParser,
296
- initWasm,
297
- renderFrameData,
298
- toBlob,
299
- toCanvas,
300
- toImageBitmap
301
- } from 'libbitsub'
419
+ import { PgsParser, initWasm, renderFrameData, toBlob, toCanvas, toImageBitmap } from 'libbitsub'
302
420
 
303
421
  await initWasm()
304
422
 
@@ -385,7 +503,7 @@ Structured diagnostic error codes currently include:
385
503
  - `WORKER_FALLBACK`
386
504
  - `UNKNOWN`
387
505
 
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.
506
+ 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
507
 
390
508
  ## MKS security and corruption checks
391
509
 
@@ -415,6 +533,8 @@ const renderer = new PgsRenderer({
415
533
  onEvent: (event) => {
416
534
  switch (event.type) {
417
535
  case 'loading':
536
+ case 'load-progress':
537
+ case 'indexed':
418
538
  case 'loaded':
419
539
  case 'error':
420
540
  case 'warning':
@@ -482,17 +602,17 @@ renderer.dispose()
482
602
 
483
603
  Emitted events:
484
604
 
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 |
605
+ | Event | Payload |
606
+ | ----------------- | --------------------------------------------------------------------------------- |
607
+ | `loading` | subtitle format |
608
+ | `loaded` | subtitle format and parser metadata |
609
+ | `error` | subtitle format and `SubtitleDiagnosticErrorLike` |
610
+ | `warning` | `SubtitleDiagnosticWarning` |
611
+ | `renderer-change` | active backend: `webgpu`, `webgl2`, or `canvas2d` |
612
+ | `worker-state` | whether worker mode is enabled, ready, fallback status, and the active session ID |
613
+ | `cache-change` | cached frame count, pending renders, and configured cache limit |
614
+ | `cue-change` | current cue metadata or `null` when nothing is displayed |
615
+ | `stats` | periodic renderer stats snapshot |
496
616
 
497
617
  ## Performance stats
498
618
 
@@ -581,6 +701,25 @@ console.log({
581
701
 
582
702
  WebGL2 and Canvas2D fallback remain automatic. Use `onWebGPUFallback`, `onWebGL2Fallback`, `onEvent`, or `onWarning` if you need to observe backend changes.
583
703
 
704
+ ## Compatibility & visual regression
705
+
706
+ A formal suite locks decoder edge cases and backend pixel parity:
707
+
708
+ - Malformed PGS / VobSub / MKS fixtures
709
+ - Palette edge cases and zero-length RLE runs
710
+ - Alternate display sizes (SD → 4K)
711
+ - Slow worker startup / shared init
712
+ - Pixel-level goldens across software, Canvas2D, WebGL2, and WebGPU
713
+
714
+ ```bash
715
+ bun run test # Rust core (includes compatibility fixtures)
716
+ bun run test:ts # TypeScript fixtures, goldens, worker startup
717
+ bun run test:visual # Headless Chromium backend parity
718
+ bun run test:all # All of the above
719
+ ```
720
+
721
+ Browser/device support matrix (including webOS TV): [docs/COMPATIBILITY.md](./docs/COMPATIBILITY.md).
722
+
584
723
  ## Notes
585
724
 
586
725
  - Worker mode is shared, but subtitle parser state is isolated per renderer session.
@@ -594,10 +733,13 @@ WebGL2 and Canvas2D fallback remain automatic. Use `onWebGPUFallback`, `onWebGL2
594
733
 
595
734
  - `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
735
  - `isWasmInitialized(): boolean` reports whether initialization has completed.
736
+ - `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.
737
+ - `isWorkerAvailable(): boolean` / `isWorkerReady(): boolean` report worker support and whether the shared worker has finished initializing.
597
738
  - `isWebGPUSupported(): boolean` checks WebGPU support.
598
739
  - `detectSubtitleFormat(source: AutoSubtitleSource): 'pgs' | 'vobsub' | null` detects the bitmap subtitle format from file hints or binary data.
599
740
  - `createAutoSubtitleRenderer(options: AutoVideoSubtitleOptions): PgsRenderer | VobSubRenderer` creates a high-level renderer after format detection.
600
741
  - `openSubtitles(source, options?): Promise<OpenedSubtitles>` initializes WASM, auto-detects the low-level format, and returns a normalized parser handle.
742
+ - `probeRangeSupport(url, options?)` / `fetchSubtitleAsset(url, options?, onChunk?)` / `fetchSubtitleText(url, options?)` perform Range-aware progressive asset downloads for custom loaders.
601
743
  - `renderFrameData(frame, options?): SubtitleRenderedFrameData | null` composes a `SubtitleData` frame into exportable pixels.
602
744
  - `toCanvas(frame, target?, options?): HTMLCanvasElement | OffscreenCanvas` draws a rendered frame to a new or existing canvas/context.
603
745
  - `toImageBitmap(frame, options?): Promise<ImageBitmap>` creates an `ImageBitmap` from a subtitle frame export.
@@ -605,6 +747,7 @@ WebGL2 and Canvas2D fallback remain automatic. Use `onWebGPUFallback`, `onWebGL2
605
747
  - `SubtitleDiagnosticError` is the structured error class libbitsub uses for coded diagnostics.
606
748
  - `createSubtitleDiagnosticError(...)` and `normalizeSubtitleError(...)` are exported for integrations that want to preserve libbitsub-style error codes.
607
749
  - Legacy aliases remain exported: `PGSRenderer`, `VobsubRenderer`, `UnifiedSubtitleRenderer`.
750
+ - 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
751
 
609
752
  ### High-level renderers
610
753
 
@@ -639,7 +782,9 @@ WebGL2 and Canvas2D fallback remain automatic. Use `onWebGPUFallback`, `onWebGL2
639
782
 
640
783
  #### `PgsParser`
641
784
 
642
- - `load(data: Uint8Array): number` loads PGS data and returns the cue count.
785
+ - `load(data: Uint8Array): number` loads PGS data and returns the cue count (simple in-memory path).
786
+ - `reset(): void` / `feed(data: Uint8Array): number` / `finishFeed(): number` progressive indexing APIs for streamed chunks.
787
+ - `pendingLen: number` incomplete trailing bytes retained between `feed()` calls.
643
788
  - `getTimestamps(): Float64Array` returns cue timestamps in milliseconds.
644
789
  - `count: number` returns the number of cues.
645
790
  - `findIndexAtTimestamp(timeSeconds: number): number` finds the cue index for a playback time in seconds.
@@ -652,7 +797,10 @@ WebGL2 and Canvas2D fallback remain automatic. Use `onWebGPUFallback`, `onWebGL2
652
797
 
653
798
  #### `VobSubParserLowLevel`
654
799
 
655
- - `loadFromData(idxContent: string, subData: Uint8Array): void` loads IDX and SUB data.
800
+ - `loadFromData(idxContent: string, subData: Uint8Array): void` loads IDX and SUB data (simple in-memory path).
801
+ - `loadFromIdx(idxContent: string): void` indexes timestamps from IDX before packet bytes arrive.
802
+ - `attachSubData(subData: Uint8Array): void` attaches `.sub` packets after an IDX-first load.
803
+ - `hasSubData: boolean` whether packet bytes are currently attached.
656
804
  - `loadFromSubOnly(subData: Uint8Array): void` loads SUB-only VobSub data.
657
805
  - `getTimestamps(): Float64Array`, `count`, `findIndexAtTimestamp()`, `renderAtIndex()`, `renderAtTimestamp()`, `getMetadata()`, `getCueMetadata()`, `clearCache()`, and `dispose()` behave like `PgsParser`.
658
806
  - `setDebandEnabled(enabled: boolean): void`, `setDebandThreshold(threshold: number): void`, `setDebandRange(range: number): void`, and `debandEnabled` control debanding.
@@ -687,6 +835,8 @@ interface VideoSubtitleOptions {
687
835
  before?: number
688
836
  after?: number
689
837
  }
838
+ streamingLoad?: boolean // default true — progressive URL loads
839
+ rangeRequests?: boolean // default true — use HTTP Range when supported
690
840
  onEvent?: (event: SubtitleRendererEvent) => void
691
841
  }
692
842
  ```