14 Commits
Author SHA1 Message Date
ryan.mcgeeandClaude Opus 4.7 9fdee30281 Document the always-available raw HTTP stream guarantee
The raw HTTP endpoints (/stream/raw, /snapshot/raw, and the
mjpg_streamer-compatible /webcam/?action=stream and ?action=snapshot)
are intentionally not gated by any runtime toggle — OctoPrint's
Classic Webcam plugin uses these for live feeds and timelapse
snapshots and they must remain available regardless of overlay master,
overlay type, or RTSP toggle state.

This was already the behavior; this commit just makes the invariant
explicit in code comments and the README so future changes do not
accidentally regress it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:11:17 -05:00
ryan.mcgeeandClaude Opus 4.7 67641ea2c0 Add overlay type toggle: baked (server-rendered) vs html (client)
The overlay can now be rendered either way at runtime:

- baked (default): server decodes the JPEG, draws the overlay, re-encodes,
  publishes to the broadcaster and ffmpeg. Works with OctoPrint, VLC, and
  any RTSP consumer. Same behavior as before.
- html: server skips decode/draw/encode entirely. The browser draws the
  stats as positioned HTML over the raw stream. No video-side overlay,
  so /stream/overlay and /snapshot/overlay return 503 and the RTSP
  overlay process is stopped while in html mode. The user's RTSP overlay
  preference is preserved and restored when switching back to baked.

Pipeline:
- overlayBaked atomic.Bool, default true
- SetOverlayBaked(bool) clears the broadcaster and stops the RTSP overlay
  process when switching to html; restarts it when switching to baked if
  the master toggle and user preference are both on
- runOverlay() skips the decode/draw/encode when not (enabled AND baked)
- pipelineState gains overlay_baked field, default true on load so
  existing installs keep their current behavior

Web:
- /api/overlay GET returns {"enabled", "type"}; POST accepts either or
  both fields (omitted fields are left unchanged)
- gateOverlay middleware also 503s when the overlay is in html mode
- /overlay full-screen page in html mode serves /stream/raw and renders
  the same HTML overlay markup as the index card

UI:
- Second toggle next to "Overlay feed" — Baked vs HTML
- Overlay card has a stream-frame wrapper so the HTML overlay can
  absolute-position on top of the image
- New .html-overlay CSS with state pill, filename, hotend/bed temps,
  progress bar, elapsed/ETA — matches the baked overlay's information
  but rendered crisp in the browser
- HTML overlay updates from the existing /api/stats poll loop

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:07:25 -05:00
ryan.mcgeeandClaude Opus 4.7 6e2d919c2c Raw feed card takes full width when overlay is disabled
The streams grid is fixed at two equal columns by default. When the
overlay card is hidden the right column was just empty space, leaving
the raw feed at half width. Add a .solo modifier that collapses the
grid to a single column and toggle it from applyOverlayVisibility().

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:00:05 -05:00
ryan.mcgeeandClaude Opus 4.7 2250cdd0ec Support hosting at a URL sub-path via OCTOCAM_BASE_PATH
When OCTOCAM_BASE_PATH is set (e.g. /octocam), all routes are served
under that prefix and the bare prefix without trailing slash gets a
301 redirect to the canonical form with trailing slash.

Implementation:
- Config: OCTOCAM_BASE_PATH env var, normalized to ensure leading
  slash and stripped trailing slash
- Server: when base path is set, wrap the existing mux with
  http.StripPrefix and add a redirect handler for the bare prefix.
  All internal routes stay registered at their root paths so the same
  mux works whether the prefix is empty or not.
- Templates: index.html and feed.html receive a BasePath template
  variable used to construct all asset, stream, link, and JS fetch
  URLs. Standalone /raw and /overlay pages prefix StreamURL and
  OtherURL in server.go before rendering.
- Redirect from /overlay (when overlay disabled) now goes to
  BasePath + "/" instead of "/"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 14:52:12 -05:00
ryan.mcgeeandClaude Opus 4.7 761e2558a6 Add runtime-toggleable master overlay feed (defaults to off)
The overlay feed (rendering, MJPEG broadcaster, RTSP) is now gated by a
master toggle persisted in the state file alongside the RTSP toggles.
A fresh install serves only the raw feed and skips the JPEG decode /
draw / re-encode loop entirely, saving the bulk of the per-frame CPU.

Pipeline changes:
- runOverlay() drops frames when the master is off, before any decode
- New SetOverlayEnabled() clears the overlay broadcaster's cached frame
  and stops the ffmpeg overlay process when disabling
- The user's RTSP overlay preference is tracked in overlayRTSPPref so
  it survives master toggles. RTSP overlay only runs when both master
  overlay and the user preference are on.
- State struct renamed pipelineState; adds overlay_enabled field. Old
  state files load cleanly (missing field defaults to false).

Web changes:
- New /api/overlay endpoint (GET/POST) for the master toggle
- /stream/overlay and /snapshot/overlay return 503 when master is off,
  so img elements show broken instead of hanging on an empty stream
- /overlay full-screen page redirects to / when master is off
- mjpeg.Broadcaster.Clear() empties the cached last frame so snapshots
  do not serve stale data after a feed is disabled

UI:
- Master "Overlay feed" toggle in the stats bar
- Overlay card is hidden when master is off; img src is removed so the
  browser does not retry against a 503 endpoint
