akarisub 0.2.1 → 1.0.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 (78) hide show
  1. package/LICENSE +3 -0
  2. package/README.md +226 -193
  3. package/THIRD_PARTY_NOTICES.md +1482 -0
  4. package/dist/COPYRIGHT +1480 -949
  5. package/dist/index.d.ts +12 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +11 -147
  8. package/dist/index.js.map +1 -0
  9. package/dist/ts/{ts/akarisub.d.ts → akarisub.d.ts} +80 -103
  10. package/dist/ts/akarisub.d.ts.map +1 -0
  11. package/dist/ts/akarisub.js +2086 -0
  12. package/dist/ts/akarisub.js.map +1 -0
  13. package/dist/ts/timing.d.ts +63 -0
  14. package/dist/ts/timing.d.ts.map +1 -0
  15. package/dist/ts/timing.js +219 -0
  16. package/dist/ts/timing.js.map +1 -0
  17. package/dist/ts/{ts/types.d.ts → types.d.ts} +105 -80
  18. package/dist/ts/types.d.ts.map +1 -0
  19. package/dist/ts/types.js +2 -0
  20. package/dist/ts/{ts/types.js.map → types.js.map} +1 -1
  21. package/dist/ts/{ts/utils.d.ts → utils.d.ts} +0 -31
  22. package/dist/ts/utils.d.ts.map +1 -0
  23. package/dist/ts/{ts/utils.js → utils.js} +0 -56
  24. package/dist/ts/utils.js.map +1 -0
  25. package/dist/ts/wasm.d.ts +12 -0
  26. package/dist/ts/wasm.d.ts.map +1 -0
  27. package/dist/ts/wasm.js +46 -0
  28. package/dist/ts/wasm.js.map +1 -0
  29. package/dist/ts/{ts/webgl2-renderer.d.ts → webgl2-renderer.d.ts} +2 -13
  30. package/dist/ts/webgl2-renderer.d.ts.map +1 -0
  31. package/dist/ts/{ts/webgl2-renderer.js → webgl2-renderer.js} +18 -42
  32. package/dist/ts/webgl2-renderer.js.map +1 -0
  33. package/dist/ts/{ts/webgpu-renderer.d.ts → webgpu-renderer.d.ts} +19 -20
  34. package/dist/ts/webgpu-renderer.d.ts.map +1 -0
  35. package/dist/ts/{ts/webgpu-renderer.js → webgpu-renderer.js} +164 -120
  36. package/dist/ts/webgpu-renderer.js.map +1 -0
  37. package/dist/ts/worker.d.ts +2 -0
  38. package/dist/ts/{ts/worker.d.ts.map → worker.d.ts.map} +1 -1
  39. package/dist/ts/{ts/worker.js → worker.js} +1182 -315
  40. package/dist/ts/worker.js.map +1 -0
  41. package/dist/wrapper.d.ts +8 -0
  42. package/dist/wrapper.d.ts.map +1 -0
  43. package/dist/{ts/index.js → wrapper.js} +4 -14
  44. package/dist/wrapper.js.map +1 -0
  45. package/package.json +25 -21
  46. package/pkg/akarisub.js +53 -0
  47. package/pkg/akarisub.wasm +0 -0
  48. package/src/wrapper.ts +46 -0
  49. package/dist/akarisub-worker.js +0 -39
  50. package/dist/akarisub-worker.wasm +0 -0
  51. package/dist/akarisub.umd.js +0 -159
  52. package/dist/ts/index.d.ts +0 -14
  53. package/dist/ts/index.d.ts.map +0 -1
  54. package/dist/ts/index.js.map +0 -1
  55. package/dist/ts/ts/akarisub.d.ts.map +0 -1
  56. package/dist/ts/ts/akarisub.js +0 -1079
  57. package/dist/ts/ts/akarisub.js.map +0 -1
  58. package/dist/ts/ts/types.d.ts.map +0 -1
  59. package/dist/ts/ts/types.js +0 -5
  60. package/dist/ts/ts/utils.d.ts.map +0 -1
  61. package/dist/ts/ts/utils.js.map +0 -1
  62. package/dist/ts/ts/webgl2-renderer.d.ts.map +0 -1
  63. package/dist/ts/ts/webgl2-renderer.js.map +0 -1
  64. package/dist/ts/ts/webgpu-renderer.d.ts.map +0 -1
  65. package/dist/ts/ts/webgpu-renderer.js.map +0 -1
  66. package/dist/ts/ts/worker.d.ts +0 -6
  67. package/dist/ts/ts/worker.js.map +0 -1
  68. package/dist/ts/wrapper.d.ts +0 -8
  69. package/dist/ts/wrapper.d.ts.map +0 -1
  70. package/dist/ts/wrapper.js +0 -9
  71. package/dist/ts/wrapper.js.map +0 -1
  72. package/src/ts/akarisub.ts +0 -1278
  73. package/src/ts/types.ts +0 -396
  74. package/src/ts/utils.ts +0 -512
  75. package/src/ts/webgl2-renderer.ts +0 -420
  76. package/src/ts/webgpu-renderer.ts +0 -736
  77. package/src/ts/worker.ts +0 -1973
  78. /package/{dist → assets}/default.woff2 +0 -0
