Commit graph

16 commits

Author SHA1 Message Date
Jean-Michel Tremblay
9d52e46d1e Add mediapi-watchdog: reboot the Pi if the mpv player wedges
On 2026-07-09 a kernel keyring-GC oops left the player stuck in
uninterruptible D-state while systemd, SSH, and the Flask app stayed
alive -- so the box sat dead all night instead of recovering. A plain
systemd/hardware watchdog only fires on a TOTAL hang and would not have
caught that partial wedge.

mediapi-watchdog.service (Type=simple, Restart=always, runs as root)
pings mpv over its JSON IPC socket every 30s; after ~3 min of continuous
failure it forces a reboot (systemctl reboot -ff, then SysRq as a
kernel-level fallback that works even when userspace is wedged). A
D-state mpv accepts the socket connect but never replies, so the ping
times out and is caught. install.sh installs + enables it.

Verified: no false positives while mpv is healthy; correctly detects a
stopped mpv and reaches the reboot decision (tested in dry-run).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 20:03:45 -04:00
Jean-Michel Tremblay
799a5d5467 Fix mpv never starting: drop the PAMName=login hang, tear down old Kodi
The mpv service inherited Kodi's VT-grabbing block (PAMName=login +
StandardInput=tty + TTYPath=/dev/tty1). Opening a "login" PAM session on
tty1 hangs in systemd's pre-exec setup, so mpv was never exec'd at all --
the service sat "running" as systemd-executor with an empty (mpv) cmdline,
no IPC socket, no output.

Two fixes:

1. Simplify the unit. mpv doesn't need a login session or the tty: as the
   sole DRM client on the seat it becomes DRM master implicitly on first
   open, so plain video+render+audio group membership is enough. Removing
   the PAM/tty block lets mpv actually start, initialize the vc4 KMS
   display, and play (verified end-to-end: play/status/next through the
   Flask API, position advancing on screen).

2. install.sh now tears down any leftover Kodi before starting mpv. The
   real trigger this time was an in-place migration: replacing the unit
   files doesn't stop an already-running Kodi, which kept DRM master and
   the tty1 seat and blocked mpv from ever getting the display.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 19:04:45 -04:00
Jean-Michel Tremblay
5972ac2736 Replace Kodi with mpv as the player; drive it over its JSON IPC socket
Kodi was overkill for a phone-driven "play this video" box: a full media
center (web server, CEC, library DB) whose surface area is exactly what
wedged the Pi -- a video-decode session left Kodi stuck in an
uninterruptible firmware-mailbox call after a kernel keyring Oops, dead
until a power cycle.

mpv is just a video player: hardware-decoded straight on KMS/DRM, no
media-center baggage. It runs as its own --idle systemd service holding
the playlist, so playback keeps going even if the app/phone/WiFi drop --
the same autonomy Kodi's native playlist gave us. The app talks to it
over its JSON IPC Unix socket.

PlayerStateManager keeps the exact same public API, so the Flask routes,
templates, and UI are unchanged (only the engine underneath swaps out).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 18:16:20 -04:00
Jean-Michel Tremblay
b610fc9652 Drive playback via Kodi's native playlist so it never depends on the app
Playback stopped advancing whenever the mediapi app disconnected/restarted,
because auto-advance ran in the app's poll loop. Move it into Kodi: playing a
file/folder loads the whole folder into Kodi's video playlist and starts it, so
Kodi advances (and loops, when keep-playing is on) entirely on its own -- it
keeps running even if the app, phone, or WiFi drop.

- play_file/play_folder build the Kodi video playlist (Playlist.Clear/Add +
  Player.Open at the chosen position); remove the in-app queue + poll-advance
- next/previous are now Player.GoTo next/previous (playlist navigation)
- keep-playing maps to Kodi repeat: on=all (loop folder), off=play through once;
  default ON, applied via Player.SetRepeat
- checkbox no longer clobbered by the poll for 1.5s after a manual toggle

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 22:53:01 -04:00
Jean-Michel Tremblay
f65600cff7 Make the progress bar draggable to seek to a position
The seek slider was display-only; enable it as a scrubber. Adds absolute-seek
in the backend (PlayerStateManager.seek_to via Kodi Player.Seek with an absolute
time) and the /api/control/seekto route. The slider sends the seek on release;
the poll loop stops overwriting the slider while dragging and for a short window
after a seek so it doesn't snap back before Kodi reports the new position.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 20:15:09 -04:00
Jean-Michel Tremblay
1290f784e7 Add Next/Previous clip buttons
Skip forward/back through the current folder queue -- the player already keeps
the folder as an ordered queue with an index, so next/previous just move within
it (no-op at the ends). New PlayerStateManager.next/previous/skip, the
/api/control/next|previous routes, and compact ⏮/⏭ buttons in the transport row.

Lets you skip clips completely without a resume/history feature.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 19:53:35 -04:00
Jean-Michel Tremblay
99b5c27e65 install.sh: route Kodi audio to HDMI + ensure media roots exist
- After Kodi starts, set audiooutput.audiodevice to the HDMI sink via JSON-RPC
  (Kodi otherwise defaults to the analog jack). Waits for the web server, is
  overridable via MEDIAPI_KODI_AUDIO_DEVICE.
- mkdir the configured MEDIAPI_MEDIA_ROOTS so browsing works and there's a spot
  to copy content into.