- RTSP overlay sub-toggle is disabled in the UI when master is off

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 14:41:12 -05:00
ryan.mcgeeandClaude Sonnet 4.6 55df910b61 Security hardening: timeouts, body limits, file perms, headers, URL validation
- HTTP server: add ReadHeaderTimeout (5s) and WriteTimeout (10s); MJPEG
  streams clear the deadline via conn.SetDeadline after hijacking so
  long-lived streams are unaffected
- OctoPrint client: cap response body at 1 MiB with io.LimitReader to
  prevent memory exhaustion from a misbehaving OctoPrint instance
- Feed toggle API: wrap r.Body with http.MaxBytesReader(1024) to prevent
  DoS via oversized POST body
- State file: tighten permissions from 0644/0755 to 0640/0750
- Middleware: add X-Content-Type-Options: nosniff to all responses
- Config: reject OCTOPRINT_URL with non-http/https scheme to prevent SSRF
  via file://, gopher://, etc.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 09:22:36 -05:00
ryan.mcgeeandClaude Sonnet 4.6 f3f9fdfcc5 Add /webcam/?action=stream and /webcam/?action=snapshot compat routes
OctoPi's default haproxy config proxies /webcam/ to port 8080 using the
traditional mjpg_streamer URL format. Without these routes octocam returns
404, which causes OctoPrint Classic Webcam to show "Webcam stream not
loaded". The handler maps ?action=stream to the raw MJPEG broadcaster
and ?action=snapshot to the raw JPEG snapshot.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 09:13:00 -05:00
ryan.mcgeeandClaude Sonnet 4.6 866002cd6c Add CORS headers and remove Connection: close from MJPEG streams
OctoPrint Classic Webcam plugin sets crossorigin="anonymous" on its
<img> element, which forces a CORS check in the browser. Without
Access-Control-Allow-Origin: * the browser silently blocks the stream
even though the snapshot (fetched server-side by OctoPrint) loads fine.

Applied via middleware so every route gets the header. Also dropped
Connection: close from the MJPEG broadcaster — it is unnecessary for
a streaming response and can confuse some HTTP clients.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 08:44:30 -05:00
ryan.mcgeeandClaude Sonnet 4.6 f1f5396d5b Bare feed pages and JPEG snapshot endpoints
/raw and /overlay now serve a no-chrome full-screen image, matching
ustreamer's minimal feed view. /snapshot/raw and /snapshot/overlay
return the latest captured JPEG frame as image/jpeg (503 until the
first frame arrives). Broadcaster stores the last published frame
for snapshot reads.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 08:34:40 -05:00
ryan.mcgeeandClaude Sonnet 4.6 cc3fa47ae7 Add runtime toggles for RTSP feeds
Each RTSP feed (raw, overlay) can now be enabled or disabled at runtime
without restarting the service.

API:
  GET  /api/feeds                  → {"raw":{enabled,running},"overlay":{...}}
  POST /api/feeds/raw              ← {"enabled": false}
  POST /api/feeds/overlay          ← {"enabled": true}

When disabled, the ffmpeg subprocess is stopped and watchExit skips the
auto-restart. Re-enabling starts a fresh subprocess immediately.

The main web page shows two toggle switches in the stats bar (one per
RTSP feed). State is loaded on page open and updated instantly on toggle.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 16:56:31 -05:00
ryan.mcgeeandClaude Sonnet 4.6 58d5b59355 Add individual feed pages at /raw and /overlay
Each feed now has its own full-screen page (black background, image
fills the viewport) with a minimal nav bar linking back to the combined
view and to the other feed. The main page gains expand links (⤢) on
each card header.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 16:49:00 -05:00
ryan.mcgeeandClaude Sonnet 4.6 4adf3a5c2f Switch browser to MJPEG HTTP, reduce CPU, fix live feeds
Browser: replace WebRTC/HLS with direct MJPEG HTTP streams
(/stream/raw, /stream/overlay) served by the Go server itself.
<img> tag, no JavaScript, works in every browser, zero extra encoding.

CPU:
- Raw RTSP now uses ffmpeg -c:v copy (MJPEG passthrough), eliminating
  one full libx264 encode pass.
- Overlay RTSP throttled to 5fps (OCTOCAM_OVERLAY_RTSP_FPS); browser
  still receives the full camera framerate via MJPEG HTTP.

Architecture: new internal/mjpeg.Broadcaster fans JPEG frames out to
any number of HTTP clients with per-client drop-on-full buffering.
RTSP URLs shown in the stats bar for VLC / OctoPrint use.

Fix .gitignore to use /octocam so it doesn't match cmd/octocam dir.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 16:43:43 -05:00
ryan.mcgeeandClaude Sonnet 4.6 3b9b9217ca Fix black streams: rebase MediaMTX URLs to public host
The web UI was embedding 127.0.0.1:8889/8888 as the MediaMTX WebRTC
and HLS base URLs. Browsers on other machines tried to connect to their
own localhost, getting nothing. Now handleIndex derives the public host
from the incoming HTTP request's Host header and substitutes it into
the MediaMTX URLs, keeping the port.

Also removed readIPs: [] from mediamtx.yml — an empty list blocks all
readers in MediaMTX, which caused it to drop the ffmpeg publisher after
a timeout (the overlay broken pipe).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 16:23:09 -05:00
ryan.mcgeeandClaude Sonnet 4.6 7d155d5b13 Initial implementation of OctoCam
Go service that reads a USB webcam via V4L2, overlays OctoPrint stats
(state, filename, temps, progress, ETA) on one stream, and publishes
both raw and overlay feeds to MediaMTX via RTSP for WebRTC/HLS/RTSP
fan-out. Includes web UI, systemd units, and installer script.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 15:48:09 -05:00