akarisub 1.0.0 → 1.1.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 (72) hide show
  1. package/README.md +217 -55
  2. package/dist/COPYRIGHT +3 -3
  3. package/dist/index.d.ts +23 -5
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +20 -4
  6. package/dist/index.js.map +1 -1
  7. package/dist/ts/akarisub.d.ts +274 -2
  8. package/dist/ts/akarisub.d.ts.map +1 -1
  9. package/dist/ts/akarisub.js +1116 -80
  10. package/dist/ts/akarisub.js.map +1 -1
  11. package/dist/ts/asset-loader.d.ts +45 -0
  12. package/dist/ts/asset-loader.d.ts.map +1 -0
  13. package/dist/ts/asset-loader.js +297 -0
  14. package/dist/ts/asset-loader.js.map +1 -0
  15. package/dist/ts/color-space.d.ts +86 -0
  16. package/dist/ts/color-space.d.ts.map +1 -0
  17. package/dist/ts/color-space.js +311 -0
  18. package/dist/ts/color-space.js.map +1 -0
  19. package/dist/ts/cue-events.d.ts +21 -0
  20. package/dist/ts/cue-events.d.ts.map +1 -0
  21. package/dist/ts/cue-events.js +79 -0
  22. package/dist/ts/cue-events.js.map +1 -0
  23. package/dist/ts/font-subsets.d.ts +27 -0
  24. package/dist/ts/font-subsets.d.ts.map +1 -0
  25. package/dist/ts/font-subsets.js +117 -0
  26. package/dist/ts/font-subsets.js.map +1 -0
  27. package/dist/ts/streaming.d.ts +4 -0
  28. package/dist/ts/streaming.d.ts.map +1 -0
  29. package/dist/ts/streaming.js +39 -0
  30. package/dist/ts/streaming.js.map +1 -0
  31. package/dist/ts/types.d.ts +359 -7
  32. package/dist/ts/types.d.ts.map +1 -1
  33. package/dist/ts/unicode-scripts.d.ts +19 -0
  34. package/dist/ts/unicode-scripts.d.ts.map +1 -0
  35. package/dist/ts/unicode-scripts.js +241 -0
  36. package/dist/ts/unicode-scripts.js.map +1 -0
  37. package/dist/ts/utils.d.ts +22 -11
  38. package/dist/ts/utils.d.ts.map +1 -1
  39. package/dist/ts/utils.js +9 -44
  40. package/dist/ts/utils.js.map +1 -1
  41. package/dist/ts/video-frame.d.ts +34 -0
  42. package/dist/ts/video-frame.d.ts.map +1 -0
  43. package/dist/ts/video-frame.js +75 -0
  44. package/dist/ts/video-frame.js.map +1 -0
  45. package/dist/ts/wasm-capabilities.d.ts +30 -0
  46. package/dist/ts/wasm-capabilities.d.ts.map +1 -0
  47. package/dist/ts/wasm-capabilities.js +64 -0
  48. package/dist/ts/wasm-capabilities.js.map +1 -0
  49. package/dist/ts/wasm.d.ts +4 -0
  50. package/dist/ts/wasm.d.ts.map +1 -1
  51. package/dist/ts/wasm.js +24 -0
  52. package/dist/ts/wasm.js.map +1 -1
  53. package/dist/ts/webgl2-renderer.d.ts +36 -3
  54. package/dist/ts/webgl2-renderer.d.ts.map +1 -1
  55. package/dist/ts/webgl2-renderer.js +100 -16
  56. package/dist/ts/webgl2-renderer.js.map +1 -1
  57. package/dist/ts/webgpu-renderer.d.ts +40 -0
  58. package/dist/ts/webgpu-renderer.d.ts.map +1 -1
  59. package/dist/ts/webgpu-renderer.js +123 -18
  60. package/dist/ts/webgpu-renderer.js.map +1 -1
  61. package/dist/ts/worker.js +729 -112
  62. package/dist/ts/worker.js.map +1 -1
  63. package/dist/wrapper.d.ts +10 -3
  64. package/dist/wrapper.d.ts.map +1 -1
  65. package/dist/wrapper.js +8 -2
  66. package/dist/wrapper.js.map +1 -1
  67. package/package.json +4 -4
  68. package/pkg/akarisub-mt.js +70 -0
  69. package/pkg/akarisub-mt.wasm +0 -0
  70. package/pkg/akarisub.js +48 -47
  71. package/pkg/akarisub.wasm +0 -0
  72. package/src/wrapper.ts +54 -2
package/README.md CHANGED
@@ -14,14 +14,17 @@ AkariSub is a JS wrapper for <a href="https://github.com/libass/libass">libass</
14
14
  - Supports most SSA/ASS features (everything libass supports)
15
15
  - Supports all OpenType, TrueType and WOFF fonts, as well as embedded fonts
