Skip to content

Persistence

Saving and restoring playback state across page loads: resume position, last-played track, volume level, subtitle preferences. The player core’s storage adapter system handles all of this.

Prerequisites: Core: Adapters covers the three built-in storage backends.

Storage backends

Three implementations ship with the core:

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

// Default: persists across sessions, synchronous, shared across tabs:
player.setup({ storage: new LocalStorageBackend() });

// Ephemeral: ideal for tests or incognito; no persistence:
player.setup({ storage: new MemoryStorageBackend() });

// High-capacity: for large datasets (EQ preset libraries, subtitle cache):
player.setup({ storage: new IndexedDBBackend({ dbName: 'my-player-db' }) });

Built-in plugins write to storage under a per-plugin namespace (nmplayer-<plugin-id>-*). The player core itself reads and writes nothing — config.storage is consumed only by plugins, via Plugin.storage.

What persists

The core persists nothing on its own. Volume, mute, and language live in memory for the session unless you persist them — listen for the relevant events and write to storage yourself, or let a plugin do it:

  • EqualizerPlugin persists EQ state when you set its persistKey option.
  • SubtitleOverlayPlugin persists subtitle style preferences.

Playback position is likewise not saved automatically, because the core does not know where your server stores watch history. That requires a consumer plugin.

Saving and restoring playback position

Write a consumer plugin that saves position to your backend. ResumePlugin is typed against NMVideoPlayer directly because it calls this.player.item()NMVideoPlayer satisfies WithCurrentItem<VideoPlaylistItem> structurally. For a portable alternative typed against the interface, use IVideoPlayer & WithCurrentItem<VideoPlaylistItem>.

TypeScript
import { Plugin } from '@nomercy-entertainment/nomercy-player-core';
import type { NMVideoPlayer, VideoPlaylistItem } from '@nomercy-entertainment/nomercy-video-player';

interface ResumeOptions {
saveIntervalMs?: number; // how often to save position. Default: 5000 (5s)
minProgressSeconds?: number; // ignore saves before this point. Default: 5
}

export class ResumePlugin extends Plugin<NMVideoPlayer, ResumeOptions, {}> {
static readonly id = 'myapp:resume';
static readonly version = '1.0.0';
static readonly description = 'Save and restore video playback position';

private saveTimer: ReturnType<typeof setInterval> | null = null;

use(): void {
const intervalMs = this.opts.saveIntervalMs ?? 5000;
const minSeconds = this.opts.minProgressSeconds ?? 5;

// Save position periodically while playing:
this.on('play', () => {
this.saveTimer = this.interval(() => {
const time = this.player.time();
const item = this.player.item();
if (item && time > minSeconds) {
this.savePosition(String(item.id), time);
}
}, intervalMs);
});

// Stop saving on pause/end:
this.on('pause', () => this.clearSaveTimer());
this.on('ended', () => this.clearSaveTimer());

// Restore position when a new item becomes current:
this.on('current', async ({ item }) => {
if (!item) return;
const saved = await this.loadPosition(String(item.id));
if (saved && saved > 5) {
// Seek to saved position after the item is loaded into the backend
this.player.once('mediaReady', () => this.player.time(saved));
}
});
}

dispose(): void {
this.clearSaveTimer();
}

private clearSaveTimer(): void {
if (this.saveTimer !== null) {
clearInterval(this.saveTimer);
this.saveTimer = null;
}
}

private async savePosition(itemId: string, time: number): Promise<void> {
// Use this.fetch to get auth headers automatically:
await this.fetch(`/api/progress/${itemId}`, {
method: 'PUT',
responseType: 'json',
body: JSON.stringify({ position: time }),
headers: { 'Content-Type': 'application/json' },
});
}

private async loadPosition(itemId: string): Promise<number | null> {
try {
const data = await this.fetch<{ position: number }>(`/api/progress/${itemId}`, {
responseType: 'json',
});
return data.position ?? null;
} catch {
return null;
}
}
}

Register it:

TypeScript
player.addPlugin(ResumePlugin, {
saveIntervalMs: 5000,
minProgressSeconds: 10, // don't save if the user watched less than 10 seconds
});

Local-only position saving (no server)

For a purely client-side implementation, use this.storage directly:

TypeScript
// Inside a plugin:
private savePosition(itemId: string, time: number): void {
this.storage.set(`resume-${itemId}`, String(time));
}

private async loadPosition(itemId: string): Promise<number | null> {
const saved = await this.storage.get(`resume-${itemId}`);
return saved !== null ? parseFloat(saved) : null;
}

this.storage reads from the same IStorage backend the player is configured with. Keys are automatically namespaced.

Choosing the right storage backend for your context

ScenarioBackend
Standard web appLocalStorageBackend (default)
SSR (Next.js, Nuxt)MemoryStorageBackend on server, LocalStorageBackend on client. See Advanced: SSR
Test suiteMemoryStorageBackend, no cross-test contamination
Large EQ preset libraryIndexedDBBackend
Sync across devicesCustom backend calling your API

IndexedDB backend

For large data (EQ snapshots, subtitle caches):

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

player.setup({
storage: new IndexedDBBackend({ dbName: 'my-player-db' }), // database name
});

IndexedDBBackend is async internally. The IStorage interface allows async returns (Promise<string | null> etc.) so the built-in IndexedDBBackend satisfies it natively. Writes complete asynchronously; reads return the last committed value. For a fully async custom backend, see Advanced: Custom Adapter.