npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

libbitsub

v1.6.0

Published

High-performance WASM renderer for graphical subtitles (PGS and VobSub)

Downloads

441

Readme

libbit(map)sub

High-performance WASM renderer for graphical subtitles (PGS and VobSub), written in Rust.

Started as a fork of Arcus92's libpgs-js, this project is re-engineered to maximize performance and extend functionality to VobSub, which was not supported by the original library. It remains fully backward compatible (only for PGS - obliviously). Special thanks to the original project for the inspiration!

Features

  • PGS (Blu-ray) subtitle parsing and rendering
  • VobSub (DVD) subtitle parsing and rendering
  • WebGPU / WebGL2 rendering GPU-accelerated rendering with automatic fallback: WebGPU → WebGL2 → Canvas2D
  • High-performance Rust-based rendering engine compiled to WebAssembly
  • Zero-copy data transfer between JS and WASM where possible
  • Caching for decoded bitmaps to optimize repeated rendering
  • TypeScript support with full type definitions

Showcase

PGS (Created using Spp2Pgs)

https://gist.github.com/user-attachments/assets/55ac8e11-1964-4fb9-923e-dcac82dc7703

Vobsub

https://gist.github.com/user-attachments/assets/a89ae9fe-23e4-4bc3-8cad-16a3f0fea665

See in action

Installation

npm / bun

npm install libbitsub
# or
bun add libbitsub

JSR (Deno)

deno add jsr:@altq/libbitsub

Setup for Web Workers (Recommended)

For best performance with large subtitle files, copy the WASM files to your public folder so Web Workers can access them:

# For Next.js, Vite, or similar frameworks
mkdir -p public/libbitsub
cp node_modules/libbitsub/pkg/libbitsub_bg.wasm public/libbitsub/
cp node_modules/libbitsub/pkg/libbitsub.js public/libbitsub/

This enables off-main-thread parsing which prevents UI freezing when loading large PGS files.

Prerequisites

To build from source, you need:

# Install wasm-pack
cargo install wasm-pack

Building

# Build WASM module and TypeScript wrapper
bun run build

# Build WASM only (for development)
bun run build:wasm

# Build release version (optimized)
bun run build:wasm:release

Usage

Initialize WASM

Before using any renderer, you must initialize the WASM module:

import { initWasm } from 'libbitsub'

// Initialize WASM (do this once at app startup)
await initWasm()

High-Level API (Video Integration)

The high-level API automatically handles video synchronization, canvas overlay, and subtitle fetching.

PGS Subtitles (Video-Integrated)

import { PgsRenderer } from 'libbitsub'

// Create renderer with video element (URL-based loading)
const renderer = new PgsRenderer({
  video: videoElement,
  subUrl: '/subtitles/movie.sup',
  workerUrl: '/libbitsub.js', // Optional, kept for API compatibility
  // Lifecycle callbacks (optional)
  onLoading: () => console.log('Loading subtitles...'),
  onLoaded: () => console.log('Subtitles loaded!'),
  onError: (error) => console.error('Failed to load:', error)
})

// Or load directly from ArrayBuffer
const response = await fetch('/subtitles/movie.sup')
const subtitleData = await response.arrayBuffer()

const renderer = new PgsRenderer({
  video: videoElement,
  subContent: subtitleData, // Load directly from ArrayBuffer
  onLoading: () => console.log('Loading subtitles...'),
  onLoaded: () => console.log('Subtitles loaded!'),
  onError: (error) => console.error('Failed to load:', error)
})

// The renderer automatically:
// - Fetches the subtitle file (if using subUrl) or uses provided ArrayBuffer
// - Creates a canvas overlay on the video
// - Syncs rendering with video playback
// - Handles resize events

// When done:
renderer.dispose()

VobSub Subtitles (Video-Integrated)

import { VobSubRenderer } from 'libbitsub'

// Create renderer with video element (URL-based loading)
const renderer = new VobSubRenderer({
  video: videoElement,
  subUrl: '/subtitles/movie.sub',
  idxUrl: '/subtitles/movie.idx', // Optional, defaults to .sub path with .idx extension
  workerUrl: '/libbitsub.js', // Optional
  // Lifecycle callbacks (optional)
  onLoading: () => setIsLoading(true),
  onLoaded: () => setIsLoading(false),
  onError: (error) => {
    setIsLoading(false)
    console.error('Subtitle error:', error)
  }
})

// Or load directly from ArrayBuffer
const [subResponse, idxResponse] = await Promise.all([fetch('/subtitles/movie.sub'), fetch('/subtitles/movie.idx')])
const subData = await subResponse.arrayBuffer()
const idxData = await idxResponse.text()

