Subtitle Plugins

Create custom subtitle styles with HTML, CSS, and JavaScript

Overview#

Subtitle plugins control how subtitles are visually presented. They receive word-level timing data and can implement karaoke-style highlighting, custom animations, and interactive features.

How Subtitle Plugins Work

  1. Plugin runs in a sandboxed iframe for security
  2. Receives subtitle data via window.mp4e.getData()
  3. Renders HTML using provided data + configuration
  4. Updates in real-time as currentWordIndex changes (karaoke mode)
  5. Communicates with player via postMessage for word clicks/hovers

Built-in Plugins#

MP4E includes 4 built-in subtitle plugins in the marketplace:

Simple Subtitles

mp4e:subtitles-simple

Classic subtitle style with semi-transparent background. Clean and readable for all content types.

Best for: General videos, documentaries, tutorials

Boxed Words

mp4e:subtitles-boxed-words

TikTok/Instagram Reels style - each word in its own colorful box with hover effects.

Best for: Short-form social content, marketing videos

Minimal Subtitles

mp4e:subtitles-minimal

No background, just text with strong shadow. Elegant and non-intrusive.

Best for: Cinematic content, artistic videos, minimal aesthetic

Karaoke Pro

mp4e:subtitles-karaoke-pro

Large golden text (32px+) with glow effects and animated underline for current word.

Best for: Music videos, lyrics, sing-along content, karaoke

Plugin Structure#

Subtitle plugins are defined in the marketplace with the subtitle category:

Subtitle plugin structure
1{
2 "namespace": "custom",
3 "identifier": "my-simple-subtitles",
4 "type": "subtitle",
5 "name": "My Simple Subtitles",
6 "category": "subtitle",
7 "manifest": {
8 "content": {
9 "html": "<div class=\"subtitle\" id=\"subtitle\"></div>",
10
11 "css": "* { margin: 0; padding: 0; } .subtitle { text-align: center; padding: 12px 20px; background: rgba(0, 0, 0, 0.8); color: white; font-size: 18px; border-radius: 8px; max-width: 90%; margin: 0 auto; }",
12
13 "script": "const container = document.getElementById('subtitle'); const data = window.mp4e?.getData() || {}; const subtitle = data.subtitle || { text: '' }; const config = data.config || {}; container.textContent = subtitle.text; if (config.textColor) container.style.color = config.textColor; if (config.fontSize) container.style.fontSize = config.fontSize; if (config.backgroundColor) container.style.background = config.backgroundColor;"
14 },
15 "configSchema": {
16 "type": "object",
17 "properties": {
18 "fontSize": {
19 "type": "string",
20 "title": "Font Size",
21 "default": "18px"
22 },
23 "textColor": {
24 "type": "string",
25 "title": "Text Color",
26 "default": "#FFFFFF"
27 },
28 "backgroundColor": {
29 "type": "string",
30 "title": "Background Color",
31 "default": "rgba(0, 0, 0, 0.8)"
32 }
33 }
34 }
35 }
36}
Category Field
The category field must be set to "subtitle" for subtitle plugins. This ensures they appear in the subtitle plugin selector.

Data Context#

Subtitle plugins receive data through the window.mp4e API:

Subtitle plugin data context
1// Data passed to subtitle plugins via window.mp4e.getData()
2interface SubtitlePluginContext {
3 subtitle: {
4 text: string // Full cue text
5 words: Word[] // Array of words with timing
6 speaker?: {
7 name: string
8 color?: string
9 }
10 id: string // Cue ID
11 trackId: string // Parent track ID
12 language: string // Track language code
13 }
14 config: Record<string, any> // Plugin configuration
15 currentWordIndex: number | null // Current word index (for karaoke)
16}
17
18// Word structure
19interface Word {
20 index: number
21 text: string
22 start: number // Word start time (seconds, estimated on import)
23 end: number // Word end time (seconds, estimated on import)
24 data?: Record<string, any> // Custom word data (defined by track.wordDataSchema)
25 objectRef?: {
26 objectId: string
27 inheritDisplaySettings?: boolean
28 }
29}
Karaoke Mode
The currentWordIndex is updated in real-time as the video plays, allowing you to highlight the currently spoken word. This is optional - if you don't need karaoke-style highlighting, you can ignore this field.

Template Variables#

Use Mustache syntax in your HTML to access subtitle data:

Template variables
1<!-- Available template variables in HTML -->
2
3<!-- Basic subtitle data -->
4{{subtitle.text}} <!-- Full cue text -->
5{{subtitle.id}} <!-- Cue ID -->
6{{subtitle.trackId}} <!-- Track ID -->
7{{subtitle.language}} <!-- Language code -->
8
9<!-- Speaker (if present) -->
10{{subtitle.speaker.name}}
11{{subtitle.speaker.color}}
12
13<!-- Word iteration -->
14{{#subtitle.words}}
15 <span class="word">{{text}}</span>
16{{/subtitle.words}}
17
18<!-- Current word index (for highlighting) -->
19{{currentWordIndex}}
20
21<!-- Configuration values -->
22{{config.fontSize}}
23{{config.textColor}}
24{{config.backgroundColor}}

Word Events#

Words can trigger displays (tooltips, product cards) on click/hover. Send events to the parent player using postMessage:

Word event handlers
1// Word click event
2document.querySelector('.word').addEventListener('click', (e) => {
3 const wordIndex = parseInt(e.target.dataset.wordIndex);
4 const word = subtitle.words[wordIndex];
5
6 // Send to parent player
7 window.parent.postMessage({
8 type: 'mp4e:wordClick',
9 word: word,
10 wordIndex: wordIndex,
11 trackId: subtitle.trackId,
12 cueId: subtitle.id
13 }, '*');
14});
15
16// Word hover event
17document.querySelector('.word').addEventListener('mouseenter', (e) => {
18 const wordIndex = parseInt(e.target.dataset.wordIndex);
19 const word = subtitle.words[wordIndex];
20 const rect = e.target.getBoundingClientRect();
21
22 window.parent.postMessage({
23 type: 'mp4e:wordHover',
24 word: word,
25 wordIndex: wordIndex,
26 trackId: subtitle.trackId,
27 cueId: subtitle.id,
28 action: 'enter',
29 position: {
30 x: rect.left + rect.width / 2,
31 y: rect.top
32 }
33 }, '*');
34});
35
36// Word hover leave
37document.querySelector('.word').addEventListener('mouseleave', (e) => {
38 window.parent.postMessage({
39 type: 'mp4e:wordHover',
40 word: word,
41 wordIndex: wordIndex,
42 trackId: subtitle.trackId,
43 cueId: subtitle.id,
44 action: 'leave'
45 }, '*');
46});

To receive updates from the player (e.g., when the current word changes):

Listening for updates
1// Listen for context updates from player
2window.addEventListener('message', (event) => {
3 if (event.data.type === 'mp4e:updateContext') {
4 // Update local state
5 if (event.data.subtitle) {
6 subtitle = event.data.subtitle;
7 }
8 if (event.data.config) {
9 config = event.data.config;
10 }
11 if (event.data.currentWordIndex !== undefined) {
12 currentWordIndex = event.data.currentWordIndex;
13 }
14
15 // Re-render with new data
16 render();
17 }
18});

Creating Custom Plugins#

To create a custom subtitle plugin:

  1. Open MP4E Studio and navigate to Library → Plugins
  2. Click "Create Plugin" and select "Subtitle Plugin" as the category
  3. Write your HTML - Use Mustache templates to access subtitle data
  4. Add CSS styling - Style your subtitle presentation
  5. Add JavaScript - Handle karaoke highlighting, word events, etc.
  6. Define config schema - Expose customization options (colors, fonts, etc.)
  7. Test in preview - Use sample subtitle data to verify rendering
  8. Save and use - Apply to your subtitle tracks
Configuration Schema
Define a configSchema to expose customization options. These will appear in the Studio UI as form fields, allowing users to customize colors, fonts, sizes, and other styling options without editing code.

Plugin Examples#

Complete Karaoke Implementation

Here's a complete example of a karaoke-style subtitle plugin with word highlighting:

Karaoke plugin (script section)
1// Complete Karaoke Plugin Implementation
2
3const container = document.getElementById("subtitle-container");
4const data = window.mp4e?.getData() || {};
5
6let subtitle = data.subtitle || { text: "", words: [] };
7let config = data.config || {};
8let currentWordIndex = data.currentWordIndex ?? null;
9
10function render() {
11 if (!subtitle.text) {
12 container.innerHTML = "";
13 return;
14 }
15
16 const textEl = document.createElement("div");
17 textEl.className = "subtitle-text";
18
19 // Apply config styling
20 if (config.fontSize) textEl.style.fontSize = config.fontSize;
21 if (config.textColor) textEl.style.color = config.textColor;
22 if (config.backgroundColor) textEl.style.background = config.backgroundColor;
23
24 // Render words
25 if (subtitle.words && subtitle.words.length > 0) {
26 subtitle.words.forEach((word, index) => {
27 const wordEl = document.createElement("span");
28 wordEl.className = "word";
29 wordEl.textContent = word.text;
30 wordEl.dataset.wordIndex = index;
31
32 // Highlight current word
33 const isCurrentWord = currentWordIndex !== null && index === currentWordIndex;
34 if (isCurrentWord) {
35 wordEl.classList.add("current");
36 wordEl.style.color = config.highlightColor || "#FFD700";
37 wordEl.style.transform = "scale(1.15)";
38 }
39
40 // Word click handler
41 wordEl.addEventListener("click", (e) => {
42 e.stopPropagation();
43 window.parent?.postMessage({
44 type: "mp4e:wordClick",
45 word: word,
46 wordIndex: index,
47 trackId: subtitle.trackId,
48 cueId: subtitle.id
49 }, "*");
50 });
51
52 textEl.appendChild(wordEl);
53 if (index < subtitle.words.length - 1) {
54 textEl.appendChild(document.createTextNode(" "));
55 }
56 });
57 } else {
58 textEl.textContent = subtitle.text;
59 }
60
61 container.innerHTML = "";
62 container.appendChild(textEl);
63}
64
65// Listen for updates
66window.addEventListener("message", (event) => {
67 if (event.data.type === "mp4e:updateContext") {
68 if (event.data.subtitle) subtitle = event.data.subtitle;
69 if (event.data.config) config = event.data.config;
70 if (event.data.currentWordIndex !== undefined) {
71 currentWordIndex = event.data.currentWordIndex;
72 }
73 render();
74 }
75});
76
77// Initial render
78render();

Best Practices

  • Always check if data exists before accessing (use || fallbacks)
  • Use e.stopPropagation() on word clicks to prevent parent handlers
  • Clean up event listeners if your plugin is destroyed (rare for subtitle plugins)
  • Test with various cue lengths - very long text should wrap gracefully
  • Support both word-level timing and plain text (some tracks may not have word data)
  • Use relative units (%, rem, em) for responsive sizing