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