feat: add playback-health script for buffering/drop warnings
This commit is contained in:
202
scripts/playback-health.lua
Normal file
202
scripts/playback-health.lua
Normal file
@@ -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)
|
||||||
Reference in New Issue
Block a user