Embed
Register EmbedPlugin when you drop the player into a third-party page, a CMS widget, or a no-code builder via an <iframe>.
The plugin wires up a postMessage channel so the parent page can send commands (play, pause, seek, and more) and listen for player events, all without any shared JavaScript context between the two pages.
If the player is not inside an <iframe>, this plugin does nothing harmful, but you do not need it for that case.
Plugin id
'embed'
Import
import { EmbedPlugin } from '@nomercy-entertainment/nomercy-video-player/plugins';
import type {
EmbedOptions,
EmbedCommand,
EmbedEventMessage,
} from '@nomercy-entertainment/nomercy-video-player/plugins';
What it does
On use(), the plugin attaches a message listener to window and subscribes to the configured set of player events.
Inbound messages that match { type: 'nm:command' } are validated against allowedOrigins and dispatched to the appropriate player method.
Player events are packaged as { type: 'nm:event', name, data } and posted back to the host page via window.parent.postMessage.
The listener and all event forwarders are cleaned up automatically on dispose().
Options
| Option | Type | Default | Description |
|---|---|---|---|
allowedOrigins | string | string[] | [] (none) | Origin(s) allowed to send nm:command messages. Empty means all inbound commands are rejected. '*' accepts any origin (development only). |
forwardEvents | ReadonlyArray<EmbedEventMessage['name']> | ['ready','play','pause','ended','time','volume','mute'] | Player events to forward to the host page as nm:event messages. |
applyIframeTweaks | boolean | true when inIframe() | When true, adds the nm-embed class to the player container and applies iframe-appropriate UI adjustments (smaller controls, suppressed popout button, etc.). Defaults to true automatically when the player detects it is running inside a nested browsing context. |
EmbedCommand
Commands sent from the parent page to the embedded player all use type: 'nm:command' as the envelope:
| Command | Description |
|---|---|
{ type: 'nm:command', action: 'play' } | Calls player.play() |
{ type: 'nm:command', action: 'pause' } | Calls player.pause() |
{ type: 'nm:command', action: 'stop' } | Calls player.stop() |
{ type: 'nm:command', action: 'seek', time: number } | Seeks to time seconds |
{ type: 'nm:command', action: 'volume', level: number } | Sets volume to level (0-100) |
{ type: 'nm:command', action: 'mute' } | Mutes audio |
{ type: 'nm:command', action: 'unmute' } | Unmutes audio |
{ type: 'nm:command', action: 'next' } | Advances to next queue item |
{ type: 'nm:command', action: 'previous' } | Returns to previous queue item |
Unknown commands are logged as warnings via this.logger and silently dropped.
EmbedEventMessage
Every forwarded message has the envelope { type: 'nm:event', name, data }.
The player’s real event payload is the data value, and the extra fields described below are properties of data, not top-level fields on the message.
The error event is the one exception: its payload is serialized to a plain object first so it survives postMessage, which cannot clone the live error event.
| Forwarded name | data shape | Description |
|---|---|---|
'ready' | {} | Player has initialised. |
'play' | { source?: string; silent?: boolean; autoplay?: boolean } | Playback started. |
'pause' | { source?: string; silent?: boolean; autoplay?: boolean } | Playback paused. |
'ended' | {} | Track finished. |
'time' | { time: number } | Periodic time update. data.time is the current position in seconds. |
'volume' | { level: number } | Volume changed. Read data.level. |
'mute' | { muted: boolean } | Mute state toggled. Read data.muted. |
'error' | { code: string; message?: string; severity: 'fatal' | 'error' | 'warning' | 'info'; scope: ErrorScope; suggestion?: string } | Player error (not in the default forwardEvents list; add explicitly). The event is serialized to a plain, clone-safe object before posting: data.code is the error code, data.message the optional message, data.severity the level, and data.suggestion an optional hint. |
Reading event data in the host page
window.addEventListener('message', (event) => {
if (event.origin !== 'https://player.example.com') return;
const msg = event.data;
if (msg?.type !== 'nm:event') return;
switch (msg.name) {
case 'time':
// msg.data = { time: number }
console.log('position', msg.data.time);
break;
case 'volume':
// msg.data = { level: number }
console.log('volume', msg.data.level);
break;
case 'mute':
// msg.data = { muted: boolean }
console.log('muted', msg.data.muted);
break;
case 'error':
// msg.data = { code, message?, severity, scope, suggestion? }
console.error('player error', msg.data.code, msg.data.message);
break;
case 'play':
case 'pause':
// msg.data = Record<string, unknown>
console.log(msg.name, 'triggered');
break;
}
});
Methods
import { EmbedPlugin } from '@nomercy-entertainment/nomercy-video-player/plugins';
const embedPlugin = player.getPlugin(EmbedPlugin)!;
// Read the current allowlist:
const currentOrigins = embedPlugin.allowedOrigins();
// Replace the allowlist at runtime:
embedPlugin.allowedOrigins('https://app.example.com');
embedPlugin.allowedOrigins(['https://app.example.com', 'https://preview.example.com']);
// Post a message to the host page directly:
embedPlugin.sendToHost({ type: 'nm:event', name: 'ready', data: {} });
// Read or update options at runtime:
const currentOptions = embedPlugin.options();
embedPlugin.options({ allowedOrigins: 'https://app.example.com' });
Updating allowedOrigins via options() or the allowedOrigins() method takes effect immediately on the next inbound message.
Security note
When exactly one origin is in the allowlist, outbound postMessage calls are pinned to that origin.
When multiple origins are listed or '*' is used, outbound messages target '*'.
Pin to a single origin in production for best security.
Required iframe attributes
For full functionality the <iframe> needs:
<iframe
src="https://player.example.com/embed/player"
allow="autoplay; fullscreen; picture-in-picture; encrypted-media;
accelerometer; gyroscope; web-share; clipboard-write"
allowfullscreen
loading="lazy"
></iframe>
The autoplay permission is load-bearing.
Without it MediaSession never activates, so OS-level controls (lock screen, Now Playing, Bluetooth) do not appear even when MediaSessionPlugin is also registered.
Registration
import nmplayer from '@nomercy-entertainment/nomercy-video-player';
import { EmbedPlugin } from '@nomercy-entertainment/nomercy-video-player/plugins';
// Inside the iframe page:
const player = nmplayer('player')
.addPlugin(EmbedPlugin, { allowedOrigins: 'https://app.example.com' })
.setup({
baseUrl: 'https://raw.githubusercontent.com/NoMercy-Entertainment/nomercy-media/master/Films',
playlist: [
{
id: '1',
title: 'Sintel',
url: '/Sintel.(2010)/Sintel.(2010).NoMercy.m3u8',
},
],
});
// In the parent (host) page:
const iframe = document.querySelector('iframe');
// Send a command to the embedded player:
iframe.contentWindow.postMessage(
{ type: 'nm:command', action: 'play' },
'https://player.example.com',
);
// Seek to 2 minutes in:
iframe.contentWindow.postMessage(
{ type: 'nm:command', action: 'seek', time: 120 },
'https://player.example.com',
);
// Listen for events coming back from the player:
window.addEventListener('message', (event) => {
if (event.origin !== 'https://player.example.com') return;
const message = event.data;
if (message?.type !== 'nm:event') return;
if (message.name === 'ended') showNextEpisodeBanner();
});