akarisub 0.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.
package/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-2026 AkariSub contributors
4
+ Copyright (c) 2021-2022 JASSUB contributors
5
+ Copyright (c) 2017-2021 JavascriptSubtitlesOctopus contributors
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,388 @@
1
+ <h1 align="center">
2
+ AkariSub
3
+ </h1>
4
+ <p align="center">
5
+ JavaScript SSA/ASS Subtitle Renderer.
6
+ </p>
7
+
8
+ > **Note:** This is a fork of [ThaUnknown's JASSUB](https://github.com/ThaUnknown/jassub) legacy version with hyper optimizations, intelligent caching, and many quality-of-life improvements.
9
+
10
+ AkariSub is a JS wrapper for <a href="https://github.com/libass/libass">libass</a>, which renders <a href="https://en.wikipedia.org/wiki/SubStation_Alpha">SSA/ASS subtitles</a> directly in your browser. It uses Emscripten to compile libass' C++ code to WASM.
11
+
12
+ ## Features
13
+
14
+ - Supports most SSA/ASS features (everything libass supports)
15
+ - Supports all OpenType, TrueType and WOFF fonts, as well as embedded fonts
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)
18
+ - Capable of using local fonts [(on browsers which support it)](https://caniuse.com/mdn-api_window_querylocalfonts)
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)
21
+ - Is asynchronous (renders when available, not in order of execution)
22
+ - Benefits from hardware acceleration (uses hardware accelerated canvas API's)
23
+ - Doesn't manipulate the DOM to render subtitles
24
+ - Easy to use - just connect it to video element
25
+
26
+ ### Fork Enhancements
27
+
28
+ - **WebGPU Support** - Hardware-accelerated rendering using the modern WebGPU API [(on browsers which support it)](https://caniuse.com/webgpu)
29
+ - **Hyper Optimizations** - Performance improvements and intelligent caching for smoother playback
30
+ - **Proper Fontconfig Implementation** - add Fontconfig support with multiple fallback fonts supported
31
+ - **Statistics Reporting** - Built-in statistics and performance metrics for debugging and monitoring
32
+ - **TypeScript Support** - Full TypeScript definitions and type safety
33
+ - **Updated Dependencies** - All dependencies updated to their latest versions, including libass
34
+
35
+ ## Usage
36
+
37
+ 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:
38
+
39
+ ```js
40
+ import AkariSub from './akarisub.es.js'
41
+
42
+ const renderer = new AkariSub({
43
+ video: document.querySelector('video'),
44
+ subUrl: './tracks/sub.ass'
45
+ })
46
+ ```
47
+
48
+ `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.
49
+
50
+ If you use a bundler like Vite, you can instead do:
51
+
52
+ ```js
53
+ import AkariSub from 'akarisub'
54
+ import workerUrl from 'akarisub/dist/akarisub-worker.js?url'
55
+ import wasmUrl from 'akarisub/dist/akarisub-worker.wasm?url'
56
+
57
+ const renderer = new AkariSub({
58
+ video: document.querySelector('video'),
59
+ subContent: subtitleString,
60
+ workerUrl, // you can also use: `new URL('akarisub/dist/akarisub-worker.js', import.meta.url)` instead of importing it as an url
61
+ wasmUrl
62
+ })
63
+ ```
64
+
65
+ ## Using only with canvas
66
+
67
+ You're also able to use it without any video. However, that requires you to set the time the subtitles should render at yourself:
68
+
69
+ ```js
70
+ import AkariSub from './akarisub.es.js'
71
+
72
+ const renderer = new AkariSub({
73
+ canvas: document.querySelector('canvas'),
74
+ subUrl: './tracks/sub.ass'
75
+ })
76
+
77
+ renderer.setCurrentTime(15)
78
+ ```
79
+
80
+ ## Changing subtitles
81
+
82
+ 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:
83
+
84
+ - `setTrackByUrl(url):` works the same as the `subUrl` option. It will set the subtitle to display by its URL.
85
+ - `setTrack(content):` works the same as the `subContent` option. It will set the subtitle to display by its content.
86
+ - `freeTrack():` this simply removes the subtitles. You can use the two methods above to set a new subtitle file to be displayed.
87
+
88
+ ```js
89
+ renderer.setTrackByUrl('/newsub.ass')
90
+ ```
91
+
92
+ ## Cleaning up the object
93
+
94
+ After you're finished with rendering the subtitles. You need to call the `destroy()` method to correctly destroy the object.
95
+
96
+ ```js
97
+ const renderer = new AkariSub(options)
98
+ // After you've finished using it...
99
+ renderer.destroy()
100
+ ```
101
+
102
+ ## Performance Statistics
103
+
104
+ Get real-time performance metrics for debugging and monitoring:
105
+
106
+ ```typescript
107
+ // Get performance statistics (Promise-based)
108
+ const stats = await renderer.getStats()
109
+ console.log(stats)
110
+ // Output:
111
+ // {
112
+ // framesRendered: 120,
113
+ // framesDropped: 2,
114
+ // avgRenderTime: 1.45,
115
+ // maxRenderTime: 8.32,
116
+ // minRenderTime: 0.12,
117
+ // lastRenderTime: 1.23,
118
+ // renderFps: 60,
119
+ // usingWorker: true,
120
+ // offscreenRender: true,
121
+ // onDemandRender: true,
122
+ // pendingRenders: 0,
123
+ // totalEvents: 847,
124
+ // cacheHits: 500,
125
+ // cacheMisses: 120
126
+ // }
127
+
128
+ // Reset statistics counters
129
+ await renderer.resetStats()
130
+ console.log('Stats reset!')
131
+
132
+ // Get lightweight counts (doesn't fetch full event/style data)
133
+ const eventCount = await renderer.getEventCount()
134
+ const styleCount = await renderer.getStyleCount()
135
+ console.log(`Events: ${eventCount}, Styles: ${styleCount}`)
136
+ ```
137
+
138
+ **Stats Reference:**
139
+
140
+ | Property | Type | Description |
141
+ |----------|------|-------------|
142
+ | `framesRendered` | number | Total frames rendered since reset |
143
+ | `framesDropped` | number | Frames dropped due to slow rendering |
144
+ | `avgRenderTime` | number | Average render time in milliseconds |
145
+ | `maxRenderTime` | number | Maximum render time in milliseconds |
146
+ | `minRenderTime` | number | Minimum render time in milliseconds |
147
+ | `lastRenderTime` | number | Most recent render time in milliseconds |
148
+ | `renderFps` | number | Estimated render FPS based on timing |
149
+ | `usingWorker` | boolean | Whether using Web Worker |
150
+ | `offscreenRender` | boolean | Whether offscreen rendering is enabled |
151
+ | `onDemandRender` | boolean | Whether on-demand rendering is enabled |
152
+ | `pendingRenders` | number | Number of pending render operations |
153
+ | `totalEvents` | number | Total subtitle events in current track |
154
+ | `cacheHits` | number | Number of cache hits (unchanged frames) |
155
+ | `cacheMisses` | number | Number of cache misses (rendered frames) |
156
+
157
+ ## WebGPU Rendering
158
+
159
+ AkariSub automatically uses WebGPU for GPU-accelerated rendering when available, with automatic fallback to Canvas2D:
160
+
161
+ ```typescript
162
+ import AkariSub from 'akarisub'
163
+
164
+ const renderer = new AkariSub({
165
+ video: document.querySelector('video'),
166
+ subUrl: './tracks/sub.ass',
167
+ preferWebGPU: true, // Enable WebGPU (default: true)
168
+ onWebGPUFallback: () => {
169
+ console.log('WebGPU unavailable, using Canvas2D fallback')
170
+ }
171
+ })
172
+
173
+ // Check if WebGPU is being used
174
+ if (renderer.isUsingWebGPU) {
175
+ console.log('GPU-accelerated rendering enabled!')
176
+ }
177
+ ```
178
+
179
+ ## Options
180
+
181
+ 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.
182
+
183
+ | Option | Type | Default | Description |
184
+ |--------|------|---------|-------------|
185
+ | `video` | HTMLVideoElement | - | Video to use as target for rendering and event listeners |
186
+ | `canvas` | HTMLCanvasElement | - | Canvas to use for manual handling (optional if video is provided) |
187
+ | `blendMode` | `'js'` \| `'wasm'` | `'wasm'` | Image blending mode. WASM is better for low-end devices, JS for hardware acceleration |
188
+ | `asyncRender` | boolean | `true` | Use async rendering with ImageBitmap for GPU offloading |
189
+ | `offscreenRender` | boolean | `true` | Render fully on the worker, greatly reduces CPU usage |
190
+ | `onDemandRender` | boolean | `true` | Render subtitles as the video player renders frames |
191
+ | `targetFps` | number | `24` | Target FPS when not using onDemandRender |
192
+ | `timeOffset` | number | `0` | Subtitle time offset in seconds |
193
+ | `debug` | boolean | `false` | Enable debug logging |
194
+ | `prescaleFactor` | number | `1.0` | Scale factor for subtitles canvas |
195
+ | `prescaleHeightLimit` | number | `1080` | Height limit for prescaling in pixels |
196
+ | `maxRenderHeight` | number | `0` | Maximum render height (0 = no limit) |
197
+ | `dropAllAnimations` | boolean | `false` | Discard all animated tags for performance |
198
+ | `dropAllBlur` | boolean | `false` | Drop all blur effects (~10x performance gain) |
199
+ | `clampPos` | boolean | `false` | Clamp `\pos` values to script resolution |
200
+ | `workerUrl` | string | `'akarisub-worker.js'` | URL to the worker script |
201
+ | `wasmUrl` | string | `'akarisub-worker.wasm'` | URL to the WASM binary |
202
+ | `subUrl` | string | - | URL of the subtitle file to play |
203
+ | `subContent` | string | - | Content of the subtitle file to play |
204
+ | `fonts` | (string \| Uint8Array)[] | - | Array of font URLs or Uint8Arrays to force load |
205
+ | `availableFonts` | Record<string, string \| Uint8Array> | `{'liberation sans': './default.woff2'}` | Available fonts map (lowercase name → URL/data) |
206
+ | `fallbackFont` | string | `'liberation sans'` | Fallback font family key |
207
+ | `useLocalFonts` | boolean | `false` | Use Local Font Access API if available |
208
+ | `libassMemoryLimit` | number | - | libass bitmap cache memory limit in MiB |
209
+ | `libassGlyphLimit` | number | - | libass glyph cache limit |
210
+ | `preferWebGPU` | boolean | `true` | Prefer WebGPU renderer if available |
211
+ | `onWebGPUFallback` | function | - | Callback when WebGPU is unavailable |
212
+
213
+ ## Methods
214
+
215
+ ### Track Management
216
+
217
+ | Method | Parameters | Description |
218
+ |--------|------------|-------------|
219
+ | `setTrackByUrl(url)` | `url: string` | Load subtitle from URL |
220
+ | `setTrack(content)` | `content: string` | Set subtitle from string content |
221
+ | `freeTrack()` | - | Remove current subtitles |
222
+
223
+ ### Playback Control
224
+
225
+ | Method | Parameters | Description |
226
+ |--------|------------|-------------|
227
+ | `setIsPaused(isPaused)` | `isPaused: boolean` | Set playback pause state |
228
+ | `setRate(rate)` | `rate: number` | Set playback rate (speed multiplier) |
229
+ | `setCurrentTime(isPaused?, currentTime?, rate?)` | `isPaused?: boolean, currentTime?: number, rate?: number` | Set current time, playback state and rate |
230
+
231
+ ### Video & Canvas
232
+
233
+ | Method | Parameters | Description |
234
+ |--------|------------|-------------|
235
+ | `setVideo(video)` | `video: HTMLVideoElement` | Change target video element |
236
+ | `resize(width?, height?, top?, left?, force?)` | `width?: number, height?: number, top?: number, left?: number, force?: boolean` | Resize the canvas |
237
+
238
+ ### Event Management
239
+
240
+ | Method | Parameters | Returns | Description |
241
+ |--------|------------|---------|-------------|
242
+ | `createEvent(event)` | `event: Partial<ASSEvent>` | `void` | Create a new ASS event |
243
+ | `setEvent(event, index)` | `event: Partial<ASSEvent>, index: number` | `void` | Overwrite event at index |
244
+ | `removeEvent(index)` | `index: number` | `void` | Remove event at index |
245
+ | `getEvents()` | - | `Promise<ASSEvent[]>` | Get all ASS events |
246
+ | `getEventCount()` | - | `Promise<number>` | Get event count (lightweight) |
247
+
248
+ ### Style Management
249
+
250
+ | Method | Parameters | Returns | Description |
251
+ |--------|------------|---------|-------------|
252
+ | `createStyle(style)` | `style: Partial<ASSStyle>` | `void` | Create a new ASS style |
253
+ | `setStyle(style, index)` | `style: Partial<ASSStyle>, index: number` | `void` | Overwrite style at index |
254
+ | `removeStyle(index)` | `index: number` | `void` | Remove style at index |
255
+ | `getStyles()` | - | `Promise<ASSStyle[]>` | Get all ASS styles |
256
+ | `getStyleCount()` | - | `Promise<number>` | Get style count (lightweight) |
257
+ | `styleOverride(style)` | `style: Partial<ASSStyle>` | `void` | Set a style override |
258
+ | `disableStyleOverride()` | - | `void` | Disable style override |
259
+
260
+ ### Font Management
261
+
262
+ | Method | Parameters | Description |
263
+ |--------|------------|-------------|
264
+ | `addFont(font)` | `font: string \| Uint8Array` | Add a font to the renderer |
265
+ | `setDefaultFont(font)` | `font: string` | Change the default font family |
266
+
267
+ ### Statistics & Debugging
268
+
269
+ | Method | Parameters | Returns | Description |
270
+ |--------|------------|---------|-------------|
271
+ | `getStats()` | - | `Promise<PerformanceStats>` | Get performance statistics |
272
+ | `resetStats()` | - | `Promise<void>` | Reset statistics counters |
273
+ | `getEventCount()` | - | `Promise<number>` | Get event count (lightweight) |
274
+ | `getStyleCount()` | - | `Promise<number>` | Get style count (lightweight) |
275
+ | `runBenchmark()` | - | `void` | Run a benchmark on the worker |
276
+
277
+ ### Lifecycle
278
+
279
+ | Method | Parameters | Description |
280
+ |--------|------------|-------------|
281
+ | `destroy(err?)` | `err?: Error \| string` | Destroy the renderer and cleanup |
282
+ | `sendMessage(target, data?, transferable?)` | `target: string, data?: Record<string, any>, transferable?: Transferable[]` | Send data to worker |
283
+
284
+ ## Properties
285
+
286
+ | Property | Type | Description |
287
+ |----------|------|-------------|
288
+ | `debug` | boolean | Enable/disable debug logging |
289
+ | `prescaleFactor` | number | Scale factor for subtitles |
290
+ | `prescaleHeightLimit` | number | Height limit for prescaling |
291
+ | `maxRenderHeight` | number | Maximum render height |
292
+ | `timeOffset` | number | Subtitle time offset in seconds |
293
+ | `busy` | boolean | Whether the renderer is currently busy |
294
+ | `isUsingWebGPU` | boolean | Whether WebGPU renderer is active (read-only) |
295
+
296
+ ## Type Definitions
297
+
298
+ ### ASSEvent
299
+
300
+ | Property | Type | Description |
301
+ |----------|------|-------------|
302
+ | `Start` | number | Start time in seconds |
303
+ | `Duration` | number | Duration in seconds |
304
+ | `Style` | string | Style name |
305
+ | `Name` | string | Character name (informational) |
306
+ | `MarginL` | number | Left margin override in pixels |
307
+ | `MarginR` | number | Right margin override in pixels |
308
+ | `MarginV` | number | Bottom margin override in pixels |
309
+ | `Effect` | string | Transition effect |
310
+ | `Text` | string | Subtitle text content |
311
+ | `ReadOrder` | number | Read order number |
312
+ | `Layer` | number | Z-index layer |
313
+ | `_index` | number | Internal index (optional) |
314
+
315
+ ### ASSStyle
316
+
317
+ | Property | Type | Description |
318
+ |----------|------|-------------|
319
+ | `Name` | string | Style name (case sensitive) |
320
+ | `FontName` | string | Font family name |
321
+ | `FontSize` | number | Font size |
322
+ | `PrimaryColour` | number | Primary color (RGBA as uint32) |
323
+ | `SecondaryColour` | number | Secondary color (RGBA as uint32) |
324
+ | `OutlineColour` | number | Outline color (RGBA as uint32) |
325
+ | `BackColour` | number | Background/shadow color (RGBA as uint32) |
326
+ | `Bold` | number | Bold (-1 = true, 0 = false) |
327
+ | `Italic` | number | Italic (-1 = true, 0 = false) |
328
+ | `Underline` | number | Underline (-1 = true, 0 = false) |
329
+ | `StrikeOut` | number | StrikeOut (-1 = true, 0 = false) |
330
+ | `ScaleX` | number | Width scale (percent) |
331
+ | `ScaleY` | number | Height scale (percent) |
332
+ | `Spacing` | number | Extra spacing between characters (pixels) |
333
+ | `Angle` | number | Rotation angle (degrees) |
334
+ | `BorderStyle` | number | Border style (1 = outline + shadow, 3 = opaque box) |
335
+ | `Outline` | number | Outline width (0-4 pixels) |
336
+ | `Shadow` | number | Shadow depth (0-4 pixels) |
337
+ | `Alignment` | number | Alignment (1-9, numpad style) |
338
+ | `MarginL` | number | Left margin (pixels) |
339
+ | `MarginR` | number | Right margin (pixels) |
340
+ | `MarginV` | number | Vertical margin (pixels) |
341
+ | `Encoding` | number | Font encoding |
342
+ | `treat_fontname_as_pattern` | number | Treat font name as pattern |
343
+ | `Blur` | number | Blur amount |
344
+ | `Justify` | number | Text justification |
345
+
346
+ # How to build?
347
+
348
+ ## Dependencies
349
+
350
+ - git
351
+ - emscripten (Configure the enviroment)
352
+ - make
353
+ - python3
354
+ - cmake
355
+ - pkgconfig
356
+ - patch
357
+ - libtool
358
+ - autotools (autoconf, automake, autopoint)
359
+ - gettext
360
+ - ragel - Required by Harfbuzz
361
+ - itstool - Required by Fontconfig
362
+ - gperf - Required by Fontconfig
363
+ - licensecheck
364
+
365
+ ## Get the Source
366
+
367
+ Run git clone --recursive https://github.com/altqx/akarisub.git
368
+
369
+ ## Build inside a Container
370
+
371
+ ### Docker
372
+
373
+ 1. Install Docker
374
+ 2. ./run-docker-build.sh
375
+ 3. Artifacts are in /dist/js
376
+
377
+ ### Buildah
378
+
379
+ 1. Install Buildah and a suitable backend for buildah run like crun or runc
380
+ 2. ./run-buildah-build.sh
381
+ 3. Artifacts are in /dist/js
382
+
383
+ ## Build without Containers
384
+
385
+ 1. Install the dependency packages listed above
386
+ 2. make
387
+ - If on macOS with libtool from brew, LIBTOOLIZE=glibtoolize make
388
+ 3. Artifacts are in /dist/js