mediapi/mediapi/config.py
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

85 lines
2.8 KiB
Python

import logging
import os
from datetime import timedelta
log = logging.getLogger(__name__)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
INSTANCE_DIR = os.path.join(BASE_DIR, "instance")
SECRET_KEY_PATH = os.path.join(INSTANCE_DIR, "secret_key")
DOTENV_PATH = os.path.join(BASE_DIR, ".env")
def load_dotenv(path=DOTENV_PATH):
"""Minimal .env loader (no dependency). Populates os.environ for any key
not already set, so real environment variables always win over the file.
Works for both `uv run run.py` and the systemd-launched service."""
if not os.path.exists(path):
return
with open(path) as f:
for line in f:
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, val = line.partition("=")
key = key.strip()
val = val.strip().strip('"').strip("'")
if key:
os.environ.setdefault(key, val)
def require(name):
val = os.environ.get(name)
if not val:
raise RuntimeError(
f"{name} is not set -- copy .env.example to .env and fill it in"
)
return val
def load_secret_key():
"""Read the persisted session-signing key, generating it if missing.
On a read-only overlay filesystem the write will fail; in that case we
fall back to an ephemeral in-memory key so the app still starts (sessions
just won't survive a restart). install.sh generates this file while the
card is writable, so in normal operation the write path isn't hit."""
if os.path.exists(SECRET_KEY_PATH):
with open(SECRET_KEY_PATH) as f:
return f.read().strip()
key = os.urandom(32).hex()
try:
os.makedirs(INSTANCE_DIR, exist_ok=True)
with open(SECRET_KEY_PATH, "w") as f:
f.write(key)
except OSError:
log.warning(
"could not persist secret key (read-only filesystem?) -- using an "
"ephemeral key; logins won't survive an app restart"
)
return key
load_dotenv()
class Config:
SECRET_KEY = load_secret_key()
PERMANENT_SESSION_LIFETIME = timedelta(days=3650)
SESSION_COOKIE_SAMESITE = "Lax"
USERNAME = require("MEDIAPI_USERNAME")
PASSWORD = require("MEDIAPI_PASSWORD")
MEDIA_ROOTS = [
p for p in os.environ.get("MEDIAPI_MEDIA_ROOTS", "/localmedia").split(":") if p
]
# A single mpv drives every HDMI output -- the X session mirrors the screens
# with xrandr (see scripts/mediapi-session.sh) -- so there is one socket.
MPV_SOCKET = os.environ.get("MEDIAPI_MPV_SOCKET", "/run/mediapi/mpv.sock")
PORT = int(os.environ.get("MEDIAPI_PORT", "8080"))
VIDEO_EXTENSIONS = {
".mp4", ".mkv", ".avi", ".mov", ".m4v", ".webm", ".mpg", ".mpeg", ".ts", ".flv", ".wmv",
}