You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

261 lines
7.2 KiB

-- use xrandr command to set output to best fitting fps rate
-- when playing videos with mpv.
utils = require 'mp.utils'
xrandr_blacklist = {}
function xrandr_parse_blacklist()
-- Parse the optional "blacklist" from a string into an array for later use.
-- For now, we only support a list of rates, since the "mode" is not subject
-- to automatic change (mpv is better at scaling than most displays) and
-- this also makes the blacklist option more easy to specify:
local b = mp.get_opt("xrandr-blacklist")
if (b == nil) then
return
end
local i = 1
for s in string.gmatch(b, "([^, ]+)") do
xrandr_blacklist[i] = 0.0 + s
i = i+1
end
end
xrandr_parse_blacklist()
function xrandr_check_blacklist(mode, rate)
-- check if (mode, rate) is black-listed - e.g. because the
-- computer display output is known to be incompatible with the
-- display at this specific mode/rate
for i=1,#xrandr_blacklist do
r = xrandr_blacklist[i]
if (r == rate) then
mp.msg.log("info", "will not use mode '" .. mode .. "' with rate " .. rate .. " because option -script-opts=xrandr-blacklist said so")
return true
end
end
return false
end
xrandr_detect_done = false
xrandr_modes = {}
xrandr_connected_outputs = {}
function xrandr_detect_available_rates()
if (xrandr_detect_done) then
return
end
xrandr_detect_done = true
-- invoke xrandr to find out which fps rates are available on which outputs
local p = {}
p["cancellable"] = false
p["args"] = {}
p["args"][1] = "xrandr"
p["args"][2] = "-q"
local res = utils.subprocess(p)
if (res["error"] ~= nil) then
mp.msg.log("info", "failed to execute 'xrand -q', error message: " .. res["error"])
return
end
mp.msg.log("v","xrandr -q\n" .. res["stdout"])
local output_idx = 1
for output in string.gmatch(res["stdout"], '\n([^ ]+) connected') do
table.insert(xrandr_connected_outputs, output)
-- the first line with a "*" after the match contains the mode associated with the mode
local mls = string.match(res["stdout"], "\n" .. string.gsub(output, "%p", "%%%1") .. " connected.*")
local r
local mode
mode, r = string.match(mls, '\n ([0-9x]+) ([^*\n]*%*[^\n]*)')
if (r == nil) then
-- if no refresh rate is reported active for an output by xrandr,
-- search for the mode that is "recommended" (marked by "+" in xrandr's output)
mode, r = string.match(mls, '\n ([0-9x]+) ([^+\n]*%+[^\n]*)')
end
mp.msg.log("info", "output " .. output .. " mode=" .. mode .. " refresh rates = " .. r)
xrandr_modes[output] = { mode = mode, rates_s = r, rates = {} }
local i = 1
for s in string.gmatch(r, "([^ +*]+)") do
-- check if rate "r" is black-listed - this is checked here because
if (not xrandr_check_blacklist(mode, 0.0 + s)) then
xrandr_modes[output].rates[i] = 0.0 + s
i = i+1
end
end
output_idx = output_idx + 1
end
end
function xrandr_find_best_fitting_rate(fps, output)
local xrandr_rates = xrandr_modes[output].rates
-- try integer multipliers of 1 to 3, in that order
for m=1,3 do
-- check for a "perfect" match (where fps rates of e.g. 60.0 are not equal 59.9 or such)
for i=1,#xrandr_rates do
r = xrandr_rates[i]
if (math.abs(r-(m * fps)) < 0.001) then
return r
end
end
end
for m=1,3 do
-- check for a "less precise" match (where fps rates of e.g. 60.0 and 59.9 are assumed "equal")
for i=1,#xrandr_rates do
r = xrandr_rates[i]
if (math.abs(r-(m * fps)) < 0.2) then
if (m == 1) then
-- pass the original rate to xrandr later, because
-- e.g. a 23.976 Hz mode might be displayed as "24.0",
-- but still xrandr may set the better matching mode
-- if the exact number is passed
return fps
else
return r
end
end
end
end
-- if no known frame rate is any "good", use the highest available frame rate,
-- as this will probably cause the least "jitter"
local mr = 0.0
for i=1,#xrandr_rates do
r = xrandr_rates[i]
-- mp.msg.log("v","r=" .. r .. " mr=" .. mr)
if (r > mr) then
mr = r
end
end
return mr
end
xrandr_active_outputs = {}
function xrandr_set_active_outputs()
local dn = mp.get_property("display-names")
if (dn ~= nil) then
mp.msg.log("v","display-names=" .. dn)
xrandr_active_outputs = {}
for w in (dn .. ","):gmatch("([^,]*),") do
table.insert(xrandr_active_outputs, w)
end
end
end
-- last detected non-nil video frame rate:
xrandr_cfps = nil
-- for each output, we remember which refresh rate we set last, so
-- we do not unnecessarily set the same refresh rate again
xrandr_previously_set = {}
function xrandr_set_rate()
local f = mp.get_property_native("fps")
if (f == nil or f == xrandr_cfps) then
-- either no change or no frame rate information, so don't set anything
return
end
xrandr_cfps = f
xrandr_detect_available_rates()
xrandr_set_active_outputs()
local vdpau_hack = false
local old_vid = nil
local old_position = nil
if (mp.get_property("options/vo") == "vdpau") then
-- enable wild hack: need to close and re-open video for vdpau,
-- because vdpau barfs if xrandr is run while it is in use
vdpau_hack = true
old_position = mp.get_property("time-pos")
old_vid = mp.get_property("vid")
mp.set_property("vid", "no")
end
local outs = {}
if (#xrandr_active_outputs == 0) then
-- No active outputs - probably because vo (like with vdpau) does
-- not provide the information which outputs are covered.
-- As a fall-back, let's assume all connected outputs are relevant.
mp.msg.log("v","no output is known to be used by mpv, assuming all connected outputs are used.")
outs = xrandr_connected_outputs
else
outs = xrandr_active_outputs
end
-- iterate over all relevant outputs used by mpv's output:
for n, output in ipairs(outs) do
local bfr = xrandr_find_best_fitting_rate(xrandr_cfps, output)
if (bfr == 0.0) then
mp.msg.log("info", "no non-blacklisted rate available, not invoking xrandr")
else
mp.msg.log("info", "container fps is " .. xrandr_cfps .. "Hz, for output " .. output .. " mode " .. xrandr_modes[output].mode .. " the best fitting display fps rate is " .. bfr .. "Hz")
if (bfr == xrandr_previously_set[output]) then
mp.msg.log("v", "output " .. output .. " was already set to " .. bfr .. "Hz before - not changing")
else
-- invoke xrandr to set the best fitting refresh rate for output
local p = {}
p["cancellable"] = false
p["args"] = {}
p["args"][1] = "xrandr"
p["args"][2] = "--output"
p["args"][3] = output
p["args"][4] = "--mode"
p["args"][5] = xrandr_modes[output].mode
p["args"][6] = "--rate"
p["args"][7] = bfr
local res = utils.subprocess(p)
if (res["error"] ~= nil) then
mp.msg.log("error", "failed to set refresh rate for output " .. output .. " using xrandr, error message: " .. res["error"])
else
xrandr_previously_set[output] = bfr
end
end
end
end
if (vdpau_hack) then
mp.set_property("vid", old_vid)
mp.commandv("seek", old_position, "absolute", "keyframes")
end
end
-- we'll consider setting refresh rates whenever the video fps or the active outputs change:
mp.observe_property("fps", "native", xrandr_set_rate)
mp.observe_property("display-names", "native", xrandr_set_rate)