const renderer = new VobSubRenderer({
  video: videoElement,
  subContent: subData, // Load .sub directly from ArrayBuffer
  idxContent: idxData, // Load .idx directly from string
  onLoading: () => setIsLoading(true),
  onLoaded: () => setIsLoading(false),
  onError: (error) => {
    setIsLoading(false)
    console.error('Subtitle error:', error)
  }
})

// When done:
renderer.dispose()

Subtitle Display Settings

Both PgsRenderer and VobSubRenderer support real-time customization of subtitle size and position:

// Get current settings
const settings = renderer.getDisplaySettings()
console.log(settings)
// Output: { scale: 1.0, verticalOffset: 0 }

// Update settings
renderer.setDisplaySettings({
  scale: 1.2, // 1.2 = 120% size
  verticalOffset: -10 // -10% (move up 10% of video height)
})

// Reset to defaults
renderer.resetDisplaySettings()

Debanding (VobSub)

VobSub subtitles often exhibit banding artifacts due to their limited 4-color palette. libbitsub includes a neo_f3kdb-style debanding filter that smooths color transitions:

import { VobSubRenderer } from 'libbitsub'

const renderer = new VobSubRenderer({
  video: videoElement,
  subUrl: '/subtitles/movie.sub'
})

// Debanding is enabled by default; call to disable if needed
// renderer.setDebandEnabled(false)

// Fine-tune debanding parameters
renderer.setDebandThreshold(64.0) // Higher = more aggressive smoothing
renderer.setDebandRange(15) // Pixel radius for sampling

// Check if debanding is active
console.log(renderer.debandEnabled) // true

Low-Level API:

import { VobSubParserLowLevel } from 'libbitsub'

const parser = new VobSubParserLowLevel()
parser.loadFromData(idxContent, subData)

// Configure debanding before rendering
parser.setDebandEnabled(true)
parser.setDebandThreshold(48.0)
parser.setDebandRange(12)

// Rendered frames will have debanding applied
const frame = parser.renderAtIndex(0)

Debanding Settings:

| Property | Type | Default | Range | Description | | ----------- | ------- | ------- | --------- | ------------------------------------------------ | | enabled | boolean | true | - | Enable/disable the debanding filter | | threshold | number | 64.0 | 0.0-255.0 | Difference threshold; higher = more smoothing | | range | number | 15 | 1-64 | Sample radius in pixels; higher = wider sampling |

Notes:

  • Debanding is applied post-decode on the RGBA output
  • Uses cross-shaped sampling with factor-based blending (neo_f3kdb sample_mode 6 style)
  • Transparent pixels are skipped for performance
  • Deterministic output (same input = same output)

Settings Reference:

  • scale (number): Scale factor for subtitles.

    • 1.0 = 100% (Original size)
    • 0.5 = 50%
    • 2.0 = 200%
    • Range: 0.1 to 3.0
  • verticalOffset (number): Vertical position offset as a percentage of video height.

    • 0 = Original position
    • Negative values move up (e.g., -10 moves up by 10% of height)
    • Positive values move down (e.g., 10 moves down by 10% of height)
    • Range: -50 to 50

Performance Statistics

Both PgsRenderer and VobSubRenderer provide real-time performance metrics:

// Get performance statistics
const stats = renderer.getStats()
console.log(stats)
// Output:
// {
//   framesRendered: 120,
//   framesDropped: 2,
//   avgRenderTime: 1.45,
//   maxRenderTime: 8.32,
//   minRenderTime: 0.12,
//   lastRenderTime: 1.23,
//   renderFps: 60,
//   usingWorker: true,
//   cachedFrames: 5,
//   pendingRenders: 0,
//   totalEntries: 847,
//   currentIndex: 42
// }

// Example: Display stats in a debug overlay
setInterval(() => {
  const stats = renderer.getStats()
  debugOverlay.textContent = `
    FPS: ${stats.renderFps}
    Frames: ${stats.framesRendered} (dropped: ${stats.framesDropped})
    Avg render: ${stats.avgRenderTime}ms
    Worker: ${stats.usingWorker ? 'Yes' : 'No'}
    Cache: ${stats.cachedFrames} frames
  `
}, 1000)

Stats Reference:

