add replay session

This commit is contained in:
larssand
2026-09-04 15:58:24 +02:00
parent c60b9e0057
commit 142c45be04
10 changed files with 409 additions and 4 deletions

143
src/session_replay.js Normal file
View File

@@ -0,0 +1,143 @@
const PRACTICE_TYPES = new Set(["open_practice", "free_practice", "practice", "qualification"]);
export function normalizeSessionReplayPassings(passings) {
return (Array.isArray(passings) ? passings : [])
.filter((passing) => Number.isFinite(Number(passing?.timestamp)))
.map((passing, index) => ({
...passing,
replayIndex: index,
timestamp: Number(passing.timestamp),
lapMs: Math.max(0, Number(passing.lapMs || 0) || 0),
transponder: String(passing.transponder || ""),
competitorKey: String(passing.competitorKey || passing.driverId || passing.teamId || passing.transponder || `passing_${index}`),
driverName: String(passing.displayName || passing.teamName || passing.driverName || passing.transponder || "Unknown"),
carName: String(passing.subLabel || passing.carName || ""),
}))
.sort((left, right) => left.timestamp - right.timestamp || left.replayIndex - right.replayIndex);
}
export function createSessionReplayRuntime() {
return {
sessionId: "",
sessionType: "",
sessionName: "",
status: "idle",
speed: 1,
elapsedMs: 0,
durationMs: 0,
startedWallAt: 0,
startedElapsedMs: 0,
passings: [],
timer: null,
};
}
export function loadSessionReplay(runtime, session, result) {
const passings = normalizeSessionReplayPassings(result?.passings);
runtime.sessionId = String(session?.id || "");
runtime.sessionType = String(session?.type || "");
runtime.sessionName = String(session?.name || "");
runtime.status = passings.length ? "paused" : "empty";
runtime.elapsedMs = 0;
runtime.durationMs = passings.length > 1 ? Math.max(0, passings[passings.length - 1].timestamp - passings[0].timestamp) : 0;
runtime.startedWallAt = 0;
runtime.startedElapsedMs = 0;
runtime.passings = passings;
return runtime;
}
export function playSessionReplay(runtime, nowTs = Date.now()) {
if (!runtime.passings.length) return false;
if (runtime.elapsedMs >= runtime.durationMs && runtime.durationMs > 0) runtime.elapsedMs = 0;
runtime.status = "running";
runtime.startedWallAt = nowTs;
runtime.startedElapsedMs = runtime.elapsedMs;
return true;
}
export function pauseSessionReplay(runtime, nowTs = Date.now()) {
if (runtime.status !== "running") return false;
advanceSessionReplay(runtime, nowTs);
runtime.status = "paused";
return true;
}
export function resetSessionReplay(runtime) {
runtime.status = runtime.passings.length ? "paused" : "idle";
runtime.elapsedMs = 0;
runtime.startedWallAt = 0;
runtime.startedElapsedMs = 0;
return runtime;
}
export function setSessionReplaySpeed(runtime, speed, nowTs = Date.now()) {
if (runtime.status === "running") {
advanceSessionReplay(runtime, nowTs);
runtime.startedWallAt = nowTs;
runtime.startedElapsedMs = runtime.elapsedMs;
}
runtime.speed = [1, 2, 5, 10].includes(Number(speed)) ? Number(speed) : 1;
return runtime.speed;
}
export function advanceSessionReplay(runtime, nowTs = Date.now()) {
if (runtime.status !== "running") return runtime.elapsedMs;
const advanced = runtime.startedElapsedMs + Math.max(0, nowTs - runtime.startedWallAt) * runtime.speed;
runtime.elapsedMs = Math.min(runtime.durationMs, advanced);
if (runtime.elapsedMs >= runtime.durationMs) runtime.status = "finished";
return runtime.elapsedMs;
}
export function buildSessionReplaySnapshot(runtime) {
const firstTimestamp = runtime.passings[0]?.timestamp || 0;
const cutoff = firstTimestamp + Math.max(0, runtime.elapsedMs);
const visiblePassings = runtime.passings.filter((passing, index) => index === 0 || passing.timestamp <= cutoff);
const competitors = new Map();
visiblePassings.forEach((passing) => {
const key = passing.competitorKey;
const row = competitors.get(key) || {
key,
driverName: passing.driverName,
carName: passing.carName,
transponder: passing.transponder,
laps: 0,
bestLapMs: 0,
lastLapMs: 0,
firstTimestamp: passing.timestamp,
lastTimestamp: passing.timestamp,
};
row.driverName = passing.driverName || row.driverName;
row.carName = passing.carName || row.carName;
row.transponder = passing.transponder || row.transponder;
row.lastTimestamp = passing.timestamp;
if (passing.validLap !== false && passing.lapMs > 0) {
row.laps += 1;
row.lastLapMs = passing.lapMs;
row.bestLapMs = row.bestLapMs ? Math.min(row.bestLapMs, passing.lapMs) : passing.lapMs;
}
competitors.set(key, row);
});
const practiceRanking = PRACTICE_TYPES.has(runtime.sessionType);
const leaderboard = [...competitors.values()]
.map((row) => ({ ...row, totalMs: Math.max(0, row.lastTimestamp - row.firstTimestamp) }))
.sort((left, right) => {
if (practiceRanking) {
return (left.bestLapMs || Number.MAX_SAFE_INTEGER) - (right.bestLapMs || Number.MAX_SAFE_INTEGER)
|| right.laps - left.laps
|| left.driverName.localeCompare(right.driverName);
}
return right.laps - left.laps || left.totalMs - right.totalMs || left.driverName.localeCompare(right.driverName);
});
return {
status: runtime.status,
elapsedMs: runtime.elapsedMs,
durationMs: runtime.durationMs,
progress: runtime.durationMs ? Math.min(1, runtime.elapsedMs / runtime.durationMs) : visiblePassings.length ? 1 : 0,
visiblePassings,
latestPassing: visiblePassings[visiblePassings.length - 1] || null,
leaderboard,
};
}