From 9f055f6fa3d5c8cc8aab7f4e8440e132d01d4d92 Mon Sep 17 00:00:00 2001 From: Andrew Kettel Date: Wed, 18 Mar 2026 10:43:13 -0700 Subject: [PATCH] Updating html to allow for multiple clients --- src/templates/index.html | 451 ++++++++++++++++++++++++--------------- 1 file changed, 278 insertions(+), 173 deletions(-) diff --git a/src/templates/index.html b/src/templates/index.html index 6ede01a..399d717 100644 --- a/src/templates/index.html +++ b/src/templates/index.html @@ -2,210 +2,315 @@ - - - Pi Camera - + #status { + font-size: 0.85rem; + color: #888; + min-height: 1.2em; + } + -

Pi Camera Stream

+

Pi Camera

-
- Stream not started - +
+
+
+ Checking stream...
+ +
-
- - -
+
-

+

- - - + + } else if (video.canPlayType("application/vnd.apple.mpegurl")) { + video.src = src; + video.play().catch(e => console.warn("Autoplay blocked:", e)); + showLive(); + setStatus("Streaming"); + } else { + setStatus("HLS not supported in this browser"); + } + } + + function stopHls() { + if (hls) { hls.destroy(); hls = null; } + video.pause(); + video.src = ""; + video.style.display = "none"; + } + + // ── Status polling ─────────────────────────────────────────────────────── + + async function fetchStatus() { + const res = await fetch("/camera/status"); + return res.json(); + } + + function setStatus(msg) { + statusEl.textContent = msg; + } + + async function waitForReady(maxAttempts = 40) { + for (let i = 0; i < maxAttempts; i++) { + const data = await fetchStatus(); + if (data.ready) return true; + await new Promise(r => setTimeout(r, 500)); + } + return false; + } + + // Called on page load and after stream errors — attach if already live + async function attachToStream() { + const data = await fetchStatus(); + if (data.ready) { + startHls(); + renderControls(true); + } else if (data.running) { + setStatus("Stream starting..."); + placeholderText.textContent = "Stream starting..."; + const ready = await waitForReady(); + if (ready) { + startHls(); + renderControls(true); + } else { + showOffline("Stream timed out"); + renderControls(false); + } + } else { + showOffline("Stream is offline"); + renderControls(false); + } + } + + function startOfflinePoll() { + stopOfflinePoll(); + pollInterval = setInterval(async () => { + const data = await fetchStatus(); + if (data.running || data.ready) { + stopOfflinePoll(); + await attachToStream(); + } + }, 5000); + } + + function stopOfflinePoll() { + if (pollInterval) { clearInterval(pollInterval); pollInterval = null; } + } + + // ── Stream start / stop ────────────────────────────────────────────────── + + async function startStream() { + // guard: re-check status in case another client just started it + const current = await fetchStatus(); + if (current.running || current.ready) { + await attachToStream(); + return; + } + + const btn = document.getElementById("btn-start"); + if (btn) btn.disabled = true; + setStatus("Starting camera..."); + placeholderText.textContent = "Starting..."; + + await fetch("/camera/start", { method: "POST" }); + + setStatus("Waiting for first segment..."); + const ready = await waitForReady(); + + if (!ready) { + showOffline("Camera timed out — check Pi logs"); + renderControls(false); + startOfflinePoll(); + return; + } + + startHls(); + renderControls(true); + stopOfflinePoll(); + } + + async function stopStream() { + stopHls(); + showOffline("Stream is offline"); + setStatus("Stopping..."); + renderControls(false); + await fetch("/camera/stop", { method: "POST" }); + setStatus("Stream stopped"); + startOfflinePoll(); + } + + // ── Init ──────────────────────────────────────────────────────────────── + + (async () => { + await attachToStream(); + const data = await fetchStatus(); + if (!data.running && !data.ready) { + startOfflinePoll(); + } + })(); + \ No newline at end of file