| Property | Type | Description | | ---------------- | ------- | -------------------------------------------------------------- | | framesRendered | number | Total frames rendered since initialization | | framesDropped | number | Frames dropped due to slow rendering (>16.67ms) | | avgRenderTime | number | Average render time in milliseconds (rolling 60-sample window) | | maxRenderTime | number | Maximum render time in milliseconds | | minRenderTime | number | Minimum render time in milliseconds | | lastRenderTime | number | Most recent render time in milliseconds | | renderFps | number | Current renders per second (based on last 1 second) | | usingWorker | boolean | Whether rendering is using Web Worker (off-main-thread) | | cachedFrames | number | Number of decoded frames currently cached | | pendingRenders | number | Number of frames currently being decoded asynchronously | | totalEntries | number | Total subtitle entries/display sets in the loaded file | | currentIndex | number | Index of the currently displayed subtitle |

GPU-Accelerated Rendering

libbitsub automatically selects the best available GPU renderer at startup, following this fallback chain:

WebGPU → WebGL2 → Canvas2D

import { PgsRenderer, isWebGPUSupported, isWebGL2Supported } from 'libbitsub'

// Check renderer support
if (isWebGPUSupported()) {
  console.log('WebGPU available')
} else if (isWebGL2Supported()) {
  console.log('WebGL2 available')
} else {
  console.log('Falling back to Canvas2D')
}

const renderer = new PgsRenderer({
  video: videoElement,
  subUrl: '/subtitles/movie.sup',
  onWebGPUFallback: () => console.log('WebGPU unavailable, trying WebGL2'),
  onWebGL2Fallback: () => console.log('WebGL2 unavailable, using Canvas2D')
})

Options:

  • onWebGPUFallback (function): Callback when WebGPU initialisation fails
  • onWebGL2Fallback (function): Callback when WebGL2 initialisation fails

Renderer capabilities:

| Renderer | Premultiplied alpha | Linear sampling | Browser support | | -------- | ------------------- | --------------- | --------------- | | WebGPU | ✅ | ✅ | Chrome 113+, Firefox 141+, Edge 113+ | | WebGL2 | ✅ | ✅ | All modern browsers | | Canvas2D | — | ✅ | Universal |

Low-Level API (Programmatic Use)

For more control over rendering, use the low-level parsers directly.

PGS Subtitles (Low-Level)

import { initWasm, PgsParser } from 'libbitsub'

await initWasm()

const parser = new PgsParser()

// Load PGS data from a .sup file
const response = await fetch('subtitles.sup')
const data = new Uint8Array(await response.arrayBuffer())
parser.load(data)

// Get timestamps
const timestamps = parser.getTimestamps() // Float64Array in milliseconds

// Render at a specific time
const subtitleData = parser.renderAtTimestamp(currentTimeInSeconds)
if (subtitleData) {
  for (const comp of subtitleData.compositionData) {
    ctx.putImageData(comp.pixelData, comp.x, comp.y)
  }
}

// Clean up
parser.dispose()

VobSub Subtitles (Low-Level)

import { initWasm, VobSubParserLowLevel } from 'libbitsub'

await initWasm()

const parser = new VobSubParserLowLevel()

// Load from IDX + SUB files
const idxResponse = await fetch('subtitles.idx')
const idxContent = await idxResponse.text()
const subResponse = await fetch('subtitles.sub')
const subData = new Uint8Array(await subResponse.arrayBuffer())

parser.loadFromData(idxContent, subData)

// Or load from SUB file only
// parser.loadFromSubOnly(subData);

// Render
const subtitleData = parser.renderAtTimestamp(currentTimeInSeconds)
if (subtitleData) {
  for (const comp of subtitleData.compositionData) {
    ctx.putImageData(comp.pixelData, comp.x, comp.y)
  }
}

parser.dispose()

Unified Parser

For handling both formats with a single API:

import { initWasm, UnifiedSubtitleParser } from 'libbitsub'

await initWasm()

const parser = new UnifiedSubtitleParser()

// Load PGS
parser.loadPgs(pgsData)

// Or load VobSub
// parser.loadVobSub(idxContent, subData);

console.log(parser.format) // 'pgs' or 'vobsub'

const subtitleData = parser.renderAtTimestamp(time)
// ... render to canvas

parser.dispose()

API Reference

High-Level (Video-Integrated)

PgsRenderer

  • constructor(options: VideoSubtitleOptions) - Create video-integrated PGS renderer
  • getDisplaySettings(): SubtitleDisplaySettings - Get current display settings
  • setDisplaySettings(settings: Partial<SubtitleDisplaySettings>): void - Update display settings
  • resetDisplaySettings(): void - Reset display settings to defaults
  • getStats(): SubtitleRendererStats - Get performance statistics
  • dispose(): void - Clean up all resources

