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>
85 lines
2.8 KiB
Python
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",
|
|
}
|