fix: improve playback-health and touch-gestures tuning

- playback-health: skip bandwidth warning when buffer is healthy
  (configurable via bandwidth_buffer_mult, default 10× warning threshold)
- touch-gestures: use finer steps at low volume/brightness (<10%)
  (configurable via volume_step_fine, brightness_step_fine, fine_threshold)
This commit is contained in:
2025-12-14 12:28:57 +01:00
parent 256cfc5f82
commit f4064e013c
2 changed files with 227 additions and 9 deletions

View File

@@ -7,6 +7,9 @@
* 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.
* Color-coded warnings: yellow for warnings, red for critical, green for recovery.
* Buffer prediction: estimates time until stall or resume based on fill rate.
* Bandwidth monitoring: warns when download speed can't keep up with playback.
Configuration:
Edit the `config` table below to tune thresholds and display options.
@@ -14,16 +17,28 @@
local config = {
cache_warning_seconds = 5, -- warn when cache falls below this (streams only)
cache_critical_seconds = 2, -- critical threshold (red warning)
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_buffer_prediction = true, -- show estimated time to fill/empty
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)
monitor_bandwidth = true, -- warn when download speed < playback rate
bandwidth_ratio_warn = 0.9, -- warn when download/playback ratio below this
bandwidth_samples = 5, -- samples to average before warning
bandwidth_buffer_mult = 10, -- skip bandwidth warning if cache above warning × this
use_colors = true, -- colorize OSD messages
color_warning = "FFFF00", -- yellow (RGB hex)
color_critical = "FF4444", -- red
color_recovery = "44FF44", -- green
color_info = "88CCFF", -- light blue for predictions
log_debug = false,
}
@@ -41,6 +56,11 @@ local state = {
last_warning = {},
active_warning = nil,
check_timer = nil,
cache_history = {},
last_cache_time = 0,
cache_rate = nil,
osd_timer = nil,
bandwidth_history = {},
}
local function dbg(text)
@@ -49,6 +69,98 @@ local function dbg(text)
end
end
local function rgb_to_bgr(rgb)
local r = rgb:sub(1, 2)
local g = rgb:sub(3, 4)
local b = rgb:sub(5, 6)
return b .. g .. r
end
local function colorize(text, color_rgb)
if not config.use_colors then
return text
end
local bgr = rgb_to_bgr(color_rgb)
return "{\\1c&H" .. bgr .. "&}" .. text .. "{\\1c&HFFFFFF&}"
end
local function clear_osd()
mp.set_osd_ass(0, 0, "")
end
local function show_osd(text, duration)
if state.osd_timer then
state.osd_timer:kill()
end
local ass = "{\\an9}" .. text
mp.set_osd_ass(0, 0, ass)
state.osd_timer = mp.add_timeout(duration, clear_osd)
end
local function update_cache_rate(duration)
local now = mp.get_time()
if state.last_cache_time == 0 then
state.last_cache_time = now
state.cache_history = { duration }
return
end
local elapsed = now - state.last_cache_time
if elapsed < 0.5 then
return
end
table.insert(state.cache_history, duration)
if #state.cache_history > 6 then
table.remove(state.cache_history, 1)
end
if #state.cache_history >= 2 then
local oldest = state.cache_history[1]
local newest = state.cache_history[#state.cache_history]
local samples = #state.cache_history - 1
local time_span = elapsed * samples / #state.cache_history
if time_span > 0 then
state.cache_rate = (newest - oldest) / time_span
end
end
state.last_cache_time = now
end
local function predict_buffer_time(current_cache, target_cache)
if not state.cache_rate or state.cache_rate == 0 then
return nil
end
local diff = target_cache - current_cache
local time = diff / state.cache_rate
if time < 0 then
return nil
end
return time
end
local function format_prediction(seconds)
if not seconds or seconds < 0 then
return nil
end
if seconds < 60 then
return ("~%.0fs"):format(seconds)
else
return ("~%.1fm"):format(seconds / 60)
end
end
local function format_bitrate(bps)
if bps >= 1000000 then
return ("%.1f Mbps"):format(bps / 1000000)
elseif bps >= 1000 then
return ("%.0f kbps"):format(bps / 1000)
else
return ("%.0f bps"):format(bps)
end
end
local function is_network_path(path)
if not path then
return false
@@ -69,20 +181,23 @@ local function should_warn(warning_type)
return true
end
local function show_warning(warning_type, message)
local function show_warning(warning_type, message, color)
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)
local col = color or config.color_warning
local styled = colorize("" .. message, col)
show_osd(styled, 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)
local styled = colorize("✓ Recovered", config.color_recovery)
show_osd(styled, config.osd_duration)
dbg("recovery shown")
end
state.active_warning = nil
@@ -94,6 +209,10 @@ local function reset_state()
state.last_drop_check = 0
state.last_warning = {}
state.active_warning = nil
state.cache_history = {}
state.last_cache_time = 0
state.cache_rate = nil
state.bandwidth_history = {}
end
local function on_path_change(_, path)
@@ -126,8 +245,24 @@ 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)
if pct then
local parts = { "Buffering..." }
if config.show_buffering_percent then
table.insert(parts, ("%d%%"):format(pct))
end
if config.show_buffer_prediction and state.cache_rate and state.cache_rate > 0 then
local cache = mp.get_property_number("demuxer-cache-duration", 0)
local target = config.cache_warning_seconds * 2
local eta = predict_buffer_time(cache, target)
local eta_str = format_prediction(eta)
if eta_str then
local pred = colorize("(" .. eta_str .. " to resume)", config.color_info)
table.insert(parts, pred)
end
end
local text = table.concat(parts, " ")
local styled = colorize("" .. text, config.color_warning)
show_osd(styled, config.osd_duration)
end
end
@@ -135,15 +270,36 @@ local function on_cache_duration(_, duration)
if not state.is_stream or not config.monitor_cache then
return
end
if duration then
update_cache_rate(duration)
end
if state.buffering then
return
end
if duration and duration < config.cache_warning_seconds and duration > 0 then
local time_remaining = mp.get_property_number("playtime-remaining", 999)
if time_remaining < config.cache_warning_seconds * 2 then
return
end
show_warning("cache", ("Low cache: %.1fs remaining"):format(duration))
local is_critical = duration < config.cache_critical_seconds
local color = is_critical and config.color_critical or config.color_warning
local parts = { ("Low cache: %.1fs"):format(duration) }
if config.show_buffer_prediction and state.cache_rate and state.cache_rate < 0 then
local eta = predict_buffer_time(duration, 0)
local eta_str = format_prediction(eta)
if eta_str then
local pred = colorize("(stall in " .. eta_str .. ")", config.color_info)
table.insert(parts, pred)
end
end
show_warning("cache", table.concat(parts, " "), color)
end
end
@@ -179,11 +335,67 @@ local function check_drop_rate()
state.last_drop_check = now
end
local function check_bandwidth()
if not config.monitor_bandwidth or not state.is_stream or state.buffering then
return
end
local cache_duration = mp.get_property_number("demuxer-cache-duration", 0)
if cache_duration >= config.cache_warning_seconds * config.bandwidth_buffer_mult then
return
end
local cache_state = mp.get_property_native("demuxer-cache-state", {})
local download_rate = cache_state["raw-input-rate"]
if not download_rate or download_rate <= 0 then
return
end
local video_bitrate = mp.get_property_number("video-bitrate", 0)
local audio_bitrate = mp.get_property_number("audio-bitrate", 0)
local playback_rate = video_bitrate + audio_bitrate
if playback_rate <= 0 then
return
end
local ratio = (download_rate * 8) / playback_rate
table.insert(state.bandwidth_history, ratio)
if #state.bandwidth_history > config.bandwidth_samples then
table.remove(state.bandwidth_history, 1)
end
if #state.bandwidth_history < config.bandwidth_samples then
return
end
local avg = 0
for _, r in ipairs(state.bandwidth_history) do
avg = avg + r
end
avg = avg / #state.bandwidth_history
if avg < config.bandwidth_ratio_warn then
local dl_mbps = format_bitrate(download_rate * 8)
local need_mbps = format_bitrate(playback_rate)
local warn_text = ("Slow network: %s < %s needed"):format(dl_mbps, need_mbps)
show_warning("bandwidth", warn_text, config.color_warning)
elseif state.active_warning == "bandwidth" and avg > 1.1 then
show_recovery()
state.bandwidth_history = {}
end
end
local function periodic_check()
check_drop_rate()
check_bandwidth()
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)
state.check_timer = mp.add_periodic_timer(config.drop_window_seconds, periodic_check)
dbg("monitoring started")
end