VobSubRenderer

  • constructor(options: VideoVobSubOptions) - Create video-integrated VobSub renderer
  • getDisplaySettings(): SubtitleDisplaySettings - Get current display settings
  • setDisplaySettings(settings: Partial<SubtitleDisplaySettings>): void - Update display settings
  • resetDisplaySettings(): void - Reset display settings to defaults
  • getStats(): SubtitleRendererStats - Get performance statistics
  • setDebandEnabled(enabled: boolean): void - Enable/disable debanding filter
  • setDebandThreshold(threshold: number): void - Set debanding threshold (0.0-255.0)
  • setDebandRange(range: number): void - Set debanding sample range (1-64)
  • debandEnabled: boolean - Check if debanding is enabled
  • dispose(): void - Clean up all resources

Low-Level (Programmatic)

PgsParser

  • load(data: Uint8Array): number - Load PGS data, returns display set count
  • getTimestamps(): Float64Array - Get all timestamps in milliseconds
  • count: number - Number of display sets
  • findIndexAtTimestamp(timeSeconds: number): number - Find index for timestamp
  • renderAtIndex(index: number): SubtitleData | undefined - Render at index
  • renderAtTimestamp(timeSeconds: number): SubtitleData | undefined - Render at time
  • clearCache(): void - Clear decoded bitmap cache
  • dispose(): void - Release resources

VobSubParserLowLevel

  • loadFromData(idxContent: string, subData: Uint8Array): void - Load IDX + SUB
  • loadFromSubOnly(subData: Uint8Array): void - Load SUB only
  • setDebandEnabled(enabled: boolean): void - Enable/disable debanding filter
  • setDebandThreshold(threshold: number): void - Set debanding threshold (0.0-255.0)
  • setDebandRange(range: number): void - Set debanding sample range (1-64)
  • debandEnabled: boolean - Check if debanding is enabled
  • Same rendering methods as PgsParser

UnifiedSubtitleParser

  • loadPgs(data: Uint8Array): number - Load PGS data
  • loadVobSub(idxContent: string, subData: Uint8Array): void - Load VobSub
  • loadVobSubOnly(subData: Uint8Array): void - Load SUB only
  • format: 'pgs' | 'vobsub' | null - Current format
  • Same rendering methods as above

Types

VideoSubtitleOptions

interface VideoSubtitleOptions {
  video: HTMLVideoElement // Video element to sync with
  subUrl?: string // URL to subtitle file (provide this OR subContent)
  subContent?: ArrayBuffer // Direct subtitle content (provide this OR subUrl)
  workerUrl?: string // Worker URL (for API compatibility)
  onLoading?: () => void // Called when subtitle loading starts
  onLoaded?: () => void // Called when subtitle loading completes
  onError?: (error: Error) => void // Called when subtitle loading fails
  onWebGPUFallback?: () => void // Called when WebGPU init fails
  onWebGL2Fallback?: () => void // Called when WebGL2 init fails
}

VideoVobSubOptions

interface VideoVobSubOptions extends VideoSubtitleOptions {
  idxUrl?: string // URL to .idx file (optional, defaults to subUrl with .idx extension)
  idxContent?: string // Direct .idx content (provide this OR idxUrl)
}

SubtitleDisplaySettings

interface SubtitleDisplaySettings {
  // Scale factor (1.0 = 100%, 0.5 = 50%, 2.0 = 200%)
  scale: number
  // Vertical offset as % of video height (-50 to 50)
  verticalOffset: number
}

SubtitleRendererStats

interface SubtitleRendererStats {
  framesRendered: number // Total frames rendered since initialization
  framesDropped: number // Frames dropped due to slow rendering
  avgRenderTime: number // Average render time in milliseconds
  maxRenderTime: number // Maximum render time in milliseconds
  minRenderTime: number // Minimum render time in milliseconds
  lastRenderTime: number // Last render time in milliseconds
  renderFps: number // Current FPS (renders per second)
  usingWorker: boolean // Whether rendering is using web worker
  cachedFrames: number // Number of cached frames
  pendingRenders: number // Number of pending renders
  totalEntries: number // Total subtitle entries/display sets
  currentIndex: number // Current subtitle index being displayed
}

SubtitleData

interface SubtitleData {
  width: number // Screen width
  height: number // Screen height
  compositionData: SubtitleCompositionData[]
}

interface SubtitleCompositionData {
  pixelData: ImageData // RGBA pixel data
  x: number // X position
  y: number // Y position
}

License

MIT