domain: player/reporting ticks -> milliseconds (phase 4c)

Playback position now crosses the IPC boundary in milliseconds. Ticks
survive only inside Rust (DB storage, Jellyfin API) and at the genuine
remote-session boundary (session seek / transfer / RemoteControls).

Rust command signatures (ms in, converted to ticks internally):
- storage_update_playback_progress / _context: position_ms
- repository_report_playback_start / _progress / _stopped: position_ms
- PlaybackProgress.position_ticks -> position_ms (converted in the query)

Frontend:
- playbackReporting, playerEvents, VideoPlayer, Queue, player/[id] resume:
  seconds*1000 / durationMs/1000 instead of tick math.
- repository-client + syncService param names -> positionMs.
- Tests updated to ms fixtures/assertions.

Out of scope (legitimately ticks): NowPlayingItem, PlayState.positionTicks,
sessionSeek, playbackModeTransferToLocal, RemoteControls, SessionCard — the
remote Jellyfin session API.

Rust 456, frontend 644, check clean.
This commit is contained in:
2026-07-23 22:02:29 +02:00
parent 93d198ce21
commit ec8a7610f5
14 changed files with 88 additions and 71 deletions
+3 -3
View File
@@ -36,9 +36,9 @@
let dragDisabled = $state(true);
const flipDurationMs = 200;
function formatDuration(ticks?: number | null): string {
if (!ticks) return "";
const seconds = Math.floor(ticks / 10000000);
function formatDuration(ms?: number | null): string {
if (!ms) return "";
const seconds = Math.floor(ms / 1000);
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}:${secs.toString().padStart(2, "0")}`;
@@ -97,8 +97,8 @@ function makeEpisode(): MediaItem {
return {
id: "ep1",
name: "Episode 1",
type: "Episode",
runTimeTicks: 24 * 60 * 10_000_000, // 24 min
kind: "episode",
durationMs: 24 * 60 * 1000, // 24 min
} as MediaItem;
}
+4 -4
View File
@@ -165,9 +165,9 @@
// Use known duration from media item (runTimeTicks is in 10M ticks/second)
// Fallback to video element duration for direct streams
const duration = $derived.by(() => {
// Explicitly check if runTimeTicks exists and is a valid number
if (media && media.runTimeTicks && media.runTimeTicks > 0) {
return media.runTimeTicks / 10_000_000;
// Explicitly check if durationMs exists and is a valid number
if (media && media.durationMs && media.durationMs > 0) {
return media.durationMs / 1000;
}
// Otherwise use the video element's duration
return videoDuration;
@@ -386,7 +386,7 @@
// Check if we're near the end of the video - if so, this is likely
// end-of-stream rather than a real error. Jellyfin transcoded HLS
// streams may not always terminate cleanly with #EXT-X-ENDLIST.
const knownDuration = media?.runTimeTicks ? media.runTimeTicks / 10_000_000 : videoDuration;
const knownDuration = media?.durationMs ? media.durationMs / 1000 : videoDuration;
const effectiveTime = currentTime + seekOffset;
const isNearEnd = knownDuration > 0 && effectiveTime > 0 && effectiveTime / knownDuration > 0.9;