From a4161daf470139b1bd697ed03eeb68fcc18e8a40 Mon Sep 17 00:00:00 2001 From: Giovanni Harting <539@idlegandalf.com> Date: Sat, 6 Dec 2025 20:09:32 +0100 Subject: [PATCH] feat: add playback-health script for buffering/drop warnings --- scripts/playback-health.lua | 202 ++++++++++++++++++++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 scripts/playback-health.lua diff --git a/scripts/playback-health.lua b/scripts/playback-health.lua new file mode 100644 index 0000000..42ff52f --- /dev/null +++ b/scripts/playback-health.lua @@ -0,0 +1,202 @@ +--[[ + playback-health.lua - monitors playback health and warns on problems. + + Features: + * Shows OSD warnings only when issues occur (buffering, frame drops, low cache). + * Silent during normal playback - no clutter when everything is fine. + * Detects network streams automatically; cache warnings only apply to streams. + * Frame drop monitoring works for all sources (local and network). + * Cooldown prevents warning spam; optional recovery message when issues resolve. + + Configuration: + Edit the `config` table below to tune thresholds and display options. +]] + +local config = { + cache_warning_seconds = 5, -- warn when cache falls below this (streams only) + drop_rate_threshold = 1.0, -- drops per second to trigger warning + drop_window_seconds = 5, -- time window for measuring drop rate + + osd_duration = 3, -- how long warnings stay visible + show_buffering_percent = true, -- show buffer % while buffering + show_recovery = true, -- show recovery message when issues resolve + cooldown_seconds = 10, -- minimum time between repeated warnings of same type + + monitor_cache = true, -- enable low cache warnings (streams) + monitor_drops = true, -- enable frame drop warnings (all sources) + + log_debug = false, +} + +-- No edits are required below unless you want to change script behavior. + +local mp = require("mp") +local msg = require("mp.msg") + +local state = { + is_stream = false, + buffering = false, + last_drop_count = 0, + last_drop_check = 0, + last_warning = {}, + active_warning = nil, + check_timer = nil, +} + +local function dbg(text) + if config.log_debug then + msg.info("playback-health: " .. text) + end +end + +local function is_network_path(path) + if not path then + return false + end + return path:match("^https?://") + or path:match("^rtmps?://") + or path:match("^rtsps?://") + or path:match("^mms://") + or path:match("^mmsh://") +end + +local function should_warn(warning_type) + local now = mp.get_time() + local last = state.last_warning[warning_type] + if last and (now - last) < config.cooldown_seconds then + return false + end + return true +end + +local function show_warning(warning_type, message) + if not should_warn(warning_type) then + dbg(("suppressed %s warning (cooldown)"):format(warning_type)) + return + end + state.last_warning[warning_type] = mp.get_time() + state.active_warning = warning_type + mp.osd_message("⚠ " .. message, config.osd_duration) + dbg(("warning: %s"):format(message)) +end + +local function show_recovery() + if state.active_warning and config.show_recovery then + mp.osd_message("✓ Recovered", config.osd_duration) + dbg("recovery shown") + end + state.active_warning = nil +end + +local function reset_state() + state.buffering = false + state.last_drop_count = 0 + state.last_drop_check = 0 + state.last_warning = {} + state.active_warning = nil +end + +local function on_path_change(_, path) + state.is_stream = is_network_path(path) + reset_state() + if path then + dbg(("loaded: %s (stream=%s)"):format(path, tostring(state.is_stream))) + end +end + +local function on_paused_for_cache(_, paused) + if not state.is_stream then + return + end + if paused and not state.buffering then + state.buffering = true + local pct = mp.get_property_number("cache-buffering-state", 0) + if config.show_buffering_percent then + show_warning("buffering", ("Buffering... %d%%"):format(pct)) + else + show_warning("buffering", "Buffering...") + end + elseif not paused and state.buffering then + state.buffering = false + show_recovery() + end +end + +local function on_cache_buffering_state(_, pct) + if not state.is_stream or not state.buffering then + return + end + if pct and config.show_buffering_percent then + mp.osd_message(("⚠ Buffering... %d%%"):format(pct), config.osd_duration) + end +end + +local function on_cache_duration(_, duration) + if not state.is_stream or not config.monitor_cache then + return + end + if state.buffering then + return + end + if duration and duration < config.cache_warning_seconds and duration > 0 then + show_warning("cache", ("Low cache: %.1fs remaining"):format(duration)) + end +end + +local function check_drop_rate() + if not config.monitor_drops then + return + end + + local now = mp.get_time() + local drops = mp.get_property_number("drop-frame-count", 0) + + if state.last_drop_check == 0 then + state.last_drop_count = drops + state.last_drop_check = now + return + end + + local elapsed = now - state.last_drop_check + if elapsed < config.drop_window_seconds then + return + end + + local new_drops = drops - state.last_drop_count + local rate = new_drops / elapsed + + if rate >= config.drop_rate_threshold then + show_warning("drops", ("Dropping frames: %.1f/s"):format(rate)) + elseif state.active_warning == "drops" and rate < config.drop_rate_threshold / 2 then + show_recovery() + end + + state.last_drop_count = drops + state.last_drop_check = now +end + +local function start_monitoring() + if state.check_timer then + state.check_timer:kill() + end + state.check_timer = mp.add_periodic_timer(config.drop_window_seconds, check_drop_rate) + dbg("monitoring started") +end + +local function stop_monitoring() + if state.check_timer then + state.check_timer:kill() + state.check_timer = nil + end + reset_state() + dbg("monitoring stopped") +end + +mp.observe_property("path", "string", on_path_change) +mp.observe_property("paused-for-cache", "bool", on_paused_for_cache) +mp.observe_property("cache-buffering-state", "number", on_cache_buffering_state) +mp.observe_property("demuxer-cache-duration", "number", on_cache_duration) + +mp.register_event("file-loaded", start_monitoring) +mp.register_event("end-file", stop_monitoring) +mp.register_event("shutdown", stop_monitoring)