const DEFAULT_INDEX_URL = "https://huggingface.co/datasets/ArnieRamesh/CounterStrike-1K/resolve/main/space_viewer/rounds.json"; const SYNC_TOLERANCE_S = 0.14; const state = { index: null, rounds: [], filteredRounds: [], activeRound: null, videos: new Map(), cards: new Map(), audioPov: -1, playing: false, internalPause: false, seeking: false, raf: 0, }; const el = { roundMeta: document.getElementById("roundMeta"), subsetFilter: document.getElementById("subsetFilter"), splitFilter: document.getElementById("splitFilter"), mapFilter: document.getElementById("mapFilter"), matchFilter: document.getElementById("matchFilter"), eventFilter: document.getElementById("eventFilter"), minDuration: document.getElementById("minDuration"), queryFilter: document.getElementById("queryFilter"), roundSelect: document.getElementById("roundSelect"), audioSelect: document.getElementById("audioSelect"), speedSelect: document.getElementById("speedSelect"), playPause: document.getElementById("playPause"), timeSlider: document.getElementById("timeSlider"), timeNow: document.getElementById("timeNow"), timeTotal: document.getElementById("timeTotal"), statusText: document.getElementById("statusText"), videoGrid: document.getElementById("videoGrid"), }; function formatTime(seconds) { const safe = Number.isFinite(seconds) ? Math.max(0, seconds) : 0; const mins = Math.floor(safe / 60); const secs = safe - mins * 60; return `${mins}:${secs.toFixed(1).padStart(4, "0")}`; } function fileUrl(path) { const params = new URLSearchParams(window.location.search); const assetBase = params.get("asset_base"); if (assetBase) { return `${assetBase.replace(/\/$/, "")}/${path}`; } const repo = state.index?.dataset_repo || "ArnieRamesh/CounterStrike-1K"; const revision = state.index?.dataset_revision || "main"; return `https://huggingface.co/datasets/${repo}/resolve/${revision}/${path}`; } function indexUrl() { const params = new URLSearchParams(window.location.search); const override = params.get("index"); if (override) { return override; } if (["localhost", "127.0.0.1"].includes(window.location.hostname)) { return "../CounterStrike-1K/space_viewer/rounds.json"; } return DEFAULT_INDEX_URL; } function masterVideo() { return state.videos.get(state.audioPov) || state.videos.get(0) || null; } function setStatus(text) { el.statusText.textContent = text; } function setControlsEnabled(enabled) { const hasIndex = state.rounds.length > 0; el.subsetFilter.disabled = !hasIndex; el.splitFilter.disabled = !hasIndex; el.mapFilter.disabled = !hasIndex; el.matchFilter.disabled = !hasIndex; el.minDuration.disabled = !hasIndex; el.queryFilter.disabled = !hasIndex; el.eventFilter.disabled = !hasIndex || !hasEventFields(); el.roundSelect.disabled = !enabled; el.audioSelect.disabled = !enabled; el.speedSelect.disabled = !enabled; el.playPause.disabled = !enabled; el.timeSlider.disabled = !enabled; } function setAudioPov(povIdx) { state.audioPov = povIdx; for (const [idx, video] of state.videos.entries()) { video.muted = idx !== povIdx; video.volume = idx === povIdx ? 1 : 0; } for (const [idx, card] of state.cards.entries()) { card.classList.toggle("activeAudio", idx === povIdx); } } function currentDuration() { const fromIndex = Number(state.activeRound?.duration_s || 0); const fromVideos = Array.from(state.videos.values()) .map((video) => video.duration) .filter(Number.isFinite); return Math.max(fromIndex, ...fromVideos, 0); } function setAllPlaybackRate(rate) { for (const video of state.videos.values()) { video.playbackRate = rate; } } function setAllCurrentTime(seconds) { const target = Math.max(0, Math.min(seconds, currentDuration())); for (const video of state.videos.values()) { const duration = Number.isFinite(video.duration) ? video.duration : target; video.currentTime = Math.min(target, duration); } el.timeSlider.value = String(target); el.timeNow.textContent = formatTime(target); } function syncFromMaster() { const master = masterVideo(); if (!master || !state.playing || state.seeking) { return; } const target = master.currentTime; for (const video of state.videos.values()) { if (video === master || video.readyState < 1) { continue; } if (Math.abs(video.currentTime - target) > SYNC_TOLERANCE_S) { video.currentTime = Math.min(target, Number.isFinite(video.duration) ? video.duration : target); } } } function tick() { const master = masterVideo(); if (master) { const now = master.currentTime; el.timeSlider.value = String(now); el.timeNow.textContent = formatTime(now); syncFromMaster(); } state.raf = requestAnimationFrame(tick); } async function playAll() { const master = masterVideo(); if (!master) { return; } setAllCurrentTime(master.currentTime); state.playing = true; el.playPause.textContent = "Pause"; setStatus("Playing"); const plays = []; for (const video of state.videos.values()) { plays.push(video.play()); } const results = await Promise.allSettled(plays); if (results.some((result) => result.status === "rejected")) { pauseAll(); setStatus("Playback blocked"); } } function pauseAll(status = "Paused") { state.internalPause = true; for (const video of state.videos.values()) { video.pause(); } state.internalPause = false; state.playing = false; el.playPause.textContent = "Play"; setStatus(status); } function uniqueSorted(values) { return [...new Set(values.filter((value) => value !== null && value !== undefined && String(value) !== ""))] .map(String) .sort((a, b) => a.localeCompare(b, undefined, { numeric: true })); } function fillSelect(select, values, allLabel) { const previous = select.value; select.innerHTML = ""; const all = document.createElement("option"); all.value = ""; all.textContent = allLabel; select.append(all); for (const value of values) { const option = document.createElement("option"); option.value = value; option.textContent = value; select.append(option); } if ([...select.options].some((option) => option.value === previous)) { select.value = previous; } } function hasEventFields() { return state.rounds.some((round) => ["is_pistol_round", "bomb_planted", "bomb_defused", "bomb_exploded", "has_awp_any_pov"] .some((field) => Object.prototype.hasOwnProperty.call(round, field)) ); } function renderFilterOptions() { fillSelect( el.subsetFilter, uniqueSorted(state.rounds.flatMap((round) => Array.isArray(round.subset_names) ? round.subset_names : [])), "All subsets", ); fillSelect(el.splitFilter, uniqueSorted(state.rounds.map((round) => round.split)), "All splits"); fillSelect(el.mapFilter, uniqueSorted(state.rounds.map((round) => round.map_slug)), "All maps"); fillSelect(el.matchFilter, uniqueSorted(state.rounds.map((round) => `match_${round.match_id}`)), "All matches"); } function roundSearchText(round) { const povKeys = Array.isArray(round.povs) ? round.povs.map((pov) => pov.sample_key).join(" ") : ""; return [ round.round_id, `match_${round.match_id}`, round.match_id, round.map_slug, round.split, String(round.round_idx), povKeys, ].join(" ").toLowerCase(); } function roundMatchesFilters(round) { const subset = el.subsetFilter.value; if (subset && !(Array.isArray(round.subset_names) && round.subset_names.includes(subset))) { return false; } if (el.splitFilter.value && String(round.split) !== el.splitFilter.value) { return false; } if (el.mapFilter.value && String(round.map_slug) !== el.mapFilter.value) { return false; } if (el.matchFilter.value && `match_${round.match_id}` !== el.matchFilter.value) { return false; } if (el.eventFilter.value && !round[el.eventFilter.value]) { return false; } const minDuration = Number(el.minDuration.value); if (Number.isFinite(minDuration) && minDuration > 0 && Number(round.duration_s || 0) < minDuration) { return false; } const query = el.queryFilter.value.trim().toLowerCase(); if (query && !roundSearchText(round).includes(query)) { return false; } return true; } function renderRoundOptions() { el.roundSelect.innerHTML = ""; state.filteredRounds.forEach((round, idx) => { const option = document.createElement("option"); option.value = String(idx); const subsets = Array.isArray(round.subset_names) && round.subset_names.length ? ` | ${round.subset_names.slice(0, 2).join(",")}` : ""; option.textContent = `${round.map_slug} | match_${round.match_id} r${String(round.round_idx).padStart(3, "0")} | ${formatTime(round.duration_s)}${subsets}`; el.roundSelect.append(option); }); } function renderAudioOptions() { el.audioSelect.innerHTML = ""; const muted = document.createElement("option"); muted.value = "-1"; muted.textContent = "Muted"; el.audioSelect.append(muted); for (let idx = 0; idx < 10; idx += 1) { const option = document.createElement("option"); option.value = String(idx); option.textContent = `POV ${String(idx).padStart(2, "0")}`; el.audioSelect.append(option); } } function makeVideoCard(pov) { const card = document.createElement("article"); card.className = "povCard"; const header = document.createElement("div"); header.className = "povHeader"; const label = document.createElement("strong"); label.textContent = `POV ${String(pov.pov_idx).padStart(2, "0")}`; const side = document.createElement("span"); side.className = pov.team_side === "CT" ? "sideCT" : pov.team_side === "T" ? "sideT" : ""; side.textContent = pov.team_side || ""; header.append(label, side); const shell = document.createElement("div"); shell.className = "videoShell"; const video = document.createElement("video"); video.preload = "metadata"; video.playsInline = true; video.controls = false; video.muted = true; video.src = fileUrl(pov.overlay_path); video.addEventListener("loadedmetadata", () => { const duration = currentDuration(); el.timeSlider.max = String(duration); el.timeTotal.textContent = formatTime(duration); if (state.playing) { syncFromMaster(); } }); video.addEventListener("pause", () => { if (!state.internalPause && state.playing && !state.seeking && !video.ended) { pauseAll("Paused"); } }); video.addEventListener("waiting", () => { if (state.playing) { syncFromMaster(); setStatus("Buffering"); } }); video.addEventListener("playing", () => { if (state.playing) { syncFromMaster(); setStatus("Playing"); } }); video.addEventListener("error", () => { card.classList.add("error"); setStatus(`Missing POV ${String(pov.pov_idx).padStart(2, "0")}`); }); shell.append(video); card.append(header, shell); return { card, video }; } function loadRound(round) { if (!round) { pauseAll("No matching rounds"); state.activeRound = null; state.videos.clear(); state.cards.clear(); el.videoGrid.innerHTML = ""; el.roundMeta.textContent = "No matching rounds"; el.timeSlider.value = "0"; el.timeSlider.max = "0"; el.timeNow.textContent = formatTime(0); el.timeTotal.textContent = formatTime(0); setControlsEnabled(false); return; } pauseAll("Loading"); state.activeRound = round; state.videos.clear(); state.cards.clear(); el.videoGrid.innerHTML = ""; el.timeSlider.value = "0"; el.timeSlider.max = String(round.duration_s || 0); el.timeNow.textContent = formatTime(0); el.timeTotal.textContent = formatTime(round.duration_s || 0); el.roundMeta.textContent = `${round.map_slug} | match_${round.match_id} | round ${String(round.round_idx).padStart(3, "0")}`; const povs = [...round.povs].sort((a, b) => a.pov_idx - b.pov_idx); for (const pov of povs) { const { card, video } = makeVideoCard(pov); state.videos.set(pov.pov_idx, video); state.cards.set(pov.pov_idx, card); el.videoGrid.append(card); } setAudioPov(Number(el.audioSelect.value)); setAllPlaybackRate(Number(el.speedSelect.value)); setControlsEnabled(true); setStatus("Ready"); } function applyFilters() { const activeRoundId = state.activeRound?.round_id || ""; state.filteredRounds = state.rounds.filter(roundMatchesFilters); renderRoundOptions(); const activeIdx = state.filteredRounds.findIndex((round) => round.round_id === activeRoundId); if (!state.filteredRounds.length) { loadRound(null); return; } const nextIdx = activeIdx >= 0 ? activeIdx : 0; el.roundSelect.value = String(nextIdx); loadRound(state.filteredRounds[nextIdx]); } function bindEvents() { el.roundSelect.addEventListener("change", () => { loadRound(state.filteredRounds[Number(el.roundSelect.value)]); }); for (const filter of [ el.subsetFilter, el.splitFilter, el.mapFilter, el.matchFilter, el.eventFilter, el.minDuration, el.queryFilter, ]) { filter.addEventListener("input", applyFilters); filter.addEventListener("change", applyFilters); } el.audioSelect.addEventListener("change", () => { setAudioPov(Number(el.audioSelect.value)); }); el.speedSelect.addEventListener("change", () => { setAllPlaybackRate(Number(el.speedSelect.value)); }); el.playPause.addEventListener("click", () => { if (state.playing) { pauseAll(); } else { playAll(); } }); el.timeSlider.addEventListener("input", () => { state.seeking = true; setAllCurrentTime(Number(el.timeSlider.value)); }); el.timeSlider.addEventListener("change", () => { setAllCurrentTime(Number(el.timeSlider.value)); state.seeking = false; }); } async function init() { bindEvents(); renderAudioOptions(); try { const response = await fetch(indexUrl(), { cache: "no-store" }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } state.index = await response.json(); state.rounds = Array.isArray(state.index.rounds) ? state.index.rounds : []; if (!state.rounds.length) { throw new Error("empty round index"); } renderFilterOptions(); state.filteredRounds = state.rounds.slice(); renderRoundOptions(); loadRound(state.filteredRounds[0]); tick(); } catch (error) { setControlsEnabled(false); el.roundMeta.textContent = "Round index unavailable"; setStatus(error instanceof Error ? error.message : "Failed"); } } init();