Skip to content

Migration Guide: v1 to v2

This page is the high-level overview. For the full breaking-change tables, go to the per-package guides:

Fast path: the compat plugin

If you have existing v1 code and want to keep it running immediately while you migrate gradually, both packages ship a v1 compatibility plugin. The migration then becomes a three-step process at whatever pace you choose.

Step 1. Install the v2 package:

Code
npm install @nomercy-entertainment/nomercy-video-player@beta
npm install @nomercy-entertainment/nomercy-music-player@beta

Step 2. Register the compat plugin for whichever players you use:

TypeScript
import { V1VideoCompatPlugin } from '@nomercy-entertainment/nomercy-video-player/plugins/v1-compat';
import { V1MusicCompatPlugin } from '@nomercy-entertainment/nomercy-music-player/plugins/v1-compat';

videoPlayer.addPlugin(V1VideoCompatPlugin);
musicPlayer.addPlugin(V1MusicCompatPlugin);

Your existing v1 code keeps working. The browser console shows a deprecation warning for each v1 API you are still using, naming the v2 replacement.

Step 3. Work through the warnings at your own pace, migrating one call site at a time. When no warnings appear on any page, remove the plugins.

The compat plugins are temporary and will be removed in the first stable 2.x release. New projects should skip this path and write v2 API directly.

For a complete description of every shimmed method, event bridge, and unshimmable API, see:

Manual migration

The rest of this page covers the high-level manual migration for teams that prefer a single planned upgrade rather than a gradual one.

Is my import broken?

The npm package names are the same. ^1.x ranges will not automatically resolve to v2, and ^2.0.0 semver ranges exclude prerelease versions by default, so you must install by the beta dist-tag during the prerelease period:

Code
npm install @nomercy-entertainment/nomercy-video-player@beta
npm install @nomercy-entertainment/nomercy-music-player@beta

Once v2.0.0 stable ships and the latest tag is updated, a normal semver range like ^2.0.0 will work as expected.

The import names nmplayer, NMVideoPlayer, nmMPlayer, NMMusicPlayer resolve identically. Your code will still break because event payload shapes, method names, and playlist item field names changed significantly.

Why the API changed

v2 is a ground-up rewrite on a shared core (nomercy-player-core). The design goals drove the breaking changes:

  • Event payload extensibility: v1 events carried raw HTMLElement objects and multi-field flat structs. v2 wraps everything: { time: number } instead of raw number, { item, index } instead of the item alone. Adding a field to a v2 event is non-breaking; it was breaking in v1.
  • Overloaded getter/setter pattern: method() reads, method(value) writes. Eliminates getX() / setX() pairs.
  • Plugin system replacement: v1 used registerPlugin(name, instance). v2 uses addPlugin(Class, opts). Class-based plugins enable TypeScript inference, tree-shaking, and typed cross-plugin events.
  • Opt-in features: EQ, Spectrum, MediaSession, auto-advance were always-on in v1. In v2, they are opt-in plugins. Zero cost if you don’t add them.

The top five breaks

These affect every v1 consumer:

1. All event payloads changed

Every player.on(...) call needs updating. Examples:

TypeScript
import type { MusicPlaylistItem } from '@nomercy-entertainment/nomercy-music-player';

// v1
player.on('song', (track) => console.log(track.name));

// v2 — item is MusicPlaylistItem here; cast if player is typed as IPlayer<BaseEventMap>
player.on('current', ({ item }) => console.log((item as MusicPlaylistItem | undefined)?.name));
TypeScript
// v1
player.on('time', (timeData) => console.log(timeData.currentTime));

// v2
player.on('time', ({ time }) => console.log(time));

2. item.path / item.fileitem.url

This is the silent-break risk. The player will load without error but fail to play anything.

Music player: item.pathitem.url Video player: item.fileitem.url

Map the field in your adapter layer:

TypeScript
// Music
player.queue(serverTracks.map((track) => ({ ...track, url: track.url ?? track.path })));

// Video
player.queue(serverItems.map((item) => ({ ...item, url: item.url ?? item.file })));

The server API should be updated to emit url directly.

3. Plugin API replaced

TypeScript
// v1
// @ts-expect-error — registerPlugin does not exist in v2
player.registerPlugin('octopus', new OctopusPlugin({ ... }));

// v2
player.addPlugin(OctopusPlugin, { /* options */ });
const plugin = player.getPlugin(OctopusPlugin); // typed

4. Methods renamed

v1v2
seek(seconds)time(seconds)
speed(rate)playbackRate(rate)
quality(idx)quality(idx)
subtitle(idx)subtitle(idx)
audioTrack(idx)audioTrack(idx)
playlist()queue()
playVideo(idx)seekToIndex(idx + 1) — 1-based; playVideo(0) becomes seekToIndex(1)
song eventcurrent event
item event (video)current event

seekToIndex is 1-based

Index convention changed. playVideo(0) in v1 loaded the first item. The v2 equivalent is seekToIndex(1).

seekToIndex uses a 1-based ordinal. Passing 0 throws RangeError. Any v1 code passing a loop index directly to playVideo(i) must use seekToIndex(i + 1).

5. Always-on features are now opt-in plugins

TypeScript
// v1: EQ, Spectrum, MediaSession wired automatically

// v2: explicit registration required:
import { AudioGraphPlugin, EqualizerPlugin, SpectrumPlugin, MediaSessionPlugin } from '...';

player
.addPlugin(AudioGraphPlugin)
.addPlugin(EqualizerPlugin)
.addPlugin(SpectrumPlugin)
.addPlugin(MediaSessionPlugin);

duration string → number (video)

v1 PlaylistItem.duration was a formatted string like "1:24:36". v2 VideoPlaylistItem.duration is a number in seconds. Formatters that expected a string will produce NaN.

Update any component that displays or compares duration:

TypeScript
// v1
display(item.duration); // already formatted: "1:24:36"

// v2
function formatDuration(secs: number) {
const hours = Math.floor(secs / 3600);
const minutes = Math.floor((secs % 3600) / 60);
const seconds = Math.floor(secs % 60);

return hours > 0
? `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
: `${minutes}:${seconds.toString().padStart(2, '0')}`;
}
display(formatDuration(item.duration));

ready event timing

v1 music player fired ready after a hardcoded delay. v2 fires ready after setup and all plugins resolve. Do not rely on timing, always await player.ready().

What’s new in v2

v2 ships features that did not exist in v1:

  • Cancellable before* events: player.on('beforePlay', event => event.preventDefault())
  • Auth pipeline with 401/403 separation and token refresh
  • Pluggable storage backend: swap localStorage for IndexedDB or a remote API
  • Platform abstraction: native-shell support (Capacitor, Tauri, Electron)
  • Phase machine: player.phase() returns a typed phase string
  • Metrics API: player.metrics(), throttled progress event
  • HDR-aware ABR (video): constrains HLS.js to SDR on SDR displays
  • LyricsPlugin: synced LRC lyrics
  • GroupListeningPlugin: multi-user synchronized playback
  • TabLeaderPlugin: single-tab coordination
  • DrmPlugin: Widevine EME
  • CastSenderPlugin: Chromecast handoff
  • Queue operations: queueMove, queueInsert, queueRemoveAt, queueSort, queueShuffle, queueClear

Full change tables

The per-package MIGRATION.md files contain complete tables for every renamed method, every changed event payload, every removed API, and every configuration field change: