Media servers only learn the position when the player checks in (~10s), so a poll can return an old number. A position up to 12s behind our running estimate is now treated as stale; positions ahead, or further behind (a rewind), are taken as-is. Phone-reported positions are exact and always taken. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MzFywujuH9TiuXbr9vjoX8
This commit is contained in:
@@ -151,6 +151,28 @@ export async function publishNowPlaying(userId: string) {
|
||||
if (dto) await redis.publish(nowPlayingChannel(userId), JSON.stringify(dto));
|
||||
}
|
||||
|
||||
/** How far behind our own estimate a reported position can be and still count as stale, not a rewind. */
|
||||
const STALE_POSITION_MS = 12_000;
|
||||
/** Sources whose position is relayed by a media server (phones report their own, exactly). */
|
||||
const LAGGY_POSITION_SOURCES = new Set<NowPlayingProvider>(['plex', 'jellyfin']);
|
||||
|
||||
/**
|
||||
* Plex and Jellyfin only learn the position when the player checks in (every ~10s),
|
||||
* so a poll can return a number from several seconds ago. Behind our running
|
||||
* estimate by a little: stale, keep the estimate (so bars don't jump back). Ahead,
|
||||
* or far behind (they rewound): believe it.
|
||||
*/
|
||||
function trustedPosition(
|
||||
current: { progressMs: number | null; updatedAt: Date; isPlaying: boolean },
|
||||
reported: number | null,
|
||||
now: Date,
|
||||
): number | null {
|
||||
if (reported == null || current.progressMs == null || !current.isPlaying) return reported;
|
||||
const expected = current.progressMs + (now.getTime() - current.updatedAt.getTime());
|
||||
const behind = expected - reported;
|
||||
return behind > 0 && behind <= STALE_POSITION_MS ? expected : reported;
|
||||
}
|
||||
|
||||
/** Apply one provider's poll result to the user's now-playing state. Returns whether they're playing. */
|
||||
export async function applyPollResult(
|
||||
userId: string,
|
||||
@@ -176,9 +198,10 @@ export async function applyPollResult(
|
||||
|
||||
const track = await upsertTrack(result.track);
|
||||
const changed = !current || current.trackId !== track.id || !current.isPlaying;
|
||||
const progressMs = changed || !LAGGY_POSITION_SOURCES.has(source) ? result.progressMs : trustedPosition(current, result.progressMs, now);
|
||||
// A fresh position from the source: send it so viewers' progress bars realign
|
||||
// (seeks, buffering, pauses we missed between polls).
|
||||
const realign = !changed && result.progressMs != null;
|
||||
const realign = !changed && progressMs != null;
|
||||
const newPlay =
|
||||
!current || current.trackId !== track.id || (!current.isPlaying && now.getTime() - current.updatedAt.getTime() > REPLAY_GAP_MS);
|
||||
const values = {
|
||||
@@ -187,7 +210,7 @@ export async function applyPollResult(
|
||||
sourceApp: opts.sourceApp ?? null,
|
||||
sourceUrl: opts.sourceUrl ?? null,
|
||||
isPlaying: true,
|
||||
progressMs: result.progressMs,
|
||||
progressMs,
|
||||
updatedAt: now,
|
||||
...(changed ? { startedAt: now } : {}),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user