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:
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:
nmplayer('main').setup({
clockSource: () => Date.now(),
});
To use a structured IClock implementation, bind its now method:
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
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:
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:
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