-- Display some stats.---- Please consult the readme for information about usage and configuration:-- https://github.com/Argon-/mpv-stats---- Please note: not every property is always available and therefore not always-- visible. local mp = require 'mp'local options = require 'mp.options'local utils = require 'mp.utils'local input = require 'mp.input' -- Optionslocal o = { -- Default key bindings key_page_1 = "1", key_page_2 = "2", key_page_3 = "3", key_page_4 = "4", key_page_5 = "5", key_page_0 = "0", -- For pages which support scrolling key_scroll_up = "UP", key_scroll_down = "DOWN", key_search = "/", scroll_lines = 1, duration = 4, redraw_delay = 1, -- acts as duration in the toggling case ass_formatting = true, persistent_overlay = false, -- whether the stats can be overwritten by other output filter_params_max_length = 100, -- show one filter per line if list exceeds this length file_tag_max_length = 128, -- only show file tags shorter than this length in bytes file_tag_max_count = 16, -- only show the first x file tags show_frame_info = false, -- whether to show the current frame info term_width_limit = -1, -- overwrites the terminal width term_height_limit = -1, -- overwrites the terminal height debug = false, -- Graph options and style plot_perfdata = true, plot_vsync_ratio = true, plot_vsync_jitter = true, plot_tonemapping_lut = false, skip_frames = 5, global_max = true, flush_graph_data = true, -- clear data buffers when toggling plot_bg_border_color = "0000FF", plot_bg_color = "262626", plot_color = "FFFFFF", plot_bg_border_width = 0.5, -- Text style font = "", font_mono = "monospace", -- monospaced digits are sufficient font_size = 8, font_color = "", border_size = 0.8, border_color = "", shadow_x_offset = 0.0, shadow_y_offset = 0.0, shadow_color = "", alpha = "11", vidscale = "auto", -- Custom header for ASS tags to style the text output. -- Specifying this will ignore the text style values above and just -- use this string instead. custom_header = "", -- Text formatting -- With ASS ass_nl = "\\N", ass_indent = "\\h\\h\\h\\h\\h", ass_prefix_sep = "\\h\\h", ass_b1 = "{\\b1}", ass_b0 = "{\\b0}", ass_it1 = "{\\i1}", ass_it0 = "{\\i0}", -- Without ASS no_ass_nl = "\n", no_ass_indent = "\t", no_ass_prefix_sep = " ", no_ass_b1 = "\027[1m", no_ass_b0 = "\027[0m", no_ass_it1 = "\027[3m", no_ass_it0 = "\027[0m", bindlist = "no", -- print page 4 to the terminal on startup and quit mpv}options.read_options(o) o.term_width_limit = tonumber(o.term_width_limit) or -1o.term_height_limit = tonumber(o.term_height_limit) or -1if o.term_width_limit < 0 then o.term_width_limit = nilendif o.term_height_limit < 0 then o.term_height_limit = nilend local format = string.formatlocal max = math.maxlocal min = math.min -- Scaled metricslocal font_size = o.font_sizelocal border_size = o.border_sizelocal shadow_x_offset = o.shadow_x_offsetlocal shadow_y_offset = o.shadow_y_offsetlocal plot_bg_border_width = o.plot_bg_border_width-- Function used to record performance datalocal recorder = nil-- Timer used for redrawing (toggling) and clearing the screen (oneshot)local display_timer = nil-- Timer used to update cache stats.local cache_recorder_timer-- Current page and <page key>:<page function> mappingslocal curr_page = o.key_page_1local pages = {}local scroll_bound = falselocal searched_textlocal tm_viz_prev = nil-- Save these sequences locally as we'll need them a lotlocal ass_start = mp.get_property_osd("osd-ass-cc/0")local ass_stop = mp.get_property_osd("osd-ass-cc/1")-- Ring buffers for the values used to construct a graph.-- .pos denotes the current position, .len the buffer length-- .max is the max value in the corresponding bufferlocal vsratio_buf, vsjitter_buflocal function init_buffers() vsratio_buf = {0, pos = 1, len = 50, max = 0} vsjitter_buf = {0, pos = 1, len = 50, max = 0}endlocal cache_ahead_buf, cache_speed_buflocal perf_buffers = {} local function graph_add_value(graph, value) graph.pos = (graph.pos % graph.len) + 1 graph[graph.pos] = value graph.max = max(graph.max, value)end local function no_ASS(t) if not o.use_ass then return t elseif not o.persistent_overlay then -- mp.osd_message supports ass-escape using osd-ass-cc/{0|1} return ass_stop .. t .. ass_start else return mp.command_native({"escape-ass", tostring(t)}) endend local function bold(t) return o.b1 .. t .. o.b0end local function it(t) return o.it1 .. t .. o.it0end local function text_style() if not o.use_ass then return "" end if o.custom_header and o.custom_header ~= "" then return o.custom_header else local style = "{\\r\\an7\\fs" .. font_size .. "\\bord" .. border_size if o.font ~= "" then style = style .. "\\fn" .. o.font end if o.font_color ~= "" then style = style .. "\\1c&H" .. o.font_color .. "&\\1a&H" .. o.alpha .. "&" end if o.border_color ~= "" then style = style .. "\\3c&H" .. o.border_color .. "&\\3a&H" .. o.alpha .. "&" end if o.shadow_color ~= "" then style = style .. "\\4c&H" .. o.shadow_color .. "&\\4a&H" .. o.alpha .. "&" end return style .. "\\xshad" .. shadow_x_offset .. "\\yshad" .. shadow_y_offset .. "}" endend local function has_vo_window() return mp.get_property_native("vo-configured") and mp.get_property_native("video-osd")end -- Generate a graph from the given values.-- Returns an ASS formatted vector drawing as string.---- values: Array/table of numbers representing the data. Used like a ring buffer-- it will get iterated backwards `len` times starting at position `i`.-- i : Index of the latest data value in `values`.-- len : The length/amount of numbers in `values`.-- v_max : The maximum number in `values`. It is used to scale all data-- values to a range of 0 to `v_max`.-- v_avg : The average number in `values`. It is used to try and center graphs-- if possible. May be left as nil-- scale : A value that will be multiplied with all data values.-- x_tics: Horizontal width multiplier for the stepslocal function generate_graph(values, i, len, v_max, v_avg, scale, x_tics) -- Check if at least one value exists if not values[i] then return "" end local x_max = (len - 1) * x_tics local y_offset = border_size local y_max = font_size * 0.66 local x = 0 if v_max > 0 then -- try and center the graph if possible, but avoid going above `scale` if v_avg and v_avg > 0 then scale = min(scale, v_max / (2 * v_avg)) end scale = scale * y_max / v_max end -- else if v_max==0 then all values are 0 and scale doesn't matter local s = {format("m 0 0 n %f %f l ", x, y_max - scale * values[i])} i = ((i - 2) % len) + 1 for _ = 1, len - 1 do if values[i] then x = x - x_tics s[#s+1] = format("%f %f ", x, y_max - scale * values[i]) end i = ((i - 2) % len) + 1 end s[#s+1] = format("%f %f %f %f", x, y_max, 0, y_max) local bg_box = format("{\\bord%f}{\\3c&H%s&}{\\1c&H%s&}m 0 %f l %f %f %f 0 0 0", plot_bg_border_width, o.plot_bg_border_color, o.plot_bg_color, y_max, x_max, y_max, x_max) return format("%s{\\rDefault}{\\pbo%f}{\\shad0}{\\alpha&H00}{\\p1}%s{\\p0}" .. "{\\bord0}{\\1c&H%s}{\\p1}%s{\\p0}%s", o.prefix_sep, y_offset, bg_box, o.plot_color, table.concat(s), text_style())end local function append(s, str, attr) if not str then return false end attr.prefix_sep = attr.prefix_sep or o.prefix_sep attr.indent = attr.indent or o.indent attr.nl = attr.nl or o.nl attr.suffix = attr.suffix or "" attr.prefix = attr.prefix or "" attr.no_prefix_markup = attr.no_prefix_markup or false attr.prefix = attr.no_prefix_markup and attr.prefix or bold(attr.prefix) local index = #s + (attr.nl == "" and 0 or 1) s[index] = s[index] or "" s[index] = s[index] .. format("%s%s%s%s%s%s", attr.nl, attr.indent, attr.prefix, attr.prefix_sep, no_ASS(str), attr.suffix) return trueend -- Format and append a property.-- A property whose value is either `nil` or empty (hereafter called "invalid")-- is skipped and not appended.-- Returns `false` in case nothing was appended, otherwise `true`.---- s : Table containing strings.-- prop : The property to query and format (based on its OSD representation).-- attr : Optional table to overwrite certain (formatting) attributes for-- this property.-- exclude: Optional table containing keys which are considered invalid values-- for this property. Specifying this will replace empty string as-- default invalid value (nil is always invalid).local function append_property(s, prop, attr, excluded) excluded = excluded or {[""] = true} local ret = mp.get_property_osd(prop) if not ret or excluded[ret] then if o.debug then print("No value for property: " .. prop) end return false end return append(s, ret, attr)end local function sorted_keys(t, comp_fn) local keys = {} for k,_ in pairs(t) do keys[#keys+1] = k end table.sort(keys, comp_fn) return keysend local function scroll_hint(search) local hint = format("(hint: scroll with %s/%s", o.key_scroll_up, o.key_scroll_down) if search then hint = hint .. " and search with " .. o.key_search end hint = hint .. ")" if not o.use_ass then return " " .. hint end return format(" {\\fs%s}%s{\\fs%s}", font_size * 0.66, hint, font_size)end local function append_perfdata(header, s, dedicated_page) local vo_p = mp.get_property_native("vo-passes") if not vo_p then return end -- Sums of all last/avg/peak values local last_s, avg_s, peak_s = {}, {}, {} for frame, data in pairs(vo_p) do last_s[frame], avg_s[frame], peak_s[frame] = 0, 0, 0 for _, pass in ipairs(data) do last_s[frame] = last_s[frame] + pass["last"] avg_s[frame] = avg_s[frame] + pass["avg"] peak_s[frame] = peak_s[frame] + pass["peak"] end end -- Pretty print measured time local function pp(i) -- rescale to microseconds for a saner display return format("%5d", i / 1000) end -- Format n/m with a font weight based on the ratio local function p(n, m) local i = 0 if m > 0 then i = tonumber(n) / m end -- Calculate font weight. 100 is minimum, 400 is normal, 700 bold, 900 is max local w = (700 * math.sqrt(i)) + 200 if not o.use_ass then local str = format("%3d%%", i * 100) return w >= 700 and bold(str) or str end return format("{\\b%d}%3d%%{\\b0}", w, i * 100) end local font_small = o.use_ass and format("{\\fs%s}", font_size * 0.66) or "" local font_normal = o.use_ass and format("{\\fs%s}", font_size) or "" local font = o.use_ass and format("{\\fn%s}", o.font) or "" local font_mono = o.use_ass and format("{\\fn%s}", o.font_mono) or "" local indent = o.use_ass and "\\h" or " " -- ensure that the fixed title is one element and every scrollable line is -- also one single element. local h = dedicated_page and header or s h[#h+1] = format("%s%s%s%s%s%s%s%s", dedicated_page and "" or o.nl, dedicated_page and "" or o.indent, bold("Frame Timings:"), o.prefix_sep, font_small, "(last/average/peak μs)", font_normal, dedicated_page and scroll_hint() or "") for _,frame in ipairs(sorted_keys(vo_p)) do -- ensure fixed display order local data = vo_p[frame] local f = "%s%s%s%s%s / %s / %s %s%s%s%s%s%s" if dedicated_page then s[#s+1] = format("%s%s%s:", o.nl, o.indent, bold(frame:gsub("^%l", string.upper))) for _, pass in ipairs(data) do s[#s+1] = format(f, o.nl, o.indent, o.indent, font_mono, pp(pass["last"]), pp(pass["avg"]), pp(pass["peak"]), o.prefix_sep .. indent, p(pass["last"], last_s[frame]), font, o.prefix_sep, o.prefix_sep, pass["desc"]) if o.plot_perfdata and o.use_ass then -- use the same line that was already started for this iteration s[#s] = s[#s] .. generate_graph(pass["samples"], pass["count"], pass["count"], pass["peak"], pass["avg"], 0.9, 0.25) end end -- Print sum of timing values as "Total" s[#s+1] = format(f, o.nl, o.indent, o.indent, font_mono, pp(last_s[frame]), pp(avg_s[frame]), pp(peak_s[frame]), o.prefix_sep, bold("Total"), font, "", "", "") else -- for the simplified view, we just print the sum of each pass s[#s+1] = format(f, o.nl, o.indent, o.indent, font_mono, pp(last_s[frame]), pp(avg_s[frame]), pp(peak_s[frame]), "", "", font, o.prefix_sep, o.prefix_sep, frame:gsub("^%l", string.upper)) end endend -- command prefix tokens to strip - includes generic property commandslocal cmd_prefixes = { osd_auto=1, no_osd=1, osd_bar=1, osd_msg=1, osd_msg_bar=1, raw=1, sync=1, async=1, expand_properties=1, repeatable=1, nonrepeatable=1, set=1, add=1, multiply=1, toggle=1, cycle=1, cycle_values=1, ["!reverse"]=1, change_list=1,}-- commands/writable-properties prefix sub-words (followed by -) to striplocal name_prefixes = { define=1, delete=1, enable=1, disable=1, dump=1, write=1, drop=1, revert=1, ab=1, hr=1, secondary=1, current=1,}-- extract a command "subject" from a command string, by removing all-- generic prefix tokens and then returning the first interesting sub-word-- of the next token. For target-script name we also check another token.-- The tokenizer works fine for things we care about - valid mpv commands,-- properties and script names, possibly quoted, white-space[s]-separated.-- It's decent in practice, and worst case is "incorrect" subject.local function cmd_subject(cmd) cmd = cmd:gsub(";.*", ""):gsub("%-", "_") -- only first cmd, s/-/_/ local TOKEN = '^%s*["\']?([%w_!]*)' -- captures+ends before (maybe) final " local tok, sname, subw repeat tok, cmd = cmd:match(TOKEN .. '["\']?(.*)') until not cmd_prefixes[tok] -- tok is the 1st non-generic command/property name token, cmd is the rest sname = tok == "script_message_to" and cmd:match(TOKEN) or tok == "script_binding" and cmd:match(TOKEN .. "/") if sname and sname ~= "" then return "script: " .. sname end -- return the first sub-word of tok which is not a useless prefix repeat subw, tok = tok:match("([^_]*)_?(.*)") until tok == "" or not name_prefixes[subw] return subw:len() > 1 and subw or "[unknown]"end -- key names are valid UTF-8, ascii7 except maybe the last/only codepoint.-- we count codepoints and ignore wcwidth. no need for grapheme clusters.-- our error for alignment is at most one cell (if last CP is double-width).-- (if k was valid but arbitrary: we'd count all bytes <0x80 or >=0xc0)local function keyname_cells(k) local klen = k:len() if klen > 1 and k:byte(klen) >= 0x80 then -- last/only CP is not ascii7 repeat klen = klen-1 until klen == 1 or k:byte(klen) >= 0xc0 -- last CP begins at klen end return klenend local function get_kbinfo_lines() -- active keys: only highest priority of each key, and not our (stats) keys local bindings = mp.get_property_native("input-bindings", {}) local active = {} -- map: key-name -> bind-info for _, bind in pairs(bindings) do if bind.priority >= 0 and ( not active[bind.key] or (active[bind.key].is_weak and not bind.is_weak) or (bind.is_weak == active[bind.key].is_weak and bind.priority > active[bind.key].priority) ) and not bind.cmd:find("script-binding stats/__forced_", 1, true) and bind.section ~= "input_forced_console" and ( searched_text == nil or (bind.key .. bind.cmd .. (bind.comment or "")):lower():find(searched_text, 1, true) ) then active[bind.key] = bind end end -- make an array, find max key len, add sort keys (.subject/.mods[_count]) local ordered = {} local kspaces = "" -- as many spaces as the longest key name for _, bind in pairs(active) do bind.subject = cmd_subject(bind.cmd) if bind.subject ~= "ignore" then ordered[#ordered+1] = bind _,_, bind.mods = bind.key:find("(.*)%+.") _, bind.mods_count = bind.key:gsub("%+.", "") if bind.key:len() > kspaces:len() then kspaces = string.rep(" ", bind.key:len()) end end end local function align_right(key) return kspaces:sub(keyname_cells(key)) .. keyLines 1–500 of 1789. Load more
-- Display some stats. -- -- Please consult the readme for information about usage and configuration: -- https://github.com/Argon-/mpv-stats -- -- Please note: not every property is always available and therefore not always -- visible. local mp = require 'mp' local options = require 'mp.options' local utils = require 'mp.utils' local input = require 'mp.input' -- Options local o = { -- Default key bindings key_page_1 = "1", key_page_2 = "2", key_page_3 = "3", key_page_4 = "4", key_page_5 = "5", key_page_0 = "0", -- For pages which support scrolling key_scroll_up = "UP", key_scroll_down = "DOWN", key_search = "/", scroll_lines = 1, duration = 4, redraw_delay = 1, -- acts as duration in the toggling case ass_formatting = true, persistent_overlay = false, -- whether the stats can be overwritten by other output filter_params_max_length = 100, -- show one filter per line if list exceeds this length file_tag_max_length = 128, -- only show file tags shorter than this length in bytes file_tag_max_count = 16, -- only show the first x file tags show_frame_info = false, -- whether to show the current frame info term_width_limit = -1, -- overwrites the terminal width term_height_limit = -1, -- overwrites the terminal height debug = false, -- Graph options and style plot_perfdata = true, plot_vsync_ratio = true, plot_vsync_jitter = true, plot_tonemapping_lut = false, skip_frames = 5, global_max = true, flush_graph_data = true, -- clear data buffers when toggling plot_bg_border_color = "0000FF", plot_bg_color = "262626", plot_color = "FFFFFF", plot_bg_border_width = 0.5, -- Text style font = "", font_mono = "monospace", -- monospaced digits are sufficient font_size = 8, font_color = "", border_size = 0.8, border_color = "", shadow_x_offset = 0.0, shadow_y_offset = 0.0, shadow_color = "", alpha = "11", vidscale = "auto", -- Custom header for ASS tags to style the text output. -- Specifying this will ignore the text style values above and just -- use this string instead. custom_header = "", -- Text formatting -- With ASS ass_nl = "\\N", ass_indent = "\\h\\h\\h\\h\\h", ass_prefix_sep = "\\h\\h", ass_b1 = "{\\b1}", ass_b0 = "{\\b0}", ass_it1 = "{\\i1}", ass_it0 = "{\\i0}", -- Without ASS no_ass_nl = "\n", no_ass_indent = "\t", no_ass_prefix_sep = " ", no_ass_b1 = "\027[1m", no_ass_b0 = "\027[0m", no_ass_it1 = "\027[3m", no_ass_it0 = "\027[0m", bindlist = "no", -- print page 4 to the terminal on startup and quit mpv } options.read_options(o) o.term_width_limit = tonumber(o.term_width_limit) or -1 o.term_height_limit = tonumber(o.term_height_limit) or -1 if o.term_width_limit < 0 then o.term_width_limit = nil end if o.term_height_limit < 0 then o.term_height_limit = nil end local format = string.format local max = math.max local min = math.min -- Scaled metrics local font_size = o.font_size local border_size = o.border_size local shadow_x_offset = o.shadow_x_offset local shadow_y_offset = o.shadow_y_offset local plot_bg_border_width = o.plot_bg_border_width -- Function used to record performance data local recorder = nil -- Timer used for redrawing (toggling) and clearing the screen (oneshot) local display_timer = nil -- Timer used to update cache stats. local cache_recorder_timer -- Current page and <page key>:<page function> mappings local curr_page = o.key_page_1 local pages = {} local scroll_bound = false local searched_text local tm_viz_prev = nil -- Save these sequences locally as we'll need them a lot local ass_start = mp.get_property_osd("osd-ass-cc/0") local ass_stop = mp.get_property_osd("osd-ass-cc/1") -- Ring buffers for the values used to construct a graph. -- .pos denotes the current position, .len the buffer length -- .max is the max value in the corresponding buffer local vsratio_buf, vsjitter_buf local function init_buffers() vsratio_buf = {0, pos = 1, len = 50, max = 0} vsjitter_buf = {0, pos = 1, len = 50, max = 0} end local cache_ahead_buf, cache_speed_buf local perf_buffers = {} local function graph_add_value(graph, value) graph.pos = (graph.pos % graph.len) + 1 graph[graph.pos] = value graph.max = max(graph.max, value) end local function no_ASS(t) if not o.use_ass then return t elseif not o.persistent_overlay then -- mp.osd_message supports ass-escape using osd-ass-cc/{0|1} return ass_stop .. t .. ass_start else return mp.command_native({"escape-ass", tostring(t)}) end end local function bold(t) return o.b1 .. t .. o.b0 end local function it(t) return o.it1 .. t .. o.it0 end local function text_style() if not o.use_ass then return "" end if o.custom_header and o.custom_header ~= "" then return o.custom_header else local style = "{\\r\\an7\\fs" .. font_size .. "\\bord" .. border_size if o.font ~= "" then style = style .. "\\fn" .. o.font end if o.font_color ~= "" then style = style .. "\\1c&H" .. o.font_color .. "&\\1a&H" .. o.alpha .. "&" end if o.border_color ~= "" then style = style .. "\\3c&H" .. o.border_color .. "&\\3a&H" .. o.alpha .. "&" end if o.shadow_color ~= "" then style = style .. "\\4c&H" .. o.shadow_color .. "&\\4a&H" .. o.alpha .. "&" end return style .. "\\xshad" .. shadow_x_offset .. "\\yshad" .. shadow_y_offset .. "}" end end local function has_vo_window() return mp.get_property_native("vo-configured") and mp.get_property_native("video-osd") end -- Generate a graph from the given values. -- Returns an ASS formatted vector drawing as string. -- -- values: Array/table of numbers representing the data. Used like a ring buffer -- it will get iterated backwards `len` times starting at position `i`. -- i : Index of the latest data value in `values`. -- len : The length/amount of numbers in `values`. -- v_max : The maximum number in `values`. It is used to scale all data -- values to a range of 0 to `v_max`. -- v_avg : The average number in `values`. It is used to try and center graphs -- if possible. May be left as nil -- scale : A value that will be multiplied with all data values. -- x_tics: Horizontal width multiplier for the steps local function generate_graph(values, i, len, v_max, v_avg, scale, x_tics) -- Check if at least one value exists if not values[i] then return "" end local x_max = (len - 1) * x_tics local y_offset = border_size local y_max = font_size * 0.66 local x = 0 if v_max > 0 then -- try and center the graph if possible, but avoid going above `scale` if v_avg and v_avg > 0 then scale = min(scale, v_max / (2 * v_avg)) end scale = scale * y_max / v_max end -- else if v_max==0 then all values are 0 and scale doesn't matter local s = {format("m 0 0 n %f %f l ", x, y_max - scale * values[i])} i = ((i - 2) % len) + 1 for _ = 1, len - 1 do if values[i] then x = x - x_tics s[#s+1] = format("%f %f ", x, y_max - scale * values[i]) end i = ((i - 2) % len) + 1 end s[#s+1] = format("%f %f %f %f", x, y_max, 0, y_max) local bg_box = format("{\\bord%f}{\\3c&H%s&}{\\1c&H%s&}m 0 %f l %f %f %f 0 0 0", plot_bg_border_width, o.plot_bg_border_color, o.plot_bg_color, y_max, x_max, y_max, x_max) return format("%s{\\rDefault}{\\pbo%f}{\\shad0}{\\alpha&H00}{\\p1}%s{\\p0}" .. "{\\bord0}{\\1c&H%s}{\\p1}%s{\\p0}%s", o.prefix_sep, y_offset, bg_box, o.plot_color, table.concat(s), text_style()) end local function append(s, str, attr) if not str then return false end attr.prefix_sep = attr.prefix_sep or o.prefix_sep attr.indent = attr.indent or o.indent attr.nl = attr.nl or o.nl attr.suffix = attr.suffix or "" attr.prefix = attr.prefix or "" attr.no_prefix_markup = attr.no_prefix_markup or false attr.prefix = attr.no_prefix_markup and attr.prefix or bold(attr.prefix) local index = #s + (attr.nl == "" and 0 or 1) s[index] = s[index] or "" s[index] = s[index] .. format("%s%s%s%s%s%s", attr.nl, attr.indent, attr.prefix, attr.prefix_sep, no_ASS(str), attr.suffix) return true end -- Format and append a property. -- A property whose value is either `nil` or empty (hereafter called "invalid") -- is skipped and not appended. -- Returns `false` in case nothing was appended, otherwise `true`. -- -- s : Table containing strings. -- prop : The property to query and format (based on its OSD representation). -- attr : Optional table to overwrite certain (formatting) attributes for -- this property. -- exclude: Optional table containing keys which are considered invalid values -- for this property. Specifying this will replace empty string as -- default invalid value (nil is always invalid). local function append_property(s, prop, attr, excluded) excluded = excluded or {[""] = true} local ret = mp.get_property_osd(prop) if not ret or excluded[ret] then if o.debug then print("No value for property: " .. prop) end return false end return append(s, ret, attr) end local function sorted_keys(t, comp_fn) local keys = {} for k,_ in pairs(t) do keys[#keys+1] = k end table.sort(keys, comp_fn) return keys end local function scroll_hint(search) local hint = format("(hint: scroll with %s/%s", o.key_scroll_up, o.key_scroll_down) if search then hint = hint .. " and search with " .. o.key_search end hint = hint .. ")" if not o.use_ass then return " " .. hint end return format(" {\\fs%s}%s{\\fs%s}", font_size * 0.66, hint, font_size) end local function append_perfdata(header, s, dedicated_page) local vo_p = mp.get_property_native("vo-passes") if not vo_p then return end -- Sums of all last/avg/peak values local last_s, avg_s, peak_s = {}, {}, {} for frame, data in pairs(vo_p) do last_s[frame], avg_s[frame], peak_s[frame] = 0, 0, 0 for _, pass in ipairs(data) do last_s[frame] = last_s[frame] + pass["last"] avg_s[frame] = avg_s[frame] + pass["avg"] peak_s[frame] = peak_s[frame] + pass["peak"] end end -- Pretty print measured time local function pp(i) -- rescale to microseconds for a saner display return format("%5d", i / 1000) end -- Format n/m with a font weight based on the ratio local function p(n, m) local i = 0 if m > 0 then i = tonumber(n) / m end -- Calculate font weight. 100 is minimum, 400 is normal, 700 bold, 900 is max local w = (700 * math.sqrt(i)) + 200 if not o.use_ass then local str = format("%3d%%", i * 100) return w >= 700 and bold(str) or str end return format("{\\b%d}%3d%%{\\b0}", w, i * 100) end local font_small = o.use_ass and format("{\\fs%s}", font_size * 0.66) or "" local font_normal = o.use_ass and format("{\\fs%s}", font_size) or "" local font = o.use_ass and format("{\\fn%s}", o.font) or "" local font_mono = o.use_ass and format("{\\fn%s}", o.font_mono) or "" local indent = o.use_ass and "\\h" or " " -- ensure that the fixed title is one element and every scrollable line is -- also one single element. local h = dedicated_page and header or s h[#h+1] = format("%s%s%s%s%s%s%s%s", dedicated_page and "" or o.nl, dedicated_page and "" or o.indent, bold("Frame Timings:"), o.prefix_sep, font_small, "(last/average/peak μs)", font_normal, dedicated_page and scroll_hint() or "") for _,frame in ipairs(sorted_keys(vo_p)) do -- ensure fixed display order local data = vo_p[frame] local f = "%s%s%s%s%s / %s / %s %s%s%s%s%s%s" if dedicated_page then s[#s+1] = format("%s%s%s:", o.nl, o.indent, bold(frame:gsub("^%l", string.upper))) for _, pass in ipairs(data) do s[#s+1] = format(f, o.nl, o.indent, o.indent, font_mono, pp(pass["last"]), pp(pass["avg"]), pp(pass["peak"]), o.prefix_sep .. indent, p(pass["last"], last_s[frame]), font, o.prefix_sep, o.prefix_sep, pass["desc"]) if o.plot_perfdata and o.use_ass then -- use the same line that was already started for this iteration s[#s] = s[#s] .. generate_graph(pass["samples"], pass["count"], pass["count"], pass["peak"], pass["avg"], 0.9, 0.25) end end -- Print sum of timing values as "Total" s[#s+1] = format(f, o.nl, o.indent, o.indent, font_mono, pp(last_s[frame]), pp(avg_s[frame]), pp(peak_s[frame]), o.prefix_sep, bold("Total"), font, "", "", "") else -- for the simplified view, we just print the sum of each pass s[#s+1] = format(f, o.nl, o.indent, o.indent, font_mono, pp(last_s[frame]), pp(avg_s[frame]), pp(peak_s[frame]), "", "", font, o.prefix_sep, o.prefix_sep, frame:gsub("^%l", string.upper)) end end end -- command prefix tokens to strip - includes generic property commands local cmd_prefixes = { osd_auto=1, no_osd=1, osd_bar=1, osd_msg=1, osd_msg_bar=1, raw=1, sync=1, async=1, expand_properties=1, repeatable=1, nonrepeatable=1, set=1, add=1, multiply=1, toggle=1, cycle=1, cycle_values=1, ["!reverse"]=1, change_list=1, } -- commands/writable-properties prefix sub-words (followed by -) to strip local name_prefixes = { define=1, delete=1, enable=1, disable=1, dump=1, write=1, drop=1, revert=1, ab=1, hr=1, secondary=1, current=1, } -- extract a command "subject" from a command string, by removing all -- generic prefix tokens and then returning the first interesting sub-word -- of the next token. For target-script name we also check another token. -- The tokenizer works fine for things we care about - valid mpv commands, -- properties and script names, possibly quoted, white-space[s]-separated. -- It's decent in practice, and worst case is "incorrect" subject. local function cmd_subject(cmd) cmd = cmd:gsub(";.*", ""):gsub("%-", "_") -- only first cmd, s/-/_/ local TOKEN = '^%s*["\']?([%w_!]*)' -- captures+ends before (maybe) final " local tok, sname, subw repeat tok, cmd = cmd:match(TOKEN .. '["\']?(.*)') until not cmd_prefixes[tok] -- tok is the 1st non-generic command/property name token, cmd is the rest sname = tok == "script_message_to" and cmd:match(TOKEN) or tok == "script_binding" and cmd:match(TOKEN .. "/") if sname and sname ~= "" then return "script: " .. sname end -- return the first sub-word of tok which is not a useless prefix repeat subw, tok = tok:match("([^_]*)_?(.*)") until tok == "" or not name_prefixes[subw] return subw:len() > 1 and subw or "[unknown]" end -- key names are valid UTF-8, ascii7 except maybe the last/only codepoint. -- we count codepoints and ignore wcwidth. no need for grapheme clusters. -- our error for alignment is at most one cell (if last CP is double-width). -- (if k was valid but arbitrary: we'd count all bytes <0x80 or >=0xc0) local function keyname_cells(k) local klen = k:len() if klen > 1 and k:byte(klen) >= 0x80 then -- last/only CP is not ascii7 repeat klen = klen-1 until klen == 1 or k:byte(klen) >= 0xc0 -- last CP begins at klen end return klen end local function get_kbinfo_lines() -- active keys: only highest priority of each key, and not our (stats) keys local bindings = mp.get_property_native("input-bindings", {}) local active = {} -- map: key-name -> bind-info for _, bind in pairs(bindings) do if bind.priority >= 0 and ( not active[bind.key] or (active[bind.key].is_weak and not bind.is_weak) or (bind.is_weak == active[bind.key].is_weak and bind.priority > active[bind.key].priority) ) and not bind.cmd:find("script-binding stats/__forced_", 1, true) and bind.section ~= "input_forced_console" and ( searched_text == nil or (bind.key .. bind.cmd .. (bind.comment or "")):lower():find(searched_text, 1, true) ) then active[bind.key] = bind end end -- make an array, find max key len, add sort keys (.subject/.mods[_count]) local ordered = {} local kspaces = "" -- as many spaces as the longest key name for _, bind in pairs(active) do bind.subject = cmd_subject(bind.cmd) if bind.subject ~= "ignore" then ordered[#ordered+1] = bind _,_, bind.mods = bind.key:find("(.*)%+.") _, bind.mods_count = bind.key:gsub("%+.", "") if bind.key:len() > kspaces:len() then kspaces = string.rep(" ", bind.key:len()) end end end local function align_right(key) return kspaces:sub(keyname_cells(key)) .. key