(function () {
"use strict";
const RAW = JSON.parse(
document.getElementById("videoData").textContent || "{}",
);
const cameras = {};
const clips = [];
Object.keys(RAW).forEach(function (key) {
const cam = RAW[key];
cameras[key] = cam.display;
(cam.videos || []).forEach(function (v) {
clips.push({
cam: key,
camDisplay: cam.display,
filename: v.filename,
timestamp: v.timestamp,
rel_path: v.rel_path,
url: v.url,
});
});
});
// Index clips by timestamp for angle synchronization
const byTimestamp = {};
clips.forEach(function (c) {
if (!byTimestamp[c.timestamp]) byTimestamp[c.timestamp] = {};
byTimestamp[c.timestamp][c.cam] = c;
});
const player = document.getElementById("player");
const placeholder = document.getElementById("videoPlaceholder");
const badge = document.getElementById("videoBadge");
const badgeText = document.getElementById("badgeText");
const clipListEl = document.getElementById("clipList");
const searchBox = document.getElementById("searchBox");
const clearSearch = document.getElementById("clearSearch");
const camTabsEl = document.getElementById("camTabs");
const sortSelect = document.getElementById("sortSelect");
const resultCount = document.getElementById("resultCount");
const camSwitchBtns = document.getElementById("camSwitchButtons");
const nowPlaying = document.getElementById("nowPlaying");
const npName = document.getElementById("npName");
const npTime = document.getElementById("npTime");
document.getElementById("countAll").textContent = clips.length;
let activeCam = "all";
let query = "";
let sortMode = "oldest";
let current = null;
function tsToDate(ts) {
const m = /^(\d{4})-(\d{2})-(\d{2})[_ ](\d{2})-(\d{2})-(\d{2})/.exec(
ts || "",
);
if (!m) return null;
return new Date(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], +m[6]);
}
function prettyTs(ts) {
const d = tsToDate(ts);
if (!d) return ts || "Unknown";
return d.toLocaleString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
}
function esc(s) {
return String(s).replace(/[&<>"']/g, function (c) {
return {
"&": "&",
"<": "<",
">": ">",
'"': """,
"'": "'",
}[c];
});
}
function getFiltered() {
let out = clips.filter(function (c) {
if (activeCam !== "all" && c.cam !== activeCam) return false;
if (query) {
const hay = (
c.filename +
" " +
c.timestamp +
" " +
c.camDisplay +
" " +
prettyTs(c.timestamp)
).toLowerCase();
if (hay.indexOf(query) === -1) return false;
}
return true;
});
out.sort(function (a, b) {
if (sortMode === "name") return a.filename.localeCompare(b.filename);
const da = tsToDate(a.timestamp),
db = tsToDate(b.timestamp);
const ta = da ? da.getTime() : 0,
tb = db ? db.getTime() : 0;
return sortMode === "oldest" ? ta - tb : tb - ta;
});
return out;
}
function render() {
const list = getFiltered();
resultCount.textContent =
list.length + (list.length === 1 ? " clip" : " clips");
if (!list.length) {
clipListEl.innerHTML =
'
No clips match your filters.
';
return;
}
let html = "";
if (activeCam === "all") {
const groups = {};
list.forEach(function (c) {
(groups[c.cam] = groups[c.cam] || []).push(c);
});
Object.keys(groups).forEach(function (key) {
html += groupHeader(key, groups[key].length);
groups[key].forEach(function (c) {
html += clipRow(c);
});
});
} else {
html += groupHeader(activeCam, list.length);
list.forEach(function (c) {
html += clipRow(c);
});
}
clipListEl.innerHTML = html;
highlightCurrent();
}
function groupHeader(key, n) {
return (
'"
);
}
function clipRow(c) {
const id = c.cam + "|" + c.timestamp + "|" + c.filename;
return (
'"
);
}
function playClip(clip) {
if (!clip) return;
current = clip;
player.src = clip.url;
player.load();
player.play().catch(function () {});
placeholder.hidden = true;
badge.classList.add("visible");
badgeText.textContent = clip.camDisplay;
nowPlaying.hidden = false;
npName.textContent = clip.filename;
npTime.textContent = prettyTs(clip.timestamp);
buildCamSwitch(clip);
highlightCurrent();
}
function buildCamSwitch(clip) {
const group = byTimestamp[clip.timestamp] || {};
const availKeys = Object.keys(group);
let html = "";
Object.keys(cameras).forEach(function (key) {
const has = group[key];
const isActive = key === clip.cam;
html +=
'";
});
if (availKeys.length <= 1) {
html +=
'' +
"Only one camera view recorded for this timestamp.";
}
camSwitchBtns.innerHTML = html;
}
function switchCam(key) {
if (!current) return;
const group = byTimestamp[current.timestamp] || {};
const target = group[key];
if (!target) return;
const savedTime = player.currentTime || 0;
const wasPlaying = !player.paused;
current = target;
player.src = target.url;
player.load();
badgeText.textContent = target.camDisplay;
npName.textContent = target.filename;
// Preserve video playback timestamp position across camera angles
player.addEventListener("loadedmetadata", function onMeta() {
player.removeEventListener("loadedmetadata", onMeta);
try {
if (savedTime && savedTime < player.duration) {
player.currentTime = savedTime;
}
} catch (e) {}
if (wasPlaying) player.play().catch(function () {});
});
camSwitchBtns.querySelectorAll("button").forEach(function (b) {
b.classList.toggle("active", b.getAttribute("data-switch") === key);
});
highlightCurrent();
}
function highlightCurrent() {
const rows = clipListEl.querySelectorAll(".clip");
rows.forEach(function (r) {
const isCur =
current &&
r.getAttribute("data-cam") === current.cam &&
r.getAttribute("data-ts") === current.timestamp &&
r.getAttribute("data-fn") === current.filename;
r.classList.toggle("playing", !!isCur);
});
}
clipListEl.addEventListener("click", function (e) {
const row = e.target.closest(".clip");
if (!row) return;
playClip({
cam: row.getAttribute("data-cam"),
camDisplay: cameras[row.getAttribute("data-cam")],
filename: row.getAttribute("data-fn"),
timestamp: row.getAttribute("data-ts"),
url: row.getAttribute("data-url"),
});
});
camSwitchBtns.addEventListener("click", function (e) {
const btn = e.target.closest("button[data-switch]");
if (!btn || btn.disabled) return;
switchCam(btn.getAttribute("data-switch"));
});
camTabsEl.addEventListener("click", function (e) {
const tab = e.target.closest(".tab");
if (!tab) return;
camTabsEl.querySelectorAll(".tab").forEach(function (t) {
t.classList.remove("active");
});
tab.classList.add("active");
activeCam = tab.getAttribute("data-cam");
render();
});
let searchTimer;
searchBox.addEventListener("input", function () {
clearSearch.classList.toggle("visible", !!searchBox.value);
clearTimeout(searchTimer);
searchTimer = setTimeout(function () {
query = searchBox.value.trim().toLowerCase();
render();
}, 120);
});
clearSearch.addEventListener("click", function () {
searchBox.value = "";
query = "";
clearSearch.classList.remove("visible");
render();
searchBox.focus();
});
sortSelect.addEventListener("change", function () {
sortMode = sortSelect.value;
render();
});
// Keyboard Shortcuts: 1-9 (Camera Switch), J/K (Next/Prev Clip)
document.addEventListener("keydown", function (e) {
if (/INPUT|TEXTAREA|SELECT/.test(document.activeElement.tagName)) return;
if (e.key >= "1" && e.key <= "9") {
const keys = Object.keys(cameras);
const idx = +e.key - 1;
if (current && keys[idx]) switchCam(keys[idx]);
} else if (e.key === "j" || e.key === "k") {
const list = getFiltered();
if (!list.length) return;
let i = list.findIndex(function (c) {
return (
current &&
c.cam === current.cam &&
c.timestamp === current.timestamp &&
c.filename === current.filename
);
});
i =
e.key === "j"
? Math.min(list.length - 1, i + 1)
: Math.max(0, i - 1);
if (list[i]) playClip(list[i]);
}
});
// Auto-play next clip of the same camera when video finishes
player.addEventListener("ended", function () {
if (!current) return;
// Build a list of clips for the SAME camera, sorted like the sidebar
const sameCam = clips
.filter(function (c) {
return c.cam === current.cam;
})
.sort(function (a, b) {
if (sortMode === "name") return a.filename.localeCompare(b.filename);
const da = tsToDate(a.timestamp),
db = tsToDate(b.timestamp);
const ta = da ? da.getTime() : 0,
tb = db ? db.getTime() : 0;
return sortMode === "oldest" ? ta - tb : tb - ta;
});
const i = sameCam.findIndex(function (c) {
return (
c.cam === current.cam &&
c.timestamp === current.timestamp &&
c.filename === current.filename
);
});
if (i > -1 && sameCam[i + 1]) playClip(sameCam[i + 1]);
});
render();
})();