Skip to content

Multi-Instance

Running more than one player on the same page: a mini-player or PiP alongside a main player, a grid of synchronized tiles, or a podcast player in the background while a video plays.

How instances are isolated

Each call to nmplayer(id) or nmMPlayer(id) returns a scoped player singleton keyed by id. Two players with different ids are completely independent:

  • Separate event buses
  • Separate plugin registries
  • Separate queue state

There is no shared mutable global between two player instances unless you introduce it explicitly (e.g. a BroadcastChannel or a shared module-level variable).

Two players on one page

TypeScript
import { nmplayer } from '@nomercy-entertainment/nomercy-video-player';
import { DesktopUiPlugin } from '@nomercy-entertainment/nomercy-video-player/plugins';
import { nmMPlayer } from '@nomercy-entertainment/nomercy-music-player';
import { AutoAdvancePlugin } from '@nomercy-entertainment/nomercy-music-player/plugins';

// Main video player:
const videoPlayer = nmplayer('video-player').setup({
baseUrl: 'https://raw.githubusercontent.com/NoMercy-Entertainment/nomercy-media/master/Films',
playlist: [{
id: 'bbb',
title: 'Big Buck Bunny',
url: '/Big.Buck.Bunny.(2008)/Big.Buck.Bunny.(2008).NoMercy.m3u8',
}],
});
videoPlayer.addPlugin(DesktopUiPlugin);

// Background music player (no UI plugin needed):
const musicPlayer = nmMPlayer('music').setup({
baseUrl: 'https://raw.githubusercontent.com/NoMercy-Entertainment/nomercy-media/master/Music',
playlist: [{
id: 'thaw',
name: 'Thaw You Out',
url: '/D/Derek%20Clegg/%5B2010%5D%20KJC/01%20Thaw%20You%20Out.mp3',
}],
});
musicPlayer.addPlugin(AutoAdvancePlugin);

await Promise.all([videoPlayer.ready(), musicPlayer.ready()]);

// Pause music when video plays:
videoPlayer.on('play', () => musicPlayer.pause());
videoPlayer.on('pause', () => musicPlayer.play());

PiP + main player pattern

The browser’s native Picture-in-Picture API keeps one <video> element in a floating window. The player exposes PiP control:

TypeScript
// Main player:
const mainPlayer = nmplayer('main').setup({ ... });

// When the user activates PiP on the main player:
mainPlayer.on('pip', ({ active }) => {
if (active) {
// Mini player (e.g. a compact transport bar) takes over the primary content area
miniTransport.show();
} else {
miniTransport.hide();
}
});

// Toggle PiP (a video-player method; the kit base player exposes PiP via platform().pip):
mainPlayer.pip(true); // enter PiP
mainPlayer.pip(false); // exit PiP

No second player instance is needed for native PiP, the single player’s <video> element moves to the PiP window.

Watch-party tile grid

For a grid of synchronized tiles (each playing the same content):

TypeScript
const tileCount = 4;
const players = Array.from({ length: tileCount }, (_, index) => {
const container = document.getElementById(`tile-${index}`)!;
return nmplayer(`tile-${index}`).setup({
container,
baseUrl: 'https://raw.githubusercontent.com/NoMercy-Entertainment/nomercy-media/master/Films',
playlist: [{
id: 'sintel',
url: '/Sintel.(2010)/Sintel.(2010).NoMercy.m3u8',
}],
});
});

await Promise.all(players.map((player) => player.ready()));

// Sync: when the first tile plays, play all others at the same position:
players[0].on('play', () => {
const time = players[0].time();
players.slice(1).forEach((tile) => {
tile.time(time); // sync position
tile.play();
});
});

players[0].on('pause', () => {
players.slice(1).forEach((tile) => tile.pause());
});

Tab-leader pattern

When multiple browser tabs have the same player open (e.g. a podcast player that persists across navigation), elect a leader tab that owns playback. Other tabs follow.

Use BroadcastChannel for cross-tab communication:

TypeScript
const CHANNEL_NAME = 'player-leader';

function electLeader(playerId: string): 'leader' | 'follower' {
const channel = new BroadcastChannel(CHANNEL_NAME);

// Announce to other tabs:
channel.postMessage({ type: 'leader-claim', playerId });

let role: 'leader' | 'follower' = 'leader';

// If another tab already claimed leadership before us:
channel.onmessage = (event) => {
if (event.data.type === 'leader-exists' && event.data.playerId !== playerId) {
role = 'follower';
}
};

// Respond to new tabs asking who is leader:
channel.onmessage = (event) => {
if (event.data.type === 'leader-claim') {
channel.postMessage({ type: 'leader-exists', playerId });
}
};

return role;
}

In practice, use the WatchPartyLitePlugin worked example as a starting point and extend it with a leader election algorithm.

Event-bus isolation

Two player instances never share an event bus. If you need a player to react to another player’s events, wire them explicitly:

TypeScript
// Correct, explicit cross-player wiring:
playerA.on('play', () => playerB.pause());

// Incorrect, trying to listen to playerA's events from playerB:
playerB.on('play', ...) // only fires for playerB's own events

For more complex cross-instance protocols, write a coordinator object outside both players:

TypeScript
class PlayerCoordinator {
constructor(
private primary: NMVideoPlayer,
private secondary: NMMusicPlayer,
) {
primary.on('play', () => secondary.pause());
primary.on('pause', () => secondary.play());
}

dispose(): void {
// Event listeners are on the player instances, they dispose with the player
}
}

Storage namespacing

Storage is namespaced by plugin id, not player id: a plugin’s keys are prefixed nmplayer-<plugin-id>-* (the player core writes nothing itself). So two player instances that share one storage backend and register the same plugin will share those keys. To isolate per player, give each instance its own storage backend, or derive per-instance plugin ids.

If you want two players to share a storage state (e.g. the same volume preference), pass the same IStorage backend instance through setup() and use a shared key namespace:

TypeScript
import { LocalStorageBackend } from '@nomercy-entertainment/nomercy-player-core';

const sharedStorage = new LocalStorageBackend();

// Both players share the same storage instance:
nmplayer('main').setup({ storage: sharedStorage });
nmplayer('pip').setup({ storage: sharedStorage });