Skip to content

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:

Code
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:

  1. A plugin’s use() returned a Promise that never resolves. player.setup() awaits all use() promises (capped by pluginInitTimeoutMs, default 30 seconds) before emitting ready. Audit each registered plugin for async work without timeouts.
  2. The container element doesn’t exist in the DOM at setup(). The core doesn’t wait for the element to appear. Ensure document.getElementById('player') returns a non-null element before calling nmplayer('id').
  3. A plugin’s use() rejected. Check the console for plugin:<id>:failed events. The failed plugin is disabled; ready still fires for the remaining plugins. If you see no ready and no failed, the promise is stuck (see #1).
  4. 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://:

Code
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):

TypeScript
import dynamic from 'next/dynamic';

const VideoPlayer = dynamic(() => import('@/components/VideoPlayer'), { ssr: false });

Nuxt / Vue:

TypeScript
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:

Astro
<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:

TypeScript
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.

TypeScript
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:

TypeScript
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:

TypeScript
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:

Code
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:

TypeScript
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_VERSION from @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