Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
executable file 385 lines (308 sloc) 11.9 KB
-- use xrandr command to set output to best fitting fps rate
-- when playing videos with mpv.
utils = require 'mp.utils'
-- if you want your display output switched to a certain mode during playback,
-- use e.g. "--script-opts=xrandr-output-mode=1920x1080"
xrandr_output_mode = mp.get_opt("xrandr-output-mode")
xrandr_blacklist = {}
function xrandr_parse_blacklist()
-- use e.g. "--script-opts=xrandr-blacklist=25" to have xrand.lua not use 25Hz refresh rate
-- 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("v", "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 rates associated with the current mode
local mls = string.match(res["stdout"], "\n" .. string.gsub(output, "%p", "%%%1") .. " connected.*")
local r
local mode = nil
local old_rate
local old_mode
-- old_rate = 0 means "no old rate known to switch to after playback"
old_rate = 0
if (xrandr_output_mode ~= nil) then
-- special case: user specified a certain preferred mode to use for playback
mp.msg.log("v", "looking for refresh rates for user supplied output mode " .. xrandr_output_mode)
mode, r = string.match(mls, '\n (' .. xrandr_output_mode .. ') ([^\n]+)')
if (mode == nil) then
mp.msg.log("info", "user preferred output mode " .. xrandr_output_mode .. " not found for output " .. output .. " - will use current mode")
else
mp.msg.log("info", "using user preferred xrandr_output_mode " .. xrandr_output_mode .. " for output " .. output)
-- try to find the "old rate" for the other, currently active mode
local oldr
old_mode, oldr = string.match(mls, '\n ([0-9x]+) ([^*\n]*%*[^\n]*)')
if (oldr ~= nil) then
for s in string.gmatch(oldr, "([^ ]+)%*") do
old_rate = s
end
end
mp.msg.log("v", "old_rate=" .. old_rate .. " found for old_mode=" .. tostring(old_mode))
end
end
if (mode == nil) then
-- normal case: use current mode
mode, r = string.match(mls, '\n ([0-9x]+) ([^*\n]*%*[^\n]*)')
old_mode = mode
end
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]*)')
old_mode = mode
if (r == nil) then
-- there is not even a "recommended" mode, so let's just use
-- whatever first mode line there is
mode, r = string.match(mls, '\n ([0-9x]+) ([^+\n]*[^\n]*)')
old_mode = mode
end
else
-- so "r" contains a hint to the current ("old") rate, let's remember
-- it for later switching back to it.
for s in string.gmatch(r, "([^ ]+)%*") do
old_rate = s
end
end
mp.msg.log("info", "output " .. output .. " mode=" .. mode .. " old rate=" .. old_rate .. " refresh rates = " .. r)
xrandr_modes[output] = { mode = mode, old_mode = old_mode, rates_s = r, rates = {}, old_rate = old_rate }
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("container-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" or mp.get_property("options/hwdec") == "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
-- unless "--script-opts=xrandr-ignore_unknown_oldrate=true" is set,
-- xrandr.lua will not touch display outputs for which it cannot
-- get information on the current refresh rate for - assuming that
-- such outputs are "disabled" somehow.
local ignore_unknown_oldrate = mp.get_opt("xrandr-ignore_unknown_oldrate")
if (ignore_unknown_oldrate == nil) then
ignore_unknown_oldrate = false
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
if (ignore_unknown_oldrate == false and xrandr_modes[output].old_rate == 0) then
mp.msg.log("info", "not touching output " .. output .. " because xrandr did not indicate a used refresh rate for it - use --script-opts=xrandr-ignore_unknown_oldrate=true if that is not what you want.")
else
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
end
if (vdpau_hack) then
mp.set_property("vid", old_vid)
if (old_position ~= nil) then
mp.commandv("seek", old_position, "absolute", "keyframes")
else
mp.msg.log("v", "old_position is 'nil' - not seeking after vdpau re-initialization")
end
end
end
function xrandr_set_old_rate()
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 old_rate = xrandr_modes[output].old_rate
if (old_rate == 0) then
mp.msg.log("v", "no previous frame rate known for output " .. output .. " - so no switching back.")
else
if (math.abs(old_rate-xrandr_previously_set[output]) < 0.001) then
mp.msg.log("v", "output " .. output .. " is already set to " .. old_rate .. "Hz - no switching back required")
else
mp.msg.log("info", "switching output " .. output .. " that was set for replay to mode " .. xrandr_modes[output].mode .. " at " .. xrandr_previously_set[output] .. "Hz back to mode " .. xrandr_modes[output].old_mode .. " with refresh rate " .. old_rate .. "Hz")
-- 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].old_mode
p["args"][6] = "--rate"
p["args"][7] = old_rate
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] = old_rate
end
end
end
end
end
-- we'll consider setting refresh rates whenever the video fps or the active outputs change:
mp.observe_property("container-fps", "native", xrandr_set_rate)
mp.observe_property("display-names", "native", xrandr_set_rate)
-- and we'll try to revert the refresh rate when mpv is shut down
mp.register_event("shutdown", xrandr_set_old_rate)