feat: keyboard seek, playback speed, and persist edited settings
Player - Arrow keys seek by 5s, Shift for a 30s jump on long recordings, J/L for +-30s, Space toggles playback, and up/down adjust volume. Dragging a scrubber across a multi-hour replay is unusable — one pixel is tens of seconds — so keys are the practical way to move around a long recording. - The shortcuts ignore key events from inputs, so typing a chapter time in the replay editor no longer scrubs the video. - Add a playback speed selector. It is a replay tool: speeding a live stream up would only push it further ahead of the encoder's buffer. Replay editor - Stop playback restarting on every edit. The player's source effect depended on the `lines` and `qualities` arrays, and callers such as the replay editor pass fresh literals on every render, so adding a chapter or typing a title was treated as a source change and reloaded the stream from the beginning. The effect now depends on the resolved source URL instead of the arrays it came from. Measured: adding three chapters leaves the media requests unchanged and the playhead advancing. Settings persistence - Settings edited in the UI were applied to the in-memory settings object and never written down, so the brand, transcode ladder and load-balancing strategy silently reverted to the config.py defaults on every restart. They are now saved to backend/settings.json (atomic temp-file replace) and re-applied at startup, with only known keys accepted so a hand-edited file cannot inject arbitrary attributes. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
1 parent
cdf591f933
commit
8e61dc111c
7 files changed
+232
-29
No files matched your search
@@ -0,0 +1,55 @@
|
||||
"""Persist the settings an administrator edits in the UI.
|
||||
|
||||
config.py holds the defaults, but anything changed at runtime has to survive a
|
||||
restart — otherwise editing the brand or the transcode ladder appears to work and
|
||||
is silently reverted the next time the backend starts. Writes are atomic (temp
|
||||
file + replace) so a crash mid-save cannot leave a half-written file that fails
|
||||
to parse on boot.
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from config import settings
|
||||
|
||||
STORE_PATH = Path(__file__).resolve().parent.parent / "settings.json"
|
||||
|
||||
# Settings the UI may change, with the attribute they map to on Settings.
|
||||
_MANAGED = (
|
||||
"brand_name",
|
||||
"brand_tagline",
|
||||
"brand_logo_url",
|
||||
"load_balance_strategy",
|
||||
"multicast_enabled",
|
||||
"transcode_profiles",
|
||||
"qualities",
|
||||
)
|
||||
|
||||
|
||||
def load_settings() -> None:
|
||||
"""Apply saved overrides on top of the config defaults. Never raises."""
|
||||
if not STORE_PATH.exists():
|
||||
return
|
||||
try:
|
||||
saved = json.loads(STORE_PATH.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError):
|
||||
# A corrupt file must not stop the server from booting; the defaults from
|
||||
# config.py are still usable.
|
||||
return
|
||||
if not isinstance(saved, dict):
|
||||
return
|
||||
for key, value in saved.items():
|
||||
# Only known keys, so a stale or hand-edited file cannot inject arbitrary
|
||||
# attributes onto the settings object.
|
||||
if key in _MANAGED and value is not None:
|
||||
setattr(settings, key, value)
|
||||
|
||||
|
||||
def save_settings() -> None:
|
||||
payload = {key: getattr(settings, key) for key in _MANAGED}
|
||||
temporary = STORE_PATH.with_suffix(".json.tmp")
|
||||
temporary.write_text(
|
||||
json.dumps(payload, ensure_ascii=False, indent=2),
|
||||
encoding="utf-8",
|
||||
)
|
||||
temporary.replace(STORE_PATH)
|
||||
Reference in new issue
Block a user