16
16
  - Supports anamorphic videos [(on browsers which support it)](https://caniuse.com/mdn-api_htmlvideoelement_requestvideoframecallback)
17
- - Supports different video color spaces [(on browsers which support it)](https://caniuse.com/mdn-api_videocolorspace)
17
+ - Supports different video color spaces, including HDR / wide-gamut overlay [(on browsers which support it)](https://caniuse.com/mdn-api_videocolorspace)
18
18
  - Capable of using local fonts [(on browsers which support it)](https://caniuse.com/mdn-api_window_querylocalfonts)
19
19
  - Works fast (all the heavy lifting is done by WebAssembly)
20
- - Is fully threaded (on browsers which support it, it's capable of working fully on a separate thread)
20
+ - Is fully threaded (worker plus optional WASM pthreads on isolated pages)
21
21
  - Is asynchronous (renders when available, not in order of execution)
22
22
  - Benefits from hardware acceleration (uses hardware accelerated canvas API's)
23
23
  - Doesn't manipulate the DOM to render subtitles
24
24
  - Easy to use - just connect it to video element
25
+ - Optional WebCodecs `VideoFrame` clock for custom players and editors
26
+ - Streaming / live tracks: append ASS fragments, Matroska packets, or events without reloading the file
27
+ - Font subset lazy loading: fetch only the unicode-range slices the current script needs (CJK fonts in particular)
25
28
 
26
29
  ### Fork Enhancements
27
30
 
@@ -30,7 +33,13 @@ AkariSub is a JS wrapper for <a href="https://github.com/libass/libass">libass</
30
33
  - **Proper Fontconfig Implementation** - add Fontconfig support with multiple fallback fonts supported
31
34
  - **Encrypted Subtitles** - optionally load AES-GCM encrypted subtitle payloads that are decrypted inside the worker, so plaintext never touches the main thread
32
35
  - **Statistics Reporting** - Built-in statistics and performance metrics for debugging and monitoring
36
+ - **Atomic Track Switching** - `preloadTrack()` / `activatePreloadedTrack()` load a second language track and its fonts before swapping, so the last frame stays visible
37
+ - **Streaming / live tracks** - `initStreamingTrack()` plus `appendSubtitleData()` / `appendSubtitleChunk()` accept HLS fragments and Matroska packets without a full file
38
+ - **Font subset lazy loading** - `availableFonts` can list unicode-range slices so CJK (and other) glyph files load only when the track needs them
39
+ - **Cue Callbacks** - `onCueEnter`, `onCueExit`, `onRender`, `onRendererChange`, and `onPerformanceWarning` for overlays and analytics without polling `getEvents()`
33
40
  - **TypeScript Support** - Full TypeScript definitions and type safety
41
+ - **HDR / Wide Color Gamut** - Matches Display P3 / Rec.2020 canvases and PQ/HLG video, and converts BT.2020 YCbCr matrices
42
+ - **WASM SIMD + pthreads** - SIMD libass kernels in the default binary; optional `akarisub-mt.wasm` blends independent regions on isolated pages
34
43
  - **Updated Dependencies** - All dependencies updated to their latest versions, including libass
35
44
 
36
45
  ## Installation
@@ -116,6 +125,39 @@ If the browser timeline is normalized from decode time rather than the first dis
116
125
 
117
126
  `subtitleTimeOffset` is signed. When the source video starts after the container subtitle clock, subtract that source lead from the reorder offset. For example, an encoded reorder gap of `0.083422` seconds and a source video start of `0.007` seconds use `subtitleTimeOffset = 0.076422`, so the first displayed frame is sampled by libass at `0.007` just like mpv.
118
127
 
128
+ ## Using with WebCodecs
129
+
130
+ Apps that decode with `VideoDecoder` instead of `HTMLVideoElement` can drive the same on-demand and frame-timeline path with each output `VideoFrame`. Pass a canvas, keep `onDemandRender` enabled, and present frames as they leave the decoder. AkariSub reads `timestamp` (microseconds), `displayWidth` / `displayHeight`, and `colorSpace` (`matrix`, `primaries`, `transfer`). HDR frames pick a Display P3 or Rec.2020 canvas when the browser can create one. It does not take ownership of the frame.
131
+
132
+ ```js
133
+ import AkariSub, { frameTimelineFromTimestamps } from 'akarisub'
134
+
135
+ const renderer = new AkariSub({
136
+ canvas: document.querySelector('canvas'),
137
+ subUrl: './tracks/sub.ass',
138
+ frameTimeline: frameTimelineFromTimestamps(packetTimestampsUs),
139
+ onDemandRender: true
140
+ })
141
+
142
+ const decoder = new VideoDecoder({
143
+ output(frame) {
144
+ renderer.presentVideoFrame(frame, {
145
+ expectedDisplayTime: performance.now(),
146
+ isPaused: false,
147
+ rate: 1
148
+ })
149
+ frame.close()
150
+ },
151
+ error(err) {
152
+ console.error(err)
153
+ }
154
+ })
155
+ ```
156
+
157
+ Paused editor and offline-preview frames pass `isPaused: true` so libass samples that exact timestamp. `setVideoColorSpace()` can apply a matrix without presenting a frame. `frameTimelineFromVideoFrames()` builds a timeline from already-decoded frames.
158
+
159
+ In browsers without `requestVideoFrameCallback`, set `onDemandRender: true` explicitly so `presentVideoFrame` uses the demand path instead of the worker RAF loop.
160
+
119
161
  ## Changing subtitles
120
162
 
121
163
  You're not limited to only display the subtitle file you referenced in your options. You're able to dynamically change subtitles on the fly. There's four methods that you can use for this specifically:
@@ -129,6 +171,79 @@ You're not limited to only display the subtitle file you referenced in your opti
129
171
  renderer.setTrackByUrl('/newsub.ass')
130
172
  ```
131
173
 
174
+ For streaming players with multiple language tracks, preload the next file so the swap does not drop a frame or hitch on font load. The last painted frame stays on screen until the new track's first frame is ready:
175
+
176
+ ```js
177
+ const ja = await renderer.preloadTrack({ kind: 'url', url: '/subs/ja.ass' })
178
+ await renderer.activatePreloadedTrack(ja.id)
179
+ ```
180
+
181
+ `preloadTrack()` also accepts ASS text or bytes, `{ kind: 'content', content }`, or `{ kind: 'encrypted', content }`. `activatePreloadedTrack()` without an id uses the most recently preloaded track.
182
+
183
+ ## Streaming and live tracks
184
+
185
+ Live streams and HLS fragments rarely have a complete ASS file up front. Initialize a header-only track, then append events as packets arrive. libass keeps styles and script info; you decide how long past cues stay in memory.
186
+
187
+ ```js
188
+ const renderer = new AkariSub({
189
+ video,
190
+ // Header only: Script Info + Styles, no Dialogue lines required
191
+ })
192
+
193
+ renderer.initStreamingTrack({
194
+ header: `[Script Info]
195
+ ScriptType: v4.00+
196
+ PlayResX: 1920
197
+ PlayResY: 1080
198
+
199
+ [V4+ Styles]
200
+ Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
201
+ Style: Default,Arial,48,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1
202
+
203
+ [Events]
204
+ Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
205
+ `,
206
+ pruneDelay: 30
207
+ })
208
+
209
+ // HLS WebVTT/ASS fragment converted to Dialogue lines:
210
+ renderer.appendSubtitleData('Dialogue: 0,0:00:01.00,0:00:03.00,Default,,0,0,0,,Hello live\n')
211
+
212
+ // Or structured events (Start/Duration are libass milliseconds, same as createEvent):
213
+ renderer.appendEvents([{ Start: 4000, Duration: 2000, Style: 'Default', Text: 'Next cue' }])
214
+
215
+ // Matroska/WebM live packets:
216
+ renderer.initStreamingTrack({ header: codecPrivate, format: 'matroska', pruneDelay: 15 })
217
+ renderer.appendSubtitleChunk(packet, startSeconds, durationSeconds)
218
+ ```
219
+
220
+ `pruneDelay` deletes events that ended more than that many seconds before the last rendered timestamp. Call `pruneEvents(video.currentTime - 30)` yourself when you want an explicit live window. `flushEvents()` drops every event and keeps the header.
221
+
222
+ Do not mix `appendSubtitleChunk` with `appendSubtitleData` / `createEvent` on the same track. libass uses ReadOrder to drop duplicate Matroska packets, and that check breaks if the event list is edited some other way.
223
+
224
+ ## Font subsetting / lazy loading
225
+
226
+ A full CJK font is often 10-20MB. Split it into unicode-range files (the same files you would ship for CSS `@font-face`) and list them under `availableFonts`. AkariSub reads the current track, keeps Latin loaded, and fetches Hiragana/Katakana/Han/Hangul (or other scripts) only when those glyphs appear, including as live events arrive.
227
+
228
+ ```js
229
+ const renderer = new AkariSub({
230
+ video,
231
+ subUrl: './tracks/sub.ass',
232
+ availableFonts: {
233
+ 'liberation sans': '/fonts/liberation-sans.woff2',
234
+ 'noto sans cjk jp': [
235
+ { src: '/fonts/noto-cjk-latin.woff2', unicodeRange: 'U+0000-00FF' },
236
+ { src: '/fonts/noto-cjk-hira.woff2', scripts: ['hira'] },
237
+ { src: '/fonts/noto-cjk-kana.woff2', scripts: ['kana'] },
238
+ { src: '/fonts/noto-cjk-han.woff2', scripts: ['hani'] }
239
+ ]
240
+ },
241
+ fallbackFonts: ['liberation sans']
242
+ })
243
+ ```
244
+
245
+ `scripts` accepts OpenType tags (`latn`, `hani`, `hira`, `kana`, `hang`) and aliases (`cjk`, `jp`, `kr`, `latin`). `fonts` still force-loads complete files. Set `lazyFonts: false` to fetch every listed slice when the family is first referenced.
246
+
132
247
  ## Cleaning up the object
133
248
 
134
249
  After you're finished with rendering the subtitles. You need to call the `destroy()` method to correctly destroy the object.
@@ -200,6 +315,8 @@ console.log(`Events: ${eventCount}, Styles: ${styleCount}`)
200
315
 
201
316
  AkariSub automatically picks the fastest available renderer: WebGPU → WebGL2 → Canvas2D. GPU renderers are used when no custom canvas is given and the browser supports them:
202
317
 
318
+ GPU failures are recovered automatically. AkariSub retains the last completed subtitle frame while rebuilding after WebGPU `device.lost` or WebGL2 `webglcontextlost`; it resumes the same backend after recovery, or switches to Canvas2D if rebuilding/restoration fails. Recovery notifications include `reason: 'device-lost' | 'context-lost'`.
319
+
203
320
  ```typescript
204
321
  import AkariSub from 'akarisub'
205
322
 
@@ -222,69 +339,114 @@ if (renderer.isUsingGPURenderer) {
222
339
 
223
340
  The default options are best, and automatically fallback to the next fastest options in line, when the API's they use are unsupported. You can however forcefully change this behavior by specifying options.
224
341
 
225
- | Option | Type | Default | Description |
226
- | --------------------- | ------------------------------------ | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
227
- | `video` | HTMLVideoElement | - | Video to use as target for rendering and event listeners |
228
- | `canvas` | HTMLCanvasElement | - | Canvas to use for manual handling (optional if video is provided) |
229
- | `blendMode` | `'js'` \| `'wasm'` | `'wasm'` | Image blending mode. WASM is better for low-end devices, JS for hardware acceleration |
230
- | `asyncRender` | boolean | auto | Render via ImageBitmap. Defaults to `true` on Canvas2D paths and `false` when a GPU renderer is active (raw buffers upload with fewer copies) or on WebKit |
231
- | `offscreenRender` | boolean | automatic | Render fully on the worker; enabled for video-managed canvases and disabled for custom canvases |
232
- | `rawAssImageGpu` | boolean | `false` | Compose raw libass masks with worker WebGL2 when offscreen rendering is active |
233
- | `onDemandRender` | boolean | `true` | Render subtitles as the video player renders frames |
234
- | `adaptiveTiming` | boolean | `true` | Compensate measured queue, worker, bitmap, IPC, and paint latency while video is playing; pause and seek renders remain frame-exact |
235
- | `frameTimeline` | FrameTimeline | - | Encoded browser-frame timestamps plus optional media/subtitle clock offsets; enables frame-locked libass sampling |
236
- | `framePrefetch` | number | `2` | Number of exact subtitle-frame bitmaps to prepare ahead (`0` disables preparation, maximum `24`) |
237
- | `targetFps` | number | `24` | Target FPS when not using onDemandRender |
238
- | `timeOffset` | number | `0` | Subtitle time offset in seconds |
239
- | `debug` | boolean | `false` | Enable debug logging |
240
- | `prescaleFactor` | number | `1.0` | Scale factor for subtitles canvas |
241
- | `prescaleHeightLimit` | number | `1080` | Height limit for prescaling in pixels |
242
- | `maxRenderHeight` | number | `0` | Maximum render height (0 = no limit) |
243
- | `dropAllAnimations` | boolean | `false` | Discard all animated tags for performance |
244
- | `dropAllBlur` | boolean | `false` | Drop all blur effects (~10x performance gain) |
245
- | `clampPos` | boolean | `false` | Clamp `\pos` values to script resolution |
246
- | `renderAhead` | number | `0` | Optional extra seconds to render ahead, in addition to adaptive timing; also applies when `onDemandRender` is disabled |
247
- | `workerUrl` | string | package worker URL | Optional worker script URL. Defaults to the package worker module URL |
248
- | `wasmUrl` | string | package WASM URL | Optional WASM binary URL. Defaults to the URL resolved from `import.meta.url` |
249
- | `glueUrl` | string | package glue URL | Optional WASM glue script URL. Defaults to the URL resolved from `import.meta.url` |
250
- | `subUrl` | string | - | URL of the subtitle file to play |
251
- | `subContent` | string \| Uint8Array \| ArrayBuffer | - | Content of the subtitle file to play |
252
- | `encryptedSubContent` | EncryptedSubtitleContent | - | AES-GCM encrypted subtitle payload, decrypted inside the worker |
253
- | `fonts` | (string \| Uint8Array)[] | - | Array of font URLs or Uint8Arrays to force load |
254
- | `availableFonts` | Record<string, string \| Uint8Array> | liberation sans from package assets | Available fonts map (lowercase name URL/data) |
255
- | `fallbackFonts` | string[] | `['liberation sans']` | Fallback font families in order, used for the fontconfig cascade |
256
- | `useLocalFonts` | boolean | `true` | Use Local Font Access API if available |
257
- | `libassMemoryLimit` | number | `128` | libass bitmap cache memory limit in MiB |
258
- | `libassGlyphLimit` | number | `2048` | libass glyph cache limit |
259
- | `fullTrackWarmup` | boolean | `false` | Pre-render early track windows after load to warm libass caches |
260
- | `onCanvasFallback` | function | - | Callback when no GPU renderer is available (Canvas2D fallback) |
342
+ | Option | Type | Default | Description |
343
+ | ---------------------- | ------------------------------------ | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
344
+ | `video` | HTMLVideoElement | - | Video to use as target for rendering and event listeners. WebCodecs players pass `canvas` and call `presentVideoFrame` instead |
345
+ | `canvas` | HTMLCanvasElement | - | Canvas to use for manual handling (optional if video is provided) |
346
+ | `blendMode` | `'js'` \| `'wasm'` | `'wasm'` | Image blending mode. WASM is better for low-end devices, JS for hardware acceleration |
347
+ | `asyncRender` | boolean | auto | Render via ImageBitmap. Defaults to `true` on Canvas2D paths and `false` when a GPU renderer is active (raw buffers upload with fewer copies) or on WebKit |
348
+ | `offscreenRender` | boolean | automatic | Render fully on the worker; enabled for video-managed canvases and disabled for custom canvases |
349
+ | `rawAssImageGpu` | boolean | `false` | Compose raw libass masks with worker WebGL2 when offscreen rendering is active |
350
+ | `onDemandRender` | boolean | `true` | Render subtitles as the video player or `presentVideoFrame` presents frames |
351
+ | `adaptiveTiming` | boolean | `true` | Compensate measured queue, worker, bitmap, IPC, and paint latency while video is playing; pause and seek renders remain frame-exact |
352
+ | `frameTimeline` | FrameTimeline | - | Encoded browser-frame timestamps plus optional media/subtitle clock offsets; enables frame-locked libass sampling |
353
+ | `framePrefetch` | number | `2` | Number of exact subtitle-frame bitmaps to prepare ahead (`0` disables preparation, maximum `24`) |
354
+ | `targetFps` | number | `24` | Target FPS when not using onDemandRender |
355
+ | `timeOffset` | number | `0` | Subtitle time offset in seconds |
356
+ | `debug` | boolean | `false` | Enable debug logging |
357
+ | `prescaleFactor` | number | `1.0` | Scale factor for subtitles canvas |
358
+ | `prescaleHeightLimit` | number | `1080` | Height limit for prescaling in pixels |
359
+ | `maxRenderHeight` | number | `0` | Maximum render height (0 = no limit) |
360
+ | `dropAllAnimations` | boolean | `false` | Discard all animated tags for performance |
361
+ | `dropAllBlur` | boolean | `false` | Drop all blur effects (~10x performance gain) |
362
+ | `clampPos` | boolean | `false` | Clamp `\pos` values to script resolution |
363
+ | `renderAhead` | number | `0` | Optional extra seconds to render ahead, in addition to adaptive timing; also applies when `onDemandRender` is disabled |
364
+ | `workerUrl` | string | package worker URL | Optional worker script URL. Defaults to the package worker module URL |
365
+ | `wasmUrl` | string | package WASM URL | Optional WASM binary URL. Defaults to the URL resolved from `import.meta.url` |
366
+ | `glueUrl` | string | package glue URL | Optional WASM glue script URL. Defaults to the URL resolved from `import.meta.url` |
367
+ | `modernWasmUrl` | string | - | Optional SIMD WASM URL used when the engine validates `v128` |
368
+ | `mtWasmUrl` | string | package `akarisub-mt.wasm` | Optional pthread SIMD WASM URL. Requires COOP/COEP (`crossOriginIsolated`) |
369
+ | `canvasColorSpace` | `'srgb'` \| `'display-p3'` \| `'rec2020'` \| `'auto'` | `'auto'` | Overlay canvas color space. `auto` follows the video primaries |
370
+ | `hdr` | boolean \| `'auto'` | `'auto'` | Request an HDR canvas when the video transfer is PQ or HLG |
371
+ | `subUrl` | string | - | URL of the subtitle file to play |
372
+ | `subContent` | string \| Uint8Array \| ArrayBuffer | - | Content of the subtitle file to play |
373
+ | `encryptedSubContent` | EncryptedSubtitleContent | - | AES-GCM encrypted subtitle payload, decrypted inside the worker |
374
+ | `fonts` | (string \| Uint8Array)[] | - | Font URLs or bytes to force-load in full |
375
+ | `availableFonts` | Record<string, FontFamilySource> | liberation sans from package assets | Family name (lowercase) → URL, bytes, or unicode-range / script slices. Slices load lazily from the current track |
376
+ | `lazyFonts` | boolean | `true` | When `availableFonts` entries declare slices, fetch only the ones the current scripts need |
377
+ | `fallbackFonts` | string[] | `['liberation sans']` | Fallback font families in order, used for the fontconfig cascade |
378
+ | `useLocalFonts` | boolean | `true` | Use Local Font Access API if available |
379
+ | `libassMemoryLimit` | number | `128` | libass bitmap cache memory limit in MiB |
380
+ | `libassGlyphLimit` | number | `2048` | libass glyph cache limit |
381
+ | `fullTrackWarmup` | boolean | `false` | Pre-render early track windows after load to warm libass caches |
382
+ | `onCanvasFallback` | function | - | Callback when no GPU renderer is available (Canvas2D fallback) |
383
+ | `onCueEnter` | `(cue) => void` | - | Dialogue event became active at the sampled media time |
384
+ | `onCueExit` | `(cue) => void` | - | Dialogue event is no longer active |
385
+ | `onRender` | `(event) => void` | - | Demand-frame render finished |
386
+ | `onRendererChange` | `(event) => void` | - | Compositor backend changed or recovered; GPU recovery includes `event.reason` |
387
+ | `onPerformanceWarning` | `(warning) => void` | - | Slow frame, dropped frames, a full demand queue, or GPU recovery |
261
388
 
262
389
  ## Methods
263
390
 
264
391
  ### Track Management
265
392
 
266
- | Method | Parameters | Description |
267
- | ---------------------------- | ---------------------------------------------- | ---------------------------------------------------------------- |
268
- | `setTrackByUrl(url)` | `url: string` | Load subtitle from URL |
269
- | `setTrack(content)` | `content: string \| Uint8Array \| ArrayBuffer` | Set subtitle from content |
270
- | `setEncryptedTrack(content)` | `content: EncryptedSubtitleContent` | Set subtitle from an encrypted payload (decrypted in the worker) |
271
- | `freeTrack()` | - | Remove current subtitles |
393
+ | Method | Parameters | Description |
394
+ | ----------------------------- | ---------------------------------------------- | -------------------------------------------------------------------------------- |
395
+ | `setTrackByUrl(url)` | `url: string` | Load subtitle from URL |
396
+ | `setTrack(content)` | `content: string \| Uint8Array \| ArrayBuffer` | Set subtitle from content |
397
+ | `setEncryptedTrack(content)` | `content: EncryptedSubtitleContent` | Set subtitle from an encrypted payload (decrypted in the worker) |
398
+ | `freeTrack()` | - | Remove current subtitles |
399
+ | `initStreamingTrack(options?)` | `StreamingTrackOptions \| string \| bytes` | Create a header-only track that can accept later fragments |
400
+ | `appendSubtitleData(content)` | `content: string \| Uint8Array \| ArrayBuffer` | Parse ASS text (Dialogue lines, extra styles) into the current track |
401
+ | `appendSubtitleChunk(data, start, duration)` | `data`, `start: number`, `duration: number` | Append one Matroska packet; `start`/`duration` are seconds |
402
+ | `appendEvents(events)` | `events: Partial<ASSEvent>[]` | Append several Dialogue events in one message |
403
+ | `pruneEvents(before)` | `before: number` | Drop events that ended before `before` seconds |
404
+ | `flushEvents()` | - | Drop every event and keep styles / script info |
405
+ | `configurePrune(delay)` | `delay: number` | Auto-drop events that ended more than `delay` seconds before the last render |
406
+ | `preloadTrack(source)` | `PreloadTrackSource \| string \| bytes` | Parse a track and load its fonts without replacing the visible one |
407
+ | `activatePreloadedTrack(id?)` | `id?: number` | Atomically swap to a preloaded track; last frame stays until the first new paint |
272
408
 
273
409
  ### Playback Control
274
410
 
275
- | Method | Parameters | Description |
276
- | ------------------------------------------------ | --------------------------------------------------------- | --------------------------------------------- |
277
- | `setIsPaused(isPaused)` | `isPaused: boolean` | Set playback pause state |
278
- | `setRate(rate)` | `rate: number` | Set playback rate (speed multiplier) |
279
- | `setCurrentTime(isPaused?, currentTime?, rate?)` | `isPaused?: boolean, currentTime?: number, rate?: number` | Set current time, playback state and rate |
280
- | `setFrameTimeline(frameTimes)` | `ArrayLike<number> \| null` | Replace or disable the encoded-frame timeline |
411
+ | Method | Parameters | Description |
412
+ | ------------------------------------------------ | ----------------------------------------------------------- | --------------------------------------------- |
413
+ | `setIsPaused(isPaused)` | `isPaused: boolean` | Set playback pause state |
414
+ | `setRate(rate)` | `rate: number` | Set playback rate (speed multiplier) |
415
+ | `setCurrentTime(isPaused?, currentTime?, rate?)` | `isPaused?: boolean, currentTime?: number, rate?: number` | Set current time, playback state and rate |
416
+ | `setFrameTimeline(frameTimes)` | `ArrayLike<number> \| null` | Replace or disable the encoded-frame timeline |
417
+ | `presentVideoFrame(frame, options?)` | `frame: VideoFrame, options?: PresentVideoFrameOptions` | Present a WebCodecs frame as the video clock |
418
+ | `setVideoColorSpace(colorSpace)` | `BT709` \| `BT601` \| `BT2020` \| matrix name \| `colorSpace` \| `null` | Set the video YCbCr matrix and optional HDR primaries/transfer |
281
419
 
282
420
  ### Video & Canvas
283
421
 
284
- | Method | Parameters | Description |
285
- | ---------------------------------------------- | ------------------------------------------------------------------------------- | --------------------------- |
286
- | `setVideo(video)` | `video: HTMLVideoElement` | Change target video element |
287
- | `resize(width?, height?, top?, left?, force?)` | `width?: number, height?: number, top?: number, left?: number, force?: boolean` | Resize the canvas |
422
+ | Method | Parameters | Description |
423
+ | ---------------------------------------------- | ------------------------------------------------------------------------------- | ----------------------------------------------------------- |
424
+ | `setVideo(video)` | `video: HTMLVideoElement` | Change target video element. Clears a WebCodecs frame clock |
425
+ | `resize(width?, height?, top?, left?, force?)` | `width?: number, height?: number, top?: number, left?: number, force?: boolean` | Resize the canvas |
426
+
427
+ ### Cue and render callbacks
428
+
429
+ Option callbacks and matching `EventTarget` events (`cueEnter`, `cueExit`, `render`, `rendererChange`, `performanceWarning`) report subtitle-clock activity and compositor lifecycle changes. Use them for chapter markers, character overlays, or analytics instead of polling `getEvents()`:
430
+
431
+ ```ts
432
+ const renderer = new AkariSub({
433
+ video,
434
+ subUrl: '/subs/en.ass',
435
+ onCueEnter: (cue) => showCharacter(cue.name, cue.text),
436
+ onCueExit: (cue) => hideCharacter(cue.index),
437
+ onRender: (event) => recordSubtitleFrame(event.time, event.renderTimeMs),
438
+ onPerformanceWarning: (warning) => {
439
+ if (warning.kind === 'slow-frame') console.warn('slow subtitle frame', warning.renderTimeMs)
440
+ if (warning.kind === 'renderer-recovery') console.warn('GPU recovery', warning.reason)
441
+ }
442
+ })
443
+
444
+ renderer.addEventListener('rendererChange', (event) => {
445
+ console.log('compositor', event.detail.rendererType, event.detail.reason)
446
+ })
447
+ ```
448
+
449
+ `CueEvent.start` and `CueEvent.duration` are seconds on the subtitle clock, matching `video.currentTime` plus `timeOffset`. Encrypted tracks omit `name` and `text`.
288
450
 
289
451
  ### Event Management
290
452
 
@@ -431,5 +593,5 @@ bun run build # builds the WASM glue and TypeScript
431
593
  ```
432
594
 
433
595
  - If on macOS with libtool from brew, `LIBTOOLIZE=glibtoolize make`
434
- - Incremental rebuilds of the WASM glue only: `bun run build:wasm` (or `make worker`)
596
+ - Incremental rebuilds of the WASM glue only: `bun run build:wasm` (or `make workers`)
435
597
  - Artifacts are in `pkg/` (WASM glue and binary) and `dist/` (TypeScript)
package/dist/COPYRIGHT CHANGED
@@ -24,7 +24,7 @@ Regenerate it with `bun run license:third-party` after dependency, submodule, fo
24
24
 
25
25
  | Package | Version | License | Source |
26
26
  | --- | --- | --- | --- |
27
- | `@types/bun` | 1.3.14 | MIT | https://github.com/DefinitelyTyped/DefinitelyTyped.git |
27
+ | `@types/bun` | 1.4.0 | MIT | https://github.com/DefinitelyTyped/DefinitelyTyped.git |
28
28
  | `@types/emscripten` | 1.41.5 | MIT | https://github.com/DefinitelyTyped/DefinitelyTyped.git |
29
29
  | `@types/node` | 25.0.9 | MIT | https://github.com/DefinitelyTyped/DefinitelyTyped.git |
30
30
  | `@typescript/typescript-aix-ppc64` | 7.0.2 | Apache-2.0 | https://github.com/microsoft/TypeScript.git |
@@ -47,8 +47,8 @@ Regenerate it with `bun run license:third-party` after dependency, submodule, fo
47
47
  | `@typescript/typescript-sunos-x64` | 7.0.2 | Apache-2.0 | https://github.com/microsoft/TypeScript.git |
48
48
  | `@typescript/typescript-win32-arm64` | 7.0.2 | Apache-2.0 | https://github.com/microsoft/TypeScript.git |
49
49
  | `@typescript/typescript-win32-x64` | 7.0.2 | Apache-2.0 | https://github.com/microsoft/TypeScript.git |
50
- | `@webgpu/types` | 0.1.71 | BSD-3-Clause | gpuweb/types |
51
- | `bun-types` | 1.3.14 | MIT | https://github.com/oven-sh/bun |
50
+ | `@webgpu/types` | 0.1.72 | BSD-3-Clause | gpuweb/types |
51
+ | `bun-types` | 1.4.0 | MIT | https://github.com/oven-sh/bun |
52
52
  | `prettier` | 3.9.6 | MIT | prettier/prettier |
53
53
  | `typescript` | 7.0.2 | Apache-2.0 | https://github.com/microsoft/TypeScript.git |
54
54
  | `undici-types` | 7.16.0 | MIT | git+https://github.com/nodejs/undici.git |
package/dist/index.d.ts CHANGED
@@ -1,12 +1,30 @@
1
1
  /**
2
- * AkariSub - JavaScript ASS/SSA Subtitle Renderer
2
+ * AkariSub ASS/SSA subtitle renderer for the browser.
3
3
  *
4
- * High-performance ASS/SSA subtitle renderer using libass compiled to WebAssembly.
4
+ * Renders libass subtitles with a WebAssembly worker and an optional WebGPU or
5
+ * WebGL2 compositor.
5
6
  *
6
- * @packageDocumentation
7
+ * @example
8
+ * ```ts
9
+ * import AkariSub from '@altq/akarisub'
10
+ *
11
+ * const renderer = new AkariSub({
12
+ * video: document.querySelector('video')!,
13
+ * subUrl: '/subtitles/example.ass'
14
+ * })
15
+ *
16
+ * renderer.destroy()
17
+ * ```
18
+ *
19
+ * @module
7
20
  */
8
21
  export { default } from './wrapper';
9
22
  export { default as AkariSub } from './wrapper';
10
- export type { ASSEvent, ASSStyle, AkariSubOptions, FrameTimeline, PerformanceStats, ASSEventCallback, ASSStyleCallback, PerformanceStatsCallback, ResetStatsCallback, RenderImage, RenderTimes, VideoFrameCallbackMetadata, SubtitleColorSpace, WebYCbCrColorSpace, EncryptedSubtitleContent } from './ts/types';
11
- export { webYCbCrMap, colorMatrixConversionMap, libassYCbCrMap, computeCanvasSize, getVideoPosition, fixAlpha, parseAss, dropBlur, fixPlayRes, testImageBugs, runFeatureTests, getAlphaBug, getBitmapBug, getColorSpaceFilterUrl, getWasmUrl, getWasmGlueUrl, getDefaultFontUrl, WebGPURenderer, isWebGPUSupported, WebGL2Renderer, isWebGL2Supported } from './wrapper';
23
+ export type { ASSEvent, ASSStyle, AkariSubOptions, CueEvent, FontBytes, FontFamilySource, FontSubsetSource, FrameTimeline, PerformanceStats, PerformanceWarning, PreloadedTrack, PreloadTrackSource, PresentVideoFrameOptions, RenderEvent, RendererChangeEvent, RendererRecoveryReason, RendererType, StreamingTrackFormat, StreamingTrackOptions, ASSEventCallback, ASSStyleCallback, PerformanceStatsCallback, ResetStatsCallback, RenderImage, RenderTimes, VideoFrameCallbackMetadata, VideoFrameLike, SubtitleColorSpace, WebYCbCrColorSpace, EncryptedSubtitleContent } from './ts/types';
24
+ export type { CanvasColorSpace, VideoColorProfile, VideoPrimaries, VideoTransfer } from './ts/color-space';
25
+ export type { ASSSection, ASSBodyEntry } from './ts/utils';
26
+ export { webYCbCrMap, colorMatrixConversionMap, libassYCbCrMap, computeCanvasSize, getVideoPosition, fixAlpha, parseAss, dropBlur, fixPlayRes, testImageBugs, runFeatureTests, getAlphaBug, getBitmapBug, getColorSpaceFilterUrl, getColorMatrix3, colorMatrix3ColumnMajor, getWasmUrl, getWasmGlueUrl, getMtWasmUrl, getMtWasmGlueUrl, getDefaultFontUrl, supportsWasmSimd, supportsWasmThreads, WebGPURenderer, isWebGPUSupported, WebGL2Renderer, isWebGL2Supported, VIDEO_FRAME_TIMESTAMP_SECONDS, frameTimelineFromTimestamps, frameTimelineFromVideoFrames, isVideoFrameLike, isWebCodecsVideoFrameSupported, videoFrameCallbackMetadata, videoFrameColorSpace, videoFrameMediaTime } from './wrapper';
27
+ export { collectUnicodeScripts, collectUnicodeScriptsFromAss, expandScriptAlias, parseUnicodeRange, stripAssOverrides } from './ts/unicode-scripts';
28
+ export { collectNeededScripts, matchFontSubsets, normalizeFontFamilySource } from './ts/font-subsets';
29
+ export { parseStreamingTrackOptions } from './ts/streaming';
12
30
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,WAAW,CAAA;AAE/C,YAAY,EACV,QAAQ,EACR,QAAQ,EACR,eAAe,EACf,aAAa,EACb,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,wBAAwB,EACxB,kBAAkB,EAClB,WAAW,EACX,WAAW,EACX,0BAA0B,EAC1B,kBAAkB,EAClB,kBAAkB,EAClB,wBAAwB,EACzB,MAAM,YAAY,CAAA;AAEnB,OAAO,EACL,WAAW,EACX,wBAAwB,EACxB,cAAc,EACd,iBAAiB,EACjB,gBAAgB,EAChB,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,UAAU,EACV,aAAa,EACb,eAAe,EACf,WAAW,EACX,YAAY,EACZ,sBAAsB,EACtB,UAAU,EACV,cAAc,EACd,iBAAiB,EACjB,cAAc,EACd,iBAAiB,EACjB,cAAc,EACd,iBAAiB,EAClB,MAAM,WAAW,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,WAAW,CAAA;AAE/C,YAAY,EACV,QAAQ,EACR,QAAQ,EACR,eAAe,EACf,QAAQ,EACR,SAAS,EACT,gBAAgB,EAChB,gBAAgB,EAChB,aAAa,EACb,gBAAgB,EAChB,kBAAkB,EAClB,cAAc,EACd,kBAAkB,EAClB,wBAAwB,EACxB,WAAW,EACX,mBAAmB,EACnB,sBAAsB,EACtB,YAAY,EACZ,oBAAoB,EACpB,qBAAqB,EACrB,gBAAgB,EAChB,gBAAgB,EAChB,wBAAwB,EACxB,kBAAkB,EAClB,WAAW,EACX,WAAW,EACX,0BAA0B,EAC1B,cAAc,EACd,kBAAkB,EAClB,kBAAkB,EAClB,wBAAwB,EACzB,MAAM,YAAY,CAAA;AACnB,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAE1G,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAE1D,OAAO,EACL,WAAW,EACX,wBAAwB,EACxB,cAAc,EACd,iBAAiB,EACjB,gBAAgB,EAChB,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,UAAU,EACV,aAAa,EACb,eAAe,EACf,WAAW,EACX,YAAY,EACZ,sBAAsB,EACtB,eAAe,EACf,uBAAuB,EACvB,UAAU,EACV,cAAc,EACd,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EAChB,mBAAmB,EACnB,cAAc,EACd,iBAAiB,EACjB,cAAc,EACd,iBAAiB,EACjB,6BAA6B,EAC7B,2BAA2B,EAC3B,4BAA4B,EAC5B,gBAAgB,EAChB,8BAA8B,EAC9B,0BAA0B,EAC1B,oBAAoB,EACpB,mBAAmB,EACpB,MAAM,WAAW,CAAA;AAElB,OAAO,EACL,qBAAqB,EACrB,4BAA4B,EAC5B,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,sBAAsB,CAAA;AAE7B,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,yBAAyB,EAC1B,MAAM,mBAAmB,CAAA;AAE1B,OAAO,EAAE,0BAA0B,EAAE,MAAM,gBAAgB,CAAA"}
package/dist/index.js CHANGED
@@ -1,11 +1,27 @@
1
1
  /**
2
- * AkariSub - JavaScript ASS/SSA Subtitle Renderer
2
+ * AkariSub ASS/SSA subtitle renderer for the browser.
3
3
  *
4
- * High-performance ASS/SSA subtitle renderer using libass compiled to WebAssembly.
4
+ * Renders libass subtitles with a WebAssembly worker and an optional WebGPU or
5
+ * WebGL2 compositor.
5
6
  *
6
- * @packageDocumentation
7
+ * @example
8
+ * ```ts
9
+ * import AkariSub from '@altq/akarisub'
10
+ *
11
+ * const renderer = new AkariSub({
12
+ * video: document.querySelector('video')!,
13
+ * subUrl: '/subtitles/example.ass'
14
+ * })
15
+ *
16
+ * renderer.destroy()
17
+ * ```
18
+ *
19
+ * @module
7
20
  */
8
21
  export { default } from './wrapper';
9
22
  export { default as AkariSub } from './wrapper';
10
- export { webYCbCrMap, colorMatrixConversionMap, libassYCbCrMap, computeCanvasSize, getVideoPosition, fixAlpha, parseAss, dropBlur, fixPlayRes, testImageBugs, runFeatureTests, getAlphaBug, getBitmapBug, getColorSpaceFilterUrl, getWasmUrl, getWasmGlueUrl, getDefaultFontUrl, WebGPURenderer, isWebGPUSupported, WebGL2Renderer, isWebGL2Supported } from './wrapper';
23
+ export { webYCbCrMap, colorMatrixConversionMap, libassYCbCrMap, computeCanvasSize, getVideoPosition, fixAlpha, parseAss, dropBlur, fixPlayRes, testImageBugs, runFeatureTests, getAlphaBug, getBitmapBug, getColorSpaceFilterUrl, getColorMatrix3, colorMatrix3ColumnMajor, getWasmUrl, getWasmGlueUrl, getMtWasmUrl, getMtWasmGlueUrl, getDefaultFontUrl, supportsWasmSimd, supportsWasmThreads, WebGPURenderer, isWebGPUSupported, WebGL2Renderer, isWebGL2Supported, VIDEO_FRAME_TIMESTAMP_SECONDS, frameTimelineFromTimestamps, frameTimelineFromVideoFrames, isVideoFrameLike, isWebCodecsVideoFrameSupported, videoFrameCallbackMetadata, videoFrameColorSpace, videoFrameMediaTime } from './wrapper';
24
+ export { collectUnicodeScripts, collectUnicodeScriptsFromAss, expandScriptAlias, parseUnicodeRange, stripAssOverrides } from './ts/unicode-scripts';
25
+ export { collectNeededScripts, matchFontSubsets, normalizeFontFamilySource } from './ts/font-subsets';
26
+ export { parseStreamingTrackOptions } from './ts/streaming';
11
27
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,WAAW,CAAA;AAoB/C,OAAO,EACL,WAAW,EACX,wBAAwB,EACxB,cAAc,EACd,iBAAiB,EACjB,gBAAgB,EAChB,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,UAAU,EACV,aAAa,EACb,eAAe,EACf,WAAW,EACX,YAAY,EACZ,sBAAsB,EACtB,UAAU,EACV,cAAc,EACd,iBAAiB,EACjB,cAAc,EACd,iBAAiB,EACjB,cAAc,EACd,iBAAiB,EAClB,MAAM,WAAW,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,WAAW,CAAA;AAsC/C,OAAO,EACL,WAAW,EACX,wBAAwB,EACxB,cAAc,EACd,iBAAiB,EACjB,gBAAgB,EAChB,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,UAAU,EACV,aAAa,EACb,eAAe,EACf,WAAW,EACX,YAAY,EACZ,sBAAsB,EACtB,eAAe,EACf,uBAAuB,EACvB,UAAU,EACV,cAAc,EACd,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EAChB,mBAAmB,EACnB,cAAc,EACd,iBAAiB,EACjB,cAAc,EACd,iBAAiB,EACjB,6BAA6B,EAC7B,2BAA2B,EAC3B,4BAA4B,EAC5B,gBAAgB,EAChB,8BAA8B,EAC9B,0BAA0B,EAC1B,oBAAoB,EACpB,mBAAmB,EACpB,MAAM,WAAW,CAAA;AAElB,OAAO,EACL,qBAAqB,EACrB,4BAA4B,EAC5B,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,sBAAsB,CAAA;AAE7B,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,yBAAyB,EAC1B,MAAM,mBAAmB,CAAA;AAE1B,OAAO,EAAE,0BAA0B,EAAE,MAAM,gBAAgB,CAAA"}