| name | libbitsub |
| description | Integration guide for libbitsub — a WASM-based high-performance bitmap subtitle renderer (PGS, VobSub, and MKS-embedded VobSub) for the browser. Use when adding graphical subtitle support to a video player (including Video.js, Shaka Player, hls.js, or React), integrating PGS (.sup), VobSub (.sub/.idx), or `.mks` files carrying embedded `S_VOBSUB`, configuring layout controls (scale, aspect mode, offset, opacity), or using the low-level parser APIs. |
libbitsub Integration
libbitsub is a Rust/WASM-powered bitmap subtitle renderer for PGS (Blu-ray .sup), VobSub (DVD .sub/.idx), and Matroska .mks files with embedded S_VOBSUB tracks. It manages canvas overlay, video sync, resize, worker offloading, and GPU rendering automatically.
Installation
npm install libbitsub
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.
If your app serves package files in a way that does not expose that emitted WASM asset to the browser, you can still provide the legacy public fallback by copying the WASM file to /libbitsub/libbitsub_bg.wasm:
mkdir -p public/libbitsub
cp node_modules/libbitsub/pkg/libbitsub_bg.wasm public/libbitsub/
The worker is still created inline. workerUrl remains in the option type only for compatibility and does not change runtime behavior.
Player integrations (optional)
Core libbitsub stays dependency-free. Optional adapters are subpath exports with optional peer dependencies:
| Import | Use when |
|---|
libbitsub/videojs | Video.js plugin via registerBitSubPlugin(videojs) then player.bitsub({ subUrl }) |
libbitsub/shaka | attachBitSubToShaka(player, { subUrl }) |
libbitsub/hlsjs | attachBitSubToHls(hls, { subUrl }) after/before attachMedia |
libbitsub/react | useBitSub(videoRef, { subUrl }) or <BitSubOverlay videoRef={...} subUrl={...} /> |
libbitsub/integrations | Shared attachBitSub(video, options) controller |
import { registerBitSubPlugin } from 'libbitsub/videojs'
import { attachBitSubToShaka } from 'libbitsub/shaka'
import { attachBitSubToHls } from 'libbitsub/hlsjs'
import { useBitSub } from 'libbitsub/react'
import { attachBitSub } from 'libbitsub/integrations'
All adapters ultimately bind a high-level renderer to an HTMLVideoElement and expose load / clear / dispose / display settings. Bitmap tracks are canvas overlays, not native text tracks. Recipes: examples/.
WASM initialization
The WASM module initializes automatically — high-level renderers (PgsRenderer, VobSubRenderer) call initWasm() internally, and the module also triggers a non-blocking pre-init on first import in browser environments. No explicit initialization is needed for renderer usage.
For low-level parsers (PgsParser, VobSubParserLowLevel), await initWasm() before calling parser methods:
import { initWasm, PgsParser } from 'libbitsub'
await initWasm()
const parser = new PgsParser()
Calling initWasm() multiple times is safe (it deduplicates).
Worker prewarm
For TV/player apps, prewarm the shared parsing worker so the first subtitle track switch does not pay worker + WASM startup:
import { warmup, ready } from 'libbitsub'
void warmup()
await ready()
warmup() and ready() share one init promise with concurrent renderer creation. The shared worker is published only after in-worker WASM init succeeds.
Range / streaming loads
URL loads stream by default and use HTTP Range for large assets when the origin supports it:
- PGS: progressive
feed() indexing while bytes arrive
- VobSub: parse
.idx first for timestamps, then attach streamed .sub packets
- MKS: Range/stream download first, then extract (container still needs full payload)
subContent / idxContent stay the simple in-memory path
Options: streamingLoad (default true), rangeRequests (default true).
Events: load-progress, indexed (partial or final), then loaded.
Helpers: probeRangeSupport(), fetchSubtitleAsset(), fetchSubtitleText().
High-level video renderers
These attach a canvas overlay to the video's parent, handle playback sync, resize, and use a shared Web Worker + GPU rendering automatically.
Requirement: the video's parent element must be position: relative (or similar non-static). The renderer sets this automatically if it detects position: static.
PGS
import { PgsRenderer } from 'libbitsub'
const renderer = new PgsRenderer({
video: videoElement,
subUrl: '/subtitles/movie.sup',
displaySettings: { scale: 1.1, aspectMode: 'stretch', bottomPadding: 4, safeArea: 5 },
cacheLimit: 32,
prefetchWindow: { before: 1, after: 2 },
debug: true,
onLoading: () => setLoading(true),
onLoaded: () => setLoading(false),
onError: (err) => console.error(err),
onWarning: (warning) => console.warn(warning.code, warning.message, warning.details),
onEvent: (event) => console.log(event)
})
renderer.dispose()
VobSub
import { VobSubRenderer } from 'libbitsub'
const renderer = new VobSubRenderer({
video: videoElement,
subUrl: '/subtitles/movie.sub',
idxUrl: '/subtitles/movie.idx'
})
const mksRenderer = new VobSubRenderer({
video: videoElement,
subUrl: '/subtitles/movie.mks',
fileName: 'movie.mks'
})
renderer.setDebandEnabled(true)
renderer.setDebandThreshold(64)
renderer.setDebandRange(15)
renderer.dispose()
Auto-detect format
import { createAutoSubtitleRenderer } from 'libbitsub'
const renderer = createAutoSubtitleRenderer({
video: videoElement,
subUrl: '/subtitles/track.sup',
fileName: 'track.sup'
})
Detection uses file extension + binary magic bytes. .mks sources resolve to VobSub only when they contain an embedded S_VOBSUB track. Throws if format cannot be determined.
Layout controls
Apply at construction via displaySettings or at runtime:
renderer.setDisplaySettings({
scale: 1.2,
aspectMode: 'cover',
verticalOffset: -8,
horizontalOffset: 2,
horizontalAlign: 'center',
bottomPadding: 6,
safeArea: 5,
opacity: 0.92
})
renderer.getDisplaySettings()
renderer.resetDisplaySettings()
aspectMode controls how the subtitle track's presentation size is mapped into the visible video box:
stretch: default behavior, scales X/Y independently.
contain: preserves subtitle bitmap shape and fits the subtitle grid inside the visible video box.
cover: preserves subtitle bitmap shape while filling the visible video box. This is the recommended mode when subtitles were authored for a taller frame, such as 1920x1080, but the encoded video has cropped black bars, such as 3840x1600.
Rendered frame exports
Use the low-level parser surface when you need exportable subtitle bitmaps for previews, image snapshots, editor thumbnails, or visual diff fixtures.
import { PgsParser, initWasm, renderFrameData, toBlob, toCanvas, toImageBitmap } from 'libbitsub'
await initWasm()
const parser = new PgsParser()
parser.load(new Uint8Array(arrayBuffer))
const frame = parser.renderAtTimestamp(120.5)
const rendered = frame ? renderFrameData(frame, { crop: 'bounds' }) : null
if (rendered) {
const canvas = toCanvas(rendered)
const bitmap = await toImageBitmap(rendered)
const blob = await toBlob(rendered, 'image/png')
}
Low-level parsers also expose direct convenience methods:
parser.renderFrameDataAtTimestamp(120.5)
parser.renderFrameDataAtIndex(42, { crop: 'screen' })
Key points:
crop: 'bounds' is the default and returns a tight image plus offsetX and offsetY telling you where that crop belongs in the original subtitle presentation area.
crop: 'screen' preserves the original subtitle presentation width and height.
toCanvas(frame) creates a new export-sized canvas.
- Passing an existing canvas resizes it by default. Passing an existing 2D context draws in place by default.
One-shot auto opener
When the caller only wants a stable low-level surface and does not care whether the source is PGS or VobSub, use openSubtitles() instead of manually combining initWasm(), UnifiedSubtitleParser, and loadAuto().
import { openSubtitles } from 'libbitsub'
const subtitles = await openSubtitles({
data: subtitleBytes,
fileName: 'track.sup'
})
console.log(subtitles.format)
console.log(subtitles.metadata)
console.log(subtitles.timestamps)
const frame = subtitles.renderAtTimestamp(120.5)
const rendered = subtitles.renderFrameDataAtTimestamp(120.5)
subtitles.dispose()
The returned handle exposes the format-agnostic low-level operations you usually need: renderAtIndex(), renderAtTimestamp(), renderFrameDataAtIndex(), renderFrameDataAtTimestamp(), getCueMetadata(), getLastRenderIssue(), clearCache(), and dispose().
Cache and prefetch
renderer.setCacheLimit(48)
await renderer.prefetchRange(10, 20)
await renderer.prefetchAroundTime(video.currentTime)
renderer.clearFrameCache()
Prefetch around seek events for smoother playback:
video.addEventListener('seeked', () => renderer.prefetchAroundTime(video.currentTime))
Observability events
new PgsRenderer({
video,
subUrl,
onEvent: (event) => {
switch (event.type) {
case 'loading':
case 'loaded':
case 'error':
case 'warning':
case 'renderer-change':
case 'worker-state':
case 'cache-change':
case 'cue-change':
case 'stats':
}
}
})
Use cue-change to track what subtitle is active; use loaded to kick off prefetching.
Diagnostics mode
libbitsub now has a first-class diagnostics layer on top of the existing observability hooks. Use it when debugging malformed PGS/VobSub inputs in the field.
High-level renderers accept:
debug: true
onWarning: (warning) => { ... }
Key diagnostics APIs:
renderer.getStats()
renderer.getCacheStats()
renderer.getLastRenderInfo()
Low-level parsers accept SubtitleDiagnosticsOptions in their constructor and expose getLastRenderIssue():
const parser = new UnifiedSubtitleParser({
debug: true,
onWarning: (warning) => console.warn(warning.code, warning.message)
})
parser.loadAuto({ data: subtitleBytes, fileName: 'track.sup' })
console.log(parser.getLastRenderIssue())
Structured error codes include UNSUPPORTED_FORMAT, BAD_IDX, MISSING_PALETTE, TRACK_NOT_FOUND, MISSING_INPUT, FETCH_FAILED, and INVALID_SUBTITLE_DATA.
Metadata inspection
renderer.getMetadata()
renderer.getCurrentCueMetadata()
renderer.getCueMetadata(42)
renderer.getStats()
renderer.getCacheStats()
renderer.getLastRenderInfo()
Low-level parsers
Use when you need programmatic access to subtitle data without video integration.
import { initWasm, PgsParser, VobSubParserLowLevel, UnifiedSubtitleParser, openSubtitles } from 'libbitsub'
await initWasm()
const pgs = new PgsParser({ debug: true })
pgs.load(new Uint8Array(arrayBuffer))
const frame = pgs.renderAtIndex(pgs.findIndexAtTimestamp(120.5))
const rendered = pgs.renderFrameDataAtTimestamp(120.5)
const meta = pgs.getMetadata()
const renderIssue = pgs.getLastRenderIssue()
const vob = new VobSubParserLowLevel({ debug: true })
vob.loadFromData(idxString, new Uint8Array(subBuffer))
vob.setDebandEnabled(true)
const frame2 = vob.renderAtTimestamp(120.5)
const renderedVob = vob.renderFrameDataAtIndex(0, { crop: 'screen' })
const vobRenderIssue = vob.getLastRenderIssue()
const mksVob = new VobSubParserLowLevel()
mksVob.loadFromMks(new Uint8Array(mksBuffer))
const parser = new UnifiedSubtitleParser({ debug: true })
const detected = parser.loadAuto({ data: subtitleBytes, fileName: 'track.sup' })
const opened = await openSubtitles({ data: subtitleBytes, fileName: 'track.sup' })
const openedFrame = opened.renderAtTimestamp(120.5)
opened.dispose()
GPU backends
libbitsub prefers WebGPU → WebGL2 → Canvas2D, with automatic fallback:
import { isWebGPUSupported } from 'libbitsub'
new PgsRenderer({
video,
subUrl,
onWebGPUFallback: () => console.warn('WebGPU unavailable, using WebGL2'),
onWebGL2Fallback: () => console.warn('WebGL2 unavailable, using Canvas2D')
})
WebGL2 and Canvas2D fallback are automatic. Use the fallback callbacks or diagnostics hooks if you need to observe the active backend path.
Key constraints
- Bitmap subtitles only (PGS, VobSub, and
.mks files carrying embedded S_VOBSUB). Does not handle SRT, ASS, or any text-based formats.
.mks support is limited to embedded S_VOBSUB tracks. It is not a general Matroska subtitle parser.
- Multiple renderers can coexist; each has its own isolated parser session.
- If the shared worker fails to start, the API falls back to main-thread rendering and emits
WORKER_FALLBACK through diagnostics/event hooks.
dispose() must be called when removing a renderer to release DOM nodes, parser memory, and worker sessions.
Full API reference
See references/api.md for the complete method signatures of all classes and top-level exports.