The server now sends each fresh position on every poll that has one, and sends positions advanced to send time; clients count only the time since they received it. Bars realign every poll and no longer drift by however far the viewer's clock is off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MzFywujuH9TiuXbr9vjoX8
This commit is contained in:
@@ -1,30 +1,25 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { formatDuration, playbackPosition } from './progress';
|
||||
|
||||
const at = Date.parse('2026-10-06T12:00:00Z');
|
||||
const np = (over: Partial<{ progressMs: number | null; isPlaying: boolean; durationMs: number | null }> = {}) => ({
|
||||
progressMs: 'progressMs' in over ? over.progressMs! : 60_000,
|
||||
isPlaying: over.isPlaying ?? true,
|
||||
updatedAt: new Date(at).toISOString(),
|
||||
track: { durationMs: 'durationMs' in over ? over.durationMs! : 200_000 },
|
||||
});
|
||||
|
||||
describe('playbackPosition', () => {
|
||||
it('advances while playing', () => {
|
||||
expect(playbackPosition(np(), at + 15_000)).toEqual({ positionMs: 75_000, durationMs: 200_000 });
|
||||
it('advances by the time since it was received while playing', () => {
|
||||
expect(playbackPosition(np(), 15_000)).toEqual({ positionMs: 75_000, durationMs: 200_000 });
|
||||
});
|
||||
it('holds still while paused', () => {
|
||||
expect(playbackPosition(np({ isPlaying: false }), at + 15_000)?.positionMs).toBe(60_000);
|
||||
expect(playbackPosition(np({ isPlaying: false }), 15_000)?.positionMs).toBe(60_000);
|
||||
});
|
||||
it('stops at the end of the song', () => {
|
||||
expect(playbackPosition(np(), at + 10 * 60_000)?.positionMs).toBe(200_000);
|
||||
expect(playbackPosition(np(), 10 * 60_000)?.positionMs).toBe(200_000);
|
||||
});
|
||||
it("is null when the source doesn't report a position or there's no length", () => {
|
||||
expect(playbackPosition(np({ progressMs: null }))).toBeNull();
|
||||
expect(playbackPosition(np({ durationMs: null }))).toBeNull();
|
||||
});
|
||||
it("doesn't go backwards if the viewer's clock is behind", () => {
|
||||
expect(playbackPosition(np(), at - 30_000)?.positionMs).toBe(60_000);
|
||||
expect(playbackPosition(np({ progressMs: null }), 0)).toBeNull();
|
||||
expect(playbackPosition(np({ durationMs: null }), 0)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -6,18 +6,18 @@ export interface PlaybackPosition {
|
||||
}
|
||||
|
||||
/**
|
||||
* Where in the song someone is right now, or null when the source doesn't say
|
||||
* (Last.fm, ListenBrainz, Apple Music). While playing, the last reported
|
||||
* position is advanced by the time since it was reported, so a progress bar can
|
||||
* tick smoothly between updates.
|
||||
* Where in the song someone is, or null when the source doesn't say (Last.fm,
|
||||
* ListenBrainz, Apple Music). The server sends the position as of when it sent
|
||||
* it; pass the time since this client received it, so the bar ticks smoothly and
|
||||
* the viewer's clock being off doesn't matter.
|
||||
*/
|
||||
export function playbackPosition(
|
||||
np: Pick<NowPlayingDTO, 'progressMs' | 'isPlaying' | 'updatedAt'> & { track: Pick<NowPlayingDTO['track'], 'durationMs'> },
|
||||
now = Date.now(),
|
||||
np: Pick<NowPlayingDTO, 'progressMs' | 'isPlaying'> & { track: Pick<NowPlayingDTO['track'], 'durationMs'> },
|
||||
msSinceReceived: number,
|
||||
): PlaybackPosition | null {
|
||||
const durationMs = np.track.durationMs;
|
||||
if (np.progressMs == null || !durationMs) return null;
|
||||
const elapsed = np.isPlaying ? Math.max(0, now - Date.parse(np.updatedAt)) : 0;
|
||||
const elapsed = np.isPlaying ? Math.max(0, msSinceReceived) : 0;
|
||||
return { positionMs: Math.min(Math.max(np.progressMs + elapsed, 0), durationMs), durationMs };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user