Skip to content

Subtitles

Subtitle rendering beyond the quickstart basics: the two rendering paths (VTT and ASS/SSA), multi-language switching, and user style preferences.

Prerequisites: Video Quick Start. For the full subtitle API see Subtitle Overlay.

VTT subtitles, default path

SubtitleOverlayPlugin renders WebVTT cues as styled DOM text over the video. It is the lightest option and works without any extra dependencies.

TypeScript
import nmplayer from '@nomercy-entertainment/nomercy-video-player';
import { SubtitleOverlayPlugin } from '@nomercy-entertainment/nomercy-video-player/plugins';

const player = nmplayer('main').setup({
baseUrl: 'https://raw.githubusercontent.com/NoMercy-Entertainment/nomercy-media/master/Films',
defaultSubtitleLanguage: 'en',
playlist: [
{
id: 'sintel',
url: '/Sintel.(2010)/Sintel.(2010).NoMercy.m3u8',
subtitles: [
{
id: 'en',
language: 'en',
label: 'English',
url: '/Sintel.(2010)/subtitles/Sintel.(2010).NoMercy.eng.full.vtt',
kind: 'subtitles',
},
],
},
],
});

player.addPlugin(SubtitleOverlayPlugin);
await player.ready();
await player.play();

Switch languages at runtime:

TypeScript
// List available tracks:
const tracks = player.subtitles();
// [{ id: 'en', language: 'en', label: 'English' }, ...]

// Activate by index:
player.subtitle(0); // English
player.subtitle(1); // Dutch

// Disable subtitles:
player.subtitle(null);

// Read which is active (null = off):
const active = player.subtitle();

Listen for changes:

TypeScript
player.on('subtitle', ({ track }) => {
// track is null when subtitles are off
updateSubtitleMenu(track);
});

ASS/SSA subtitles, OctopusPlugin

Use OctopusPlugin when you need full ASS/SSA typesetting: custom fonts, karaoke effects, complex positioning, motion. It loads @nomercy-entertainment/nomercy-subtitle-octopus at runtime via a dynamic import.

Copy worker files to your public directory (Vite example):

TypeScript
// vite.config.ts
import { viteStaticCopy } from 'vite-plugin-static-copy';

export default {
plugins: [
viteStaticCopy({
targets: [
{
// Worker JS files from the octopus package go into your public/static folder
src: 'node_modules/@nomercy-entertainment/nomercy-subtitle-octopus/dist/*.js',
dest: 'static',
},
],
}),
],
};

Register the plugin:

TypeScript
import nmplayer from '@nomercy-entertainment/nomercy-video-player';
import { OctopusPlugin } from '@nomercy-entertainment/nomercy-video-player/plugins';

const player = nmplayer('main').setup({
// baseUrl is a media library root (the same folder the media server exposes).
baseUrl: 'https://raw.githubusercontent.com/NoMercy-Entertainment/nomercy-media/master/Anime',
defaultSubtitleLanguage: 'en',
playlist: [
{
id: 'rail-wars',
url: '/Rail.Wars!.(2014)/Rail.Wars!.S00E00/Rail.Wars!.(2014).S00E00.NoMercy.m3u8',
subtitles: [
{
id: 'en',
language: 'en',
label: 'English (ASS)',
url: '/Rail.Wars!.(2014)/Rail.Wars!.S00E00/subtitles/Rail.Wars!.(2014).S00E00.NoMercy.eng.full.ass',
kind: 'subtitles',
},
],
fonts: [
{
file: '/Rail.Wars!.(2014)/Rail.Wars!.S00E00/fonts.json',
},
],
},
],
});

player.addPlugin(OctopusPlugin, {
workerUrl: '/static/subtitles-octopus-worker.js',
legacyWorkerUrl: '/static/subtitles-octopus-worker-legacy.js',
fallbackFont: '/static/fonts/Roboto.ttf',
targetFps: 24,
});

nomercy-subtitle-octopus ships as a dependency of nomercy-video-player. The plugin loads it lazily, so if it cannot be resolved at runtime (e.g. bundler exclusion), it falls back to no rendering with no error thrown. Enable logLevel: 'debug' to confirm the fallback path.

Font manifest format

The font manifest is a JSON array. The plugin fetches each font before first render. Use a path relative to your own site for a self-hosted font, or a CDN URL. Bunny Fonts (a privacy-friendly Google Fonts mirror) is a good source:

JSON
[
{ "file": "/fonts/NotoSans-Regular.ttf", "mimeType": "font/ttf" },
{ "file": "https://fonts.bunny.net/noto-sans/files/noto-sans-latin-700-normal.woff2", "mimeType": "font/woff2" }
]

Multi-language switching with VTT and ASS mixed

You can mix VTT and ASS tracks on the same item. The player selects the renderer based on the kind field:

TypeScript
subtitles: [
// VTT tracks, rendered by SubtitleOverlayPlugin
{ id: 'en-vtt', language: 'en', label: 'English (VTT)', url: '/subs/ep1.en.vtt', kind: 'subtitles' },
// ASS tracks, rendered by OctopusPlugin
{ id: 'en-ass', language: 'en', label: 'English (Styled)', url: '/subs/ep1.en.ass', kind: 'subtitles' },
],

Register both plugins. The correct one activates for each track.

Subtitle style customization

Subtitle style preferences (font size, color, background opacity) are read and written via player.subtitleStyle(). Changes emit the 'subtitleStyle' event and SubtitleOverlayPlugin repaints active cues immediately.

TypeScript
// Read current style
const style = player.subtitleStyle();

// Merge a partial update
player.subtitleStyle({
fontSize: 120,
textColor: 'yellow',
edgeStyle: 'dropShadow',
});

To persist preferences across sessions, call player.subtitleStyle() in a 'subtitleStyle' listener and save the result to your own storage, then restore it at startup by passing the saved patch to player.subtitleStyle() after ready().

TypeScript
player.on('subtitleStyle', (style) => {
localStorage.setItem('subtitle-style', JSON.stringify(style));
});

player.on('ready', () => {
const saved = localStorage.getItem('subtitle-style');
if (saved) player.subtitleStyle(JSON.parse(saved));
});