Troubleshooting
Every entry below is verified against the core source. If the symptom you’re hitting isn’t here, file an issue with a minimal reproduction.
Octopus (ASS/SSA) subtitles do not render
Symptom: ASS/SSA subtitles are enabled and selected, but nothing appears on screen. No errors in the console.
Cause: OctopusPlugin uses a dynamic import('@nomercy-entertainment/nomercy-subtitle-octopus') and falls back to a degraded no-render mode if the package isn’t present.
The plugin never throws, it stays silent.
Fix: @nomercy-entertainment/nomercy-subtitle-octopus is a regular dependency of @nomercy-entertainment/nomercy-video-player, so it should already be present after npm install.
If your bundler tree-shakes it out or you are using a selective install, add it explicitly:
npm install @nomercy-entertainment/nomercy-subtitle-octopus
Confirm the degraded mode is the cause: Set setup({ logLevel: 'debug' }) and look for [nmplayer][octopus] log lines.
A degraded-mode line confirms the dynamic import failed; any other error message tells you something else is wrong.
Player never reaches ready
Symptom: await player.ready() hangs indefinitely.
The ready event never fires.
Causes and fixes:
- A plugin’s
use()returned a Promise that never resolves.player.setup()awaits alluse()promises (capped bypluginInitTimeoutMs, default 30 seconds) before emittingready. Audit each registered plugin for async work without timeouts. - The container element doesn’t exist in the DOM at
setup(). The core doesn’t wait for the element to appear. Ensuredocument.getElementById('player')returns a non-null element before callingnmplayer('id'). - A plugin’s
use()rejected. Check the console forplugin:<id>:failedevents. The failed plugin is disabled;readystill fires for the remaining plugins. If you see noreadyand nofailed, the promise is stuck (see #1). - You’re inspecting the wrong instance.
nmplayer('main')returns the same instance on every call with the same id. If two parts of your code call setup on different ids, they’re different players.
file:// dev mode: HLS manifest fails to load
Symptom: Locally served .m3u8 files fail when opening the HTML directly via file://.
Cause: Browser security policy blocks cross-origin requests from file:// origins.
HLS segment fetches from any other origin (including localhost) are blocked.
Fix: Serve over http://:
npx serve .
Or use Vite / Webpack dev server / any local HTTP server.
Framework SSR: player crashes during server-side render
Symptom: Build or render crashes with document is not defined or window is not defined during SSR (Next.js, Nuxt, SvelteKit, Astro).
Cause: The player depends on browser APIs (document, window, HTMLVideoElement, AudioContext).
These don’t exist in a Node server environment.
Fix: Lazy-load the player on the client only.
Next.js (React):
import dynamic from 'next/dynamic';
const VideoPlayer = dynamic(() => import('@/components/VideoPlayer'), { ssr: false });
Nuxt / Vue:
const player = shallowRef<NMVideoPlayer | null>(null);
onMounted(() => {
import('@nomercy-entertainment/nomercy-video-player').then(({ nmplayer }) => {
player.value = nmplayer('main').setup({
/* config */
});
});
});
Astro: Use client:only so the component never renders on the server.
Pick the directive that matches the component’s framework:
<VideoPlayer client:only="react" />
<!-- or -->
<VideoPlayer client:only="vue" />
SvelteKit: Guard with if (browser) from $app/environment, or use onMount which only fires client-side.
Two players on the same page share state
Symptom: Two players appear to share state, controlling one affects the other.
Cause: nmplayer('id') and nmMPlayer('id') are factories backed by an internal registry.
Calls with the same id return the same instance.
Two components using the same id are pointing at the same player.
Fix: Use unique ids per instance:
const main = nmplayer('main-player');
const pip = nmplayer('pip-player');
For a single-player app, picking a static id is fine. For multi-player UIs (PiP, watch parties, tile grids), generate a stable id per slot.
Volume resets every page load
Symptom: The player ignores the user’s saved volume preference.
Cause: The core doesn’t auto-persist volume, that’s a plugin concern.
Fix: Persist it in a small plugin via the namespaced storage adapter (this.storage), not raw localStorage.
Volume is on the 0–100 scale.
import { Plugin } from '@nomercy-entertainment/nomercy-player-core';
class VolumeMemoryPlugin extends Plugin {
static readonly id = 'volume-memory';
async use() {
const saved = await this.storage.getJSON<number>('level');
if (saved !== null) this.player.volume(saved);
this.on('volume', ({ level }) => this.storage.setJSON('level', level));
}
}
player.addPlugin(VolumeMemoryPlugin);
NotImplementedError thrown by music player method
Symptom: Calling player.subtitles() on a music player throws NotImplementedError with code core:not-implemented/subtitles.
Cause: Audio backends don’t have subtitle tracks.
subtitles() is on the shared player surface but is intentionally not implemented on NMMusicPlayer.
Fix: Catch the error by type.
Import NotImplementedError from the core:
import { NotImplementedError } from '@nomercy-entertainment/nomercy-player-core';
try {
const tracks = player.subtitles();
} catch (error) {
if (error instanceof NotImplementedError) {
// expected on music players, skip subtitle UI
} else {
throw error;
}
}
Or, simpler, only call subtitles() on video players.
GroupListeningPlugin, DrmPlugin, or LiveTranscodingPlugin throws on use()
Symptom: Adding these music-player plugins, then plugin:<id>:failed fires with a NotImplementedError.
ready still fires for the remaining plugins.
Cause: All three are stubbed in 2.0.0-beta.x and intentionally throw NotImplementedError.
Full implementations are planned for 2.1.
Fix: Don’t register them yet. Track progress in Versioning.
HLS auth: segments return 401 after a delay
Symptom: HLS manifest loads, playback starts, then segment requests start returning 401 after some time. Or every segment 401s from the first request.
Cause (token expiry): auth.bearerToken is a static string.
The core injects whatever value you passed at setup.
If the token expires between manifest load and segment fetch, the injected value is stale.
Cause (no refresh): auth.refreshOnUnauthenticated isn’t configured.
The core retries a 401 exactly once if a refresh function is provided; without it, the request fails immediately.
Fix: Pass bearerToken as a factory and provide a refresh function:
player.setup({
auth: {
bearerToken: () => myAuth.getAccessToken(),
refreshOnUnauthenticated: async () => {
await myAuth.refresh(); // bearerToken() is re-read automatically after this resolves
},
},
});
The factory is called on every authenticated request, so the player always sees the current token.
On a 401, refreshOnUnauthenticated is called once and the request retries automatically.
A 403 propagates and is never retried. That’s an access-policy problem on the server, not an auth-token problem.
Inline import of the core fails: Cannot resolve '@nomercy-entertainment/nomercy-player-core'
Symptom: A consumer or a custom plugin imports from @nomercy-entertainment/nomercy-player-core and the resolver can’t find it.
Cause: The core is the foundation under both nomercy-video-player and nomercy-music-player but isn’t a transitive runtime dependency a consumer would see.
If you’re writing a plugin or adapter against the core directly, you need to install the core alongside.
Fix:
npm install @nomercy-entertainment/nomercy-player-core@beta
If you only use the video or music package’s surface (no custom plugins), you don’t need this, the per-library package re-exports what you need.
Container element class never updates
Symptom: The container should gain the bare state classes playing / paused (alongside the base nomercyplayer class) but they are missing.
Cause: State classes live on the element passed as container to setup().
If you re-mount the player into a fresh DOM node (e.g. SSR hydration replacing the host element), the core’s reference is to the old node.
Fix: Don’t re-create the container after setup.
If you have to (HMR, SPA navigation), dispose() the player first and setup() again on the new node.
State classes are owned by the core itself, not by DesktopUiPlugin, so you get them even on a raw HTML container with no UI plugin installed.
Custom plugin’s static onError never fires
Symptom: A plugin declares static onError with recovery actions but nothing happens when it throws.
Cause: static onError is a declarative map of error code to a recovery action: { 'error-code': 'retry-once' | 'fallback' | 'disable' | 'ignore' }.
The core consults it when the plugin throws via this.throw(...).
If your plugin throws directly with throw new Error(...) or emits via player.emit('error', ...), the recovery map is bypassed.
Fix: Always raise plugin errors through the base class helper:
this.throw({
severity: 'fatal',
code: 'my-plugin/connection-lost',
message: 'WebSocket disconnected after 3 retries',
});
See Plugin Standard §1.8 for the full error escalation contract.
Plugin runs twice / plugin event listeners double up
Symptom: Event handlers in a plugin’s use() fire twice for every event.
Cause: Most commonly, use() is being called manually in addition to the player calling it.
Don’t invoke use() yourself, addPlugin() triggers the lifecycle.
Second cause: two different Plugin classes share the same static readonly id.
The kit allows this for replacement (subclass with same id replaces the original) but if both are added independently, both register and both listen.
Fix: Ensure each plugin id is unique.
Use vendor:name ids for third-party plugins ('fillz:winamp', 'nomercy:sync') to avoid collisions with core-shipped plugins.
Reporting a bug
If the symptom isn’t here, file an issue with:
- Core version (
KIT_VERSIONfrom@nomercy-entertainment/nomercy-player-core) - Player package + version
- Browser + version
- Minimal HTML/JS reproduction (a public CodeSandbox / StackBlitz link is ideal)
- Console output with
logLevel: 'debug'enabled