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
- Plugin runs in a sandboxed iframe for security
- Receives subtitle data via
window.mp4e.getData() - Renders HTML using provided data + configuration
- Updates in real-time as
currentWordIndexchanges (karaoke mode) - 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-simpleClassic subtitle style with semi-transparent background. Clean and readable for all content types.
Boxed Words
mp4e:subtitles-boxed-wordsTikTok/Instagram Reels style - each word in its own colorful box with hover effects.
Minimal Subtitles
mp4e:subtitles-minimalNo background, just text with strong shadow. Elegant and non-intrusive.
Karaoke Pro
mp4e:subtitles-karaoke-proLarge golden text (32px+) with glow effects and animated underline for current word.
Plugin Structure#
Subtitle plugins are defined in the marketplace with the subtitle category:
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>",1011 "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; }",1213 "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 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:
1// Data passed to subtitle plugins via window.mp4e.getData()2interface SubtitlePluginContext {3 subtitle: {4 text: string // Full cue text5 words: Word[] // Array of words with timing6 speaker?: {7 name: string8 color?: string9 }10 id: string // Cue ID11 trackId: string // Parent track ID12 language: string // Track language code13 }14 config: Record<string, any> // Plugin configuration15 currentWordIndex: number | null // Current word index (for karaoke)16}1718// Word structure19interface Word {20 index: number21 text: string22 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: string27 inheritDisplaySettings?: boolean28 }29}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:
1<!-- Available template variables in HTML -->23<!-- Basic subtitle data -->4{{subtitle.text}} <!-- Full cue text -->5{{subtitle.id}} <!-- Cue ID -->6{{subtitle.trackId}} <!-- Track ID -->7{{subtitle.language}} <!-- Language code -->89<!-- Speaker (if present) -->10{{subtitle.speaker.name}}11{{subtitle.speaker.color}}1213<!-- Word iteration -->14{{#subtitle.words}}15 <span class="word">{{text}}</span>16{{/subtitle.words}}1718<!-- Current word index (for highlighting) -->19{{currentWordIndex}}2021<!-- 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:
1// Word click event2document.querySelector('.word').addEventListener('click', (e) => {3 const wordIndex = parseInt(e.target.dataset.wordIndex);4 const word = subtitle.words[wordIndex];56 // Send to parent player7 window.parent.postMessage({8 type: 'mp4e:wordClick',9 word: word,10 wordIndex: wordIndex,11 trackId: subtitle.trackId,12 cueId: subtitle.id13 }, '*');14});1516// Word hover event17document.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();2122 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.top32 }33 }, '*');34});3536// Word hover leave37document.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):
1// Listen for context updates from player2window.addEventListener('message', (event) => {3 if (event.data.type === 'mp4e:updateContext') {4 // Update local state5 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 }1415 // Re-render with new data16 render();17 }18});Creating Custom Plugins#
To create a custom subtitle plugin:
- Open MP4E Studio and navigate to Library → Plugins
- Click "Create Plugin" and select "Subtitle Plugin" as the category
- Write your HTML - Use Mustache templates to access subtitle data
- Add CSS styling - Style your subtitle presentation
- Add JavaScript - Handle karaoke highlighting, word events, etc.
- Define config schema - Expose customization options (colors, fonts, etc.)
- Test in preview - Use sample subtitle data to verify rendering
- Save and use - Apply to your subtitle tracks
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:
1// Complete Karaoke Plugin Implementation23const container = document.getElementById("subtitle-container");4const data = window.mp4e?.getData() || {};56let subtitle = data.subtitle || { text: "", words: [] };7let config = data.config || {};8let currentWordIndex = data.currentWordIndex ?? null;910function render() {11 if (!subtitle.text) {12 container.innerHTML = "";13 return;14 }1516 const textEl = document.createElement("div");17 textEl.className = "subtitle-text";1819 // Apply config styling20 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;2324 // Render words25 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;3132 // Highlight current word33 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 }3940 // Word click handler41 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.id49 }, "*");50 });5152 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 }6061 container.innerHTML = "";62 container.appendChild(textEl);63}6465// Listen for updates66window.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});7677// Initial render78render();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