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

2 years ago
  1. -- use xrandr command to set output to best fitting fps rate
  2. -- when playing videos with mpv.
  3. utils = require 'mp.utils'
  4. xrandr_blacklist = {}
  5. function xrandr_parse_blacklist()
  6. -- Parse the optional "blacklist" from a string into an array for later use.
  7. -- For now, we only support a list of rates, since the "mode" is not subject
  8. -- to automatic change (mpv is better at scaling than most displays) and
  9. -- this also makes the blacklist option more easy to specify:
  10. local b = mp.get_opt("xrandr-blacklist")
  11. if (b == nil) then
  12. return
  13. end
  14. local i = 1
  15. for s in string.gmatch(b, "([^, ]+)") do
  16. xrandr_blacklist[i] = 0.0 + s
  17. i = i+1
  18. end
  19. end
  20. xrandr_parse_blacklist()
  21. function xrandr_check_blacklist(mode, rate)
  22. -- check if (mode, rate) is black-listed - e.g. because the
  23. -- computer display output is known to be incompatible with the
  24. -- display at this specific mode/rate
  25. for i=1,#xrandr_blacklist do
  26. r = xrandr_blacklist[i]
  27. if (r == rate) then
  28. mp.msg.log("info", "will not use mode '" .. mode .. "' with rate " .. rate .. " because option -script-opts=xrandr-blacklist said so")
  29. return true
  30. end
  31. end
  32. return false
  33. end
  34. xrandr_detect_done = false
  35. xrandr_modes = {}
  36. xrandr_connected_outputs = {}
  37. function xrandr_detect_available_rates()
  38. if (xrandr_detect_done) then
  39. return
  40. end
  41. xrandr_detect_done = true
  42. -- invoke xrandr to find out which fps rates are available on which outputs
  43. local p = {}
  44. p["cancellable"] = false
  45. p["args"] = {}
  46. p["args"][1] = "xrandr"
  47. p["args"][2] = "-q"
  48. local res = utils.subprocess(p)
  49. if (res["error"] ~= nil) then
  50. mp.msg.log("info", "failed to execute 'xrand -q', error message: " .. res["error"])
  51. return
  52. end
  53. mp.msg.log("v","xrandr -q\n" .. res["stdout"])
  54. local output_idx = 1
  55. for output in string.gmatch(res["stdout"], '\n([^ ]+) connected') do
  56. table.insert(xrandr_connected_outputs, output)
  57. -- the first line with a "*" after the match contains the mode associated with the mode
  58. local mls = string.match(res["stdout"], "\n" .. string.gsub(output, "%p", "%%%1") .. " connected.*")
  59. local r
  60. local mode
  61. mode, r = string.match(mls, '\n ([0-9x]+) ([^*\n]*%*[^\n]*)')
  62. if (r == nil) then
  63. -- if no refresh rate is reported active for an output by xrandr,
  64. -- search for the mode that is "recommended" (marked by "+" in xrandr's output)
  65. mode, r = string.match(mls, '\n ([0-9x]+) ([^+\n]*%+[^\n]*)')
  66. end
  67. mp.msg.log("info", "output " .. output .. " mode=" .. mode .. " refresh rates = " .. r)
  68. xrandr_modes[output] = { mode = mode, rates_s = r, rates = {} }
  69. local i = 1
  70. for s in string.gmatch(r, "([^ +*]+)") do
  71. -- check if rate "r" is black-listed - this is checked here because
  72. if (not xrandr_check_blacklist(mode, 0.0 + s)) then
  73. xrandr_modes[output].rates[i] = 0.0 + s
  74. i = i+1
  75. end
  76. end
  77. output_idx = output_idx + 1
  78. end
  79. end
  80. function xrandr_find_best_fitting_rate(fps, output)
  81. local xrandr_rates = xrandr_modes[output].rates
  82. -- try integer multipliers of 1 to 3, in that order
  83. for m=1,3 do
  84. -- check for a "perfect" match (where fps rates of e.g. 60.0 are not equal 59.9 or such)
  85. for i=1,#xrandr_rates do
  86. r = xrandr_rates[i]
  87. if (math.abs(r-(m * fps)) < 0.001) then
  88. return r
  89. end
  90. end
  91. end
  92. for m=1,3 do
  93. -- check for a "less precise" match (where fps rates of e.g. 60.0 and 59.9 are assumed "equal")
  94. for i=1,#xrandr_rates do
  95. r = xrandr_rates[i]
  96. if (math.abs(r-(m * fps)) < 0.2) then
  97. if (m == 1) then
  98. -- pass the original rate to xrandr later, because
  99. -- e.g. a 23.976 Hz mode might be displayed as "24.0",
  100. -- but still xrandr may set the better matching mode
  101. -- if the exact number is passed
  102. return fps
  103. else
  104. return r
  105. end
  106. end
  107. end
  108. end
  109. -- if no known frame rate is any "good", use the highest available frame rate,
  110. -- as this will probably cause the least "jitter"
  111. local mr = 0.0
  112. for i=1,#xrandr_rates do
  113. r = xrandr_rates[i]
  114. -- mp.msg.log("v","r=" .. r .. " mr=" .. mr)
  115. if (r > mr) then
  116. mr = r
  117. end
  118. end
  119. return mr
  120. end
  121. xrandr_active_outputs = {}
  122. function xrandr_set_active_outputs()
  123. local dn = mp.get_property("display-names")
  124. if (dn ~= nil) then
  125. mp.msg.log("v","display-names=" .. dn)
  126. xrandr_active_outputs = {}
  127. for w in (dn .. ","):gmatch("([^,]*),") do
  128. table.insert(xrandr_active_outputs, w)
  129. end
  130. end
  131. end
  132. -- last detected non-nil video frame rate:
  133. xrandr_cfps = nil
  134. -- for each output, we remember which refresh rate we set last, so
  135. -- we do not unnecessarily set the same refresh rate again
  136. xrandr_previously_set = {}
  137. function xrandr_set_rate()
  138. local f = mp.get_property_native("fps")
  139. if (f == nil or f == xrandr_cfps) then
  140. -- either no change or no frame rate information, so don't set anything
  141. return
  142. end
  143. xrandr_cfps = f
  144. xrandr_detect_available_rates()
  145. xrandr_set_active_outputs()
  146. local vdpau_hack = false
  147. local old_vid = nil
  148. local old_position = nil
  149. if (mp.get_property("options/vo") == "vdpau") then
  150. -- enable wild hack: need to close and re-open video for vdpau,
  151. -- because vdpau barfs if xrandr is run while it is in use
  152. vdpau_hack = true
  153. old_position = mp.get_property("time-pos")
  154. old_vid = mp.get_property("vid")
  155. mp.set_property("vid", "no")
  156. end
  157. local outs = {}
  158. if (#xrandr_active_outputs == 0) then
  159. -- No active outputs - probably because vo (like with vdpau) does
  160. -- not provide the information which outputs are covered.
  161. -- As a fall-back, let's assume all connected outputs are relevant.
  162. mp.msg.log("v","no output is known to be used by mpv, assuming all connected outputs are used.")
  163. outs = xrandr_connected_outputs
  164. else
  165. outs = xrandr_active_outputs
  166. end
  167. -- iterate over all relevant outputs used by mpv's output:
  168. for n, output in ipairs(outs) do
  169. local bfr = xrandr_find_best_fitting_rate(xrandr_cfps, output)
  170. if (bfr == 0.0) then
  171. mp.msg.log("info", "no non-blacklisted rate available, not invoking xrandr")
  172. else
  173. 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")
  174. if (bfr == xrandr_previously_set[output]) then
  175. mp.msg.log("v", "output " .. output .. " was already set to " .. bfr .. "Hz before - not changing")
  176. else
  177. -- invoke xrandr to set the best fitting refresh rate for output
  178. local p = {}
  179. p["cancellable"] = false
  180. p["args"] = {}
  181. p["args"][1] = "xrandr"
  182. p["args"][2] = "--output"
  183. p["args"][3] = output
  184. p["args"][4] = "--mode"
  185. p["args"][5] = xrandr_modes[output].mode
  186. p["args"][6] = "--rate"
  187. p["args"][7] = bfr
  188. local res = utils.subprocess(p)
  189. if (res["error"] ~= nil) then
  190. mp.msg.log("error", "failed to set refresh rate for output " .. output .. " using xrandr, error message: " .. res["error"])
  191. else
  192. xrandr_previously_set[output] = bfr
  193. end
  194. end
  195. end
  196. end
  197. if (vdpau_hack) then
  198. mp.set_property("vid", old_vid)
  199. mp.commandv("seek", old_position, "absolute", "keyframes")
  200. end
  201. end
  202. -- we'll consider setting refresh rates whenever the video fps or the active outputs change:
  203. mp.observe_property("fps", "native", xrandr_set_rate)
  204. mp.observe_property("display-names", "native", xrandr_set_rate)