-- 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)