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.

Complete plugin example — Countdown Timer
{
  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:

  1. Create an overlay in the Overlays panel and select "Custom Plugin" as the type
  2. Open the Plugin Editor — the full-screen editor appears with a two-panel layout
  3. Write your HTML in the HTML tab, using {{variable}} syntax for dynamic values
  4. Style with CSS in the CSS tab — use var(--s) for responsive scaling
  5. Add interactivity in the Script tab using the mp4e bridge API
  6. Define inputs in the Inputs tab — creates form fields in the overlay settings
  7. Define events in the Emits tab — makes events available as rule triggers
  8. Preview live in the Preview tab — changes reflect instantly
Two-Panel Workflow
Hold 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

TabPurposeMaps To
HTMLPlugin markup with template interpolationcontent.html
CSSScoped styles, responsive via CSS custom propertiescontent.css
ScriptJavaScript — use mp4e API here, NOT template syntaxcontent.script
InputsJSON Schema for configuration fields (generates form UI)configSchema
OutputsVariables the plugin writes tooutputs
EmitsCustom events with data schemas (become rule triggers)emits
ActionsActions other plugins/rules can call on this pluginactions
VariablesAuto-created project variables on installvariables
DisplayPosition, animation, auto-close settingsdisplaySettings
SettingsName, version, type, category, author, license, tagsname, version, etc.
PreviewLive iframe preview — updates as you typeUI only
API DocsQuick reference for the mp4e bridge APIUI 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:

HTML template syntax
<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:

CSS styling
/* 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

Template Syntax Does NOT Work in JavaScript

{{variable}} interpolation only works in HTML and CSS. In JavaScript, use the mp4e API:

CORRECT:var config = mp4e.getConfig(); var title = config.title;
WRONG:var title = "{{title}}";
JavaScript — using the mp4e API
// 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 destroyed

Configuration Schema (Inputs)

Define configurable properties using JSON Schema in the Inputs tab. The Studio automatically generates form UI based on your schema:

Configuration 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.

Output variable declarations
{
  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.

Event definitions
{
  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.

Plugin actions
{
  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:

Plugin variables
{
  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 onVariableChange by changedIds to 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 undefined even if schema has a default
  • Always provide JS fallbacks: var mode = config.mode || 'sequential'
  • Template {{variable}} works in HTML/CSS only, use mp4e.getConfig() in JS