Subtitle renderer and parser for web video, with full ASS/SSA support and multi-format parsing (SRT, WebVTT, SBV, TTML/DFXP), powered by Canvas 2D + Rust WASM.
One package for all usage modes:
- Core / Vanilla JS:
sweet-subtitle - React:
sweet-subtitle/react - Vue:
sweet-subtitle/vue - Angular:
sweet-subtitle/angular
- Multi-format — SRT, WebVTT, ASS/SSA, SBV, TTML/DFXP
- ASS advanced rendering — styles, positioning (
\pos,\move,\org), rotation (\frz/frx/fry), animation (\twith accel), karaoke (\k/\kf/\ko), clip (\clip/\iclip), drawing commands (\p), fade (\fad/\fade), per-character alpha, border styles, etc. - Gaussian blur —
\blurand\berendered via Rust WASM (3-pass box blur), with CSS filter fallback - Text decorations — underline (
\u), strikeout (\s) - Scale transforms — horizontal scale (
\fscx), vertical scale (\fscy) - Auto text wrapping — long lines wrap within video bounds
- Font size control —
medium(default) /large/smallscaling for both text and ASS renderers - Encoding detection — UTF-8, UTF-16LE/BE (with BOM), GBK auto-detection
- Rust WASM bundled — WASM binary ships inside the package, zero extra config for users
- Lightweight — zero runtime dependencies, tree-shakeable ESM/CJS dual output
- Resilient parsing — tolerates malformed ASS files (wrong Format lines, missing PlayRes, case-insensitive sections, BOM, flexible timestamps)
| Format | Parse | Render |
|---|---|---|
| SRT | Yes | Yes |
| WebVTT | Yes | Yes |
| ASS/SSA | Yes | Yes (advanced effects) |
| SBV | Yes | Yes |
| TTML/DFXP | Yes | Yes |
npm install sweet-subtitleThe WASM binary is bundled inside the package — no extra setup needed.
Framework adapters are included in this same package via subpath imports.
- Set repository topics:
subtitle,ass,srt,webvtt,ttml,wasm,react,vue,angular,video. - Keep release notes updated for each npm publish (see changelog and release template below).
- Pin a short usage snippet in repo description/About for quick conversion from visitors.
Release notes and history:
- Changelog: CHANGELOG.md
- GitHub release template: .github/RELEASE_TEMPLATE.md
import { SweetSubtitle } from 'sweet-subtitle'
const video = document.querySelector('video')
const sub = new SweetSubtitle(video, {
src: '/path/to/subtitle.ass',
})
sub.on('ready', () => console.log('Subtitle loaded'))
sub.on('error', (err) => console.error(err))Or use Promise-based loading for clearer async flow:
import { SweetSubtitle } from 'sweet-subtitle'
const video = document.querySelector('video')!
const sub = new SweetSubtitle(video, { enableWasm: true })
await sub.loadFromUrl('/path/to/subtitle.ass')
// await sub.loadFromText(subtitleText)import { SweetSubtitle, decodeBuffer } from 'sweet-subtitle'
const buffer = await fetch('/sub.srt').then(r => r.arrayBuffer())
const content = decodeBuffer(buffer) // auto-detect encoding
const sub = new SweetSubtitle(video, { content })import { parse, detectFormat } from 'sweet-subtitle'
const format = detectFormat(content) // 'srt' | 'vtt' | 'ass' | 'sbv' | 'ttml'
const track = parse(content)
console.log(track.cues) // [{ id, start, end, text }, ...]| Option | Type | Description |
|---|---|---|
src |
string |
URL to subtitle file |
content |
string |
Subtitle text content |
format |
'srt' | 'vtt' | 'ass' | 'sbv' | 'ttml' |
Force format (auto-detected if omitted) |
offset |
number |
Time offset in seconds |
size |
'large' | 'medium' | 'small' |
Font size scale (default: 'medium') |
encoding |
string |
Force text decoding encoding for URL-loaded subtitles, e.g. utf-8, utf-16le, gbk |
fallbackEncodings |
string[] |
Custom decode fallback chain for URL-loaded subtitles (default: ['gbk', 'big5', 'shift_jis']) |
| Method | Description |
|---|---|
loadFromUrl(url, format?) |
Load subtitle from URL, returns Promise<void> |
loadFromText(content, format?) |
Load subtitle from string, returns Promise<void> |
show() |
Show subtitle overlay |
hide() |
Hide subtitle overlay |
destroy() |
Remove overlay and stop rendering |
setOffset(seconds) |
Adjust subtitle timing |
setSize(size) |
Set font size scale: 'large' / 'medium' / 'small' |
once(event, callback) |
Subscribe once and auto-unsubscribe |
on(event, callback) |
Subscribe to events, returns unsubscribe |
off(event, callback) |
Unsubscribe |
Encoding notes:
decodeBufferauto-detects BOM (utf-8,utf-16le,utf-16be).- For no-BOM files, it applies UTF-16 heuristic detection and UTF-8 validation.
- You can override detection using
encodingand control fallback order usingfallbackEncodings.
const sub = new SweetSubtitle(videoEl)
const unsubscribe = sub.on('error', console.error)
await sub.loadFromUrl('/demo.srt')
// cleanup
unsubscribe()
sub.destroy()import { useEffect, useRef } from 'react'
import { useSweetSubtitle } from 'sweet-subtitle/react'
export function Player({ src }: { src: string }) {
const videoRef = useRef<HTMLVideoElement>(null)
const { error } = useSweetSubtitle(videoRef, { src, enableWasm: true })
useEffect(() => {
if (error) console.error(error)
}, [error])
return <video ref={videoRef} controls />
}import { computed, ref, watch } from 'vue'
import { useSweetSubtitle } from 'sweet-subtitle/vue'
const videoRef = ref<HTMLVideoElement | null>(null)
const { error } = useSweetSubtitle(videoRef, {
src: computed(() => props.subtitleUrl),
enableWasm: true,
})
watch(error, (err) => {
if (err) console.error(err)
})import { Component } from '@angular/core'
import { SweetSubtitleDirective } from 'sweet-subtitle/angular'
@Component({
selector: 'app-player',
standalone: true,
imports: [SweetSubtitleDirective],
template: `
<video
controls
sweetSubtitle
[subtitleSrc]="'/assets/demo.ass'"
[subtitleEnableWasm]="true"
(subtitleError)="onSubtitleError($event)">
</video>
`,
})
export class PlayerComponent {
onSubtitleError(err: Error) {
console.error(err)
}
}| Event | Payload | Description |
|---|---|---|
ready |
— | Track parsed and renderer initialized |
error |
Error |
Parse or load failure |
cuechange |
SubtitleCue[] |
Active cues changed |
sweet-subtitle/
├── packages/
│ ├── core/ # npm package "sweet-subtitle"
│ │ ├── scripts/
│ │ │ └── setup-wasm.mjs # copies wasm-pack output into src before build
│ │ └── src/
│ │ ├── parser/ # SRT / VTT / ASS parsers
│ │ ├── renderer/ # Canvas 2D renderers (text + ASS)
│ │ ├── wasm/ # WASM bridge (lazy-load, JS fallback)
│ │ ├── SweetSubtitle.ts
│ │ ├── encoding.ts # Encoding detection (UTF-8/16/GBK)
│ │ └── types.ts
│ └── wasm/ # Rust WASM crate
│ └── src/
│ ├── blur.rs # Gaussian blur (3-pass box blur)
│ └── drawing.rs # ASS drawing command rasterizer
└── playground/ # Vite + React + Tailwind demo app
# Install dependencies
pnpm install
# Build WASM (requires Rust + wasm-pack)
pnpm build:wasm
# Build the library
pnpm build
# Run playground
pnpm dev
# Type check
pnpm typecheck
# Run tests
pnpm test
# Dry run publish (recommended)
pnpm run release:dry
# Publish single package to npm (sweet-subtitle)
pnpm run release
# One-command publish (sync + check + build + dry-run + publish)
pnpm run release:oneNote: This repo now publishes one npm package only:
sweet-subtitle. React/Vue/Angular adapters are included as subpath exports (sweet-subtitle/react,sweet-subtitle/vue,sweet-subtitle/angular).
pnpm build:wasmmust be run at least once beforepnpm buildorpnpm run release. TheprepublishOnlyhook rebuilds WASM automatically on every publish.
| Tag | Description |
|---|---|
\b, \i, \u, \s |
Bold, italic, underline, strikeout |
\fn, \fs, \fsp |
Font name, size, spacing |
\fscx, \fscy |
Scale X/Y |
\frz, \frx, \fry |
Rotation Z/X/Y |
\c, \1c-\4c |
Colors (primary, secondary, outline, shadow) |
\alpha, \1a-\4a |
Alpha channels |
\bord, \shad |
Border, shadow |
\blur, \be |
Gaussian blur (WASM-accelerated) |
\pos, \move, \org |
Position, animation, rotation origin |
\an, \a |
Alignment (numpad / legacy) |
\fad, \fade |
Fade in/out (2-param and 7-param) |
\clip, \iclip |
Rectangular / drawing clip |
\t |
Animation with accel curve |
\k, \kf, \ko, \K |
Karaoke effects |
\p |
Drawing mode |
\r |
Style reset |
MIT