Creating Plugins
Step-by-step guide to creating custom MP4E plugins using the Plugin Editor.
Getting Started
Custom plugins let you create any interactive content using HTML, CSS, and JavaScript. They run in a sandboxed iframe and communicate with the player through the mp4e bridge API.
{
id: 'acme:countdown',
name: 'Countdown Timer',
type: 'overlay',
description: 'Displays a countdown to a specific date',
configSchema: {
type: 'object',
properties: {
targetDate: { type: 'string', format: 'date-time', title: 'Target Date' },
title: { type: 'string', title: 'Title', default: 'Coming Soon' },
showSeconds: { type: 'boolean', title: 'Show Seconds', default: true }
},
required: ['targetDate']
},
emits: {
complete: { label: 'Countdown Complete', description: 'Fired when countdown reaches zero' }
},
content: {
html: `
<div class="countdown">
<h3 class="title">{{title}}</h3>
<div class="time-display">
<div class="unit"><span class="val" id="days">00</span><span class="lbl">Days</span></div>
<div class="unit"><span class="val" id="hours">00</span><span class="lbl">Hours</span></div>
<div class="unit"><span class="val" id="minutes">00</span><span class="lbl">Minutes</span></div>
<div class="unit seconds" id="sec-wrap"><span class="val" id="seconds">00</span><span class="lbl">Seconds</span></div>
</div>
</div>`,
css: `
.countdown { text-align:center; padding:24px; background:linear-gradient(135deg,#667eea,#764ba2); border-radius:16px; color:#fff; font-family:system-ui,sans-serif; }
.title { margin:0 0 16px; font-size:18px; font-weight:600; opacity:.9; }
.time-display { display:flex; gap:16px; justify-content:center; }
.unit { display:flex; flex-direction:column; align-items:center; }
.val { font-size:36px; font-weight:700; line-height:1; font-variant-numeric:tabular-nums; }
.lbl { font-size:12px; text-transform:uppercase; opacity:.7; margin-top:4px; }
.seconds.hidden { display:none; }`,
script: `
var config = mp4e.getConfig();
var targetDate = new Date(config.targetDate);
if (!config.showSeconds) {
document.getElementById('sec-wrap').classList.add('hidden');
}
function tick() {
var diff = targetDate - new Date();
if (diff <= 0) {
mp4e.emit('complete');
document.querySelector('.time-display').innerHTML = '<span class="val">Done!</span>';
clearInterval(iv);
return;
}
document.getElementById('days').textContent = String(Math.floor(diff/864e5)).padStart(2,'0');
document.getElementById('hours').textContent = String(Math.floor(diff%864e5/36e5)).padStart(2,'0');
document.getElementById('minutes').textContent = String(Math.floor(diff%36e5/6e4)).padStart(2,'0');
document.getElementById('seconds').textContent = String(Math.floor(diff%6e4/1e3)).padStart(2,'0');
}
tick();
var iv = setInterval(tick, 1000);`
}
}Using the Plugin Editor
The easiest way to create plugins is through the Studio's built-in Plugin Editor:
- Create an overlay in the Overlays panel and select "Custom Plugin" as the type
- Open the Plugin Editor — the full-screen editor appears with a two-panel layout
- Write your HTML in the HTML tab, using
{{variable}}syntax for dynamic values - Style with CSS in the CSS tab — use
var(--s)for responsive scaling - Add interactivity in the Script tab using the
mp4ebridge API - Define inputs in the Inputs tab — creates form fields in the overlay settings
- Define events in the Emits tab — makes events available as rule triggers
- Preview live in the Preview tab — changes reflect instantly
Cmd/Ctrl and click any tab to open it in the right panel. A common workflow: HTML on the left, Preview on the right. Or Script on the left, API Docs on the right.Editor Tabs
| Tab | Purpose | Maps To |
|---|---|---|
| HTML | Plugin markup with template interpolation | content.html |
| CSS | Scoped styles, responsive via CSS custom properties | content.css |
| Script | JavaScript — use mp4e API here, NOT template syntax | content.script |
| Inputs | JSON Schema for configuration fields (generates form UI) | configSchema |
| Outputs | Variables the plugin writes to | outputs |
| Emits | Custom events with data schemas (become rule triggers) | emits |
| Actions | Actions other plugins/rules can call on this plugin | actions |
| Variables | Auto-created project variables on install | variables |
| Display | Position, animation, auto-close settings | displaySettings |
| Settings | Name, version, type, category, author, license, tags | name, version, etc. |
| Preview | Live iframe preview — updates as you type | UI only |
| API Docs | Quick reference for the mp4e bridge API | UI only |
Plugin Structure
A plugin has three content sections plus metadata:
HTML
Structure and content. Supports {{variable}} template interpolation for dynamic values from config and project variables.
CSS
Full CSS including animations, media queries, and {{variable}} interpolation via custom properties.
JavaScript
Logic and interactivity via the mp4e bridge API. Template syntax does NOT work here.
HTML Section
HTML templates support double-curly-brace syntax for interpolation:
<div class="product-card">
<!-- Simple interpolation from config -->
<h2>{{title}}</h2>
<!-- Object data (for object-display plugins) -->
<p>{{object.userLabel}}</p>
<span class="price">${{object.data.price}}</span>
<!-- Project variables -->
<span class="cart-badge">{{cartCount}} items</span>
<!-- HTML escaping (default) vs unescaped -->
<p>{{description}}</p> <!-- Escaped -->
<div>{{{richContent}}}</div> <!-- Unescaped HTML -->
</div>CSS Section
Full CSS support. Use var(--s) for scale-responsive sizing:
/* Scale-responsive sizing via CSS custom property */
.card {
padding: calc(16px * var(--s, 1));
font-size: calc(14px * var(--s, 1));
border-radius: calc(8px * var(--s, 1));
}
/* Config value interpolation via custom properties */
:root {
--accent: {{accentColor}};
}
.button { background: var(--accent); }
/* Animations */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.animated { animation: fadeIn 0.3s ease-out; }
/* Responsive */
@media (max-width: 480px) {
.hide-mobile { display: none; }
}JavaScript Section
{{variable}} interpolation only works in HTML and CSS. In JavaScript, use the mp4e API:
var config = mp4e.getConfig(); var title = config.title;var title = "{{title}}";// Access configuration (sync)
var config = mp4e.getConfig();
var title = config.title;
var price = config.price;
// Access bound object data (for object-display/group-bound plugins)
var obj = window.object; // { id, label, userLabel, data, position }
// DOM manipulation
document.getElementById('title').textContent = title;
// Event listeners
document.getElementById('buy-btn').addEventListener('click', function() {
// Emit custom event (triggers rules)
mp4e.emit('purchased', { productId: config.productId, price: price });
// Execute player actions
mp4e.executeActions([
{ type: 'setVariable', variableId: 'cartCount', operation: 'increment', value: 1 },
{ type: 'showNotification', message: 'Added to cart!', variant: 'success' }
]);
});
// Listen for variable changes
mp4e.onVariableChange(function(variables, changedIds) {
// changedIds is an array of changed variable IDs, or undefined for full broadcast
if (!changedIds || changedIds.includes('cartCount')) {
document.getElementById('cart-badge').textContent = variables.cartCount + ' items';
}
});
// Listen for config updates (when overlay config changes in studio)
mp4e.onConfigUpdate(function(newConfig) {
document.getElementById('title').textContent = newConfig.title;
});
// Video events
mp4e.on('mp4e:video:timeupdate', function(state) {
console.log('Time:', state.currentTime);
});
// Async operations
mp4e.getVariable('score').then(function(score) {
mp4e.setVariable('score', score + 10);
});
// Cleanup intervals when plugin is destroyed
var iv = setInterval(tick, 1000);
// Note: intervals are automatically cleared when iframe is destroyedConfiguration Schema (Inputs)
Define configurable properties using JSON Schema in the Inputs tab. The Studio automatically generates form UI based on your schema:
{
configSchema: {
type: 'object',
properties: {
// String — text input
title: {
type: 'string',
title: 'Title',
description: 'Product title',
default: 'Product Name'
},
// Number — numeric input with constraints
price: {
type: 'number',
title: 'Price',
minimum: 0,
maximum: 999999,
default: 0
},
// Enum — dropdown select
currency: {
type: 'string',
title: 'Currency',
enum: ['USD', 'EUR', 'GBP', 'JPY'],
default: 'USD'
},
// Boolean — checkbox/toggle
showRating: {
type: 'boolean',
title: 'Show Rating',
default: true
},
// Color — color picker
accentColor: {
type: 'string',
format: 'color',
title: 'Accent Color',
default: '#3b82f6'
},
// Variable reference — dropdown of project variables
scoreVariable: {
type: 'string',
title: 'Score Variable',
'x-ref': 'variable' // Shows variable picker in Studio
},
// Array
tags: {
type: 'array',
title: 'Tags',
items: { type: 'string' },
default: []
},
// Nested object
styling: {
type: 'object',
title: 'Styling',
properties: {
borderRadius: { type: 'number', title: 'Border Radius', default: 8 },
shadow: { type: 'boolean', title: 'Shadow', default: true }
}
}
},
required: ['title', 'price']
}
}Config values are accessible in HTML via {{fieldName}} and in JavaScript via mp4e.getConfig().fieldName.
Outputs
Outputs declare which project variables the plugin writes to. This is informational — it tells other developers and the Studio which variables this plugin modifies. The actual writing is done via mp4e.setVariable() in your JavaScript.
{
outputs: [
{
variableId: 'score',
name: 'Quiz Score',
description: 'Updated when user answers a question'
},
{
variableId: 'quizComplete',
name: 'Quiz Complete',
description: 'Set to true when all questions answered'
}
]
}Emits (Events)
Define events the plugin can emit. These become available as rule triggers in the Studio, so users can attach actions to your plugin's events.
{
emits: {
// Simple event (no data)
clicked: {
label: 'Clicked',
description: 'Fired when the plugin is clicked'
},
// Event with typed data payload
purchased: {
label: 'Purchase',
description: 'Fired when user completes a purchase',
dataSchema: {
type: 'object',
properties: {
productId: { type: 'string', description: 'Product ID' },
price: { type: 'number', description: 'Purchase price' },
quantity: { type: 'number', description: 'Quantity' }
}
}
}
}
}
// Emitting from JavaScript:
mp4e.emit('purchased', {
productId: mp4e.getConfig().productId,
price: mp4e.getConfig().price,
quantity: 1
});
// The event is routed to rules as: plugin:<namespace>:<identifier>:purchased
// Rules can filter on event data using eventData conditions:
// { type: 'eventData', path: 'price', operator: 'gte', value: 100 }Actions
Actions are methods that other plugins or rules can call on your plugin via the pluginAction action type.
{
actions: [
{
id: 'nextQuestion',
label: 'Next Question',
description: 'Advance to next quiz question',
configSchema: {
type: 'object',
properties: {
skipAnimation: { type: 'boolean', default: false }
}
}
},
{
id: 'reset',
label: 'Reset',
description: 'Reset plugin to initial state'
}
]
}
// Handle incoming actions in JavaScript:
mp4e.onAction('nextQuestion', function(config) {
var skip = config && config.skipAnimation;
advanceToNextQuestion(skip);
});
mp4e.onAction('reset', function() {
resetState();
});
// Other plugins/rules call these via:
// { type: 'pluginAction', pluginId: 'quiz', actionId: 'nextQuestion', config: { skipAnimation: true } }Variables
Plugins can declare variables that are auto-created as project variables when the plugin is installed. They are prefixed with the plugin's namespace:
{
variables: [
{
id: 'score',
name: 'Quiz Score',
type: 'number',
default: 0,
editable: false // Users can't modify in Studio
},
{
id: 'currentQuestion',
name: 'Current Question',
type: 'number',
default: 0,
editable: true
}
]
}
// If plugin namespace is "mp4e:quiz", the variables become:
// mp4e:quiz:score
// mp4e:quiz:currentQuestion
// Access in JavaScript with full prefixed name:
mp4e.getVariable('mp4e:quiz:score').then(function(score) {
console.log('Score:', score);
});
mp4e.setVariable('mp4e:quiz:score', 100);Best Practices
Performance
- Minimize DOM operations — batch updates when possible
- Use CSS animations instead of JavaScript animations
- Filter
onVariableChangebychangedIdsto avoid unnecessary work - Use
var(--s)CSS scaling instead of JavaScript-based resizing
Accessibility
- Use semantic HTML elements
- Add ARIA labels for interactive elements
- Ensure sufficient color contrast
- Support keyboard navigation (focusable elements)
Error Handling
- Wrap async operations in try/catch
- Provide fallback content for failed loads
- Use
mp4e.emit()to surface errors to rules - Validate config values before using (defaults may be undefined)
Config Gotchas
mp4e.getConfig()returns saved config, NOT merged with defaults- If a field was never saved, it will be
undefinedeven if schema has a default - Always provide JS fallbacks:
var mode = config.mode || 'sequential' - Template
{{variable}}works in HTML/CSS only, usemp4e.getConfig()in JS