Verified live on the Pi: playback + HDMI audio work end to end via the app's
Player.Open path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 19:09:08 -04:00
Jean-Michel Tremblay
afe1ab3e2f Replace mpv with Kodi as the player; mediapi drives it over JSON-RPC
Playback moves to Kodi (standalone on GBM/KMS -- the smooth, hardware-decoded
LibreELEC path). mediapi becomes a thin remote: it browses media and controls
Kodi over its JSON-RPC HTTP API. This drops every mpv/DRM/X-mirror problem
(DRM-master exclusivity, gpu-next "export failed" wedges, X-mirror A/V desync,
software-decode choppiness) -- none of which had a working single config.

- new mediapi/kodi.py (stdlib JSON-RPC client); player.py rewritten to poll
  and drive Kodi (Player.Open/PlayPause/Seek, Application.SetVolume,
  Player.GetProperties), same public interface + keep-playing auto-advance
- config.py: KODI_* settings replace MPV_SOCKET; __init__ + api_routes updated
- delete mpv_ipc.py, the mpv/X units, start-mpv/session scripts
- new systemd/mediapi-kodi.service.template (standalone Kodi on tty1) +
  scripts/configure-kodi.py (headlessly enable Kodi's JSON-RPC web server)
- install.sh now bootstraps a BARE Pi OS Lite end to end: apt base packages,
  installs uv, installs Kodi, enables its web API, configures the AP, installs
  + starts services (only git need be preinstalled). Auto-seeds .env from the
  example on first run.
- README + .env.example updated for the Kodi architecture

Dual HDMI is handled in hardware (external splitter off one port); no software
mirror -- see git history for why that can't work smoothly on this Pi.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 22:42:04 -04:00
Jean-Michel Tremblay
ee72d8b735 Drive both HDMI outputs via X kiosk + single mirrored mpv
The Pi 4's two HDMI connectors share one vc4 DRM card, and DRM master is
exclusive per card -- so the old per-connector-mpv design could never light
the second screen (the mirror mpv died with "Failed to acquire DRM master:
Permission denied"). Replace it with a minimal X server started by the
mediapi-mpv unit via xinit: X is the single DRM master, xrandr --same-as
clones the first output onto every other connected HDMI, and one fullscreen
mpv renders to both. Uses hwdec=v4l2m2m-copy (Pi HW decoder, ~1/3 the CPU of
software) and carries audio + the app's sole IPC socket.

- new scripts/mediapi-session.sh (X client: waits for connectors, mirrors,
  execs mpv); rewritten mediapi-mpv unit (xinit on VT7)
- delete scripts/start-mpv.py and all mirror-socket/broadcast code in
  player.py, config.py, __init__.py -- a single mpv means one socket
- install.sh installs xserver-xorg-core/xinit/x11-xserver-utils, writes
  /etc/X11/Xwrapper.config, adds the user to input,tty
- README + troubleshooting updated

Verified from a cold boot on the Pi: both pixelvalve CRTCs scan the same
framebuffer; app healthy; HW decode engaged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 20:59:05 -04:00
Jean-Michel Tremblay
8ae4b40263 fixes 2026-07-06 21:21:17 -04:00
Jean-Michel Tremblay
cbda10c62c fully automated install script 2026-07-06 20:33:46 -04:00
Jean-Michel Tremblay
db06ae252d mpv unit: wait for udev/DRM devices before starting
Cold-boot race could leave mpv running but not rendering to HDMI (console
stays visible) even though the same command works when run manually later.
Order the unit after systemd-udev-settle so /dev/dri/* exists first.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 22:19:52 -04:00
Jean-Michel Tremblay
ed9da9a065 Extract config into .env; add deploy.sh with overlay guard + rollback
- Move user-specific config (login, media roots, port, AP ssid/psk, service
  user) into a gitignored .env; add .env.example template.
- config.py reads .env at startup via a tiny zero-dependency parser (works for
  both `uv run` and systemd), and fails loudly if required vars are unset.
- load_secret_key falls back to an ephemeral key on a read-only filesystem
  instead of crashing.
- systemd units become .template files; deploy.sh renders them with the
  .env-derived user/paths.
- deploy.sh: refuses to run on a read-only overlay, git pull --ff-only,
  uv sync, ensure secret key, re-apply the AP from .env, install units,
  restart, health-check with automatic rollback to the previous commit.
- Scrub literal credentials/ssid out of the README; document .env + deploy.sh.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 22:06:47 -04:00
Jean-Michel Tremblay
3a50ba1e3f Configure origin to push to both remotes at once
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 21:48:29 -04:00
Jean-Michel Tremblay
b61698db02 Document git remotes (forgejo primary, github mirror)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 21:47:31 -04:00
Jean-Michel Tremblay
b8cd65b8ca Initial mediapi: WiFi AP + media browser with HDMI playback control
Raspberry Pi 4 appliance that boots as a WiFi access point and serves a
phone-controlled web app for browsing /localmedia and driving video
playback out the Pi's HDMI port.

- NetworkManager-based AP setup (documented in README)
- Flask app + vanilla HTML/JS single-page UI (Control / Playback modes)
- mpv persistent daemon (DRM/KMS HDMI output) controlled over JSON IPC
- long-lived session-cookie auth
- "keep playing" folder auto-advance, including when started mid-folder
- systemd units for both mpv and the app, autostart on boot

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 21:45:57 -04:00