Skip to content

IClock

By default the player reads the wall-clock time through Date.now(). That works fine in most situations. Where it falls short is anything that needs to agree on the current moment across multiple devices: group listening sessions, watch parties, or any plugin that schedules actions against a server-side timeline. Date.now() drifts per device, so two clients that think they’re in sync can be off by hundreds of milliseconds.

The clock port lets you supply a corrected time source without touching any plugin. You pass a plain function to setup({ clockSource }), and anything that calls player.now() picks up your corrected value from that moment on. The IClock interface is the shape you implement when you want a structured object, which you then hand over as clockSource: myNtpClock.now.bind(myNtpClock).

Everything you import from this page lives in these two lines:

TypeScript
import { systemClock } from '@nomercy-entertainment/nomercy-player-core/adapters/clock';
import type { IClock } from '@nomercy-entertainment/nomercy-player-core/adapters/clock';

Built-in adapter

systemClock

A plain object that wraps Date.now(). The core uses this value whenever clockSource is not configured. You never construct it yourself, but you can import it to use as a fallback inside a custom clock or to verify the contract shape in tests.

Usage

Pass any zero-argument function returning a millisecond epoch timestamp:

TypeScript
nmplayer('main').setup({
clockSource: () => Date.now(),
});

To use a structured IClock implementation, bind its now method:

TypeScript
const myClock = new NtpAdjustedClock();

nmplayer('main').setup({
clockSource: myClock.now.bind(myClock),
});

After setup, player.now() returns whatever your clockSource returns. Plugins coordinating timestamps should call player.now() rather than Date.now() directly so they transparently pick up any correction you install.

Interface

TypeScript
interface IClock {
now(): number;
}

One method. It returns the current time as a Unix-epoch millisecond integer. The simplicity is intentional: anything that can produce a number from a moment in time satisfies this contract.

Custom implementation

The common case is a clock corrected by an NTP-style offset you fetch from your own backend:

TypeScript
class NtpAdjustedClock implements IClock {
private offsetMs: number = 0;

async sync(): Promise<void> {
const requestSentAt = Date.now();
const response = await fetch('https://api.example.com/time');
const { serverTime } = await response.json() as { serverTime: number };
const roundTripMs = Date.now() - requestSentAt;

this.offsetMs = serverTime - Date.now() + Math.round(roundTripMs / 2);
}

now(): number {
return Date.now() + this.offsetMs;
}
}

const ntpClock = new NtpAdjustedClock();
await ntpClock.sync();

nmplayer('main').setup({
clockSource: ntpClock.now.bind(ntpClock),
});

If the sync fails or hasn’t run yet, offsetMs stays 0 and the clock degrades gracefully to Date.now().

This sync uses the platform fetch on purpose, not the core’s built-in authFetch, because the round-trip measurement must not be thrown off by automatic retries. That is the exception, not the rule. For ordinary data requests, prefer the built-in client: authFetch outside a plugin, or this.fetch inside one, so your auth and retry policy are applied for you. See Auth and Fetch.

For tests, a controllable fake is the main use case:

TypeScript
class FakeClock implements IClock {
private currentTime: number = 0;

now(): number {
return this.currentTime;
}

tick(deltaMs: number): void {
this.currentTime += deltaMs;
}
}

const fakeClock = new FakeClock();

nmplayer('test-player').setup({
clockSource: fakeClock.now.bind(fakeClock),
});

fakeClock.tick(5000);
// player.now() now returns 5000

See also

  • Adapters, the full port catalog
  • Realtime, the WebSocket channel port, used by group-listening features that pair with a corrected clock