Files
tesla-cam-view/static/js/app.js
T
zwitschi d12d5e5414 Refactor video player and camera angle switching
- Removed inline JSON data for video clips; replaced with API endpoint to fetch video data.
- Simplified camera angle switching UI with a 3-column button grid around a Model Y graphic.
- Updated camera view mapping to use 'back' instead of 'rear'.
- Enhanced noise canvas handling during video loading.
- Streamlined clip playback logic and added auto-play for next clips in the same camera.
- Improved search and sorting functionality by removing unnecessary elements.
- Updated HTML structure for better accessibility and user experience.
2026-07-25 00:43:43 +02:00

301 lines
8.9 KiB
JavaScript

(function () {
"use strict";
// ---- DOM refs ----
const player = document.getElementById("player");
const noiseCanvas = document.getElementById("noiseCanvas");
const placeholder = document.getElementById("videoPlaceholder");
const badge = document.getElementById("videoBadge");
const badgeText = document.getElementById("badgeText");
const nowPlaying = document.getElementById("nowPlaying");
const npName = document.getElementById("npName");
const npTime = document.getElementById("npTime");
const csBtns = document.querySelectorAll(".cs-btn[data-cam]");
// ---- State ----
let allVideos = {}; // { camKey: { display, videos: [...] } }
let byTimestamp = {}; // { timestamp: { camKey: clip } }
let currentClip = null; // currently playing clip object
let currentCam = "front"; // active camera key
// ---- Static noise canvas ----
const noiseCtx = noiseCanvas.getContext("2d");
let noiseAnimId = null;
let noiseRunning = false;
function noiseStart() {
if (noiseRunning) return;
noiseRunning = true;
noiseCanvas.classList.remove("hidden");
noiseTick();
}
function noiseStop() {
noiseRunning = false;
noiseCanvas.classList.add("hidden");
if (noiseAnimId) {
cancelAnimationFrame(noiseAnimId);
noiseAnimId = null;
}
}
function noiseTick() {
if (!noiseRunning) return;
let w = noiseCanvas.width;
let h = noiseCanvas.height;
const rect = noiseCanvas.getBoundingClientRect();
const targetW = Math.max(1, Math.min(160, (rect.width * 0.25) | 0));
const ratio = rect.height / Math.max(1, rect.width);
const targetH = Math.max(1, (targetW * ratio) | 0);
if (w !== targetW || h !== targetH) {
noiseCanvas.width = targetW;
noiseCanvas.height = targetH;
w = targetW;
h = targetH;
}
if (w === 0 || h === 0) {
noiseAnimId = requestAnimationFrame(noiseTick);
return;
}
const imageData = noiseCtx.createImageData(w, h);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
const v = (Math.random() * 255) | 0;
data[i] = v;
data[i + 1] = v;
data[i + 2] = v;
data[i + 3] = 255;
}
noiseCtx.putImageData(imageData, 0, 0);
noiseAnimId = requestAnimationFrame(noiseTick);
}
// ---- Helpers ----
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",
});
}
// ---- Prefetch ----
const prefetchedUrls = {};
function prefetchVideo(url) {
if (!url || prefetchedUrls[url]) return;
prefetchedUrls[url] = true;
const link = document.createElement("link");
link.rel = "prefetch";
link.as = "video";
link.href = url;
document.head.appendChild(link);
}
function schedulePrefetch(clip) {
const doPrefetch = function () {
// Sibling angles at same timestamp
const group = byTimestamp[clip.timestamp] || {};
Object.keys(group).forEach(function (key) {
if (key !== clip.cam) prefetchVideo(group[key].url);
});
// Next clip in same camera (oldest-first)
const sameCam = (allVideos[clip.cam] && allVideos[clip.cam].videos) || [];
const idx = sameCam.findIndex(function (c) {
return c.timestamp === clip.timestamp && c.filename === clip.filename;
});
if (idx > -1 && sameCam[idx + 1]) prefetchVideo(sameCam[idx + 1].url);
};
if (typeof requestIdleCallback === "function") {
requestIdleCallback(doPrefetch, { timeout: 2000 });
} else {
setTimeout(doPrefetch, 100);
}
}
// ---- Playback ----
function playClip(clip) {
if (!clip) return;
currentClip = clip;
currentCam = clip.cam;
player.src = clip.url;
player.load();
noiseStart();
let prefetchDone = false;
function onPlaying() {
noiseStop();
if (prefetchDone) return;
prefetchDone = true;
player.removeEventListener("playing", onPlaying);
schedulePrefetch(clip);
}
player.addEventListener("playing", onPlaying);
player.play().catch(function () {
noiseStop();
if (!prefetchDone) {
prefetchDone = true;
player.removeEventListener("playing", onPlaying);
schedulePrefetch(clip);
}
});
placeholder.hidden = true;
badge.classList.add("visible");
badgeText.textContent = clip.camDisplay;
nowPlaying.hidden = false;
npName.textContent = clip.filename;
npTime.textContent = prettyTs(clip.timestamp);
updateCamButtons(clip.cam);
}
function switchCam(camKey) {
if (!currentClip) return;
const group = byTimestamp[currentClip.timestamp] || {};
const target = group[camKey];
if (!target) return;
const savedTime = player.currentTime || 0;
const wasPlaying = !player.paused;
currentClip = target;
currentCam = camKey;
player.src = target.url;
player.load();
badgeText.textContent = target.camDisplay;
npName.textContent = target.filename;
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 () {});
});
updateCamButtons(camKey);
}
function updateCamButtons(activeKey) {
csBtns.forEach(function (btn) {
const key = btn.getAttribute("data-cam");
btn.classList.toggle("active", key === activeKey);
});
}
// ---- Auto-play next clip in same camera ----
player.addEventListener("ended", function () {
if (!currentClip) return;
const sameCam =
(allVideos[currentClip.cam] && allVideos[currentClip.cam].videos) || [];
const idx = sameCam.findIndex(function (c) {
return (
c.timestamp === currentClip.timestamp &&
c.filename === currentClip.filename
);
});
if (idx > -1 && sameCam[idx + 1]) playClip(sameCam[idx + 1]);
});
// ---- Camera button clicks ----
document.querySelector(".cs-grid").addEventListener("click", function (e) {
const btn = e.target.closest(".cs-btn[data-cam]");
if (!btn || btn.disabled) return;
const camKey = btn.getAttribute("data-cam");
if (camKey === currentCam) return; // already active
// Try same timestamp first, else fall back to oldest in that camera
const group = currentClip ? byTimestamp[currentClip.timestamp] || {} : {};
const sameTs = group[camKey];
if (sameTs) {
switchCam(camKey);
} else {
// No same-timestamp match: play oldest clip for this camera
const videos = (allVideos[camKey] && allVideos[camKey].videos) || [];
if (videos.length > 0) playClip(videos[0]);
}
});
// ---- Keyboard shortcuts: 1-6 for camera angles ----
const camOrder = [
"front",
"left_repeater",
"right_repeater",
"back",
"left_pillar",
"right_pillar",
];
document.addEventListener("keydown", function (e) {
if (/INPUT|TEXTAREA|SELECT/.test(document.activeElement.tagName)) return;
if (e.key >= "1" && e.key <= "6") {
const idx = +e.key - 1;
const camKey = camOrder[idx];
if (!camKey) return;
const group = currentClip ? byTimestamp[currentClip.timestamp] || {} : {};
const sameTs = group[camKey];
if (sameTs) {
switchCam(camKey);
} else {
const videos = (allVideos[camKey] && allVideos[camKey].videos) || [];
if (videos.length > 0) playClip(videos[0]);
}
}
});
// ---- Init: fetch full video data, then auto-play front ----
function init() {
fetch("/api/videos")
.then(function (r) {
return r.json();
})
.then(function (data) {
allVideos = data;
// Build byTimestamp index
Object.keys(data).forEach(function (camKey) {
const videos = data[camKey].videos || [];
videos.forEach(function (v) {
v.cam = camKey;
v.camDisplay = data[camKey].display;
if (!byTimestamp[v.timestamp]) byTimestamp[v.timestamp] = {};
byTimestamp[v.timestamp][camKey] = v;
});
});
// Auto-play front if video element has a src
if (player.src && player.src !== window.location.href) {
const frontVideos =
(allVideos["front"] && allVideos["front"].videos) || [];
if (frontVideos.length > 0) {
playClip(frontVideos[0]);
}
}
})
.catch(function (err) {
console.error("Failed to load video data:", err);
});
}
// Start noise immediately
noiseStart();
init();
})();