| 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(); |
|
|