View File

@@ -22,7 +22,10 @@ local config = {
steps_per_screen = 40, -- target steps across full height; 50% height ≈ 100% change with 5% steps
deadzone_px = 4, -- ignore tiny jiggles before the first step (lower = snappier)
volume_step = 5, -- percent per step
volume_step_fine = 1, -- step when below fine_threshold
brightness_step = 5, -- screen brightness percent per step
brightness_step_fine = 1, -- step when below fine_threshold
fine_threshold = 10, -- use fine steps below this percent
seek_step = 5, -- seconds per seek step
max_volume = 130, -- clamp volume to this ceiling
min_brightness = 1, -- minimum brightness percent (avoid turning off screen)
@@ -281,14 +284,17 @@ local function apply_steps(kind, steps)
if not current then
return
end
local next_value = current + (steps * config.volume_step)
local step = current < config.fine_threshold and config.volume_step_fine or config.volume_step
local next_value = current + (steps * step)
set_volume_percent(next_value)
elseif kind == "brightness" then
local current = current_brightness_percent()
if not current then
return
end
local next_value = current + (steps * config.brightness_step)
local step = current < config.fine_threshold and config.brightness_step_fine
or config.brightness_step
local next_value = current + (steps * step)
set_brightness_percent(next_value)
elseif kind == "seek" then
local seconds = steps * config.seek_step