Last active
August 15, 2020 20:53
-
-
Save iwalton3/764b95871a52cbb23c4959a4da76e3d6 to your computer and use it in GitHub Desktop.
MPV Xrandr Custom Modes - Allows auto-setting framerates like 48 fps with MPV/SVP.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- use xrandr command to set output to best fitting fps rate | |
-- when playing videos with mpv. | |
-- From https://gitlab.com/lvml/mpv-plugin-xrandr/-/blob/master/xrandr.lua | |
-- Available under the terms of the GPLv2 | |
-- You need to customize this and also install custom modes. See line 241. | |
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=2,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.002) 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() | |
-- Please note with Nvidia you need to follow: | |
-- https://www.monitortests.com/forum/Thread-Guide-to-Nvidia-monitor-overclocking-on-Linux | |
-- | |
-- Add custom modes here. For instance: | |
-- | |
-- gtf 1920 1080 47.952 | |
-- xrandr --newmode "1920x1080_47.95_2" 135.41 1920 2032 2232 2544 1080 1081 1084 1110 -HSync +Vsync | |
-- xrandr --addmode DP-5 1920x1080_47.95_2 | |
-- gtf 1920 1080 48 | |
-- xrandr --newmode "1920x1080_48.00_2" 135.54 1920 2032 2232 2544 1080 1081 1084 1110 -HSync +Vsync | |
-- xrandr --addmode DP-5 1920x1080_48.00_2 | |
if (xrandr_modes["DP-5"] ~= nil) then | |
table.insert(xrandr_modes["DP-5"].rates, 47.95) | |
table.insert(xrandr_modes["DP-5"].rates, 48.00) | |
end | |
if (xrandr_modes["HDMI-0"] ~= nil) then | |
table.insert(xrandr_modes["HDMI-0"].rates, 47.95) | |
table.insert(xrandr_modes["HDMI-0"].rates, 48.00) | |
end | |
custom_modes = {} | |
custom_modes["DP-5"] = { | |
[47.95]="1920x1080_47.95_2", | |
[48.00]="1920x1080_48.00_2" | |
} | |
custom_modes["HDMI-0"] = { | |
[47.95]="1920x1080_47.95", | |
[48.00]="1920x1080_48.00" | |
} | |
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. | |
ignore_unknown_oldrate = true | |
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 | |
if (custom_modes[output][bfr] ~= nil) then | |
p["args"][4] = "--mode" | |
p["args"][5] = custom_modes[output][bfr] | |
else | |
p["args"][4] = "--mode" | |
p["args"][5] = xrandr_modes[output].mode | |
p["args"][6] = "--rate" | |
p["args"][7] = tostring(bfr) | |
end | |
local cmd_as_string = "" | |
for k, v in pairs(p["args"]) do | |
cmd_as_string = cmd_as_string .. v .. " " | |
end | |
mp.msg.log("debug", "executing as subprocess: \"" .. cmd_as_string .. "\"") | |
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 or xrandr_previously_set[output] == nil ) 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 | |
-- handler | |
pending_action = false | |
function handle_playback(name, value) | |
if name == "playback-abort" | |
then | |
if value then | |
mp.log("info", "Setting old rate.") | |
xrandr_set_old_rate() | |
else | |
pending_action = true | |
mp.log("info", "Setting pending action flag.") | |
end | |
elseif name == "duration" and pending_action and value ~= nil | |
then | |
mp.log("info", "Setting new rate.") | |
xrandr_set_rate() | |
pending_action = false | |
end | |
end | |
mp.observe_property("playback-abort", "native", handle_playback) | |
mp.observe_property("duration", "native", handle_playback) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment