EmbedPlugin
EmbedPlugin is what you reach for when you embed the music player inside an <iframe> and need the host page to talk to it.
The host sends nm:command messages; the plugin validates the origin, then calls the matching player method.
The plugin also listens to player events and forwards them back to the host as nm:event messages.
Nothing else in the player knows about postMessage; this plugin owns the whole protocol.
This plugin is a direct re-export of the player core’s EmbedPlugin. No music-specific overrides.
import { EmbedPlugin } from '@nomercy-entertainment/nomercy-music-player/plugins';
import type { EmbedCommand, EmbedEventMessage, EmbedOptions } from '@nomercy-entertainment/nomercy-music-player/plugins';
Plugin id: 'embed'
Options
| Option | Type | Default | Description |
|---|---|---|---|
allowedOrigins | string | string[] | [] | Origin(s) allowed to send inbound nm:command messages. '*' accepts any origin (development only). An empty list rejects all inbound commands. The plugin still forwards events outward even when the list is empty. |
forwardEvents | ReadonlyArray<'ready' | 'play' | 'pause' | 'ended' | 'time' | 'volume' | 'mute' | 'error'> | ['ready','play','pause','ended','time','volume','mute'] | Player events to forward to the host as nm:event postMessages. 'error' is in the type but not in the default list; add it explicitly if you need it. |
applyIframeTweaks | boolean | true when inside a nested browsing context | When true, adds the nm-embed CSS class to the player container. Defaults to true when inIframe() detects a nested browsing context. Pass false to suppress the class even when the player is iframed. |
Inbound commands (host to embed)
The host page sends commands as postMessage objects matching the EmbedCommand type.
All commands share the envelope { type: 'nm:command', action: '...' }.
action | Extra fields | Description |
|---|---|---|
'play' | Calls player.play() | |
'pause' | Calls player.pause() | |
'stop' | Calls player.stop() | |
'seek' | time: number | Calls player.time(time) |
'volume' | level: number | Calls player.volume(level) |
'mute' | Calls player.mute() | |
'unmute' | Calls player.unmute() | |
'next' | Calls player.next() | |
'previous' | Calls player.previous() |
Outbound events (embed to host)
The plugin forwards the events listed in forwardEvents as nm:event postMessages.
The default set is ready, play, pause, ended, time, volume, and mute.
error is part of the EmbedEventMessage union but is not forwarded unless you add it to forwardEvents explicitly.
Every outbound message has the envelope { type: 'nm:event', name, data }.
The player event payload is always nested under data; there are no extra fields at the top level alongside type and name.
name | data shape | Description |
|---|---|---|
'ready' | {} | Player is ready to play |
'play' | ActionOptions ({ source?, silent?, autoplay? }) | Playback started |
'pause' | ActionOptions ({ source?, silent?, autoplay? }) | Playback paused |
'ended' | {} | Track ended |
'time' | { time: number } | Periodic time update (seconds) |
'volume' | { level: number } | Volume changed |
'mute' | { muted: boolean } | Mute state changed |
'error' | { code: string; message?: string; severity: 'fatal' | 'error' | 'warning' | 'info'; scope: ErrorScope; suggestion?: string } | Player error (not forwarded by default). Serialized to a plain, clone-safe object before posting. |
Reading event data on the host
window.addEventListener('message', (event) => {
if (event.origin !== 'https://player.example.com') return;
if (event.data?.type !== 'nm:event') return;
const { name, data } = event.data;
if (name === 'time') {
console.log('current time:', data.time);
}
if (name === 'error') {
console.error('player error:', data.code, data.message);
}
});
Methods
sendToHost(message)
sendToHost(message: EmbedEventMessage): void
Sends a structured event message to the host page via window.parent.postMessage.
When exactly one origin is listed in the allowlist, the message is pinned to that origin.
When the list has more than one entry or contains '*', the target is '*'.
Swallows any cross-origin DOMException so the player never crashes due to a missing or unloaded parent frame.
allowedOrigins()
allowedOrigins(): readonly string[]
allowedOrigins(origins: string | string[]): void
Reads or replaces the allowed-origins list at runtime. The read form returns a snapshot array; mutations to the returned array do not affect the live list. The write form replaces the list immediately; the new list takes effect on the next inbound message.
Registration
import nmMPlayer from '@nomercy-entertainment/nomercy-music-player';
import { EmbedPlugin } from '@nomercy-entertainment/nomercy-music-player/plugins';
const player = nmMPlayer('main')
.addPlugin(EmbedPlugin, {
allowedOrigins: ['https://api.example.com'],
forwardEvents: ['ready', 'play', 'pause', 'ended', 'time', 'volume', 'mute', 'error'],
})
.setup({ playlist: myTracks });
Required <iframe> attributes
<iframe
src="https://player.example.com/embed"
allow="autoplay; fullscreen; picture-in-picture; encrypted-media;
accelerometer; gyroscope; web-share; clipboard-write"
allowfullscreen
loading="lazy"
></iframe>
autoplay is load-bearing.
Without it, MediaSession never activates and OS-level controls (lock screen, Bluetooth, Now Playing) will not appear even if MediaSessionPlugin is registered.
Host page example
const iframe = document.getElementById('music-player');
// Send a command to the embedded player:
iframe.contentWindow.postMessage(
{ type: 'nm:command', action: 'play' },
'https://player.example.com',
);
// Receive events from the embedded player:
window.addEventListener('message', (event) => {
if (event.origin !== 'https://player.example.com') return;
if (event.data?.type !== 'nm:event') return;
const { name, data } = event.data;
console.log('Player event:', name, data);
});
Security
When exactly one origin is listed in allowedOrigins, outbound postMessages are pinned to that origin.
When multiple origins are listed or '*' is used, outbound messages use '*' as the target origin.
Always pin to a single explicit origin in production.
An empty allowedOrigins list (the default) rejects all inbound commands but still sends outbound events.
Extension points
EmbedPlugin exposes four protected methods you can override in a subclass to customise the protocol:
handleCommand(msg)- add custom command types beyond the built-in nine.formatEvent(name, data)- change the outbound message envelope or suppress specific events by returningnull.isOriginAllowed(origin)- apply custom logic such as wildcard subdomain matching.inIframe()- override in tests to inject a fixed value.