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:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user