Migration Guide: v1 to v2
This page is the high-level overview. For the full breaking-change tables, go to the per-package guides:
- Video player:
packages/nomercy-video-player-v2/MIGRATION.md - Music player:
packages/nomercy-music-player-v2/MIGRATION.md - Core (adapter ports + subpath imports):
packages/nomercy-player-kit/MIGRATION.md
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:
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:
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:
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
HTMLElementobjects and multi-field flat structs. v2 wraps everything:{ time: number }instead of rawnumber,{ 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. EliminatesgetX()/setX()pairs. - Plugin system replacement: v1 used
registerPlugin(name, instance). v2 usesaddPlugin(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:
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));
// v1
player.on('time', (timeData) => console.log(timeData.currentTime));
// v2
player.on('time', ({ time }) => console.log(time));
2. item.path / item.file → item.url
This is the silent-break risk. The player will load without error but fail to play anything.
Music player: item.path → item.url
Video player: item.file → item.url
Map the field in your adapter layer:
// 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
// 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
| v1 | v2 |
|---|---|
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 event | current event |
item event (video) | current event |
seekToIndex is 1-based
Index convention changed.
playVideo(0)in v1 loaded the first item. The v2 equivalent isseekToIndex(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
// 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:
// 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
localStoragefor 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(), throttledprogressevent - HDR-aware ABR (video): constrains HLS.js to SDR on SDR displays
LyricsPlugin: synced LRC lyricsGroupListeningPlugin: multi-user synchronized playbackTabLeaderPlugin: single-tab coordinationDrmPlugin: Widevine EMECastSenderPlugin: 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: