|
|
|
@@ -23,28 +23,25 @@
|
|
|
|
|
/* player wrapper */
|
|
|
|
|
.player-wrap { position: relative; background: #000; border-radius: 12px; overflow: hidden; }
|
|
|
|
|
.player-wrap video { display: block; width: 100%; aspect-ratio: 16/9; object-fit: contain; background: #000; }
|
|
|
|
|
.empty-state { position: absolute; inset: 0; display: grid; place-content: center; justify-items: center; gap: 10px; color: #222; background: #f1f1f1; pointer-events: none; }
|
|
|
|
|
.empty-state svg { width: 52px; height: 52px; fill: #222; }
|
|
|
|
|
.empty-state span { font-size: 14px; }
|
|
|
|
|
.player-wrap.has-video .empty-state { display: none; }
|
|
|
|
|
|
|
|
|
|
/* big center play */
|
|
|
|
|
.big-play { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); width: 68px; height: 48px; background: rgba(0,0,0,.6); border-radius: 12px; display: grid; place-items: center; pointer-events: none; opacity: 0; transition: opacity .15s; }
|
|
|
|
|
.big-play { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); width: 68px; height: 48px; background: rgba(255,255,255,.92); color: #111; border-radius: 12px; display: grid; place-items: center; pointer-events: none; opacity: 0; transition: opacity .15s; box-shadow: 0 2px 12px rgba(0,0,0,.35); }
|
|
|
|
|
.player-wrap.paused .big-play { opacity: 1; }
|
|
|
|
|
.big-play svg { width: 28px; height: 28px; }
|
|
|
|
|
|
|
|
|
|
/* bottom gradient */
|
|
|
|
|
.controls-bar { position: absolute; bottom: 0; left: 0; right: 0; background: linear-gradient(transparent, rgba(0,0,0,.85)); padding: 30px 12px 10px; }
|
|
|
|
|
|
|
|
|
|
/* scrub bar */
|
|
|
|
|
.scrub { position: relative; height: 4px; background: rgba(255,255,255,.2); cursor: pointer; margin-bottom: 8px; border-radius: 2px; transition: height .1s; }
|
|
|
|
|
.scrub:hover { height: 6px; }
|
|
|
|
|
.scrub-fill { position: absolute; left: 0; top: 0; bottom: 0; background: var(--red); border-radius: 2px; pointer-events: none; }
|
|
|
|
|
.scrub-hover { position: absolute; top: -28px; background: rgba(0,0,0,.8); color: #fff; font-size: 12px; padding: 3px 7px; border-radius: 3px; pointer-events: none; white-space: nowrap; display: none; }
|
|
|
|
|
.scrub:hover .scrub-hover { display: block; }
|
|
|
|
|
.controls-bar { position: absolute; bottom: 0; left: 0; right: 0; background: linear-gradient(transparent, rgba(0,0,0,.85)); padding: 18px 12px 10px; }
|
|
|
|
|
|
|
|
|
|
/* control row */
|
|
|
|
|
.ctrl-row { display: flex; align-items: center; gap: 8px; }
|
|
|
|
|
.ctrl-row button { background: none; border: none; color: #fff; cursor: pointer; padding: 4px; border-radius: 50%; display: grid; place-items: center; }
|
|
|
|
|
.ctrl-row { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
|
|
|
.ctrl-row button { width: 30px; height: 30px; flex: 0 0 30px; background: none; border: none; color: #fff; cursor: pointer; padding: 4px; border-radius: 50%; display: grid; place-items: center; }
|
|
|
|
|
.ctrl-row button:hover { background: rgba(255,255,255,.15); }
|
|
|
|
|
.ctrl-row svg { width: 22px; height: 22px; fill: currentColor; }
|
|
|
|
|
.time-label { font-size: 13px; color: #ddd; font-variant-numeric: tabular-nums; margin-left: 4px; user-select: none; }
|
|
|
|
|
.time-label { font-size: 13px; color: #ddd; font-variant-numeric: tabular-nums; margin-left: 4px; user-select: none; white-space: nowrap; }
|
|
|
|
|
.spacer { flex: 1; }
|
|
|
|
|
.vol-wrap { display: flex; align-items: center; gap: 4px; }
|
|
|
|
|
.vol-wrap input { width: 0; opacity: 0; transition: width .2s, opacity .2s; accent-color: #fff; }
|
|
|
|
@@ -62,9 +59,23 @@
|
|
|
|
|
.hours { display: flex; justify-content: space-between; margin-top: 4px; color: var(--dim); font-size: 10px; user-select: none; }
|
|
|
|
|
.legend { display: flex; gap: 16px; margin-top: 10px; font-size: 11px; color: var(--dim); }
|
|
|
|
|
.legend span { display: flex; align-items: center; gap: 5px; }
|
|
|
|
|
.legend span { cursor: pointer; }
|
|
|
|
|
.legend span::before { content: ""; display: block; width: 14px; height: 6px; border-radius: 2px; }
|
|
|
|
|
.legend .rec::before { background: var(--accent); }
|
|
|
|
|
.legend .gap::before { background: #2a2a2a; border: 1px solid #444; }
|
|
|
|
|
.legend .highlight::before { background: #ffd43b; }
|
|
|
|
|
.legend .system::before { background: #a87900; }
|
|
|
|
|
.highlights { margin-top: 18px; background: var(--surface); border-radius: 8px; padding: 14px 16px; }
|
|
|
|
|
.highlights h2 { font-size: 14px; font-weight: 500; margin-bottom: 10px; }
|
|
|
|
|
.highlight-actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
|
|
|
|
|
.highlight-actions button, .highlight-actions input { background: #333; border: 1px solid #555; color: var(--text); padding: 7px 10px; border-radius: 3px; font-size: 12px; }
|
|
|
|
|
.highlight-actions button { cursor: pointer; }
|
|
|
|
|
.highlight-actions button:hover { background: #444; }
|
|
|
|
|
.mark-time { color: var(--accent); font-size: 12px; min-width: 70px; }
|
|
|
|
|
.highlight-list { margin-top: 12px; display: grid; gap: 6px; }
|
|
|
|
|
.highlight-item { display: flex; align-items: center; gap: 10px; padding: 8px; background: #1a1a1a; border-radius: 4px; font-size: 12px; }
|
|
|
|
|
.highlight-item .highlight-label { flex: 1; }
|
|
|
|
|
.highlight-item button { background: none; border: none; color: var(--dim); cursor: pointer; }
|
|
|
|
|
|
|
|
|
|
/* encoding panel */
|
|
|
|
|
.encode-panel { margin-top: 16px; background: var(--surface); border-radius: 8px; overflow: hidden; }
|
|
|
|
@@ -82,7 +93,12 @@
|
|
|
|
|
|
|
|
|
|
@media (max-width: 600px) {
|
|
|
|
|
.page { padding: 8px; }
|
|
|
|
|
.ctrl-row svg { width: 18px; height: 18px; }
|
|
|
|
|
.ctrl-row { gap: 3px; }
|
|
|
|
|
.ctrl-row button { width: 32px; height: 32px; flex-basis: 32px; }
|
|
|
|
|
.ctrl-row svg { width: 20px; height: 20px; }
|
|
|
|
|
.time-label { font-size: 11px; }
|
|
|
|
|
.ctrl-row .spacer { display: none; }
|
|
|
|
|
.vol-wrap input { width: 54px; opacity: 1; }
|
|
|
|
|
.hours span:nth-child(even) { display: none; }
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
@@ -109,19 +125,16 @@
|
|
|
|
|
|
|
|
|
|
<div class="player-wrap paused" id="player-wrap">
|
|
|
|
|
<video id="video" playsinline></video>
|
|
|
|
|
<div class="empty-state" id="empty-state"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 5h10a2 2 0 0 1 2 2v2.2l3-1.7v9l-3-1.7V17a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2zm0 2v10h10V7H4zm12 4.5v2.1l1-.6v-1z"/></svg><span>Select a recording from the timeline</span></div>
|
|
|
|
|
<div class="big-play"><svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg></div>
|
|
|
|
|
<div class="controls-bar">
|
|
|
|
|
<div class="scrub" id="scrub">
|
|
|
|
|
<div class="scrub-fill" id="scrub-fill"></div>
|
|
|
|
|
<div class="scrub-hover" id="scrub-hover">00:00</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="ctrl-row">
|
|
|
|
|
<button id="btn-play" aria-label="Play">
|
|
|
|
|
<svg id="icon-play" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>
|
|
|
|
|
<svg id="icon-pause" viewBox="0 0 24 24" style="display:none"><path d="M6 19h4V5H6zm8-14v14h4V5z"/></svg>
|
|
|
|
|
</button>
|
|
|
|
|
<button id="btn-back" aria-label="Back 10s"><svg viewBox="0 0 24 24"><path d="M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z"/><text x="10" y="16" font-size="7" fill="currentColor" text-anchor="middle" font-family="sans-serif">10</text></svg></button>
|
|
|
|
|
<button id="btn-fwd" aria-label="Forward 10s"><svg viewBox="0 0 24 24"><path d="M12 5V1l5 5-5 5V7c-3.31 0-6 2.69-6 6s2.69 6 6 6 6-2.69 6-6h2c0 4.42-3.58 8-8 8s-8-3.58-8-8 3.58-8 8-8z"/><text x="14" y="16" font-size="7" fill="currentColor" text-anchor="middle" font-family="sans-serif">10</text></svg></button>
|
|
|
|
|
<button id="btn-back" aria-label="Back 10 seconds"><svg viewBox="0 0 24 24"><path d="M12 5V2L7 7l5 5V8a5 5 0 1 1-5 5H5a7 7 0 1 0 7-7z"/><text x="12" y="15" font-size="6.5" fill="currentColor" text-anchor="middle" font-family="sans-serif" font-weight="bold">10</text></svg></button>
|
|
|
|
|
<button id="btn-fwd" aria-label="Forward 10 seconds"><svg viewBox="0 0 24 24"><path d="M12 5V2l5 5-5 5V8a5 5 0 1 0 5 5h2a7 7 0 1 1-7-7z"/><text x="12" y="15" font-size="6.5" fill="currentColor" text-anchor="middle" font-family="sans-serif" font-weight="bold">10</text></svg></button>
|
|
|
|
|
<div class="vol-wrap">
|
|
|
|
|
<button id="btn-vol" aria-label="Volume">
|
|
|
|
|
<svg id="icon-vol-on" viewBox="0 0 24 24"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3A4.5 4.5 0 0 0 14 8.3v7.4a4.5 4.5 0 0 0 2.5-3.7zM14 3.2v2.1a7 7 0 0 1 0 13.4v2.1A9 9 0 0 0 14 3.2z"/></svg>
|
|
|
|
@@ -129,9 +142,8 @@
|
|
|
|
|
</button>
|
|
|
|
|
<input id="volume" type="range" min="0" max="1" step=".01" value=".8">
|
|
|
|
|
</div>
|
|
|
|
|
<span class="time-label"><span id="time-cur">0:00</span> / <span id="time-dur">0:00</span></span>
|
|
|
|
|
<span class="time-label" id="time-cur" style="color:var(--accent);font-weight:500;">--:--:--</span>
|
|
|
|
|
<span class="spacer"></span>
|
|
|
|
|
<span class="time-label" id="clock" style="color:var(--accent);font-weight:500;">--:--:--</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
@@ -148,9 +160,20 @@
|
|
|
|
|
<canvas id="timeline"></canvas>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="hours" id="hours"></div>
|
|
|
|
|
<div class="legend"><span class="rec">Recorded</span><span class="gap">No recording</span></div>
|
|
|
|
|
<div class="legend"><span class="rec" data-jump="recorded">Recorded</span><span class="highlight" data-jump="highlight">Highlight</span><span class="system" data-jump="system">System highlight</span><span class="gap" data-jump="gap">No recording</span></div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<section class="highlights">
|
|
|
|
|
<h2>Highlights</h2>
|
|
|
|
|
<div class="highlight-actions">
|
|
|
|
|
<button id="mark-start">Mark start</button><span class="mark-time" id="highlight-start">--:--:--</span>
|
|
|
|
|
<button id="mark-end">Mark end</button><span class="mark-time" id="highlight-end">--:--:--</span>
|
|
|
|
|
<input id="highlight-label" placeholder="Highlight name" maxlength="120">
|
|
|
|
|
<button id="save-highlight">Add highlight</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="highlight-list" id="highlight-list"><span style="color:var(--dim);font-size:12px">No highlights yet.</span></div>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<div class="encode-panel">
|
|
|
|
|
<button class="encode-toggle" id="encode-toggle"><svg viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>Encoding</button>
|
|
|
|
|
<div class="encode-body" id="encode-body">
|
|
|
|
@@ -166,14 +189,15 @@
|
|
|
|
|
const wrap = document.querySelector("#player-wrap");
|
|
|
|
|
const dateSelect = document.querySelector("#date");
|
|
|
|
|
const timeline = document.querySelector("#timeline");
|
|
|
|
|
const scrub = document.querySelector("#scrub");
|
|
|
|
|
const scrubFill = document.querySelector("#scrub-fill");
|
|
|
|
|
const scrubHover = document.querySelector("#scrub-hover");
|
|
|
|
|
let clips = [];
|
|
|
|
|
let activeClip = null;
|
|
|
|
|
let initialOffset = 0;
|
|
|
|
|
let zoom = 1;
|
|
|
|
|
let focus = 43200;
|
|
|
|
|
let highlights = [];
|
|
|
|
|
let systemHighlights = [];
|
|
|
|
|
let highlightStart = null;
|
|
|
|
|
let highlightEnd = null;
|
|
|
|
|
let encodingWasRunning = false;
|
|
|
|
|
|
|
|
|
|
// encoding
|
|
|
|
@@ -246,21 +270,98 @@
|
|
|
|
|
return [s/3600, s%3600/60, s%60].map(v => String(Math.floor(v)).padStart(2,"0")).join(":");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// scrub bar
|
|
|
|
|
function updateScrub() {
|
|
|
|
|
if (!activeClip || !video.duration) { scrubFill.style.width = "0%"; return; }
|
|
|
|
|
scrubFill.style.width = (video.currentTime / video.duration * 100) + "%";
|
|
|
|
|
function archiveTime() {
|
|
|
|
|
return activeClip ? activeClip.start + initialOffset + (video.currentTime || 0) : focus;
|
|
|
|
|
}
|
|
|
|
|
scrub.addEventListener("click", e => {
|
|
|
|
|
if (!activeClip || !video.duration) return;
|
|
|
|
|
video.currentTime = (e.offsetX / scrub.clientWidth) * video.duration;
|
|
|
|
|
});
|
|
|
|
|
scrub.addEventListener("mousemove", e => {
|
|
|
|
|
if (!activeClip || !video.duration) return;
|
|
|
|
|
const t = (e.offsetX / scrub.clientWidth) * video.duration;
|
|
|
|
|
scrubHover.textContent = fmt(t);
|
|
|
|
|
scrubHover.style.left = e.offsetX + "px";
|
|
|
|
|
});
|
|
|
|
|
function jumpToNext(kind) {
|
|
|
|
|
const now = archiveTime();
|
|
|
|
|
if (kind === "recorded") {
|
|
|
|
|
const clip = clips.find(item => item.start > now + 1) || clips[0];
|
|
|
|
|
if (clip) { focus = clip.start; playClip(clip, 0); }
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const items = kind === "system" ? systemHighlights : highlights;
|
|
|
|
|
if (kind === "highlight" || kind === "system") {
|
|
|
|
|
const item = items.find(entry => entry.start > now + 1) || items[0];
|
|
|
|
|
if (!item) return;
|
|
|
|
|
const clip = clips.find(entry => item.start >= entry.start && item.start < entry.start + entry.duration)
|
|
|
|
|
|| clips.find(entry => entry.start >= item.start);
|
|
|
|
|
if (clip) { focus = clip.start; playClip(clip, Math.max(0, item.start - clip.start)); }
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const gaps = [];
|
|
|
|
|
if (clips.length && clips[0].start > 0) gaps.push({start: 0, end: clips[0].start});
|
|
|
|
|
clips.forEach((clip, index) => {
|
|
|
|
|
const end = clip.start + clip.duration;
|
|
|
|
|
const next = clips[index + 1];
|
|
|
|
|
if (next && next.start > end) gaps.push({start: end, end: next.start});
|
|
|
|
|
});
|
|
|
|
|
const gap = gaps.find(item => item.start > now + 1) || gaps[0];
|
|
|
|
|
if (gap) {
|
|
|
|
|
video.pause(); activeClip = null; focus = gap.start;
|
|
|
|
|
document.querySelector("#time-cur").textContent = timeOfDay(gap.start);
|
|
|
|
|
drawTimeline();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
document.querySelectorAll(".legend [data-jump]").forEach(item => item.addEventListener("click", () => jumpToNext(item.dataset.jump)));
|
|
|
|
|
function setMark(which) {
|
|
|
|
|
const value = archiveTime();
|
|
|
|
|
if (value === null) return;
|
|
|
|
|
if (which === "start") {
|
|
|
|
|
highlightStart = value;
|
|
|
|
|
document.querySelector("#highlight-start").textContent = timeOfDay(value);
|
|
|
|
|
} else {
|
|
|
|
|
highlightEnd = value;
|
|
|
|
|
document.querySelector("#highlight-end").textContent = timeOfDay(value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
async function loadHighlights() {
|
|
|
|
|
const response = await fetch("/api/highlights");
|
|
|
|
|
highlights = (await response.json()).filter(item => item.date === dateSelect.value);
|
|
|
|
|
const items = highlights;
|
|
|
|
|
drawTimeline();
|
|
|
|
|
const list = document.querySelector("#highlight-list");
|
|
|
|
|
list.replaceChildren();
|
|
|
|
|
if (!items.length) {
|
|
|
|
|
list.innerHTML = '<span style="color:var(--dim);font-size:12px">No highlights yet.</span>';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
items.forEach(item => {
|
|
|
|
|
const row = document.createElement("div");
|
|
|
|
|
row.className = "highlight-item";
|
|
|
|
|
const label = document.createElement("span");
|
|
|
|
|
label.className = "highlight-label";
|
|
|
|
|
label.textContent = `${item.label} (${timeOfDay(item.start)} - ${timeOfDay(item.end)})`;
|
|
|
|
|
const play = document.createElement("button");
|
|
|
|
|
play.textContent = "Play";
|
|
|
|
|
play.onclick = () => {
|
|
|
|
|
const clip = clips.find(c => item.start >= c.start && item.start < c.start + c.duration);
|
|
|
|
|
if (clip) playClip(clip, item.start - clip.start);
|
|
|
|
|
};
|
|
|
|
|
const remove = document.createElement("button");
|
|
|
|
|
remove.textContent = "Remove";
|
|
|
|
|
remove.onclick = async () => { await fetch(`/api/highlights?id=${item.id}`, {method: "DELETE"}); loadHighlights(); };
|
|
|
|
|
row.append(label, play, remove);
|
|
|
|
|
list.append(row);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
async function loadSystemHighlights() {
|
|
|
|
|
const response = await fetch(`/api/system-highlights?date=${dateSelect.value}`);
|
|
|
|
|
systemHighlights = await response.json();
|
|
|
|
|
drawTimeline();
|
|
|
|
|
}
|
|
|
|
|
document.querySelector("#mark-start").onclick = () => setMark("start");
|
|
|
|
|
document.querySelector("#mark-end").onclick = () => setMark("end");
|
|
|
|
|
document.querySelector("#save-highlight").onclick = async () => {
|
|
|
|
|
if (highlightStart === null || highlightEnd === null || highlightStart >= highlightEnd) return;
|
|
|
|
|
await fetch("/api/highlights", {method: "POST", headers: {"Content-Type": "application/json"},
|
|
|
|
|
body: JSON.stringify({date: dateSelect.value, start: highlightStart, end: highlightEnd, label: document.querySelector("#highlight-label").value || "Untitled highlight"})});
|
|
|
|
|
highlightStart = highlightEnd = null;
|
|
|
|
|
document.querySelector("#highlight-start").textContent = "--:--:--";
|
|
|
|
|
document.querySelector("#highlight-end").textContent = "--:--:--";
|
|
|
|
|
document.querySelector("#highlight-label").value = "";
|
|
|
|
|
loadHighlights();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// seek buttons
|
|
|
|
|
function seekBy(sec) {
|
|
|
|
@@ -309,6 +410,18 @@
|
|
|
|
|
const r = ((i + 1) * 60 - winStart) / winSize * w;
|
|
|
|
|
if (r > 0 && l < w) ctx.fillRect(Math.max(0, l), 0, Math.min(w, r) - Math.max(0, l), 56);
|
|
|
|
|
});
|
|
|
|
|
ctx.fillStyle = "#ffd43b";
|
|
|
|
|
highlights.forEach(item => {
|
|
|
|
|
const l = (item.start - winStart) / winSize * w;
|
|
|
|
|
const r = (item.end - winStart) / winSize * w;
|
|
|
|
|
if (r > 0 && l < w) ctx.fillRect(Math.max(0, l), 0, Math.min(w, r) - Math.max(0, l), 56);
|
|
|
|
|
});
|
|
|
|
|
ctx.fillStyle = "#a87900";
|
|
|
|
|
systemHighlights.forEach(item => {
|
|
|
|
|
const l = (item.start - winStart) / winSize * w;
|
|
|
|
|
const r = (item.end - winStart) / winSize * w;
|
|
|
|
|
if (r > 0 && l < w) ctx.fillRect(Math.max(0, l), 0, Math.min(w, r) - Math.max(0, l), 56);
|
|
|
|
|
});
|
|
|
|
|
// hour lines
|
|
|
|
|
ctx.strokeStyle = "rgba(255,255,255,.08)";
|
|
|
|
|
for (let h = Math.ceil(winStart / 3600); h < winEnd / 3600; h++) {
|
|
|
|
@@ -334,9 +447,20 @@
|
|
|
|
|
const winStart = Math.max(0, Math.min(86400 - winSize, focus - winSize / 2));
|
|
|
|
|
const sec = winStart + e.offsetX / timeline.clientWidth * winSize;
|
|
|
|
|
focus = sec;
|
|
|
|
|
const systemEvent = systemHighlights.find(item => sec >= item.start && sec < item.end);
|
|
|
|
|
if (systemEvent) {
|
|
|
|
|
const eventClip = clips.find(clip => clip.start >= systemEvent.start);
|
|
|
|
|
if (eventClip) return playClip(eventClip, 0);
|
|
|
|
|
}
|
|
|
|
|
const savedHighlight = highlights.find(item => sec >= item.start && sec < item.end);
|
|
|
|
|
if (savedHighlight) {
|
|
|
|
|
const highlightClip = clips.find(clip => savedHighlight.start >= clip.start && savedHighlight.start < clip.start + clip.duration)
|
|
|
|
|
|| clips.find(clip => clip.start >= savedHighlight.start);
|
|
|
|
|
if (highlightClip) return playClip(highlightClip, Math.max(0, savedHighlight.start - highlightClip.start));
|
|
|
|
|
}
|
|
|
|
|
const clip = clips.find(c => sec >= c.start && sec < c.start + c.duration)
|
|
|
|
|
|| clips.find(c => c.start >= sec) || clips[clips.length - 1];
|
|
|
|
|
if (clip) playClip(clip, Math.max(0, Math.min(clip.duration - .1, sec - clip.start)));
|
|
|
|
|
if (clip) playClip(clip, 0);
|
|
|
|
|
});
|
|
|
|
|
timeline.addEventListener("mousemove", e => {
|
|
|
|
|
const winSize = 86400 / zoom;
|
|
|
|
@@ -348,24 +472,23 @@
|
|
|
|
|
|
|
|
|
|
// load date
|
|
|
|
|
async function loadDate(date) {
|
|
|
|
|
video.pause(); video.removeAttribute("src"); activeClip = null; setPlaying(false);
|
|
|
|
|
video.pause(); video.removeAttribute("src"); activeClip = null; wrap.classList.remove("has-video"); setPlaying(false);
|
|
|
|
|
const r = await fetch(`/api/recordings?date=${encodeURIComponent(date)}`);
|
|
|
|
|
const d = await r.json();
|
|
|
|
|
clips = d.recordings;
|
|
|
|
|
dateSelect.innerHTML = d.dates.map(day => `<option value="${day}">${day.slice(0,4)}-${day.slice(4,6)}-${day.slice(6)}</option>`).join("");
|
|
|
|
|
dateSelect.value = date || d.dates[0];
|
|
|
|
|
document.querySelector("#clock").textContent = "--:--:--";
|
|
|
|
|
document.querySelector("#time-cur").textContent = "0:00";
|
|
|
|
|
document.querySelector("#time-dur").textContent = "0:00";
|
|
|
|
|
scrubFill.style.width = "0%";
|
|
|
|
|
document.querySelector("#time-cur").textContent = "--:--:--";
|
|
|
|
|
drawTimeline();
|
|
|
|
|
loadHighlights();
|
|
|
|
|
loadSystemHighlights();
|
|
|
|
|
if (!date && d.dates[0]) loadDate(d.dates[0]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function playClip(clip, offset = 0) {
|
|
|
|
|
activeClip = clip;
|
|
|
|
|
initialOffset = offset;
|
|
|
|
|
document.querySelector("#clock").textContent = timeOfDay(clip.start + offset);
|
|
|
|
|
document.querySelector("#time-cur").textContent = timeOfDay(clip.start + offset);
|
|
|
|
|
video.src = `/video?date=${dateSelect.value}&name=${encodeURIComponent(clip.name)}&offset=${offset.toFixed(3)}`;
|
|
|
|
|
video.play().catch(() => {});
|
|
|
|
|
drawTimeline();
|
|
|
|
@@ -373,13 +496,10 @@
|
|
|
|
|
|
|
|
|
|
video.addEventListener("timeupdate", () => {
|
|
|
|
|
if (!activeClip) return;
|
|
|
|
|
document.querySelector("#clock").textContent = timeOfDay(activeClip.start + initialOffset + video.currentTime);
|
|
|
|
|
document.querySelector("#time-cur").textContent = fmt(video.currentTime);
|
|
|
|
|
updateScrub();
|
|
|
|
|
document.querySelector("#time-cur").textContent = timeOfDay(activeClip.start + initialOffset + video.currentTime);
|
|
|
|
|
drawTimeline();
|
|
|
|
|
});
|
|
|
|
|
video.addEventListener("loadedmetadata", () => {
|
|
|
|
|
document.querySelector("#time-dur").textContent = fmt(video.duration);
|
|
|
|
|
if (initialOffset > 0 && video.currentTime < initialOffset) video.currentTime = initialOffset;
|
|
|
|
|
});
|
|
|
|
|
video.addEventListener("ended", () => {
|
|
|
|
|