package/LICENSE CHANGED
@@ -21,3 +21,6 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
21
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
22
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
23
  SOFTWARE.
24
+
25
+ Third-party components and bundled assets are licensed separately. See
26
+ THIRD_PARTY_NOTICES.md, also shipped as dist/COPYRIGHT, for dependency notices.
package/README.md CHANGED
@@ -25,9 +25,10 @@ AkariSub is a JS wrapper for <a href="https://github.com/libass/libass">libass</
25
25
 
26
26
  ### Fork Enhancements
27
27
 
28
- - **WebGPU Support** - Hardware-accelerated rendering using the modern WebGPU API [(on browsers which support it)](https://caniuse.com/webgpu)
28
+ - **GPU Rendering** - Hardware-accelerated rendering with an automatic fallback chain: WebGPU [(on browsers which support it)](https://caniuse.com/webgpu) → WebGL2 → Canvas2D
29
29
  - **Hyper Optimizations** - Performance improvements and intelligent caching for smoother playback
30
30
  - **Proper Fontconfig Implementation** - add Fontconfig support with multiple fallback fonts supported
31
+ - **Encrypted Subtitles** - optionally load AES-GCM encrypted subtitle payloads that are decrypted inside the worker, so plaintext never touches the main thread
31
32
  - **Statistics Reporting** - Built-in statistics and performance metrics for debugging and monitoring
32
33
  - **TypeScript Support** - Full TypeScript definitions and type safety
33
34
  - **Updated Dependencies** - All dependencies updated to their latest versions, including libass
@@ -48,10 +49,10 @@ deno add jsr:@altq/akarisub
48
49
 
49
50
  ## Usage
50
51
 
51
- By default all you need to do is copy the files from the `dist/` folder of the repository into the same folder as where your JS runs, then do:
52
+ In most bundler-based projects, no manual worker setup is required. AkariSub resolves the WASM glue and binary relative to the package module URL, so bundlers such as Vite, webpack, and Rollup can emit the assets automatically.
52
53
 
53
54
  ```js
54
- import AkariSub from './akarisub.es.js'
55
+ import AkariSub from 'akarisub'
55
56
 
56
57
  const renderer = new AkariSub({
57
58
  video: document.querySelector('video'),
@@ -59,45 +60,70 @@ const renderer = new AkariSub({
59
60
  })
60
61
  ```
61
62
 
62
- `Note:` while the `dist/` folder includes a UMD dist it still uses modern syntax. If you want backwards compatibility with older browsers I recommend you run it tru babel.
63
+ If your app serves package files in a way that does not expose those emitted assets to the browser, you can still provide the public fallback by copying the WASM file and its JS glue to `/akarisub/`:
64
+
65
+ ```bash
66
+ mkdir -p public/akarisub
67
+ cp node_modules/akarisub/pkg/akarisub.wasm node_modules/akarisub/pkg/akarisub.js public/akarisub/
68
+ ```
69
+
70
+ The worker is created from the package module URL. `workerUrl`, `wasmUrl`, and `glueUrl` remain in the option type for overrides and do not need to be set in bundler-based apps.
71
+
72
+ ## Using only with canvas
63
73
 
64
- If you use a bundler like Vite, you can instead do:
74
+ You're also able to use it without any video. However, that requires you to set the time the subtitles should render at yourself. Disable `onDemandRender` (it relies on video frame callbacks) and drive the clock manually:
65
75
 
66
76
  ```js
67
77
  import AkariSub from 'akarisub'
68
- import workerUrl from 'akarisub/worker?url'
69
- import wasmUrl from 'akarisub/worker.wasm?url'
70
78
 
71
79
  const renderer = new AkariSub({
72
- video: document.querySelector('video'),
73
- subContent: subtitleString,
74
- workerUrl,
75
- wasmUrl
80
+ canvas: document.querySelector('canvas'),
81
+ subUrl: './tracks/sub.ass',
82
+ onDemandRender: false
76
83
  })
84
+
85
+ // setCurrentTime(isPaused?, currentTime?, rate?)
86
+ renderer.setCurrentTime(true, 15)
77
87
  ```
78
88
 
79
- ## Using only with canvas
89
+ Custom canvases stay on the main thread by default. Set both `offscreenRender: true` and `rawAssImageGpu: true` to opt into worker WebGL2 raw-mask composition when dense overlays benefit from it.
80
90
 
81
- You're also able to use it without any video. However, that requires you to set the time the subtitles should render at yourself:
91
+ ### Timing semantics
82
92
 
83
- ```js
84
- import AkariSub from './akarisub.es.js'
93
+ AkariSub sends libass an integer millisecond timestamp. Fractional media times are floored to the current millisecond, matching libass's `Start <= now < Start + Duration` event boundaries. `timeOffset` and `renderAhead` are measured in seconds.
94
+
95
+ By default, `adaptiveTiming` predicts the media time at which a completed canvas paint will become visible, compensating bounded rendering and delivery latency. Pause and seek frames always use the exact media timestamp. Set `adaptiveTiming: false` when deterministic frame sampling is more important than compensating live presentation latency.
85
96
 
97
+ For frame-locked VOD playback, provide the encoded video's presentation timestamps in seconds. AkariSub prepares a small window of full subtitle bitmaps and commits the matching bitmap inside `requestVideoFrameCallback`, which keeps fast `\t` and `\move` animation sampling aligned with libass:
98
+
99
+ ```js
100
+ const frameTimeline = new Float64Array([0, 0.041708, 0.083417])
86
101
  const renderer = new AkariSub({
87
- canvas: document.querySelector('canvas'),
88
- subUrl: './tracks/sub.ass'
102
+ video,
103
+ subContent,
104
+ frameTimeline,
105
+ framePrefetch: 2
89
106
  })
90
107
 
91
- renderer.setCurrentTime(15)
108
+ // Timelines can also arrive after renderer construction.
109
+ renderer.setFrameTimeline(frameTimeline)
110
+ renderer.setFrameTimeline(null) // return to adaptive continuous-time rendering
92
111
  ```
93
112
 
113
+ Use timestamps from the encoded rendition the browser plays, normalized to the browser media clock. Passing a timeline remains optional; cache misses and unsupported paths automatically use normal adaptive rendering.
114
+
115
+ If the browser timeline is normalized from decode time rather than the first display PTS, preserve that initial B-frame reorder gap in the array and set `frameTimeline.subtitleTimeOffset` to the first frame timestamp. AkariSub uses the unmodified array to locate the visible video frame, then removes the offset only when sampling libass.
116
+
117
+ `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
+
94
119
  ## Changing subtitles
95
120
 
96
- 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 three methods that you can use for this specifically:
121
+ 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:
97
122
 
98
123
  - `setTrackByUrl(url):` works the same as the `subUrl` option. It will set the subtitle to display by its URL.
99
- - `setTrack(content):` works the same as the `subContent` option. It will set the subtitle to display by its content.
100
- - `freeTrack():` this simply removes the subtitles. You can use the two methods above to set a new subtitle file to be displayed.
124
+ - `setTrack(content):` works the same as the `subContent` option. It will set the subtitle to display by its content (string, `Uint8Array` or `ArrayBuffer`).
125
+ - `setEncryptedTrack(content):` works the same as the `encryptedSubContent` option. The payload is decrypted inside the worker, so plaintext subtitles are never materialized on the main thread.
126
+ - `freeTrack():` this simply removes the subtitles. You can use the methods above to set a new subtitle file to be displayed.
101
127
 
102
128
  ```js
103
129
  renderer.setTrackByUrl('/newsub.ass')
@@ -129,6 +155,7 @@ console.log(stats)
129
155
  // maxRenderTime: 8.32,
130
156
  // minRenderTime: 0.12,
131
157
  // lastRenderTime: 1.23,
158
+ // timingCompensationMs: 4.8,
132
159
  // renderFps: 60,
133
160
  // usingWorker: true,
134
161
  // offscreenRender: true,
@@ -151,26 +178,27 @@ console.log(`Events: ${eventCount}, Styles: ${styleCount}`)
151
178
 
152
179
  **Stats Reference:**
153
180
 
154
- | Property | Type | Description |
155
- |----------|------|-------------|
156
- | `framesRendered` | number | Total frames rendered since reset |
157
- | `framesDropped` | number | Frames dropped due to slow rendering |
158
- | `avgRenderTime` | number | Average render time in milliseconds |
159
- | `maxRenderTime` | number | Maximum render time in milliseconds |
160
- | `minRenderTime` | number | Minimum render time in milliseconds |
161
- | `lastRenderTime` | number | Most recent render time in milliseconds |
162
- | `renderFps` | number | Estimated render FPS based on timing |
163
- | `usingWorker` | boolean | Whether using Web Worker |
164
- | `offscreenRender` | boolean | Whether offscreen rendering is enabled |
165
- | `onDemandRender` | boolean | Whether on-demand rendering is enabled |
166
- | `pendingRenders` | number | Number of pending render operations |
167
- | `totalEvents` | number | Total subtitle events in current track |
168
- | `cacheHits` | number | Number of cache hits (unchanged frames) |
169
- | `cacheMisses` | number | Number of cache misses (rendered frames) |
170
-
171
- ## WebGPU Rendering
172
-
173
- AkariSub automatically uses WebGPU for GPU-accelerated rendering when available, with automatic fallback to Canvas2D:
181
+ | Property | Type | Description |
182
+ | ---------------------- | ------- | ----------------------------------------------------------------------- |
183
+ | `framesRendered` | number | Total frames rendered since reset |
184
+ | `framesDropped` | number | Frames dropped due to slow rendering |
185
+ | `avgRenderTime` | number | Average render time in milliseconds |
186
+ | `maxRenderTime` | number | Maximum render time in milliseconds |
187
+ | `minRenderTime` | number | Minimum render time in milliseconds |
188
+ | `lastRenderTime` | number | Most recent render time in milliseconds |
189
+ | `timingCompensationMs` | number | Automatically learned presentation-latency compensation in milliseconds |
190
+ | `renderFps` | number | Estimated render FPS based on timing |
191
+ | `usingWorker` | boolean | Whether using Web Worker |
192
+ | `offscreenRender` | boolean | Whether offscreen rendering is enabled |
193
+ | `onDemandRender` | boolean | Whether on-demand rendering is enabled |
194
+ | `pendingRenders` | number | Number of pending render operations |
195
+ | `totalEvents` | number | Total subtitle events in current track |
196
+ | `cacheHits` | number | Number of cache hits (unchanged frames) |
197
+ | `cacheMisses` | number | Number of cache misses (rendered frames) |
198
+
199
+ ## GPU Rendering
200
+
201
+ 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:
174
202
 
175
203
  ```typescript
176
204
  import AkariSub from 'akarisub'
@@ -178,14 +206,14 @@ import AkariSub from 'akarisub'
178
206
  const renderer = new AkariSub({
179
207
  video: document.querySelector('video'),
180
208
  subUrl: './tracks/sub.ass',
181
- preferWebGPU: true, // Enable WebGPU (default: true)
182
- onWebGPUFallback: () => {
183
- console.log('WebGPU unavailable, using Canvas2D fallback')
209
+ onCanvasFallback: () => {
210
+ console.log('No GPU renderer available, using Canvas2D fallback')
184
211
  }
185
212
  })
186
213
 
187
- // Check if WebGPU is being used
188
- if (renderer.isUsingWebGPU) {
214
+ console.log(renderer.rendererType) // 'webgpu' | 'webgl2' | 'canvas2d'
215
+
216
+ if (renderer.isUsingGPURenderer) {
189
217
  console.log('GPU-accelerated rendering enabled!')
190
218
  }
191
219
  ```
@@ -194,178 +222,190 @@ if (renderer.isUsingWebGPU) {
194
222
 
195
223
  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.
196
224
 
197
- | Option | Type | Default | Description |
198
- |--------|------|---------|-------------|
199
- | `video` | HTMLVideoElement | - | Video to use as target for rendering and event listeners |
200
- | `canvas` | HTMLCanvasElement | - | Canvas to use for manual handling (optional if video is provided) |
201
- | `blendMode` | `'js'` \| `'wasm'` | `'wasm'` | Image blending mode. WASM is better for low-end devices, JS for hardware acceleration |
202
- | `asyncRender` | boolean | `true` | Use async rendering with ImageBitmap for GPU offloading |
203
- | `offscreenRender` | boolean | `true` | Render fully on the worker, greatly reduces CPU usage |
204
- | `onDemandRender` | boolean | `true` | Render subtitles as the video player renders frames |
205
- | `targetFps` | number | `24` | Target FPS when not using onDemandRender |
206
- | `timeOffset` | number | `0` | Subtitle time offset in seconds |
207
- | `debug` | boolean | `false` | Enable debug logging |
208
- | `prescaleFactor` | number | `1.0` | Scale factor for subtitles canvas |
209
- | `prescaleHeightLimit` | number | `1080` | Height limit for prescaling in pixels |
210
- | `maxRenderHeight` | number | `0` | Maximum render height (0 = no limit) |
211
- | `dropAllAnimations` | boolean | `false` | Discard all animated tags for performance |
212
- | `dropAllBlur` | boolean | `false` | Drop all blur effects (~10x performance gain) |
213
- | `clampPos` | boolean | `false` | Clamp `\pos` values to script resolution |
214
- | `workerUrl` | string | `'akarisub-worker.js'` | URL to the worker script |
215
- | `wasmUrl` | string | `'akarisub-worker.wasm'` | URL to the WASM binary |
216
- | `subUrl` | string | - | URL of the subtitle file to play |
217
- | `subContent` | string | - | Content of the subtitle file to play |
218
- | `fonts` | (string \| Uint8Array)[] | - | Array of font URLs or Uint8Arrays to force load |
219
- | `availableFonts` | Record<string, string \| Uint8Array> | `{'liberation sans': './default.woff2'}` | Available fonts map (lowercase name URL/data) |
220
- | `fallbackFont` | string | `'liberation sans'` | Fallback font family key |
221
- | `useLocalFonts` | boolean | `false` | Use Local Font Access API if available |
222
- | `libassMemoryLimit` | number | - | libass bitmap cache memory limit in MiB |
223
- | `libassGlyphLimit` | number | - | libass glyph cache limit |
224
- | `preferWebGPU` | boolean | `true` | Prefer WebGPU renderer if available |
225
- | `onWebGPUFallback` | function | - | Callback when WebGPU is unavailable |
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) |
226
261
 
227
262
  ## Methods
228
263
 
229
264
  ### Track Management
230
265
 
231
- | Method | Parameters | Description |
232
- |--------|------------|-------------|
233
- | `setTrackByUrl(url)` | `url: string` | Load subtitle from URL |
234
- | `setTrack(content)` | `content: string` | Set subtitle from string content |
235
- | `freeTrack()` | - | Remove current subtitles |
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 |
236
272
 
237
273
  ### Playback Control
238
274
 
239
- | Method | Parameters | Description |
240
- |--------|------------|-------------|
241
- | `setIsPaused(isPaused)` | `isPaused: boolean` | Set playback pause state |
242
- | `setRate(rate)` | `rate: number` | Set playback rate (speed multiplier) |
243
- | `setCurrentTime(isPaused?, currentTime?, rate?)` | `isPaused?: boolean, currentTime?: number, rate?: number` | Set current time, playback state and rate |
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 |
244
281
 
245
282
  ### Video & Canvas
246
283
 
247
- | Method | Parameters | Description |
248
- |--------|------------|-------------|
249
- | `setVideo(video)` | `video: HTMLVideoElement` | Change target video element |
250
- | `resize(width?, height?, top?, left?, force?)` | `width?: number, height?: number, top?: number, left?: number, force?: boolean` | Resize the canvas |
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 |
251
288
 
252
289
  ### Event Management
253
290
 
254
- | Method | Parameters | Returns | Description |
255
- |--------|------------|---------|-------------|
256
- | `createEvent(event)` | `event: Partial<ASSEvent>` | `void` | Create a new ASS event |
257
- | `setEvent(event, index)` | `event: Partial<ASSEvent>, index: number` | `void` | Overwrite event at index |
258
- | `removeEvent(index)` | `index: number` | `void` | Remove event at index |
259
- | `getEvents()` | - | `Promise<ASSEvent[]>` | Get all ASS events |
260
- | `getEventCount()` | - | `Promise<number>` | Get event count (lightweight) |
291
+ | Method | Parameters | Returns | Description |
292
+ | ------------------------ | ----------------------------------------- | --------------------- | ----------------------------- |
293
+ | `createEvent(event)` | `event: Partial<ASSEvent>` | `void` | Create a new ASS event |
294
+ | `setEvent(event, index)` | `event: Partial<ASSEvent>, index: number` | `void` | Overwrite event at index |
295
+ | `removeEvent(index)` | `index: number` | `void` | Remove event at index |
296
+ | `getEvents()` | - | `Promise<ASSEvent[]>` | Get all ASS events |
297
+ | `getEventCount()` | - | `Promise<number>` | Get event count (lightweight) |
261
298
 
262
299
  ### Style Management
263
300
 
264
- | Method | Parameters | Returns | Description |
265
- |--------|------------|---------|-------------|
266
- | `createStyle(style)` | `style: Partial<ASSStyle>` | `void` | Create a new ASS style |
267
- | `setStyle(style, index)` | `style: Partial<ASSStyle>, index: number` | `void` | Overwrite style at index |
268
- | `removeStyle(index)` | `index: number` | `void` | Remove style at index |
269
- | `getStyles()` | - | `Promise<ASSStyle[]>` | Get all ASS styles |
270
- | `getStyleCount()` | - | `Promise<number>` | Get style count (lightweight) |
271
- | `styleOverride(style)` | `style: Partial<ASSStyle>` | `void` | Set a style override |
272
- | `disableStyleOverride()` | - | `void` | Disable style override |
301
+ | Method | Parameters | Returns | Description |
302
+ | ------------------------ | ----------------------------------------- | --------------------- | ----------------------------- |
303
+ | `createStyle(style)` | `style: Partial<ASSStyle>` | `void` | Create a new ASS style |
304
+ | `setStyle(style, index)` | `style: Partial<ASSStyle>, index: number` | `void` | Overwrite style at index |
305
+ | `removeStyle(index)` | `index: number` | `void` | Remove style at index |
306
+ | `getStyles()` | - | `Promise<ASSStyle[]>` | Get all ASS styles |
307
+ | `getStyleCount()` | - | `Promise<number>` | Get style count (lightweight) |
308
+ | `styleOverride(style)` | `style: Partial<ASSStyle>` | `void` | Set a style override |
309
+ | `disableStyleOverride()` | - | `void` | Disable style override |
273
310
 
274
311
  ### Font Management
275
312
 
276
- | Method | Parameters | Description |
277
- |--------|------------|-------------|
278
- | `addFont(font)` | `font: string \| Uint8Array` | Add a font to the renderer |
279
- | `setDefaultFont(font)` | `font: string` | Change the default font family |
313
+ | Method | Parameters | Description |
314
+ | ---------------------- | ---------------------------- | ------------------------------ |
315
+ | `addFont(font)` | `font: string \| Uint8Array` | Add a font to the renderer |
316
+ | `setDefaultFont(font)` | `font: string` | Change the default font family |
280
317
 
281
318
  ### Statistics & Debugging
282
319
 
283
- | Method | Parameters | Returns | Description |
284
- |--------|------------|---------|-------------|
285
- | `getStats()` | - | `Promise<PerformanceStats>` | Get performance statistics |
286
- | `resetStats()` | - | `Promise<void>` | Reset statistics counters |
287
- | `getEventCount()` | - | `Promise<number>` | Get event count (lightweight) |
288
- | `getStyleCount()` | - | `Promise<number>` | Get style count (lightweight) |
289
- | `runBenchmark()` | - | `void` | Run a benchmark on the worker |
320
+ | Method | Parameters | Returns | Description |
321
+ | ----------------- | ---------- | --------------------------- | ----------------------------- |
322
+ | `getStats()` | - | `Promise<PerformanceStats>` | Get performance statistics |
323
+ | `resetStats()` | - | `Promise<void>` | Reset statistics counters |
324
+ | `getEventCount()` | - | `Promise<number>` | Get event count (lightweight) |
325
+ | `getStyleCount()` | - | `Promise<number>` | Get style count (lightweight) |
290
326
 
291
327
  ### Lifecycle
292
328
 
293
- | Method | Parameters | Description |
294
- |--------|------------|-------------|
295
- | `destroy(err?)` | `err?: Error \| string` | Destroy the renderer and cleanup |
296
- | `sendMessage(target, data?, transferable?)` | `target: string, data?: Record<string, any>, transferable?: Transferable[]` | Send data to worker |
329
+ | Method | Parameters | Description |
330
+ | ------------------------------------------- | --------------------------------------------------------------------------- | -------------------------------- |
331
+ | `destroy(err?)` | `err?: Error \| string` | Destroy the renderer and cleanup |
332
+ | `sendMessage(target, data?, transferable?)` | `target: string, data?: Record<string, any>, transferable?: Transferable[]` | Send data to worker |
297
333
 
298
334
  ## Properties
299
335
 
300
- | Property | Type | Description |
301
- |----------|------|-------------|
302
- | `debug` | boolean | Enable/disable debug logging |
303
- | `prescaleFactor` | number | Scale factor for subtitles |
304
- | `prescaleHeightLimit` | number | Height limit for prescaling |
305
- | `maxRenderHeight` | number | Maximum render height |
306
- | `timeOffset` | number | Subtitle time offset in seconds |
307
- | `busy` | boolean | Whether the renderer is currently busy |
308
- | `isUsingWebGPU` | boolean | Whether WebGPU renderer is active (read-only) |
336
+ | Property | Type | Description |
337
+ | --------------------- | ---------------------------------------- | ------------------------------------------------------------- |
338
+ | `debug` | boolean | Enable/disable debug logging |
339
+ | `prescaleFactor` | number | Scale factor for subtitles |
340
+ | `prescaleHeightLimit` | number | Height limit for prescaling |
341
+ | `maxRenderHeight` | number | Maximum render height |
342
+ | `timeOffset` | number | Subtitle time offset in seconds |
343
+ | `renderAhead` | number | Optional extra seconds to render ahead of the video clock |
344
+ | `framePrefetch` | number | Number of exact subtitle frames prepared ahead |
345
+ | `busy` | boolean | Whether the renderer is currently busy |
346
+ | `rendererType` | `'webgpu'` \| `'webgl2'` \| `'canvas2d'` | Active renderer backend (read-only) |
347
+ | `isUsingGPURenderer` | boolean | Whether a hardware-accelerated renderer is active (read-only) |
348
+ | `isUsingWebGPU` | boolean | _Deprecated_ - use `rendererType === 'webgpu'` |
309
349
 
310
350
  ## Type Definitions
311
351
 
312
352
  ### ASSEvent
313
353
 
314
- | Property | Type | Description |
315
- |----------|------|-------------|
316
- | `Start` | number | Start time in seconds |
317
- | `Duration` | number | Duration in seconds |
318
- | `Style` | string | Style name |
319
- | `Name` | string | Character name (informational) |
320
- | `MarginL` | number | Left margin override in pixels |
321
- | `MarginR` | number | Right margin override in pixels |
322
- | `MarginV` | number | Bottom margin override in pixels |
323
- | `Effect` | string | Transition effect |
324
- | `Text` | string | Subtitle text content |
325
- | `ReadOrder` | number | Read order number |
326
- | `Layer` | number | Z-index layer |
327
- | `_index` | number | Internal index (optional) |
354
+ | Property | Type | Description |
355
+ | ----------- | ------ | -------------------------------- |
356
+ | `Start` | number | Start time in milliseconds |
357
+ | `Duration` | number | Duration in milliseconds |
358
+ | `Style` | string | Style name |
359
+ | `Name` | string | Character name (informational) |
360
+ | `MarginL` | number | Left margin override in pixels |
361
+ | `MarginR` | number | Right margin override in pixels |
362
+ | `MarginV` | number | Bottom margin override in pixels |
363
+ | `Effect` | string | Transition effect |
364
+ | `Text` | string | Subtitle text content |
365
+ | `ReadOrder` | number | Read order number |
366
+ | `Layer` | number | Z-index layer |
367
+ | `_index` | number | Internal index (optional) |
328
368
 
329
369
  ### ASSStyle
330
370
 
331
- | Property | Type | Description |
332
- |----------|------|-------------|
333
- | `Name` | string | Style name (case sensitive) |
334
- | `FontName` | string | Font family name |
335
- | `FontSize` | number | Font size |
336
- | `PrimaryColour` | number | Primary color (RGBA as uint32) |
337
- | `SecondaryColour` | number | Secondary color (RGBA as uint32) |
338
- | `OutlineColour` | number | Outline color (RGBA as uint32) |
339
- | `BackColour` | number | Background/shadow color (RGBA as uint32) |
340
- | `Bold` | number | Bold (-1 = true, 0 = false) |
341
- | `Italic` | number | Italic (-1 = true, 0 = false) |
342
- | `Underline` | number | Underline (-1 = true, 0 = false) |
343
- | `StrikeOut` | number | StrikeOut (-1 = true, 0 = false) |
344
- | `ScaleX` | number | Width scale (percent) |
345
- | `ScaleY` | number | Height scale (percent) |
346
- | `Spacing` | number | Extra spacing between characters (pixels) |
347
- | `Angle` | number | Rotation angle (degrees) |
348
- | `BorderStyle` | number | Border style (1 = outline + shadow, 3 = opaque box) |
349
- | `Outline` | number | Outline width (0-4 pixels) |
350
- | `Shadow` | number | Shadow depth (0-4 pixels) |
351
- | `Alignment` | number | Alignment (1-9, numpad style) |
352
- | `MarginL` | number | Left margin (pixels) |
353
- | `MarginR` | number | Right margin (pixels) |
354
- | `MarginV` | number | Vertical margin (pixels) |
355
- | `Encoding` | number | Font encoding |
356
- | `treat_fontname_as_pattern` | number | Treat font name as pattern |
357
- | `Blur` | number | Blur amount |
358
- | `Justify` | number | Text justification |
371
+ | Property | Type | Description |
372
+ | --------------------------- | ------ | --------------------------------------------------- |
373
+ | `Name` | string | Style name (case sensitive) |
374
+ | `FontName` | string | Font family name |
375
+ | `FontSize` | number | Font size |
376
+ | `PrimaryColour` | number | Primary color (RGBA as uint32) |
377
+ | `SecondaryColour` | number | Secondary color (RGBA as uint32) |
378
+ | `OutlineColour` | number | Outline color (RGBA as uint32) |
379
+ | `BackColour` | number | Background/shadow color (RGBA as uint32) |
380
+ | `Bold` | number | Bold (-1 = true, 0 = false) |
381
+ | `Italic` | number | Italic (-1 = true, 0 = false) |
382
+ | `Underline` | number | Underline (-1 = true, 0 = false) |
383
+ | `StrikeOut` | number | StrikeOut (-1 = true, 0 = false) |
384
+ | `ScaleX` | number | Width scale (percent) |
385
+ | `ScaleY` | number | Height scale (percent) |
386
+ | `Spacing` | number | Extra spacing between characters (pixels) |
387
+ | `Angle` | number | Rotation angle (degrees) |
388
+ | `BorderStyle` | number | Border style (1 = outline + shadow, 3 = opaque box) |
389
+ | `Outline` | number | Outline width (0-4 pixels) |
390
+ | `Shadow` | number | Shadow depth (0-4 pixels) |
391
+ | `Alignment` | number | Alignment (1-9, numpad style) |
392
+ | `MarginL` | number | Left margin (pixels) |
393
+ | `MarginR` | number | Right margin (pixels) |
394
+ | `MarginV` | number | Vertical margin (pixels) |
395
+ | `Encoding` | number | Font encoding |
396
+ | `treat_fontname_as_pattern` | number | Treat font name as pattern |
397
+ | `Blur` | number | Blur amount |
398
+ | `Justify` | number | Text justification |
359
399
 
360
400
  # How to build?
361
401
 
362
402
  ## Dependencies
363
403
 
404
+ [mise](https://mise.jdx.dev) manages the toolchain (emsdk, bun, cmake — see `mise.toml`). You additionally need the usual autotools build dependencies:
405
+
364
406
  - git
365
- - emscripten (Configure the enviroment)
366
407
  - make
367
408
  - python3
368
- - cmake
369
409
  - pkgconfig
370
410
  - patch
371
411
  - libtool
@@ -374,29 +414,22 @@ The default options are best, and automatically fallback to the next fastest opt
374
414
  - ragel - Required by Harfbuzz
375
415
  - itstool - Required by Fontconfig
376
416
  - gperf - Required by Fontconfig
377
- - licensecheck
378
417
 
379
418
  ## Get the Source
380
419
 
381
- Run git clone --recursive https://github.com/altqx/akarisub.git
382
-
383
- ## Build inside a Container
384
-
385
- ### Docker
386
-
387
- 1. Install Docker
388
- 2. ./run-docker-build.sh
389
- 3. Artifacts are in /dist/js
390
-
391
- ### Buildah
420
+ ```bash
421
+ git clone --recursive https://github.com/altqx/akarisub.git
422
+ ```
392
423
 
393
- 1. Install Buildah and a suitable backend for buildah run like crun or runc
394
- 2. ./run-buildah-build.sh
395
- 3. Artifacts are in /dist/js
424
+ ## Build
396
425
 
397
- ## Build without Containers
426
+ ```bash
427
+ mise install # installs emsdk, bun, cmake
428
+ bun install # JS dependencies
429
+ make # builds the static libs (fribidi, freetype, harfbuzz, fontconfig, libass, ...) and the WASM glue
430
+ bun run build # builds the WASM glue and TypeScript
431
+ ```
398
432
 
399
- 1. Install the dependency packages listed above
400
- 2. make
401
- - If on macOS with libtool from brew, LIBTOOLIZE=glibtoolize make
402
- 3. Artifacts are in /dist/js
433
+ - 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`)
435
+ - Artifacts are in `pkg/` (WASM glue and binary) and `dist/` (TypeScript)